package de.onyxbits.pocketbandit;
import com.badlogic.gdx.*;
import com.badlogic.gdx.graphics.g2d.*;
import com.badlogic.gdx.scenes.scene2d.*;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.audio.*;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.actions.*;
import com.badlogic.gdx.scenes.scene2d.utils.*;
import com.badlogic.gdx.assets.*;
import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.math.*;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
import de.onyxbits.bureauengine.*;
import de.onyxbits.bureauengine.screen.*;
/**
* Represents the actual game screen. This class also doubles as the config screen.
*/
public class GambleScreen<T extends SlotMachine> extends StageScreen<T> implements EventListener {
/**
* 3x3 symbols packed into a single array. First reel goes from 0 to 2, second from 3 to 5 and
* third from 6 to 8. Symbols within a reel are potentially unordered.
*/
private Symbol[] reelSymbols = new Symbol[9];
/**
* For toggling the number of coins the player may bet in each game.
*/
private ImageButton[] bet = new ImageButton[3];
/**
* Amount of coins on hand
*/
private Label credits;
/**
* The lever used to spin the reels
*/
private Image knob;
/**
* How often the lever has been pulled.
*/
private Label turns;
/**
* Displays a message to the player
*/
private Label feedbackMessage;
/**
* Displays a symbol to the player
*/
private Image feedbackSymbol;
/**
* Groups feedbackMessage and feedbackSymbol
*/
private Group feedbackGroup;
/**
* Notifies the player that s/he hit the lucky bonus
*/
private Image feedbackBonus;
/**
* Switch between game view and options view
*/
private ImageButton viewSwitch;
/**
* Mute music
*/
private ImageButton musicStatus;
/**
* Mute sound
*/
private ImageButton soundStatus;
/**
* Cash out and exit to the menu screen
*/
private ImageButton exit;
/**
* Config mode: select previous game variation
*/
private ImageButton previousVariation;
/**
* Config mode: select next game variation
*/
private ImageButton nextVariation;
/**
* Displays the name of the variation
*/
private Table deviceName;
/**
* Rules
*/
private Variation variation;
/**
* Game state
*/
private Player player;
/**
* Number of <code>Symbol</code>S in motion.
*/
private int spinning;
/**
* Symbols on the reel
*/
private Drawable[] symbols;
/**
* Symbols on the paytable
*/
private Drawable[] smallSymbols;
/**
* Container for the paytable
*/
private ScrollPane scrollTable;
/**
* Contains the actual automaton UI
*/
private Group deviceGroup = new Group();
/**
* Contains the name, paytable and menu buttons
*/
private Group infoGroup = new Group();
/**
* For <code>playSoundEffect()</code>
*/
protected static final int TRIGGERSOUND=0;
/**
* For <code>playSoundEffect()</code>
*/
protected static final int WINSOUND=1;
/**
* For <code>playSoundEffect()</code>
*/
protected static final int EJECTCOINSOUND=2;
/**
* For <code>playSoundEffect()</code>
*/
protected static final int REELSTOPSOUND=3;
private Sound triggerSound;
private Sound winSound;
private Sound ejectCoinSound;
private Sound reelStopSound;
private static final AssetDescriptor[] ASSETS = {
new AssetDescriptor<TextureAtlas>("textures/gamblescreen.atlas",TextureAtlas.class),
new AssetDescriptor<Music>("music/Theme for Harold var 3.mp3",Music.class),
new AssetDescriptor<Sound>("sfx/Pellet Gun Pump-SoundBible.com-517750307.mp3",Sound.class),
new AssetDescriptor<Sound>("sfx/135936__bradwesson__collectcoin.ogg",Sound.class),
new AssetDescriptor<Sound>("sfx/56246__q-k__latch-04.ogg",Sound.class),
};
/**
* Construct a new Gamble/Options screen
* @param game reference to the game object
* @param player game state. May be null. If null, the gamble screen allows for selecting
* a variation.
* @param variation the game variant
*/
public GambleScreen(T game, Player player, Variation variation) {
super(game);
this.player = player;
this.variation=variation;
}
@Override
protected AssetDescriptor[] getAssets() {
return ASSETS;
}
public void readyScreen() {
this.stage = new Stage(320, 480, true,game.spriteBatch);
deviceGroup.setTransform(false);
triggerSound=game.assetManager.get("sfx/Pellet Gun Pump-SoundBible.com-517750307.mp3",Sound.class);
winSound=game.assetManager.get("sfx/135936__bradwesson__collectcoin.ogg",Sound.class);
reelStopSound=game.assetManager.get("sfx/56246__q-k__latch-04.ogg",Sound.class);
music = game.assetManager.get("music/Theme for Harold var 3.mp3",Music.class);
music.setLooping(true);
//FIXME: Do these two need to be dispose()d off?
TextureAtlas localAtlas =game.assetManager.get("textures/gamblescreen.atlas",TextureAtlas.class);
TextureAtlas globalAtlas =game.assetManager.get("textures/global.atlas",TextureAtlas.class);
Drawable up,down,checked; // Reusables for making buttons.
symbols=new Drawable[variation.symbolNames.length];
smallSymbols=new Drawable[variation.symbolNames.length];
Drawable backgroundImage = new NinePatchDrawable(new NinePatch(globalAtlas.findRegion("roundbox_grey"),8,8,8,8));
// Note: Ideally this would be done in renderBackground() without the use of actors. Unfortunately,
// something about the stage being larger than the physical screen seems to mess with the camera
TextureRegion background = localAtlas.findRegion("spr_background");
for (int x=-12;x<Gdx.graphics.getWidth();x+=background.getRegionWidth()) {
for (int y=-12;y<Gdx.graphics.getHeight();y+=background.getRegionHeight()) {
Image img = new Image(background);
img.setPosition(x,y);
stage.addActor(img);
}
}
for(int i=0;i<symbols.length;i++) {
symbols[i]=new TextureRegionDrawable(new TextureRegion(localAtlas.findRegion("sym_"+variation.symbolNames[i])));
smallSymbols[i]=new TextureRegionDrawable(new TextureRegion(localAtlas.findRegion("sym_small_"+variation.symbolNames[i])));
}
Image frontPanel = new Image(new TextureRegionDrawable(localAtlas.findRegion("spr_frontpanel")));
frontPanel.setPosition(19,61);
deviceGroup.addActor(frontPanel);
knob = new Image(localAtlas.findRegion("spr_knob"));
knob.setPosition(235,219);
KnobHandler handler = new KnobHandler(this,240,69);
knob.addListener(handler);
handler.restKnob(knob);
deviceGroup.addActor(knob);
ClippingGroup reelGroup = new ClippingGroup(new Rectangle(0,56,Gdx.graphics.getWidth(),87));
int[] initialFaces = variation.getInitialFaces();
int pos = 0;
for (int i=0;i<reelSymbols.length;i++) {
reelSymbols[i] = new Symbol(variation,symbols,initialFaces[i],i/3,this);
reelGroup.addActor(reelSymbols[i]);
if (i>0 && i%3==0) {
pos+=75;
}
reelSymbols[i].setPosition(pos, (i%3)*reelSymbols[0].getHeight());
}
reelGroup.setPosition(53,284);
deviceGroup.addActor(reelGroup);
Group coinGroup = new Group();
for (int i=0;i<bet.length;i++) {
up = new TextureRegionDrawable(localAtlas.findRegion("btn_bet_up"));
down = new TextureRegionDrawable(localAtlas.findRegion("btn_bet_down"));
checked = new TextureRegionDrawable(localAtlas.findRegion("btn_bet_checked"));
bet[i] = new ImageButton(up,down,checked);
bet[i].setPosition(0,3*bet[i].getHeight()-i*bet[i].getHeight());
bet[i].addListener(this);
coinGroup.addActor(bet[i]);
}
bet[0].setChecked(true);
coinGroup.setPosition(10,40);
deviceGroup.addActor(coinGroup);
Table statusBar = new Table(((SlotMachine)game).skin);
statusBar.setBackground(backgroundImage);
statusBar.setBounds(10,10,300,48);
if (player!=null) {
Image knobCount = new Image(new TextureRegionDrawable(localAtlas.findRegion("spr_turns")));
statusBar.add(knobCount).left();
turns = new Label("x 0",((SlotMachine)game).skin);
statusBar.add(turns).width(30).right().padLeft(5).padRight(20);
Image coinCount = new Image(new TextureRegionDrawable(localAtlas.findRegion("spr_cash")));
statusBar.add(coinCount);
credits = new Label("x "+player.credit,((SlotMachine)game).skin);
statusBar.add(credits).width(30).right().padLeft(5).padRight(20);
up = new TextureRegionDrawable(localAtlas.findRegion("btn_view_up"));
down = new TextureRegionDrawable(localAtlas.findRegion("btn_view_down"));
checked = new TextureRegionDrawable(localAtlas.findRegion("btn_view_checked"));
viewSwitch = new ImageButton(up,down,checked);
viewSwitch.addListener(this);
statusBar.add(viewSwitch).right();
}
else {
// Setup screen
up = new TextureRegionDrawable(localAtlas.findRegion("btn_left_up"));
down = new TextureRegionDrawable(localAtlas.findRegion("btn_left_down"));
previousVariation = new ImageButton(up,down);
previousVariation.addListener(this);
up = new TextureRegionDrawable(localAtlas.findRegion("btn_right_up"));
down = new TextureRegionDrawable(localAtlas.findRegion("btn_right_down"));
nextVariation = new ImageButton(up,down);
nextVariation.addListener(this);
statusBar.add(previousVariation).padRight(30);
statusBar.add("Select game").padRight(30);
statusBar.add(nextVariation);
}
stage.addActor(statusBar);
feedbackMessage = new Label("",((SlotMachine)game).skin);
feedbackSymbol = new Image(new TextureRegionDrawable(localAtlas.findRegion("spr_feedbackcoins")));
feedbackGroup = new Group();
feedbackGroup.addActor(feedbackMessage);
feedbackGroup.addActor(feedbackSymbol);
feedbackMessage.setPosition(10+feedbackSymbol.getWidth(),feedbackSymbol.getHeight()/2-feedbackMessage.getHeight()/2);
feedbackGroup.getColor().a=0;
feedbackBonus = new Image(new TextureRegionDrawable(localAtlas.findRegion("spr_bonus")));
//feedbackBonus.setPosition(reelGroup.getX()+reelGroup.getWidth()/2-feedbackBonus.getMinWidth()/2, reelGroup.getY()+reelGroup.getHeight()/2-feedbackBonus.getMinHeight()/2);
//feedbackBonus.setPosition(reelGroup.getX(), reelGroup.getY());
//feedbackBonus.getColor().a=0;
deviceGroup.addActor(feedbackBonus);
deviceGroup.addActor(feedbackGroup);
deviceName = new Table(((SlotMachine)game).skin);
deviceName.setBackground(backgroundImage);
deviceName.add(variation.machineName);
deviceName.setBounds(10,422,300,48);
infoGroup.addActor(deviceName);
Table buttons = new Table();
buttons.setBounds(262,150,48,200);
buttons.setBackground(backgroundImage);
up = new TextureRegionDrawable(localAtlas.findRegion("btn_musicmuted_up"));
down = new TextureRegionDrawable(localAtlas.findRegion("btn_musicmuted_down"));
checked = new TextureRegionDrawable(localAtlas.findRegion("btn_musicmuted_checked"));
musicStatus = new ImageButton(up,down,checked);
musicStatus.setChecked(game.muteManager.isMusicMuted());
musicStatus.addListener(this);
up = new TextureRegionDrawable(localAtlas.findRegion("btn_soundmuted_up"));
down = new TextureRegionDrawable(localAtlas.findRegion("btn_soundmuted_down"));
checked = new TextureRegionDrawable(localAtlas.findRegion("btn_soundmuted_checked"));
soundStatus = new ImageButton(up,down,checked);
soundStatus.setChecked(game.muteManager.isSoundMuted());
soundStatus.addListener(this);
up = new TextureRegionDrawable(globalAtlas.findRegion("btn_close_up"));
down = new TextureRegionDrawable(globalAtlas.findRegion("btn_close_down"));
exit = new ImageButton(up,down);
exit.addListener(this);
buttons.add(musicStatus).padBottom(20).row();
buttons.add(soundStatus).padBottom(20).row();
buttons.add(exit);
infoGroup.addActor(buttons);
scrollTable = new ScrollPane(null,((SlotMachine)game).skin);
setVariant(variation);
scrollTable.setOverscroll(true,true);
scrollTable.setBounds(10,68,320-30-48,334);
if (player==null) {
// Use as setup screen
infoGroup.setPosition(0,0);
deviceGroup.setPosition(stage.getWidth(),0);
}
else {
infoGroup.setPosition(stage.getWidth(),0);
}
infoGroup.addActor(scrollTable);
stage.addActor(deviceGroup);
stage.addActor(infoGroup);
}
/**
* setup the info on the inforscreen
* @param variant the game to visualize
* @return a pay table.
*/
private void setVariant(Variation variant) {
Table paytable = new Table(((SlotMachine)game).skin);
TextureAtlas localAtlas= game.assetManager.get("textures/gamblescreen.atlas",TextureAtlas.class);
TextureRegion coin = new TextureRegion(localAtlas.findRegion("spr_small_cash"));
for (int x=0;x<variant.paytable.length;x++) {
for (int y=0;y<variant.paytable[x].length-1;y++) {
if (variant.paytable[x][y]==-1) {
// Wild symbol == empty space
paytable.add().pad(2,2,2,2);
}
else {
paytable.add(new Image(smallSymbols[variant.paytable[x][y]])).pad(2,2,8,2);
}
}
paytable.add("=").padLeft(15).padRight(10);
paytable.add(""+variant.paytable[x][3]).right();
paytable.add(new Image(coin)).padLeft(4);
paytable.row();
}
// Only show extra information on the setup screen, as the paytable visual is not desgined
// to keep in sync with dynamically updated information
if (player==null) {
Player tmp = game.loader.getPlayer(variant);
tmp.reVisit();
paytable.add("- - - - - - - - - - - - - - - - -").colspan(5).center();
paytable.row();
if (variant.luckyCoinBonus>0) {
paytable.add("Lucky Bonus").colspan(4).left();
paytable.add(""+variant.luckyCoinBonus).right();
paytable.add(new Image(coin)).padLeft(4);
paytable.row();
}
paytable.add("Seed capital").colspan(4).left();
paytable.add(""+variant.seedCapital).right();
paytable.add(new Image(coin)).padLeft(4);
paytable.row();
paytable.add("On hand").colspan(4).left();
paytable.add(""+tmp.credit).right();
paytable.add(new Image(coin)).padLeft(4);
paytable.row();
paytable.add("Highscore").colspan(4).left();
paytable.add(""+tmp.highscore).right();
paytable.add(new Image(coin)).padLeft(4);
}
paytable.pack();
this.variation= variant;
scrollTable.setWidget(paytable);
deviceName.clear();
deviceName.add(variant.machineName);
}
@Override
public void hide() {
}
@Override
public void dispose() {
super.dispose();
if (triggerSound!=null) triggerSound.dispose();
if (ejectCoinSound!=null) ejectCoinSound.dispose();
if (winSound!=null) winSound.dispose();
if (reelStopSound!=null) reelStopSound.dispose();
}
@Override
public void renderBackground(float delta) {
Gdx.gl.glClearColor(0.72f, 0.74f, 0.71f, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
}
/**
* Sum up the coins that are currently visible and checked.
* @return amount of coins bet.
*/
public int getBet() {
int ret=0;
for (int i=0;i<bet.length;i++) {
if (bet[i].isChecked() && bet[i].isVisible()) ret++;
}
return ret;
}
@Override
public boolean handle(Event event) {
if (! (event instanceof InputEvent)) return false;
InputEvent input = (InputEvent) event;
Actor actor = input.getListenerActor();
Vector2 coords = Vector2.tmp.set(input.getStageX(), input.getStageY());
event.getListenerActor().stageToLocalCoordinates(coords);
boolean isOver = actor.hit(coords.x,coords.y,true)!=null;
if (isOver && actor==viewSwitch && input.getType().equals(InputEvent.Type.touchUp)) {
if (viewSwitch.isChecked()) {
// Note: moveBy takes time in which the user may trigger another moveBy, potentially
// scrolling the viewport into empty space. As a workaround, make sure, that groups
// are where they are suppose to be before doing a relative movement.
deviceGroup.setX(0);
infoGroup.setX(stage.getWidth());
deviceGroup.addAction(Actions.moveBy(-stage.getWidth(),0,0.1f));
infoGroup.addAction(Actions.moveBy(-stage.getWidth(),0,0.1f));
}
else {
deviceGroup.setX(-stage.getWidth());
infoGroup.setX(0);
deviceGroup.addAction(Actions.moveBy(stage.getWidth(),0,0.1f));
infoGroup.addAction(Actions.moveBy(stage.getWidth(),0,0.1f));
}
}
if (isOver && actor==exit && input.getType().equals(InputEvent.Type.touchUp)) {
SlotMachine.fadeOverScreen.fadeTo(new MenuScreen<SlotMachine>(game),0.5f);
game.setScreen(SlotMachine.fadeOverScreen);
}
if (isOver && actor==musicStatus && input.getType().equals(InputEvent.Type.touchUp)) {
game.muteManager.setMusicMuted(musicStatus.isChecked());
}
if (isOver && actor==soundStatus && input.getType().equals(InputEvent.Type.touchUp)) {
game.muteManager.setSoundMuted(soundStatus.isChecked());
}
if (isOver && actor==nextVariation && input.getType().equals(InputEvent.Type.touchUp)) {
setVariant(game.loader.next());
}
if (isOver && actor==previousVariation && input.getType().equals(InputEvent.Type.touchUp)) {
setVariant(game.loader.previous());
}
return true;
}
/**
* Mark a <code>Symbol</code> as either in motion or at rest.
* @param symbol the <code>Symbol</code> in question.
* @param moving true if spinning, false at rest
*/
protected synchronized void inMotion(Symbol symbol, boolean moving) {
if (moving) spinning++;
else spinning--;
// Provide audible feedback for a stopping reel
if (symbol.isOnPayline() && !moving) {
player.payline[symbol.getReel()]=symbol.getFace();
playSoundEffect(REELSTOPSOUND);
}
// All reels stopped -> evaluate
if (spinning==0) {
int win=variation.getPayout(player.bet,player.payline);
if (win>=0) { // Player hit a winning combination...
if (win==0) { // but did not bet -> nothing won, but it still counts statistically.
feedbackMessage.setText("+ 0");
player.win(0);
}
else { // Player won for sure, but is s/he eligable for a bonus?
int bonusPay = 0;
if(bet[player.luckyCoin].isChecked() && (bonusPay=variation.getBonus(getBet()))>0) { // Bonus win!
feedbackMessage.setText("+ "+win+"\n+ "+bonusPay);
float ow= feedbackBonus.getWidth();
float oh= feedbackBonus.getHeight();
feedbackBonus.addAction(sequence(fadeIn(0.1f),delay(1),fadeOut(0.1f),sizeTo(ow,oh)));
player.win(win+bonusPay);
}
else { // Plain win, no bonus
feedbackMessage.setText("+ "+win);
player.win(win);
}
}
float centerPos = stage.getWidth()/2-(feedbackMessage.getWidth()+10+feedbackSymbol.getWidth())/2;
feedbackGroup.addAction(sequence(moveTo(centerPos,85),fadeIn(0.4f),moveBy(0,-50,1f),fadeOut(0.4f)));
playSoundEffect(WINSOUND);
}
else { // Player lost this round
player.loose();
}
// Make sure, the player can not bet more coins than s/he has. NOTE: There is no explicit
// Game Over check. The player just runs into a dead end eventually from which on s/he can
// only play wagerless rounds. This is intended! The game design calls for free games as a
// means of skipping over (suspected) loosing rounds (strategy element).
// Since this allows the player to play forever by never betting, a Game Over screen would
// be meaningless.
for (int i=0;i<bet.length;i++) {
bet[i].setVisible(player.credit>i);
}
credits.setText("x "+player.credit);
turns.setText("x "+player.round);
}
}
/**
* Query if the reels are still in motion
* @return true if the reels are spinning
*/
public synchronized boolean isSpinning() {
return spinning!=0;
}
/**
* Start a new round.
* @param velocity how fast to spin the reels.
*/
public synchronized void newRound(int velocity) {
if (isSpinning()) return;
player.gamble(getBet());
credits.setText("x "+player.credit);
for (int i=0;i<reelSymbols.length;i++) {
reelSymbols[i].spin((1+i/3 )* 3 +velocity,velocity);
}
}
/**
* Trigger a sound effect
* @param which which sound effect to play
*/
protected void playSoundEffect(int which) {
if (game.muteManager.isSoundMuted()) return;
// FIXME: We should probably throttle the music volume a bit to make sfx more noticeable.
// Problem with that: Sound.play() only triggers the sound effect and returns immediately.
switch(which) {
case TRIGGERSOUND: {
triggerSound.play();
break;
}
case WINSOUND: {
winSound.play();
break;
}
case EJECTCOINSOUND: {
ejectCoinSound.play();
break;
}
case REELSTOPSOUND: {
reelStopSound.play();
break;
}
}
}
/**
* Try stopping the wheels. This may or may not succeed (skill element).
* Wheels are stopped from left to right.
*/
public synchronized void brakeWheels() {
switch (spinning) {
case 1:
case 2:
case 3: {
reelSymbols[6].handbrake();
reelSymbols[7].handbrake();
reelSymbols[8].handbrake();
break;
}
case 4:
case 5:
case 6: {
reelSymbols[3].handbrake();
reelSymbols[4].handbrake();
reelSymbols[5].handbrake();
break;
}
case 7:
case 8:
case 9: {
reelSymbols[0].handbrake();
reelSymbols[1].handbrake();
reelSymbols[2].handbrake();
break;
}
default: {
return;
}
}
}
}