diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/MyActions.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/MyActions.java index 51c34b2..7202721 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/MyActions.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/MyActions.java @@ -3,7 +3,7 @@ package eu.jonahbauer.wizard.client.libgdx.actions; import com.badlogic.gdx.scenes.scene2d.Action; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Group; -import com.badlogic.gdx.scenes.scene2d.actions.Actions; +import com.badlogic.gdx.scenes.scene2d.actions.*; public class MyActions extends Actions { public static ChangeParentAction changeParent(Group parent) { @@ -12,19 +12,30 @@ public class MyActions extends Actions { return action; } - public static T target(T action, Actor target) { - action.setTarget(target); - return action; - } - - static public SilentlyRemoveActorAction removeActorSilently () { + public static SilentlyRemoveActorAction removeActorSilently () { return action(SilentlyRemoveActorAction.class); } - static public SilentlyRemoveActorAction removeActorSilently(Actor removeActor) { - var action = action(SilentlyRemoveActorAction.class); - action.setTarget(removeActor); + public static WaitAction delay(Actor actor) { + var action = action(WaitAction.class); + action.setTarget(actor); return action; } + public static void finish(Action action) { + if (action instanceof ParallelAction parallel) { + var subactions = parallel.getActions(); + for (int i = 0; i < subactions.size; i++) { + finish(subactions.get(i)); + } + } else if (action instanceof DelayAction delay) { + delay.finish(); + finish(delay.getAction()); + } else if (action instanceof DelegateAction delegate) { + var subaction = delegate.getAction(); + finish(subaction); + } else if (action instanceof TemporalAction temporal) { + temporal.finish(); + } + } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/WaitAction.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/WaitAction.java new file mode 100644 index 0000000..700bf62 --- /dev/null +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/WaitAction.java @@ -0,0 +1,10 @@ +package eu.jonahbauer.wizard.client.libgdx.actions; + +import com.badlogic.gdx.scenes.scene2d.Action; + +public class WaitAction extends Action { + @Override + public boolean act(float delta) { + return getTarget().getActions().size == 0; + } +} diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardActor.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardActor.java index 18bcfa5..85ecb38 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardActor.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardActor.java @@ -12,6 +12,7 @@ import lombok.Setter; import java.util.Collections; import java.util.EnumMap; import java.util.Map; +import java.util.NoSuchElementException; @Getter @Setter @@ -126,6 +127,7 @@ public class CardActor extends Actor { public void setCard(Card card) { this.card = card; this.background = atlas.findRegion(ATLAS_PATHS.get(card)); + if (this.background == null) throw new NoSuchElementException("Could not find texture for card " + card + "."); } public void setCard(Card.Suit suit) { @@ -137,6 +139,7 @@ public class CardActor extends Actor { case BLUE -> GameAtlas.CARDS_BLUE; case RED -> GameAtlas.CARDS_RED; }); + if (this.background == null) throw new NoSuchElementException("Could not find texture for suit " + suit + "."); } @Override diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardStack.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardStack.java index 18b3f42..3a16444 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardStack.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardStack.java @@ -126,7 +126,7 @@ public class CardStack extends Group { if (action != null) actor.removeAction(action); action = parallel( - moveTo(seat.getCardX() - actor.getWidth() / 2, seat.getCardY() - actor.getHeight() / 2, EXPAND_DURATION), + moveTo(seat.getFrontX() - actor.getWidth() / 2, seat.getFrontY() - actor.getHeight() / 2, EXPAND_DURATION), rotateTo(expandedRotation, EXPAND_DURATION) ); diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/PadOfTruth.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/PadOfTruth.java index 884c1a0..b984c11 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/PadOfTruth.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/PadOfTruth.java @@ -17,7 +17,7 @@ public class PadOfTruth extends Table { private static final float EXTENDED_WIDTH = 636; private static final float EXTENDED_HEIGHT = 824; private static final float COLLAPSE_DURATION = 0.25f; - private static final float COLLAPSED_SCALE = 0.2f; + private static final float COLLAPSED_SCALE = CardActor.PREF_HEIGHT / EXTENDED_HEIGHT; private final Label[] names = new Label[6]; private final Label[][] predictions = new Label[20][]; diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/ChangePredictionOverlay.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/ChangePredictionOverlay.java index 774c94d..4fb08ec 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/ChangePredictionOverlay.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/ChangePredictionOverlay.java @@ -7,9 +7,10 @@ import eu.jonahbauer.wizard.common.model.Card; public class ChangePredictionOverlay extends MakePredictionOverlay { - public ChangePredictionOverlay(GameScreen gameScreen, int round, int oldPrediction) { + public ChangePredictionOverlay(GameScreen gameScreen, long timeout, int round, int oldPrediction) { super( gameScreen, + timeout, oldPrediction == 0 ? new int[] {oldPrediction + 1} : oldPrediction == round + 1 ? new int[] {oldPrediction - 1} : new int[] {oldPrediction - 1, oldPrediction + 1} diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/InteractionOverlay.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/InteractionOverlay.java new file mode 100644 index 0000000..5788a9b --- /dev/null +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/InteractionOverlay.java @@ -0,0 +1,4 @@ +package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay; + +public interface InteractionOverlay { +} diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/MakePredictionOverlay.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/MakePredictionOverlay.java index 5387220..89cc725 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/MakePredictionOverlay.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/MakePredictionOverlay.java @@ -1,26 +1,27 @@ package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay; -import com.badlogic.gdx.scenes.scene2d.InputEvent; +import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; -import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; +import eu.jonahbauer.wizard.common.messages.player.PredictMessage; import java.util.stream.IntStream; -public class MakePredictionOverlay extends Overlay { +public class MakePredictionOverlay extends Overlay implements InteractionOverlay { private final int[] values; private final TextButton[] buttons; - public MakePredictionOverlay(GameScreen gameScreen, int round) { - this(gameScreen, IntStream.range(0, round + 2).toArray()); + public MakePredictionOverlay(GameScreen gameScreen, long timeout, int round) { + this(gameScreen, timeout, IntStream.range(0, round + 2).toArray()); } - protected MakePredictionOverlay(GameScreen gameScreen, int[] values) { - super(gameScreen); + protected MakePredictionOverlay(GameScreen gameScreen, long timeout, int[] values) { + super(gameScreen, timeout); this.values = values; this.buttons = new TextButton[values.length]; } @@ -32,30 +33,23 @@ public class MakePredictionOverlay extends Overlay { var prompt = new Label("Please make your prediction:", data.skin); var buttonGroup = new HorizontalGroup().space(20); - for (int i = 0; i < values.length; i++) { - buttons[i] = new TextButton(String.valueOf(i), data.skin); - buttonGroup.addActor(buttons[i]); - } - - buttonGroup.addListener(new ClickListener() { - private boolean finished = false; - + var listener = new ChangeListener() { @Override - public void clicked(InputEvent event, float x, float y) { - if (finished) return; - - finished = true; - var target = event.getTarget(); + public void changed(ChangeEvent event, Actor actor) { for (int i = 0; i < values.length; i++) { - if (buttons[i] == target) { - System.out.println(values[i]); + if (actor == buttons[i]) { + screen.send(new PredictMessage(values[i])); break; } } - - getRoot().remove(); } - }); + }; + + for (int i = 0; i < values.length; i++) { + buttons[i] = new TextButton(String.valueOf(i), data.skin); + buttons[i].addListener(listener); + buttonGroup.addActor(buttons[i]); + } root.addActor(prompt); root.addActor(buttonGroup); diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/Overlay.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/Overlay.java index cb9a8c2..c9c4d78 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/Overlay.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/Overlay.java @@ -1,26 +1,52 @@ package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay; +import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.TextureAtlas; -import com.badlogic.gdx.scenes.scene2d.Actor; -import com.badlogic.gdx.scenes.scene2d.Stage; -import com.badlogic.gdx.scenes.scene2d.Touchable; +import com.badlogic.gdx.math.Interpolation; +import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.ui.Container; -import com.badlogic.gdx.scenes.scene2d.utils.Drawable; +import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; +import eu.jonahbauer.wizard.client.libgdx.UiskinAtlas; import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; -public abstract class Overlay { +import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*; +public abstract class Overlay extends Action { + protected static final float OVERLAY_TIME = 5.0f; + + protected final GameScreen screen; protected final WizardGame.Data data; protected final TextureAtlas atlas; + private long timeout; private Container root; - private final Drawable background; + private boolean started; + private boolean finished; - public Overlay(GameScreen gameScreen) { + public Overlay(GameScreen gameScreen, long timeout) { + this.screen = gameScreen; this.data = gameScreen.getData(); this.atlas = gameScreen.getAtlas(); - this.background = gameScreen.getOverlay(); + this.timeout = timeout; + } + + @Override + public boolean act(float delta) { + if (!started) { + if (System.currentTimeMillis() > timeout) { + return true; + } + + started = true; + show((Group) getActor()); + } + + if (System.currentTimeMillis() > timeout) { + finishInternal(); + } + + return finished; } protected abstract Actor createContent(); @@ -28,7 +54,7 @@ public abstract class Overlay { protected Container getRoot() { if (root == null) { root = new Container<>(createContent()); - root.setBackground(background); + root.setBackground(new TextureRegionDrawable(data.uiskinAtlas.findRegion(UiskinAtlas.WHITE)).tint(new Color(0, 0, 0, 0.5f))); root.setSize(WizardGame.WIDTH, WizardGame.HEIGHT); root.setTouchable(Touchable.enabled); } @@ -36,7 +62,25 @@ public abstract class Overlay { return root; } - public void show(Stage stage) { - stage.addActor(getRoot()); + protected void show(Group parent) { + parent.addActor(getRoot()); + } + + protected void finishInternal() { + if (!finished) { + finished = true; + getRoot().remove(); + } + } + + public void finish() { + getRoot().addAction(sequence( + targeting(root, alpha(0.0f, .1f, Interpolation.pow2Out)), + run(this::finishInternal) + )); + } + + public void timeout() { + timeout = 0; } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PickTrumpOverlay.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PickTrumpOverlay.java index ccc3fcd..c7c48a3 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PickTrumpOverlay.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PickTrumpOverlay.java @@ -8,18 +8,19 @@ import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; +import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage; import eu.jonahbauer.wizard.common.model.Card; import java.util.EnumMap; -public class PickTrumpOverlay extends Overlay { +public class PickTrumpOverlay extends Overlay implements InteractionOverlay { private final boolean allowNone; private final EnumMap cards = new EnumMap<>(Card.Suit.class); - public PickTrumpOverlay(GameScreen gameScreen, boolean allowNone) { - super(gameScreen); + public PickTrumpOverlay(GameScreen gameScreen, long timeout, boolean allowNone) { + super(gameScreen, timeout); this.allowNone = allowNone; } @@ -41,23 +42,15 @@ public class PickTrumpOverlay extends Overlay { cards.values().forEach(cardGroup::addActor); cardGroup.addListener(new ClickListener() { - private boolean finished = false; - @Override public void clicked(InputEvent event, float x, float y) { - if (finished) return; - - finished = true; var target = event.getTarget(); for (Card.Suit suit : Card.Suit.values()) { if (cards.get(suit) == target) { - // TODO - System.out.println(suit); + screen.send(new PickTrumpMessage(suit)); break; } } - - getRoot().remove(); } }); diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/StartRoundOverlay.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/StartRoundOverlay.java index 82db1dd..01de769 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/StartRoundOverlay.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/StartRoundOverlay.java @@ -2,20 +2,20 @@ package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.scenes.scene2d.Actor; -import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.Group; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; +import eu.jonahbauer.wizard.client.libgdx.actions.MyActions; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*; -import static eu.jonahbauer.wizard.client.libgdx.actions.MyActions.*; public class StartRoundOverlay extends Overlay { private final int round; public StartRoundOverlay(GameScreen gameScreen, int round) { - super(gameScreen); + super(gameScreen, Long.MAX_VALUE); this.round = round; } @@ -31,13 +31,19 @@ public class StartRoundOverlay extends Overlay { } @Override - public void show(Stage stage) { + public void show(Group parent) { + super.show(parent); + var root = getRoot(); - stage.addActor(root); - stage.addAction(sequence( - delay(2.0f), - target(alpha(0.0f, .1f, Interpolation.pow2Out), root), - removeActor(root) + root.addAction(sequence( + delay(OVERLAY_TIME), + targeting(root, alpha(0.0f, .5f, Interpolation.pow2Out)), + run(this::finishInternal) )); } + + @Override + public void finish() { + MyActions.finish(getRoot().getActions().get(0)); + } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/TrumpOverlay.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/TrumpOverlay.java index a6aea10..506354f 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/TrumpOverlay.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/TrumpOverlay.java @@ -1,31 +1,96 @@ package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay; import com.badlogic.gdx.math.Interpolation; +import com.badlogic.gdx.scenes.scene2d.Action; import com.badlogic.gdx.scenes.scene2d.Actor; -import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.Group; +import com.badlogic.gdx.scenes.scene2d.actions.ParallelAction; +import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; +import eu.jonahbauer.wizard.client.libgdx.actions.MyActions; import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; import eu.jonahbauer.wizard.common.model.Card; +import java.util.Map; + import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*; import static eu.jonahbauer.wizard.client.libgdx.actions.MyActions.*; public class TrumpOverlay extends Overlay { + @SuppressWarnings("RedundantTypeArguments") + private static final Map DEFAULT_SUITES = Map.ofEntries( + Map.entry(Card.BLUE_1, Card.Suit.BLUE), + Map.entry(Card.BLUE_2, Card.Suit.BLUE), + Map.entry(Card.BLUE_3, Card.Suit.BLUE), + Map.entry(Card.BLUE_4, Card.Suit.BLUE), + Map.entry(Card.BLUE_5, Card.Suit.BLUE), + Map.entry(Card.BLUE_6, Card.Suit.BLUE), + Map.entry(Card.BLUE_7, Card.Suit.BLUE), + Map.entry(Card.BLUE_8, Card.Suit.BLUE), + Map.entry(Card.BLUE_9, Card.Suit.BLUE), + Map.entry(Card.BLUE_10, Card.Suit.BLUE), + Map.entry(Card.BLUE_11, Card.Suit.BLUE), + Map.entry(Card.BLUE_12, Card.Suit.BLUE), + Map.entry(Card.BLUE_13, Card.Suit.BLUE), + Map.entry(Card.RED_1, Card.Suit.RED), + Map.entry(Card.RED_2, Card.Suit.RED), + Map.entry(Card.RED_3, Card.Suit.RED), + Map.entry(Card.RED_4, Card.Suit.RED), + Map.entry(Card.RED_5, Card.Suit.RED), + Map.entry(Card.RED_6, Card.Suit.RED), + Map.entry(Card.RED_7, Card.Suit.RED), + Map.entry(Card.RED_8, Card.Suit.RED), + Map.entry(Card.RED_9, Card.Suit.RED), + Map.entry(Card.RED_10, Card.Suit.RED), + Map.entry(Card.RED_11, Card.Suit.RED), + Map.entry(Card.RED_12, Card.Suit.RED), + Map.entry(Card.RED_13, Card.Suit.RED), + Map.entry(Card.GREEN_1, Card.Suit.GREEN), + Map.entry(Card.GREEN_2, Card.Suit.GREEN), + Map.entry(Card.GREEN_3, Card.Suit.GREEN), + Map.entry(Card.GREEN_4, Card.Suit.GREEN), + Map.entry(Card.GREEN_5, Card.Suit.GREEN), + Map.entry(Card.GREEN_6, Card.Suit.GREEN), + Map.entry(Card.GREEN_7, Card.Suit.GREEN), + Map.entry(Card.GREEN_8, Card.Suit.GREEN), + Map.entry(Card.GREEN_9, Card.Suit.GREEN), + Map.entry(Card.GREEN_10, Card.Suit.GREEN), + Map.entry(Card.GREEN_11, Card.Suit.GREEN), + Map.entry(Card.GREEN_12, Card.Suit.GREEN), + Map.entry(Card.GREEN_13, Card.Suit.GREEN), + Map.entry(Card.YELLOW_1, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_2, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_3, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_4, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_5, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_6, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_7, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_8, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_9, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_10, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_11, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_12, Card.Suit.YELLOW), + Map.entry(Card.YELLOW_13, Card.Suit.YELLOW) + ); private final String player; private final Card card; private final Card.Suit suit; private final CardActor trumpCardActor; + private final CardActor trumpSuitActor; + + private boolean animateCard = true; public TrumpOverlay(GameScreen gameScreen, String player, Card card, Card.Suit suit) { - super(gameScreen); + super(gameScreen, Long.MAX_VALUE); this.player = player; this.card = card; this.suit = suit; this.trumpCardActor = gameScreen.getTrumpCardActor(); + this.trumpSuitActor = gameScreen.getTrumpSuitActor(); } @Override @@ -35,53 +100,91 @@ public class TrumpOverlay extends Overlay { String text; if (player == null) { - text = switch (suit) { + text = suit != null ? switch (suit) { case YELLOW -> "The trump suit is [#ffff00]yellow[#ffffff]."; case GREEN -> "The trump suit is [#00ff00]green[#ffffff]."; case BLUE -> "The trump suit is [#0000ff]blue[#ffffff]."; case RED -> "The trump suit is [#ff0000]red[#ffffff]."; default -> "There is no trump suit."; - }; + } : "To be determined."; } else { - text = player + switch (suit) { + text = player + (suit != null ? switch (suit) { case YELLOW -> " chose the trump suit [#ffff00]yellow[#ffffff]."; case GREEN -> " chose the trump suit [#00ff00]green[#ffffff]."; case BLUE -> " chose the trump suit [#0000ff]blue[#ffffff]."; case RED -> " chose the trump suit [#ff0000]red[#ffffff]."; default -> " has decided there will be no trump suit this round."; - }; + } : " to be determined."); } var label = new Label(text, data.skin); label.getStyle().font.getData().markupEnabled = true; label.setFontScale(1.5f); - root.addActor(label); - root.addActor(trumpCardActor); + + var cardGroup = new HorizontalGroup().space(20); + root.addActor(cardGroup); + + if (trumpCardActor.getParent() != null && trumpCardActor.getCard() == card && suit != null) { + // if card actor is already correct then dont change it + animateCard = false; + } else { + trumpCardActor.remove(); + cardGroup.addActor(trumpCardActor); + trumpCardActor.setCard(card); + } + + trumpSuitActor.remove(); + if (suit != DEFAULT_SUITES.get(card)) { + trumpSuitActor.setRotation(0); + trumpSuitActor.setOrigin(0, 0); + cardGroup.addActor(trumpSuitActor); + trumpSuitActor.setCard(suit); + } return root; } @Override - public void show(Stage stage) { - var root = getRoot(); + public void show(Group parent) { + super.show(parent); + + ParallelAction cardAnimation = parallel(); + if (animateCard) { + cardAnimation.addAction(sequence( + targeting(trumpCardActor, removeActorSilently()), + targeting(trumpCardActor, changeParent(parent)), + targeting(trumpCardActor, moveTo(10, 10, .3f)) + )); + } - trumpCardActor.setCard(card); + if (suit != null) { + cardAnimation.addAction(sequence( + targeting(trumpSuitActor, removeActorSilently()), + targeting(trumpSuitActor, changeParent(parent)), + run(trumpCardActor::toFront), + parallel( + targeting(trumpSuitActor, rotateTo(-90, .3f)), + targeting(trumpSuitActor, + moveTo(10, 10 + (trumpSuitActor.getHeight() + trumpSuitActor.getWidth()) / 2, .3f) + ) + ) + )); + } - stage.addActor(root); - stage.addAction(sequence( - delay(2.0f), + var root = getRoot(); + root.addAction(sequence( + delay(OVERLAY_TIME), parallel( - sequence( - target(alpha(0.0f, .1f, Interpolation.pow2Out), root), - removeActor(root) - ), - sequence( - removeActorSilently(trumpCardActor), - target(changeParent(stage.getRoot()), trumpCardActor), - target(moveTo(10, 10, .25f), trumpCardActor) - ) - ) + targeting(root, alpha(0.0f, .3f, Interpolation.pow2Out)), + cardAnimation + ), + run(this::finishInternal) )); } + + @Override + public void finish() { + MyActions.finish(getRoot().getActions().get(0)); + } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/GameScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/GameScreen.java index 15baa3d..e89db6c 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/GameScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/GameScreen.java @@ -3,105 +3,90 @@ package eu.jonahbauer.wizard.client.libgdx.screens; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.TextureAtlas; -import com.badlogic.gdx.scenes.scene2d.Actor; -import com.badlogic.gdx.scenes.scene2d.Group; +import com.badlogic.gdx.scenes.scene2d.Action; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.Container; import com.badlogic.gdx.scenes.scene2d.ui.Label; -import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; -import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; -import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; import com.badlogic.gdx.utils.Align; import eu.jonahbauer.wizard.client.libgdx.GameAtlas; -import eu.jonahbauer.wizard.client.libgdx.UiskinAtlas; import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor; import eu.jonahbauer.wizard.client.libgdx.actors.game.CardStack; import eu.jonahbauer.wizard.client.libgdx.actors.game.CardsGroup; import eu.jonahbauer.wizard.client.libgdx.actors.game.PadOfTruth; import eu.jonahbauer.wizard.client.libgdx.actors.game.overlay.*; +import eu.jonahbauer.wizard.client.libgdx.state.Game; import eu.jonahbauer.wizard.client.libgdx.util.Pair; -import eu.jonahbauer.wizard.common.messages.observer.*; -import eu.jonahbauer.wizard.common.messages.server.GameMessage; -import eu.jonahbauer.wizard.common.messages.server.ServerMessage; +import eu.jonahbauer.wizard.common.messages.client.InteractionMessage; +import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; +import eu.jonahbauer.wizard.common.messages.player.ContinueMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; import eu.jonahbauer.wizard.common.model.Card; import lombok.Getter; import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.concurrent.atomic.AtomicBoolean; import static eu.jonahbauer.wizard.client.libgdx.actions.MyActions.*; +import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*; public class GameScreen extends MenuScreen { - private final WizardGame game; - @Getter private TextureAtlas atlas; - @Getter - private Drawable overlay; - private final List players; + private Label.LabelStyle labelStyleDefault; + private Label.LabelStyle labelStyleActive; - private final UUID self; - private final UUID session; - private final Map names; - private final Map> scores = new HashMap<>(); + private final Game state; - private int round = -1; - private final Map> hands = new HashMap<>(); - private final Map predictions = new HashMap<>(); - private final Map>> tricks = new HashMap<>(); - private Card.Suit trumpSuit; - private Card trumpCard; + private final List players; - private int trick = -1; - private final List> stack = new ArrayList<>(); private Pair activePlayer; private CardsGroup handCards; private CardStack cardStack; private PadOfTruth padOfTruth; + @Getter private CardActor trumpCardActor; + @Getter + private CardActor trumpSuitActor; + private VerticalGroup messages; + private Label persistentMessage; + + private Action currentAction; + private final Queue pendingActions = new LinkedList<>(); private final Map seats = new HashMap<>(); private final Map nameLabels = new HashMap<>(); + private final AtomicBoolean sending = new AtomicBoolean(); + private final AtomicBoolean pendingSync = new AtomicBoolean(); + public GameScreen(WizardGame game) { super(game); - this.game = game; - - // - this.session = UUID.randomUUID(); - this.names = new HashMap<>(); - - int playerCount = 6; - var players = new UUID[playerCount]; - for (int i = 0; i < playerCount; i++) { - players[i] = UUID.randomUUID(); - names.put(players[i], "Player " + i); - } - - this.self = players[0]; - this.players = Stream.of(players).sorted().toList(); - // + this.state = (Game) game.getClient().getState(); + this.players = new ArrayList<>(state.getPlayers().keySet()); } @Override public void show() { super.show(); atlas = new TextureAtlas(Gdx.files.internal(GameAtlas.$PATH)); - overlay = new TextureRegionDrawable(game.data.uiskinAtlas.findRegion(UiskinAtlas.WHITE)).tint(new Color(0,0,0,0.5f)); +// overlay = new TextureRegionDrawable(game.data.uiskinAtlas.findRegion(UiskinAtlas.WHITE)).tint(new Color(0,0,0,0.5f)); + labelStyleDefault = game.data.skin.get(Label.LabelStyle.class); + labelStyleActive = new Label.LabelStyle(labelStyleDefault); + labelStyleActive.fontColor = Color.RED; seat(); prepareLabels(); handCards = new CardsGroup(Collections.emptyList(), atlas); - handCards.setOnClickListener(card -> playCard(self, card.getCard())); + handCards.setOnClickListener(card -> send(new PlayCardMessage(card.getCard()))); var container = new Container<>(handCards); container.setPosition(360, 75); container.setSize(1200, CardActor.PREF_HEIGHT); @@ -126,121 +111,6 @@ public class GameScreen extends MenuScreen { game.data.stage.addActor(cardStack); game.data.stage.addActor(padOfTruth); game.data.stage.addActor(messages); - game.data.stage.addActor(debugButtons()); - } - - private Group debugButtons() { - var trump = new TextButton("Trump", game.data.skin, "simple"); - trump.align(Align.left); - trump.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - onMessage(new GameMessage(new TrumpMessage( - Card.values()[(int)(Math.random() * Card.values().length)], - Card.Suit.values()[(int)(Math.random() * Card.Suit.values().length)] - ))); - } - }); - - var playCard = new TextButton("Play Card", game.data.skin, "simple"); - playCard.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - var hand = hands.get(self); - if (hand != null && hand.size() > 0) { - onMessage(new GameMessage(new CardMessage( - self, - hand.get((int) (Math.random() * hand.size())) - ))); - } - } - }); - - var deal = new TextButton("Deal", game.data.skin, "simple"); - deal.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - var hand = Stream.of(Card.values()).limit(20).toList(); - onMessage(new GameMessage(new HandMessage(self, hand))); - } - }); - - var round = new TextButton("Round", game.data.skin, "simple"); - round.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - onMessage(new GameMessage(new StateMessage("starting_round"))); - round.setText("Round " + GameScreen.this.round); - } - }); - - var scores = new TextButton("Scores", game.data.skin, "simple"); - scores.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - var scores = players.stream().collect(Collectors.toMap(p -> p, p -> (int) (Math.random() * 5) * 10)); - onMessage(new GameMessage(new ScoreMessage(scores))); - } - }); - - var predictions = new TextButton("Predictions", game.data.skin, "simple"); - predictions.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - players.forEach(player -> onMessage(new GameMessage(new PredictionMessage(player, (int) (Math.random() * 5))))); - } - }); - - var playOtherCard = new TextButton("Play Other Card", game.data.skin, "simple"); - playOtherCard.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - var card = Card.values()[(int) (Math.random() * Card.values().length)]; - UUID player; - do { - player = players.get((int) (Math.random() * players.size())); - } while (self.equals(player)); - onMessage(new GameMessage(new CardMessage(player, card))); - } - }); - - var makePrediction = new TextButton("Make Prediction", game.data.skin, "simple"); - makePrediction.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - onMessage(new GameMessage(new UserInputMessage(self, UserInputMessage.Action.MAKE_PREDICTION, Long.MAX_VALUE))); - } - }); - - var changePrediction = new TextButton("Change Prediction", game.data.skin, "simple"); - changePrediction.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - onMessage(new GameMessage(new UserInputMessage(self, UserInputMessage.Action.CHANGE_PREDICTION, Long.MAX_VALUE))); - } - }); - - var chooseTrump = new TextButton("Choose Trump", game.data.skin, "simple"); - chooseTrump.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - onMessage(new GameMessage(new UserInputMessage(self, UserInputMessage.Action.PICK_TRUMP, Long.MAX_VALUE))); - } - }); - - var buttons = new VerticalGroup(); - buttons.top().left().columnLeft().setPosition(0, WizardGame.HEIGHT - buttons.getHeight()); - buttons.addActor(round); - buttons.addActor(deal); - buttons.addActor(trump); - buttons.addActor(predictions); - buttons.addActor(playCard); - buttons.addActor(playOtherCard); - buttons.addActor(scores); - buttons.addActor(chooseTrump); - buttons.addActor(makePrediction); - buttons.addActor(changePrediction); - return buttons; } @Override @@ -261,7 +131,35 @@ public class GameScreen extends MenuScreen { } @Override - protected void renderForeground(float delta) {} + protected void renderForeground(float delta) { + if (pendingActions.size() > 0) { + var actions = game.data.stage.getRoot().getActions(); + boolean running = false; + if (currentAction != null) { + for (int i = 0; i < actions.size; i++) { + if (actions.get(i) == currentAction) { + running = true; + break; + } + } + } + + if (!running) { + currentAction = pendingActions.poll(); + game.data.stage.addAction(currentAction); + } + } else if (pendingSync.getAndSet(false)) { + send(new ContinueMessage()); + } + } + + private void execute(Action action) { + pendingActions.add(action); + } + + private void execute(Runnable runnable) { + pendingActions.add(run(runnable)); + } private void seat() { var count = players.size(); @@ -272,17 +170,18 @@ public class GameScreen extends MenuScreen { case 6 -> new Seat[] {Seat.LEFT, Seat.TOP_LEFT, Seat.TOP, Seat.TOP_RIGHT, Seat.RIGHT}; default -> throw new AssertionError(); }; - int index = players.indexOf(self); + int index = players.indexOf(state.getSelf()); for (int i = 1; i < count; i++) { var player = players.get((index + i) % count); var seat = seats[i - 1]; this.seats.put(player, seat); } + this.seats.put(state.getSelf(), Seat.BOTTOM); } private void prepareLabels() { for (UUID player : players) { - if (self.equals(player)) continue; + if (state.getSelf().equals(player)) continue; var label = new Label("", game.data.skin); var seat = seats.get(player); label.setX(seat.getLabelX()); @@ -296,185 +195,243 @@ public class GameScreen extends MenuScreen { private void setNames() { for (int i = 0; i < players.size(); i++) { var player = players.get(i); - var name = names.get(player); + var name = state.getPlayers().get(player); padOfTruth.setName(i, name); - if (!self.equals(player)) { + if (!state.getSelf().equals(player)) { nameLabels.get(player).setText(name); } } } - private void startRound() { - round++; - tricks.clear(); - predictions.clear(); - - trumpSuit = null; - trumpCard = null; - if (trumpCardActor != null) { - trumpCardActor.remove(); - } - - trick = -1; - - handCards.clearChildren(); - hands.clear(); + public void startRound(int round) { + execute(parallel( + run(() -> { + if (trumpCardActor != null) { + trumpCardActor.remove(); + } - cardStack.clearChildren(); - stack.clear(); + if (trumpSuitActor != null) { + trumpSuitActor.remove(); + } - new StartRoundOverlay(this, round).show(game.data.stage); + handCards.clearChildren(); + cardStack.clearChildren(); + }), + new StartRoundOverlay(this, round) + )); } - private void startTrick() { - trick++; - - setActivePlayer(null, null); - - cardStack.clearChildren(); - stack.clear(); + public void startTrick() { + setActivePlayer(null, null, 0); + execute(() -> cardStack.clearChildren()); } - private void finishTrick(UUID player, List cards) { - stack.clear(); - tricks.computeIfAbsent(player, p -> new ArrayList<>()).add(cards); - // TODO + public void finishTrick(UUID player, List cards) { + var seat = seats.get(player); + + var action = parallel(); + execute(sequence( + run(() -> cardStack.removeAll().forEach(card -> action.addAction(sequence( + targeting(card, changeParent(game.data.stage.getRoot())), + parallel( + targeting(card, rotateTo(0, 0.1f)), + targeting(card, moveTo( + seat.getFrontX() - card.getWidth() / 2, + seat.getFrontY() - card.getHeight() / 2, + 0.25f + )) + ), + targeting(card, alpha(0, 0.5f)), + removeActor(card) + )))), + action + )); } - private void setHand(UUID player, List cards) { - var hand = hands.computeIfAbsent(player, p -> new ArrayList<>()); - hand.clear(); - hand.addAll(cards); - - if (self.equals(player)) { - handCards.update(hand); + public void setHand(UUID player, List cards) { + if (state.getSelf().equals(player)) { + execute(() -> handCards.update(cards)); } } - private void setTrump(Card trumpCard, Card.Suit trumpSuit) { - this.trumpCard = trumpCard; - this.trumpSuit = trumpSuit; - if (trumpCardActor != null) { - trumpCardActor.remove(); - } else { + public void setTrump(Card trumpCard, Card.Suit trumpSuit) { + if (trumpCardActor == null) { trumpCardActor = new CardActor(Card.HIDDEN, atlas); } - new TrumpOverlay(this, null, trumpCard, trumpSuit).show(game.data.stage); - } + if (trumpSuitActor == null) { + trumpSuitActor = new CardActor(Card.HIDDEN, atlas); + } + + String player = null; + if (activePlayer != null && activePlayer.second() == PICK_TRUMP) { + player = state.getPlayers().get(activePlayer.first()); + setActivePlayer(null, null, 0); + } - private void addPrediction(UUID player, int prediction) { - addMessage(names.get(player) + " predicted " + prediction + "."); - predictions.put(player, prediction); - padOfTruth.setPrediction(players.indexOf(player), round, prediction); + execute(new TrumpOverlay(this, player, trumpCard, trumpSuit)); } - private void playCard(UUID player, Card card) { - addMessage(names.get(player) + " played a card."); + public void addPrediction(int round, UUID player, int prediction) { + if (activePlayer != null && activePlayer.first().equals(player) && (activePlayer.second() == CHANGE_PREDICTION || activePlayer.second() == MAKE_PREDICTION)) { + setActivePlayer(null, null, 0); + } - var handCard = switch (card) { - case CHANGELING_JESTER, CHANGELING_WIZARD -> Card.CHANGELING; - case CLOUD_BLUE, CLOUD_RED, CLOUD_GREEN, CLOUD_YELLOW -> Card.CLOUD; - case JUGGLER_BLUE, JUGGLER_RED, JUGGLER_GREEN, JUGGLER_YELLOW -> Card.JUGGLER; - default -> card; - }; + addMessage(state.getPlayers().get(player) + " predicted " + prediction + "."); + execute(() -> padOfTruth.setPrediction(players.indexOf(player), round, prediction)); + } - if (hands.containsKey(player)) { - hands.get(player).remove(handCard); + public void playCard(UUID player, Card card) { + if (activePlayer != null && activePlayer.first().equals(player) && activePlayer.second() == PLAY_CARD) { + setActivePlayer(null, null, 0); } - stack.add(Pair.of(player, card)); - CardActor actor; - Seat seat; - if (self.equals(player)) { - actor = handCards.remove(handCard); - seat = Seat.BOTTOM; - } else { - actor = new CardActor(card, atlas); - seat = seats.get(player); - actor.setPosition(seat.getX() - actor.getWidth() / 2, seat.getY() - actor.getHeight() / 2); - actor.setRotation(seat.getAngle()); - } - actor.setOrigin(actor.getWidth() / 2, actor.getHeight() / 2); - cardStack.add(seat, actor); - } + addMessage(state.getPlayers().get(player) + " played a card."); - private void addScores(Map scores) { - this.scores.put(round, scores); + Seat seat = seats.get(player); - for (int i = 0; i < players.size(); i++) { - UUID player = players.get(i); - padOfTruth.setScore(i, round, scores.get(player)); - } + var sequence = sequence(); + sequence.addAction(run(() -> { + CardActor actor; + if (state.getSelf().equals(player)) { + actor = handCards.remove(card); + } else { + actor = new CardActor(card, atlas); + actor.setPosition(seat.getHandX() - actor.getWidth() / 2, seat.getHandY() - actor.getHeight() / 2); + actor.setRotation(seat.getHandAngle()); + } + actor.setOrigin(actor.getWidth() / 2, actor.getHeight() / 2); + cardStack.add(seat, actor); + sequence.addAction(delay(actor)); + sequence.addAction(delay(0.5f)); + })); + execute(sequence); + } + + public void addScores(int round, Map scores) { + execute(() -> { + for (int i = 0; i < players.size(); i++) { + UUID player = players.get(i); + padOfTruth.setScore(i, round, scores.get(player)); + } + }); } - private void setActivePlayer(UUID player, UserInputMessage.Action action) { + public void setActivePlayer(UUID player, UserInputMessage.Action action, long timeout) { if (activePlayer != null && nameLabels.containsKey(activePlayer.getKey())) { - nameLabels.get(activePlayer.getKey()).getStyle().fontColor = Color.WHITE; + var label = nameLabels.get(activePlayer.getKey()); + execute(() -> label.setStyle(labelStyleDefault)); } + if (player == null && action == null) { activePlayer = null; - } else { - activePlayer = Pair.of(player, action); - if (nameLabels.containsKey(player)) { - nameLabels.get(player).getStyle().fontColor = Color.RED; + setPersistentMessage(null); + return; + } + + activePlayer = Pair.of(player, action); + if (nameLabels.containsKey(player)) { + var label = nameLabels.get(player); + execute(() -> label.setStyle(labelStyleActive)); + } + + if (state.getSelf().equals(player)) { + setPersistentMessage(null); + switch (action) { + case PICK_TRUMP -> execute(new PickTrumpOverlay(this, timeout, false)); + case MAKE_PREDICTION -> execute(new MakePredictionOverlay(this, timeout, state.getRound())); + case CHANGE_PREDICTION -> execute(new ChangePredictionOverlay(this, timeout, state.getRound(), state.getPredictions().get(state.getSelf()))); + case PLAY_CARD -> setPersistentMessage("It is your turn to play a card."); } + // TODO do something + } else { + var name = state.getPlayers().get(player); + setPersistentMessage("It is " + name + "'s turn to " + switch (action) { + case CHANGE_PREDICTION -> "change their prediction"; + case JUGGLE_CARD -> "juggle a card"; + case PLAY_CARD -> "play a card"; + case PICK_TRUMP -> "pick the trump suit"; + case MAKE_PREDICTION -> "make a prediction"; + default -> throw new AssertionError(); + }); } } - private void addMessage(String text) { + public void addMessage(String text) { + addMessage(text, false); + } + + public void addMessage(String text, boolean immediate) { var label = new Label(text, game.data.skin); label.addAction(sequence( delay(1.5f), alpha(0, 0.25f), removeActor() )); - messages.addActor(label); - } - @Override - public void dispose() { - super.dispose(); - if (atlas != null) atlas.dispose(); + Runnable runnable = () -> { + if (persistentMessage != null && persistentMessage.getParent() == messages) { + messages.addActorBefore(persistentMessage, label); + } else { + messages.addActor(label); + } + }; + + if (immediate) { + runnable.run(); + } else { + execute(runnable); + } } - public void onMessage(ServerMessage serverMessage) { - if (serverMessage instanceof GameMessage gameMessage) { - var observerMessage = gameMessage.getObserverMessage(); - if (observerMessage instanceof CardMessage card) { - playCard(card.getPlayer(), card.getCard()); - } else if (observerMessage instanceof TrickMessage trick) { - finishTrick(trick.getPlayer(), trick.getCards()); - } else if (observerMessage instanceof HandMessage hand) { - setHand(hand.getPlayer(), hand.getHand()); - } else if (observerMessage instanceof ScoreMessage score) { - addScores(score.getPoints()); - } else if (observerMessage instanceof TrumpMessage trump) { - setTrump(trump.getCard(), trump.getSuit()); - } else if (observerMessage instanceof PredictionMessage prediction) { - addPrediction(prediction.getPlayer(), prediction.getPrediction()); - } else if (observerMessage instanceof UserInputMessage userInput) { - setActivePlayer(userInput.getPlayer(), userInput.getAction()); - if (self.equals(userInput.getPlayer())) { - switch (userInput.getAction()) { - case PICK_TRUMP -> new PickTrumpOverlay(this, false).show(game.data.stage); - case MAKE_PREDICTION -> new MakePredictionOverlay(this, round).show(game.data.stage); - case CHANGE_PREDICTION -> new ChangePredictionOverlay(this, round, predictions.get(self)).show(game.data.stage); - } - // TODO do something - } - } else if (observerMessage instanceof StateMessage state) { - switch (state.getState()) { - case "starting_round" -> startRound(); - case "starting_trick" -> startTrick(); - case "finished", "error" -> { - // TODO do something - } + public void setPersistentMessage(String text) { + execute(() -> { + if (text != null) { + if (persistentMessage == null) { + persistentMessage = new Label(text, getData().skin); + } else { + persistentMessage.setText(text); } + messages.addActor(persistentMessage); + } else if (persistentMessage != null) { + persistentMessage.remove(); } + }); + } + + public void send(PlayerMessage message) { + if (!sending.getAndSet(true) || (message instanceof ContinueMessage)) { + game.getClient().send(new InteractionMessage(message)); + } else { + addMessage("Please slow down.", true); } } + public void ready(boolean success) { + if (!pendingSync.get()) { + sending.set(false); + } + + if (success && currentAction instanceof Overlay overlay && overlay instanceof InteractionOverlay) { + overlay.finish(); + } + } + + public void timeout() { + addMessage("Timed out."); + ready(true); + } + + public void sync() { + pendingSync.set(true); + sending.set(true); + } + + @Override + public void dispose() { + if (atlas != null) atlas.dispose(); + } + public WizardGame.Data getData() { return game.data; } @@ -488,27 +445,31 @@ public class GameScreen extends MenuScreen { TOP(WizardGame.WIDTH * 0.5f, WizardGame.HEIGHT, WizardGame.WIDTH * 0.5f, WizardGame.HEIGHT - 50, Align.top, WizardGame.WIDTH * 0.5f, WizardGame.HEIGHT - 200), TOP_RIGHT(WizardGame.WIDTH * 0.75f, WizardGame.HEIGHT, WizardGame.WIDTH * 0.75f, WizardGame.HEIGHT - 50, Align.top, WizardGame.WIDTH * 0.75f, WizardGame.HEIGHT - 200); - private final float x; - private final float y; + // position of the hand, should be offscreen + private final float handX; + private final float handY; + private final float handAngle; + + // position of the label private final float labelX; private final float labelY; private final int labelAlign; - private final float angle; - private final float cardX; - private final float cardY; - Seat(float x, float y, float labelX, float labelY, int labelAlign, float cardX, float cardY) { - this.x = x; - this.y = y; + private final float frontX; + private final float frontY; + + Seat(float handX, float handY, float labelX, float labelY, int labelAlign, float frontX, float frontY) { + this.handX = handX; + this.handY = handY; this.labelX = labelX; this.labelY = labelY; this.labelAlign = labelAlign; - this.cardX = cardX; - this.cardY = cardY; + this.frontX = frontX; + this.frontY = frontY; - var deltaX = WizardGame.WIDTH * 0.5f - x; - var deltaY = WizardGame.HEIGHT * 0.5f - y; - this.angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX) + Math.PI / 2); + var deltaX = WizardGame.WIDTH * 0.5f - handX; + var deltaY = WizardGame.HEIGHT * 0.5f - handY; + this.handAngle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX) + Math.PI / 2); } } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java new file mode 100644 index 0000000..6d9fc15 --- /dev/null +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java @@ -0,0 +1,245 @@ +package eu.jonahbauer.wizard.client.libgdx.state; + +import eu.jonahbauer.wizard.client.libgdx.Client; +import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; +import eu.jonahbauer.wizard.client.libgdx.util.Pair; +import eu.jonahbauer.wizard.common.messages.data.PlayerData; +import eu.jonahbauer.wizard.common.messages.data.SessionData; +import eu.jonahbauer.wizard.common.messages.observer.*; +import eu.jonahbauer.wizard.common.messages.server.AckMessage; +import eu.jonahbauer.wizard.common.messages.server.GameMessage; +import eu.jonahbauer.wizard.common.messages.server.NackMessage; +import eu.jonahbauer.wizard.common.messages.server.ServerMessage; +import eu.jonahbauer.wizard.common.model.Card; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; + +@Log4j2 +@Getter +public final class Game extends BaseState { + private final UUID self; + private final UUID session; + private final String sessionName; + private final String secret; + + private final Map players; + private final Map scores = new HashMap<>(); + + private int round = -1; + private final Map predictions = new HashMap<>(); + private final Map> hands = new HashMap<>(); + private final Map>> tricks = new HashMap<>(); + + private int trick = -1; + private final List> stack = new ArrayList<>(); + + private Card trumpCard; + private Card.Suit trumpSuit; + + private GameScreen gameScreen; + + public Game(UUID self, UUID session, String sessionName, String secret, Map players) { + this.self = self; + this.session = session; + this.sessionName = sessionName; + this.secret = secret; + this.players = players; + } + + @Override + public Optional onEnter(Client client) { + gameScreen = new GameScreen(client.getGame()); + client.getGame().setScreen(gameScreen); + return super.onEnter(client); + } + + @Override + public Optional onMessage(Client client, ServerMessage message) { + log(message); + + if (message instanceof GameMessage game) { + var observerMessage = game.getObserverMessage(); + if (observerMessage instanceof StateMessage state) { + switch (state.getState()) { + case "starting_round" -> { + round ++; + predictions.clear(); + tricks.clear(); + trumpSuit = null; + trumpCard = null; + stack.clear(); + trick = -1; + gameScreen.startRound(round); + } + case "starting_trick" -> { + trick ++; + stack.clear(); + gameScreen.startTrick(); + } + case "finished", "error" -> { + return returnToSession(); + } + } + } else if (observerMessage instanceof HandMessage hand) { + hands.put(hand.getPlayer(), hand.getHand()); + gameScreen.setHand(hand.getPlayer(), hand.getHand()); + } else if (observerMessage instanceof PredictionMessage prediction) { + predictions.put(prediction.getPlayer(), prediction.getPrediction()); + gameScreen.addPrediction(round, prediction.getPlayer(), prediction.getPrediction()); + } else if (observerMessage instanceof TrumpMessage trump) { + trumpCard = trump.getCard(); + trumpSuit = trump.getSuit(); + gameScreen.setTrump(trumpCard, trumpSuit); + } else if (observerMessage instanceof TrickMessage trick) { + this.stack.clear(); + this.tricks.computeIfAbsent(trick.getPlayer(), player -> new ArrayList<>()) + .add(trick.getCards()); + gameScreen.finishTrick(trick.getPlayer(), trick.getCards()); + } else if (observerMessage instanceof CardMessage card) { + this.stack.add(Pair.of(card.getPlayer(), card.getCard())); + + var handCard = switch (card.getCard()) { + case CHANGELING_JESTER, CHANGELING_WIZARD -> Card.CHANGELING; + case JUGGLER_BLUE, JUGGLER_GREEN, JUGGLER_RED, JUGGLER_YELLOW -> Card.JUGGLER; + case CLOUD_BLUE, CLOUD_GREEN, CLOUD_RED, CLOUD_YELLOW -> Card.CLOUD; + default -> card.getCard(); + }; + + var hand = this.hands.get(card.getPlayer()); + if (hand != null) { + hand.remove(handCard); + } + + gameScreen.playCard(card.getPlayer(), handCard); + } else if (observerMessage instanceof ScoreMessage score) { + score.getPoints().forEach((player, points) -> scores.merge(player, points, Integer::sum)); + gameScreen.addScores(round, score.getPoints()); + } else if (observerMessage instanceof UserInputMessage input) { + if (input.getAction() == UserInputMessage.Action.SYNC) { + gameScreen.sync(); + } else { + gameScreen.setActivePlayer(input.getPlayer(), input.getAction(), input.getTimeout()); + } + } else if (observerMessage instanceof TimeoutMessage) { + gameScreen.timeout(); + } else { + return Optional.of(new Menu()); + } + return Optional.empty(); + } else if (message instanceof NackMessage nack) { + int code = nack.getCode(); + if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) { + gameScreen.addMessage(nack.getMessage()); + gameScreen.ready(false); + return Optional.empty(); + } else { + return unexpectedMessage(client, message); + } + } else if (message instanceof AckMessage) { + gameScreen.ready(true); + return Optional.empty(); + } else { + return unexpectedMessage(client, message); + } + } + + private Optional returnToSession() { + return Optional.of(new Session( + new SessionData(session, sessionName, -1, null), + players.entrySet().stream() + .map(entry -> new PlayerData(entry.getKey(), entry.getValue(), false)) + .toList(), + self, secret + )); + } + + private void log(ServerMessage message) { + if (message instanceof GameMessage gameMessage) { + var observerMessage = gameMessage.getObserverMessage(); + if (observerMessage instanceof StateMessage state) { + switch (state.getState()) { + case "starting_round" -> log.info("Round {} is starting...", round + 1); + case "starting_trick" -> log.info("Trick {} is starting...", trick + 1); + case "finished" -> log.info("The game has finished."); + case "error" -> log.info("The game has finished with an error."); + } + } else if (observerMessage instanceof HandMessage hand) { + if (hand.getPlayer().equals(self)) { + log.info("Your hand cards are: {}", hand.getHand()); + } else { + log.info("{}'s hand cards are: {}", nameOf(hand.getPlayer()), hand.getHand()); + } + } else if (observerMessage instanceof PredictionMessage prediction) { + if (prediction.getPlayer().equals(self)) { + log.info("You predicted: {}%n", prediction.getPrediction()); + } else { + log.info("{} predicted: {}%n", nameOf(prediction.getPlayer()), prediction.getPrediction()); + } + } else if (observerMessage instanceof TrumpMessage trump) { + trumpCard = trump.getCard(); + trumpSuit = trump.getSuit(); + if (trumpCard == null) { + log.info("There is no trump in this round."); + } else { + log.info("The trump suit is {} ({}).", trumpSuit, trumpCard); + } + } else if (observerMessage instanceof TrickMessage trick) { + log.info("This trick {} goes to {}.", trick.getCards(), nameOf(trick.getPlayer())); + } else if (observerMessage instanceof CardMessage card) { + if (card.getPlayer().equals(self)) { + log.info("You played {}.", card.getCard()); + } else { + log.info("{} played {}.", nameOf(card.getPlayer()), card.getCard()); + } + } else if (observerMessage instanceof ScoreMessage score) { + log.info("The scores are as follows: " + score.getPoints()); + } else if (observerMessage instanceof UserInputMessage input) { + if (input.getAction() != UserInputMessage.Action.SYNC) { + if (self.equals(input.getPlayer())) { + log.info("It is your turn to {}. You have time until {}.", switch (input.getAction()) { + case CHANGE_PREDICTION -> "change your prediction"; + case JUGGLE_CARD -> "juggle a card"; + case PLAY_CARD -> "play a card"; + case PICK_TRUMP -> "pick the trump suit"; + case MAKE_PREDICTION -> "make a prediction"; + default -> throw new AssertionError(); + }, LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()), ZoneId.systemDefault())); + } else { + log.info( + "Waiting for input {} from {}. (times out at {})", + input.getAction(), + nameOf(input.getPlayer()), + LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()), ZoneId.systemDefault()) + ); + } + } + } else if (observerMessage instanceof TimeoutMessage) { + log.info("The previous interaction timed out."); + } else { + log.fatal("Unknown observer message type {}.", observerMessage.getClass()); + } + } else if (message instanceof NackMessage nack) { + int code = nack.getCode(); + if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) { + log.error(nack.getMessage()); + } else { + log.fatal("Unexpected message: {}", message); + } + } else if (message instanceof AckMessage) { + log.info("OK"); + } + } + + private String nameOf(UUID player) { + if (player == null) { + return "all players"; + } else { + return players.get(player); + } + } +} diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Session.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Session.java index 6b3860f..fe76d0a 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Session.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Session.java @@ -74,11 +74,10 @@ public final class Session extends BaseState { return Optional.empty(); } else if (message instanceof StartingGameMessage) { - return Optional.empty(); -// return Optional.of(new Game( -// self, session, sessionName, secret, -// players.stream().collect(Collectors.toMap(PlayerData::getUuid, PlayerData::getName)) -// )); + return Optional.of(new Game( + self, session, sessionName, secret, + players.stream().collect(Collectors.toMap(PlayerData::getUuid, PlayerData::getName)) + )); } else if (sending && message instanceof NackMessage nack) { // TODO display error log.error(nack.getMessage()); diff --git a/wizard-client/wizard-client-libgdx/core/src/main/textures/uiskin/default-select-selection.9.png b/wizard-client/wizard-client-libgdx/core/src/main/textures/uiskin/default-select-selection.9.png new file mode 100644 index 0000000..4649412 Binary files /dev/null and b/wizard-client/wizard-client-libgdx/core/src/main/textures/uiskin/default-select-selection.9.png differ