diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/AnimationTimings.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/AnimationTimings.java index 1c85cc3..e65e8c7 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/AnimationTimings.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/AnimationTimings.java @@ -4,26 +4,29 @@ import lombok.experimental.UtilityClass; @UtilityClass public class AnimationTimings { - public static final float JUGGLE = 0.25f; + private static final float FACTOR = 1; - public static final float STACK_EXPAND = 0.25f; - public static final float STACK_COLLAPSE = 0.25f; + public static final float JUGGLE = 0.25f * FACTOR; - public static final float STACK_FINISH_MOVE = 0.25f; - public static final float STACK_FINISH_ROTATE = 0.1f; - public static final float STACK_FINISH_FADE = 0.5f; - public static final float STACK_HOLD = 0.5f; + public static final float STACK_EXPAND = 0.25f * FACTOR; + public static final float STACK_COLLAPSE = 0.25f * FACTOR; - public static final float PAD_OF_TRUTH_EXPAND = 0.25f; - public static final float PAD_OF_TRUTH_COLLAPSE = 0.25f; + public static final float STACK_FINISH_MOVE = 0.25f * FACTOR; + public static final float STACK_FINISH_ROTATE = 0.1f * FACTOR; + public static final float STACK_FINISH_FADE = 0.5f * FACTOR; + public static final float STACK_HOLD = 0.5f * FACTOR; - public static final float HAND_LAYOUT = 0.15f; + public static final float WEREWOLF_SWAP = 0.25f * FACTOR; - public static final float OVERLAY_HOLD = 3f; - public static final float OVERLAY_FADE = 0.1f; + public static final float PAD_OF_TRUTH_EXPAND = 0.25f * FACTOR; + public static final float PAD_OF_TRUTH_COLLAPSE = 0.25f * FACTOR; - public static final float OVERLAY_TRUMP = .3f; + public static final float HAND_LAYOUT = 0.15f * FACTOR; - public static final float MESSAGE_HOLD = 1.5f; - public static final float MESSAGE_FADE = 0.5f; + public static final float OVERLAY_HOLD = 3f * FACTOR; + public static final float OVERLAY_FADE = 0.1f * FACTOR; + public static final float OVERLAY_SHARED_ELEMENT = .3f * FACTOR; + + public static final float MESSAGE_HOLD = 1.5f * FACTOR; + public static final float MESSAGE_FADE = 0.5f * FACTOR; } 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 85ecb38..9113f59 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 @@ -112,6 +112,7 @@ public class CardActor extends Actor { setWidth(PREF_WIDTH); setHeight(PREF_HEIGHT); setColor(0.8f, 0.8f, 0.8f, 1.0f); + setOrigin(PREF_WIDTH / 2, PREF_HEIGHT / 2); } public CardActor(Card card, TextureAtlas atlas) { diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardsGroup.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardsGroup.java index 5ab9101..553e7cc 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardsGroup.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardsGroup.java @@ -41,8 +41,6 @@ public class CardsGroup extends WidgetGroup { private CardActor dragTarget; private float dragStartX; - @Getter - private SelectMode selectMode = SelectMode.NONE; @Setter @Getter private Card selected; @@ -130,8 +128,7 @@ public class CardsGroup extends WidgetGroup { for (var child : getChildren()) { if (child instanceof CardActor card) { float height = card.getHeight(); - if (selectMode == SelectMode.NONE && (child == dragTarget || dragTarget == null && child == target) - || selectMode == SelectMode.SINGLE && card.getCard() == selected) { + if ((child == dragTarget || dragTarget == null && child == target) || card.getCard() == selected) { height += speed * delta; } else { height -= speed * delta; @@ -207,19 +204,8 @@ public class CardsGroup extends WidgetGroup { } public CardActor remove(Card card) { - // find actor - CardActor actor = null; - var children = getChildren(); - for (int i = 0; i < children.size; i++) { - if (children.get(i) instanceof CardActor cardActor && cardActor.getCard() == card) { - actor = cardActor; - break; - } - } - - if (actor == null) { - throw new NoSuchElementException(); - } + var actor = find(card); + if (actor == null) return null; // adjust actor actor.setY(getY() + actor.getHeight() - getHeight()); @@ -237,20 +223,22 @@ public class CardsGroup extends WidgetGroup { return actor; } + public CardActor find(Card card) { + var children = getChildren(); + for (int i = 0; i < children.size; i++) { + if (children.get(i) instanceof CardActor cardActor && cardActor.getCard() == card) { + return cardActor; + } + } + return null; + } + @Override public float getMinHeight() { return 0; } - public void setSelectMode(SelectMode selectMode) { - this.selectMode = Objects.requireNonNull(selectMode); - } - public void setOnClickListener(Consumer onClickListener) { this.onClickListener = onClickListener; } - - public enum SelectMode { - SINGLE, NONE - } } 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 cb707fb..f6ac585 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 @@ -13,16 +13,20 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Align; import eu.jonahbauer.wizard.client.libgdx.AnimationTimings; +import lombok.Setter; public class PadOfTruth extends Table { - private static final float EXTENDED_WIDTH = 636; - private static final float EXTENDED_HEIGHT = 824; - private static final float COLLAPSED_SCALE = CardActor.PREF_HEIGHT / EXTENDED_HEIGHT; + public static final float EXTENDED_WIDTH = 636; + public static final float EXTENDED_HEIGHT = 824; + public static final float COLLAPSED_SCALE = CardActor.PREF_HEIGHT / EXTENDED_HEIGHT; private final Label[] names = new Label[6]; private final Label[][] predictions = new Label[20][]; private final Label[][] scores = new Label[20][]; + @Setter + private boolean enabled = true; + public PadOfTruth(Skin skin, Drawable background) { super(skin); setTouchable(Touchable.enabled); @@ -32,7 +36,6 @@ public class PadOfTruth extends Table { setHeight(EXTENDED_HEIGHT); setTransform(true); - setOrigin(0, 0); setScale(COLLAPSED_SCALE); addListener(new InputListener() { @@ -40,6 +43,7 @@ public class PadOfTruth extends Table { @Override public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) { + if (!enabled) return; if (fromActor != null && isAscendantOf(fromActor)) return; if (action != null) removeAction(action); action = Actions.scaleTo(1, 1, AnimationTimings.PAD_OF_TRUTH_EXPAND); @@ -48,6 +52,7 @@ public class PadOfTruth extends Table { @Override public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) { + if (!enabled) return; if (toActor != null && isAscendantOf(toActor)) return; if (action != null) removeAction(action); action = Actions.scaleTo(COLLAPSED_SCALE, COLLAPSED_SCALE, AnimationTimings.PAD_OF_TRUTH_COLLAPSE); 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 index 5788a9b..870d80f 100644 --- 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 @@ -1,4 +1,5 @@ package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay; public interface InteractionOverlay { + void close(); } 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 bd17cc5..8805def 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 @@ -36,9 +36,10 @@ public class MakePredictionOverlay extends Overlay implements InteractionOverlay var listener = new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { + if (isClosing()) return; for (int i = 0; i < values.length; i++) { if (actor == buttons[i]) { - screen.send(new PredictMessage(values[i])); + screen.onPredictionMade(values[i]); break; } } 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 267c875..ebf375e 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 @@ -11,6 +11,8 @@ import eu.jonahbauer.wizard.client.libgdx.AnimationTimings; import eu.jonahbauer.wizard.client.libgdx.UiskinAtlas; import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; +import lombok.AccessLevel; +import lombok.Getter; import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*; @@ -23,7 +25,9 @@ public abstract class Overlay extends Action { private Container root; private boolean started; - private boolean finished; + @Getter(AccessLevel.PROTECTED) + private boolean closing; + private boolean closed; public Overlay(GameScreen gameScreen, long timeout) { this.screen = gameScreen; @@ -44,11 +48,12 @@ public abstract class Overlay extends Action { show((Group) getActor()); } - if (System.currentTimeMillis() > timeout) { - finishInternal(); + if (System.currentTimeMillis() > timeout && !closing) { + closing = true; + onClosing(); } - return finished; + return closed; } protected abstract Actor createContent(); @@ -68,21 +73,20 @@ public abstract class Overlay extends Action { parent.addActor(getRoot()); } - protected void finishInternal() { - if (!finished) { - finished = true; - getRoot().remove(); - } + protected final void onClosed() { + if (closed) return; + closed = true; + getRoot().remove(); } - public void finish() { + protected void onClosing() { getRoot().addAction(sequence( targeting(root, alpha(0.0f, AnimationTimings.OVERLAY_FADE, Interpolation.pow2Out)), - run(this::finishInternal) + run(this::onClosed) )); } - public void timeout() { + public void close() { 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 24b2bf4..cacd76b 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,7 +8,6 @@ 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; @@ -44,10 +43,11 @@ public class PickTrumpOverlay extends Overlay implements InteractionOverlay { cardGroup.addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { + if (isClosing()) return; var target = event.getTarget(); for (Card.Suit suit : Card.Suit.values()) { if (cards.get(suit) == target) { - screen.send(new PickTrumpMessage(suit)); + screen.onSuitClicked(suit); break; } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PlayColoredCardOverlay.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PlayColoredCardOverlay.java index b45e55c..ea0e0d8 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PlayColoredCardOverlay.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PlayColoredCardOverlay.java @@ -10,7 +10,6 @@ import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; 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.PlayCardMessage; import eu.jonahbauer.wizard.common.model.Card; import java.util.EnumMap; @@ -51,10 +50,11 @@ public class PlayColoredCardOverlay extends Overlay implements InteractionOverla cardGroup.addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { + if (isClosing()) return; var target = event.getTarget(); for (Card.Suit suit : Card.Suit.values()) { if (actors.get(suit) == target) { - screen.send(new PlayCardMessage(cards.get(suit))); + screen.onCardClicked(cards.get(suit)); break; } } @@ -68,7 +68,7 @@ public class PlayColoredCardOverlay extends Overlay implements InteractionOverla cancel.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { - finishInternal(); + close(); } }); root.addActor(cancel); 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 3b8e4e5..42bc186 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 @@ -1,12 +1,10 @@ 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.Group; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; import eu.jonahbauer.wizard.client.libgdx.AnimationTimings; -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.*; @@ -38,13 +36,7 @@ public class StartRoundOverlay extends Overlay { var root = getRoot(); root.addAction(sequence( delay(AnimationTimings.OVERLAY_HOLD), - targeting(root, alpha(0.0f, AnimationTimings.OVERLAY_FADE, Interpolation.pow2Out)), - run(this::finishInternal) + run(this::close) )); } - - @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 d6ea219..b120b80 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,7 +1,6 @@ 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.Group; import com.badlogic.gdx.scenes.scene2d.actions.ParallelAction; @@ -161,7 +160,7 @@ public class TrumpOverlay extends Overlay { cardAnimation.addAction(sequence( targeting(trumpCardActor, removeActorSilently()), targeting(trumpCardActor, changeParent(parent)), - targeting(trumpCardActor, moveTo(10, 10, AnimationTimings.OVERLAY_TRUMP)) + targeting(trumpCardActor, moveTo(10, 10, AnimationTimings.OVERLAY_SHARED_ELEMENT)) )); } @@ -171,9 +170,9 @@ public class TrumpOverlay extends Overlay { targeting(trumpSuitActor, changeParent(parent)), run(trumpCardActor::toFront), parallel( - targeting(trumpSuitActor, rotateTo(-90, AnimationTimings.OVERLAY_TRUMP)), + targeting(trumpSuitActor, rotateTo(-90, AnimationTimings.OVERLAY_SHARED_ELEMENT)), targeting(trumpSuitActor, - moveTo(10, 10 + (trumpSuitActor.getHeight() + trumpSuitActor.getWidth()) / 2, AnimationTimings.OVERLAY_TRUMP) + moveTo(10, 10 + (trumpSuitActor.getHeight() + trumpSuitActor.getWidth()) / 2, AnimationTimings.OVERLAY_SHARED_ELEMENT) ) ) )); @@ -186,12 +185,14 @@ public class TrumpOverlay extends Overlay { targeting(root, alpha(0.0f, AnimationTimings.OVERLAY_FADE, Interpolation.pow2Out)), cardAnimation ), - run(this::finishInternal) + run(this::close) )); } @Override - public void finish() { - MyActions.finish(getRoot().getActions().get(0)); + protected void onClosing() { + var actions = getRoot().getActions(); + if (actions.size > 0) MyActions.finish(actions.get(0)); + onClosed(); } } 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 9c53c84..ab5b343 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 @@ -22,22 +22,17 @@ 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.Triple; -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.JuggleMessage; -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 org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.*; 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 { @Getter @@ -46,11 +41,9 @@ public class GameScreen extends MenuScreen { private Label.LabelStyle labelStyleDefault; private Label.LabelStyle labelStyleActive; - private final Game state; - - private final List players; - - private Triple activePlayer; + private final UUID self; + private final LinkedHashMap players; + private final List orderedPlayers; private CardsGroup handCards; private CardStack cardStack; @@ -70,18 +63,16 @@ public class GameScreen extends MenuScreen { private final Map seats = new HashMap<>(); private final Map nameLabels = new HashMap<>(); - private final AtomicBoolean sending = new AtomicBoolean(); private final AtomicBoolean pendingSync = new AtomicBoolean(); - private boolean juggling; - private Card juggledCard; - - public GameScreen(WizardGame game) { + public GameScreen(WizardGame game, @NotNull UUID self, @NotNull LinkedHashMap players) { super(game); - this.state = (Game) game.getClient().getState(); - this.players = new ArrayList<>(state.getPlayers().keySet()); + this.self = self; + this.players = players; + this.orderedPlayers = players.keySet().stream().toList(); } + // @Override public void show() { super.show(); @@ -92,10 +83,9 @@ public class GameScreen extends MenuScreen { labelStyleActive.fontColor = Color.RED; seat(); - prepareLabels(); handCards = new CardsGroup(Collections.emptyList(), atlas); - handCards.setOnClickListener(this::onCardClicked); + handCards.setOnClickListener(actor -> onCardClicked(actor.getCard())); var container = new Container<>(handCards); container.setPosition(360, 75); container.setSize(1200, CardActor.PREF_HEIGHT); @@ -107,13 +97,13 @@ public class GameScreen extends MenuScreen { messages.setTouchable(Touchable.disabled); padOfTruth = new PadOfTruth(game.data.skin, new TextureRegionDrawable(atlas.findRegion(GameAtlas.PAD_OF_TRUTH))); - padOfTruth.setPosition(1910 - 636f, 10); - padOfTruth.setOrigin(636f, 0); + padOfTruth.setPosition(WizardGame.WIDTH - 10 - PadOfTruth.EXTENDED_WIDTH, 10); + padOfTruth.setOrigin(PadOfTruth.EXTENDED_WIDTH, 0); cardStack = new CardStack(); cardStack.setHoverBounds(0.5f * WizardGame.WIDTH - 50, 0.5f * WizardGame.HEIGHT - 50, 100, 100); - setNames(); + addLabels(); Gdx.input.setInputProcessor(game.data.stage); game.data.stage.addActor(container); @@ -122,14 +112,53 @@ public class GameScreen extends MenuScreen { game.data.stage.addActor(messages); } + private void seat() { + var count = players.size(); + Seat[] seats = switch (count) { + case 3 -> new Seat[] {Seat.BOTTOM, Seat.TOP_LEFT, Seat.TOP_RIGHT}; + case 4 -> new Seat[] {Seat.BOTTOM, Seat.LEFT, Seat.TOP, Seat.RIGHT}; + case 5 -> new Seat[] {Seat.BOTTOM, Seat.LEFT, Seat.TOP_LEFT, Seat.TOP_RIGHT, Seat.RIGHT}; + case 6 -> new Seat[] {Seat.BOTTOM, Seat.LEFT, Seat.TOP_LEFT, Seat.TOP, Seat.TOP_RIGHT, Seat.RIGHT}; + default -> throw new AssertionError(); + }; + + int index = orderedPlayers.indexOf(self); + for (int i = 0; i < count; i++) { + var player = orderedPlayers.get((index + i) % count); + var seat = seats[i]; + this.seats.put(player, seat); + } + } + + private void addLabels() { + int i = 0; + for (var entry : players.entrySet()) { + UUID uuid = entry.getKey(); + String name = entry.getValue(); + + padOfTruth.setName(i++, name); + + if (isSelf(uuid)) continue; + var label = new Label("", game.data.skin); + var seat = seats.get(uuid); + label.setX(seat.getLabelX()); + label.setY(seat.getLabelY()); + label.setAlignment(seat.getLabelAlign()); + label.setText(name); + nameLabels.put(uuid, label); + game.data.stage.addActor(label); + } + } + // + @Override protected void renderBackground(float delta) { float scale = Math.max( game.data.extendViewport.getWorldWidth() / WizardGame.WIDTH, game.data.extendViewport.getWorldHeight() / WizardGame.HEIGHT ); - game.batch.setColor(1, 1, 1, 0.25f); game.batch.draw(game.data.background, 0,0, scale * WizardGame.WIDTH, scale * WizardGame.HEIGHT); + game.batch.setColor(1, 1, 1, 0.25f); game.batch.draw( game.data.title, (game.data.extendViewport.getWorldWidth() - game.data.title.getRegionWidth() * 0.75f) / 2f, @@ -158,7 +187,7 @@ public class GameScreen extends MenuScreen { game.data.stage.addAction(currentAction); } } else if (pendingSync.getAndSet(false)) { - send(new ContinueMessage()); + game.getClient().execute(Game.class, Game::sync); } } @@ -170,90 +199,25 @@ public class GameScreen extends MenuScreen { pendingActions.add(run(runnable)); } - private void seat() { - var count = players.size(); - Seat[] seats = switch (count) { - case 3 -> new Seat[] {Seat.TOP_LEFT, Seat.TOP_RIGHT}; - case 4 -> new Seat[] {Seat.LEFT, Seat.TOP, Seat.RIGHT}; - case 5 -> new Seat[] {Seat.LEFT, Seat.TOP_LEFT, Seat.TOP_RIGHT, Seat.RIGHT}; - case 6 -> new Seat[] {Seat.LEFT, Seat.TOP_LEFT, Seat.TOP, Seat.TOP_RIGHT, Seat.RIGHT}; - default -> throw new AssertionError(); - }; - 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 (isSelf(player)) continue; - var label = new Label("", game.data.skin); - var seat = seats.get(player); - label.setX(seat.getLabelX()); - label.setY(seat.getLabelY()); - label.setAlignment(seat.getLabelAlign()); - this.nameLabels.put(player, label); - game.data.stage.addActor(label); - } - } - - private void setNames() { - for (int i = 0; i < players.size(); i++) { - var player = players.get(i); - var name = state.getPlayers().get(player); - padOfTruth.setName(i, name); - if (!isSelf(player)) { - nameLabels.get(player).setText(name); - } - } + private boolean isSelf(UUID uuid) { + return self.equals(uuid); } - private void onCardClicked(CardActor actor) { - var card = actor.getCard(); - - if (checkAction(PLAY_CARD)) { - var timeout = activePlayer.third(); - if (card == Card.CLOUD) { - execute(new PlayColoredCardOverlay(this, - timeout, - Card.CLOUD, - Card.CLOUD_RED, - Card.CLOUD_GREEN, - Card.CLOUD_BLUE, - Card.CLOUD_YELLOW - )); - } else if (card == Card.JUGGLER) { - execute(new PlayColoredCardOverlay(this, - timeout, - Card.JUGGLER, - Card.JUGGLER_RED, - Card.JUGGLER_GREEN, - Card.JUGGLER_BLUE, - Card.JUGGLER_YELLOW - )); - } else { - send(new PlayCardMessage(card)); - } - } else if (checkAction(JUGGLE_CARD)) { - juggledCard = card; - send(new JuggleMessage(card)); - } else { - addMessage("You cannot do that right now."); - } + public void onCardClicked(Card card) { + game.getClient().execute(Game.class, (s, c) -> s.onCardClicked(c, card)); } - private boolean checkAction(UserInputMessage.Action action) { - return activePlayer != null && (activePlayer.first() == null || isSelf(activePlayer.first())) && activePlayer.second() == action; + public void onSuitClicked(Card.Suit suit) { + game.getClient().execute(Game.class, (s, c) -> s.onSuitClicked(c, suit)); } - private boolean isSelf(UUID uuid) { - return state.getSelf().equals(uuid); + public void onPredictionMade(int prediction) { + game.getClient().execute(Game.class, (s, c) -> s.onPredictionMade(c, prediction)); } + /** + * Resets all round-scoped and shows an {@linkplain StartRoundOverlay overlay}. + */ public void startRound(int round) { execute(parallel( run(() -> { @@ -277,24 +241,8 @@ public class GameScreen extends MenuScreen { execute(() -> cardStack.clearChildren()); } - /** - * Indicates that the next call to {@link #setHand(UUID, List)} should be animated. - */ - public void setJuggling(boolean juggling) { - execute(() -> { - this.juggling = juggling; - if (juggling) { - handCards.setSelectMode(CardsGroup.SelectMode.SINGLE); - } else { - handCards.setSelectMode(CardsGroup.SelectMode.NONE); - juggledCard = null; - handCards.setSelected(null); - } - }); - } - - public void finishTrick(UUID player, List cards) { - var seat = seats.get(player); + public void finishTrick(UUID player) { + var seat = seats.getOrDefault(player, Seat.FALLBACK); var action = parallel(); execute(sequence( @@ -321,15 +269,17 @@ public class GameScreen extends MenuScreen { )); } - public void setHand(UUID player, List cards) { + /** + * Updates the given players hand cards. + */ + public void setHand(UUID player, List cards, boolean juggle) { if (isSelf(player)) { var sequence = sequence(); sequence.addAction(run(() -> { var changes = handCards.update(cards); // animate card changes - if (juggling) { - setJuggling(false); + if (juggle) { sequence.addAction(animateJuggle(changes.first(), changes.second())); } })); @@ -337,62 +287,39 @@ public class GameScreen extends MenuScreen { } } - public void setTrump(Card trumpCard, Card.Suit trumpSuit) { - if (trumpCardActor == null) { - trumpCardActor = new CardActor(Card.HIDDEN, atlas); - } - - 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()); - clearActivePlayer(); - } - - execute(new TrumpOverlay(this, player, trumpCard, trumpSuit)); - } - - public void addPrediction(int round, UUID player, int prediction) { - boolean changed = false; - - if (activePlayer != null && activePlayer.first().equals(player) && (activePlayer.second() == CHANGE_PREDICTION || activePlayer.second() == MAKE_PREDICTION)) { - changed = activePlayer.second() == CHANGE_PREDICTION; - clearActivePlayer(); - } - + /** + * Adds the prediction for the given round and player to the {@linkplain #padOfTruth pad of truth} and + * shows a corresponding message. + */ + public void addPrediction(int round, UUID player, int prediction, boolean changed) { if (isSelf(player)) { addMessage(game.messages.format("game.action." + (changed ? "change" : "make") + "_prediction.self", prediction)); } else { - var name = state.getPlayers().get(player); + var name = players.get(player); addMessage(game.messages.format("game.action." + (changed ? "change" : "make") + "_prediction.other", name, prediction)); } - execute(() -> padOfTruth.setPrediction(players.indexOf(player), round, prediction)); + execute(() -> padOfTruth.setPrediction(orderedPlayers.indexOf(player), round, prediction)); } + /** + * Removes the card from the players hand and puts it into the {@linkplain #cardStack stack}. + */ public void playCard(UUID player, Card card) { - if (activePlayer != null && activePlayer.first().equals(player) && activePlayer.second() == PLAY_CARD) { - clearActivePlayer(); - } - if (isSelf(player)) { addMessage(game.messages.get("game.action.play_card.self")); } else { - var name = state.getPlayers().get(player); + var name = players.get(player); addMessage(game.messages.format("game.action.play_card.other", name)); } - Seat seat = seats.get(player); + Seat seat = seats.getOrDefault(player, Seat.FALLBACK); var sequence = sequence(); sequence.addAction(run(() -> { CardActor actor = null; if (isSelf(player)) { actor = handCards.remove(card); - actor.setOrigin(actor.getWidth() / 2, actor.getHeight() / 2); } if (actor == null) { @@ -406,55 +333,35 @@ public class GameScreen extends MenuScreen { execute(sequence); } + /** + * Adds the scores for a round to the corresponding row of the {@linkplain #padOfTruth pad of truth}. + */ public void addScores(int round, Map scores) { execute(() -> { - for (int i = 0; i < players.size(); i++) { - UUID player = players.get(i); + for (int i = 0; i < orderedPlayers.size(); i++) { + UUID player = orderedPlayers.get(i); padOfTruth.setScore(i, round, scores.get(player)); } }); } + /** + * Removes all visual changes done by {@link #setActivePlayer(UUID, UserInputMessage.Action, long)}. + */ public void clearActivePlayer() { setActivePlayer(null, null, 0); } + /** + * Highlights the given players label and sets the {@linkplain #setPersistentMessage(String) persistent message} + * accordingly. + */ public void setActivePlayer(UUID player, UserInputMessage.Action action, long timeout) { - if (action == SYNC) throw new IllegalArgumentException(); + execute(() -> nameLabels.forEach((p, l) -> l.setStyle(p.equals(player) ? labelStyleActive : labelStyleDefault))); - // reset label color - if (activePlayer != null && nameLabels.containsKey(activePlayer.first())) { - var label = nameLabels.get(activePlayer.first()); - execute(() -> label.setStyle(labelStyleDefault)); - } - - if (player == null && action == null) { - activePlayer = null; + if (isSelf(player) || player == null && action == null) { setPersistentMessage(null); - return; - } - - activePlayer = Triple.of(player, action, timeout); - // set label color - if (nameLabels.containsKey(player)) { - var label = nameLabels.get(player); - execute(() -> label.setStyle(labelStyleActive)); - } - - boolean isSelf = state.getSelf().equals(player); - if (isSelf || player == null) { - // show interface - 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(game.messages.get("game.message.play_card.self")); - } - } - - if (!isSelf) { - // show message + } else { var key = switch (action) { case CHANGE_PREDICTION -> "game.message.change_prediction."; case JUGGLE_CARD -> "game.message.juggle_card."; @@ -464,15 +371,141 @@ public class GameScreen extends MenuScreen { default -> throw new AssertionError(); }; - if (player == null) { + if (player != null) { + setPersistentMessage(game.messages.format(key + "other", players.get(player))); + } else { setPersistentMessage(game.messages.get(key + "all")); + } + } + } + + /** + * Visually highlights the given card on the players hand. + */ + public void setSelectedCard(@Nullable Card card) { + handCards.setSelected(card); + } + + /** + * Swaps the current {@linkplain #trumpCardActor trump card} (if present) with a {@linkplain Card#WEREWOLF werewolf} + * card on the given players hand. + */ + public void swapTrumpCard(UUID player) { + var seat = seats.getOrDefault(player, Seat.FALLBACK); + + var sequence = sequence(); + sequence.addAction(run(() -> { + if (trumpCardActor == null || !trumpCardActor.hasParent()) return; + + if (isSelf(player)) { + var handCard = handCards.find(Card.WEREWOLF); + if (handCard == null) return; + + handCard.setCard(trumpCardActor.getCard()); + trumpCardActor.setCard(Card.WEREWOLF); + + float localHandX = handCard.getX(), localHandY = handCard.getY(); + float stageTrumpX = 10, stageTrumpY = 10; + float localTrumpX, localTrumpY; + float stageHandX, stageHandY; + + var pos = new Vector2(handCard.getX(), handCard.getY()); + handCards.localToStageCoordinates(pos); + stageHandX = pos.x; + stageHandY = pos.y; + + pos.set(stageTrumpX, stageTrumpY); + handCards.stageToLocalCoordinates(pos); + localTrumpX = pos.x; + localTrumpY = pos.y; + + trumpCardActor.setPosition(stageHandX, stageHandY); + trumpCardActor.setRotation(0); + + handCard.setPosition(localTrumpX, localTrumpY); + handCard.setRotation(0); + + sequence.addAction(parallel( + targeting(trumpCardActor, moveTo(stageTrumpX, stageTrumpY, AnimationTimings.WEREWOLF_SWAP)), + targeting(handCard, moveTo(localHandX, localHandY, AnimationTimings.WEREWOLF_SWAP)) + )); } else { - var name = state.getPlayers().get(player); - setPersistentMessage(game.messages.format(key + "other", name)); + var handCard = new CardActor(trumpCardActor.getCard(), atlas); + handCard.setPosition(10, 10); + trumpCardActor.setCard(Card.WEREWOLF); + game.data.stage.addActor(handCard); + + sequence.addAction(seat.moveToHand(trumpCardActor, 0)); + sequence.addAction(parallel( + targeting(trumpCardActor, moveTo(10,10, AnimationTimings.WEREWOLF_SWAP)), + targeting(trumpCardActor, rotateTo(0, AnimationTimings.WEREWOLF_SWAP)), + seat.moveToHand(handCard, AnimationTimings.WEREWOLF_SWAP) + )); + sequence.addAction(removeActor(handCard)); } + })); + execute(sequence); + } + + // + public void showTrumpOverlay(UUID player, Card trumpCard, Card.Suit trumpSuit) { + if (trumpCardActor == null) { + trumpCardActor = new CardActor(Card.HIDDEN, atlas); } + + if (trumpSuitActor == null) { + trumpSuitActor = new CardActor(Card.HIDDEN, atlas); + } + + execute(new TrumpOverlay(this, players.get(player), trumpCard, trumpSuit)); + } + + public void showColoredCardOverlay(Card card, long timeout) { + if (card == Card.JUGGLER) { + execute(new PlayColoredCardOverlay(this, timeout, card, Card.JUGGLER_RED, Card.JUGGLER_GREEN, Card.JUGGLER_BLUE, Card.JUGGLER_YELLOW)); + } else if (card == Card.CLOUD) { + execute(new PlayColoredCardOverlay(this, timeout, card, Card.CLOUD_RED, Card.CLOUD_GREEN, Card.CLOUD_BLUE, Card.CLOUD_YELLOW)); + } else { + throw new IllegalArgumentException(); + } + } + + public InteractionOverlay showPickTrumpOverlay(long timeout, boolean allowNone) { + var overlay = new PickTrumpOverlay(this, timeout, allowNone); + execute(overlay); + return overlay; } + public void showScoreOverlay() { + execute(sequence( + run(() -> padOfTruth.setEnabled(false)), + parallel( + targeting(padOfTruth, scaleTo(1, 1, AnimationTimings.OVERLAY_SHARED_ELEMENT)), + targeting(padOfTruth, moveTo((WizardGame.WIDTH - padOfTruth.getWidth()) / 2, (WizardGame.HEIGHT - padOfTruth.getHeight()) / 2, AnimationTimings.OVERLAY_SHARED_ELEMENT)) + ), + delay(AnimationTimings.OVERLAY_HOLD), + parallel( + targeting(padOfTruth, scaleTo(PadOfTruth.COLLAPSED_SCALE, PadOfTruth.COLLAPSED_SCALE, AnimationTimings.OVERLAY_SHARED_ELEMENT)), + targeting(padOfTruth, moveTo(WizardGame.WIDTH - 10 - PadOfTruth.EXTENDED_WIDTH, 10)) + ), + run(() -> padOfTruth.setEnabled(true)) + )); + } + + public InteractionOverlay showMakePredictionOverlay(int round, long timeout) { + var overlay = new MakePredictionOverlay(this, timeout, round); + execute(overlay); + return overlay; + } + + public InteractionOverlay showChangePredictionOverlay(int round, int oldPrediction, long timeout) { + var overlay = new ChangePredictionOverlay(this, timeout, round, oldPrediction); + execute(overlay); + return overlay; + } + // + + // public void addMessage(@Nls String text) { addMessage(text, false); } @@ -512,44 +545,33 @@ public class GameScreen extends MenuScreen { } if (text != null) { - persistentMessage = new Label(text, getData().skin); + persistentMessage = new Label(text, game.data.skin); messages.addActor(persistentMessage); } }); } - - 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(); - } - - if (checkAction(JUGGLE_CARD) && juggledCard != null) { - handCards.setSelected(juggledCard); - juggledCard = null; - } + if (success) closeInteractionOverlay(); } public void timeout() { addMessage(game.messages.get("game.message.timeout")); - ready(true); + closeInteractionOverlay(); clearActivePlayer(); } public void sync() { pendingSync.set(true); - sending.set(true); + } + + public void closeInteractionOverlay() { + execute(() -> { + if (currentAction instanceof Overlay overlay && overlay instanceof InteractionOverlay) { + overlay.close(); + } + }); } @Override @@ -579,8 +601,8 @@ public class GameScreen extends MenuScreen { var animation = parallel(); removed.forEach(actor -> { - getData().stage.addActor(actor); - animation.addAction(targeting(actor, left.moveToHand(AnimationTimings.JUGGLE))); + game.data.stage.addActor(actor); + animation.addAction(left.moveToHand(actor, AnimationTimings.JUGGLE)); }); added.forEach(actor -> { @@ -600,7 +622,6 @@ public class GameScreen extends MenuScreen { // apply start values var startPos = new Vector2(right.getHandX(), right.getHandY()); handCards.stageToLocalCoordinates(startPos); - actor.setOrigin(actor.getWidth() / 2, actor.getHeight() / 2); animation.addAction(targeting(actor, moveTo(startPos.x, startPos.y))); animation.addAction(targeting(actor, rotateTo(right.getHandAngle()))); @@ -617,6 +638,7 @@ public class GameScreen extends MenuScreen { @Getter public enum Seat { + FALLBACK(WizardGame.WIDTH * 0.5f, WizardGame.HEIGHT * 0.5f, 0, 0, 0, WizardGame.WIDTH * 0.5f, WizardGame.HEIGHT * 0.5f), BOTTOM(WizardGame.WIDTH * 0.5f, 0, 0, 0, Align.bottom, WizardGame.WIDTH * 0.5f, 300), LEFT(0, WizardGame.HEIGHT * 0.5f, 50, WizardGame.HEIGHT * 0.5f + 110f, Align.bottomLeft, 117.5f, WizardGame.HEIGHT * 0.5f), TOP_LEFT(WizardGame.WIDTH * 0.25f, WizardGame.HEIGHT, WizardGame.WIDTH * 0.25f, WizardGame.HEIGHT - 50, Align.top, WizardGame.WIDTH * 0.25f, WizardGame.HEIGHT - 200), @@ -655,14 +677,13 @@ public class GameScreen extends MenuScreen { var actor = new CardActor(card, atlas); actor.setPosition(getHandX() - actor.getWidth() / 2, getHandY() - actor.getHeight() / 2); actor.setRotation(getHandAngle()); - actor.setOrigin(actor.getWidth() / 2, actor.getHeight() / 2); return actor; } - public Action moveToHand(float duration) { + public Action moveToHand(CardActor actor, float duration) { return parallel( - moveTo(handX, handY, duration), - rotateTo(handAngle, duration) + targeting(actor, moveTo(handX, handY, duration)), + targeting(actor, rotateTo(handAngle, duration)) ); } } 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 index 6cbc5da..8e70bc4 100644 --- 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 @@ -1,23 +1,32 @@ package eu.jonahbauer.wizard.client.libgdx.state; import eu.jonahbauer.wizard.client.libgdx.Client; +import eu.jonahbauer.wizard.client.libgdx.actors.game.overlay.InteractionOverlay; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; import eu.jonahbauer.wizard.client.libgdx.util.Pair; +import eu.jonahbauer.wizard.common.messages.client.InteractionMessage; 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.player.*; 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.Data; import lombok.Getter; +import lombok.experimental.Accessors; import lombok.extern.log4j.Log4j2; +import java.lang.ref.WeakReference; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*; @Log4j2 @Getter @@ -27,7 +36,7 @@ public final class Game extends BaseState { private final String sessionName; private final String secret; - private final Map players; + private final LinkedHashMap players; private final Map scores = new HashMap<>(); private int round = -1; @@ -37,13 +46,20 @@ public final class Game extends BaseState { private int trick = -1; private final List> stack = new ArrayList<>(); + private Interaction currentInteraction; + private int pendingClearActivePlayer = 0; private Card trumpCard; private Card.Suit trumpSuit; private GameScreen gameScreen; + private final AtomicBoolean sending = new AtomicBoolean(false); + + private boolean juggling; + private Card juggleCard; + private boolean werewolf; - public Game(UUID self, UUID session, String sessionName, String secret, Map players) { + public Game(UUID self, UUID session, String sessionName, String secret, LinkedHashMap players) { this.self = self; this.session = session; this.sessionName = sessionName; @@ -53,103 +69,306 @@ public final class Game extends BaseState { @Override public Optional onEnter(Client client) { - gameScreen = new GameScreen(client.getGame()); + gameScreen = new GameScreen(client.getGame(), self, players); 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(); + try { + if (message instanceof GameMessage game) { + var observerMessage = game.getObserverMessage(); + if (observerMessage instanceof StateMessage state) { + switch (state.getState()) { + case "starting_round" -> onStartRound(); + case "starting_trick" -> onStartTrick(); + case "juggling" -> onJuggle(); + case "finished" -> { + onFinished(); + return returnToSession(); + } + case "error" -> { + onError(); + return returnToSession(); + } } - case "finished", "error" -> { - return returnToSession(); - } - case "juggling" -> gameScreen.setJuggling(true); - } - } 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 if (observerMessage instanceof HandMessage hand) { + onHandMessage(hand.getPlayer(), hand.getHand()); + } else if (observerMessage instanceof PredictionMessage prediction) { + onPredictionMessage(prediction.getPlayer(), prediction.getPrediction()); + } else if (observerMessage instanceof TrumpMessage trump) { + onTrumpMessage(trump.getCard(), trump.getSuit()); + } else if (observerMessage instanceof TrickMessage trick) { + onTrickMessage(trick.getPlayer(), trick.getCards()); + } else if (observerMessage instanceof CardMessage card) { + onCardMessage(card.getPlayer(), card.getCard()); + } else if (observerMessage instanceof ScoreMessage score) { + onScoreMessage(score.getPoints()); + } else if (observerMessage instanceof UserInputMessage input) { + onUserInputMessage(input.getPlayer(), input.getAction(), input.getTimeout()); + } else if (observerMessage instanceof TimeoutMessage) { + onTimeoutMessage(); } else { - gameScreen.setActivePlayer(input.getPlayer(), input.getAction(), input.getTimeout()); + log.fatal("Unknown observer message type {}.", observerMessage.getClass()); + // TODO user feedback + return Optional.of(new Menu()); } - } else if (observerMessage instanceof TimeoutMessage) { - gameScreen.timeout(); - } else { - // TODO user feedback - 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(), true); - gameScreen.ready(false); + return Optional.empty(); + } else if (message instanceof NackMessage nack) { + return onNackMessage(client, message, nack); + } else if (message instanceof AckMessage) { + onAckMessage(); return Optional.empty(); } else { return unexpectedMessage(client, message); } - } else if (message instanceof AckMessage) { - gameScreen.ready(true); + } finally { + if (pendingClearActivePlayer > 0 && --pendingClearActivePlayer == 0) { + finishInteraction(); + } + } + } + + private void onStartRound() { + log.info("Round {} is starting...", round + 1); + + round ++; + predictions.clear(); + tricks.clear(); + trumpSuit = null; + trumpCard = null; + stack.clear(); + trick = -1; + gameScreen.startRound(round); + } + + private void onStartTrick() { + log.info("Trick {} is starting...", trick + 1); + trick ++; + stack.clear(); + finishInteraction(); + gameScreen.startTrick(); + } + + private void onJuggle() { + juggling = true; + juggleCard = null; + } + + private void onFinished() { + log.info("The game has finished."); + } + + private void onError() { + log.error("The game has finished with an error."); + } + + private void onHandMessage(UUID player, List hand) { + checkPlayer(player); + log.info("{} hand cards are: {}", nameOf(player, true, true), hand); + + if (juggling) checkActivePlayer(player, JUGGLE_CARD); + finishInteraction(); + + hands.put(player, hand); + gameScreen.setSelectedCard(null); + gameScreen.setHand(player, hand, juggling); + juggling = false; + } + + private void onPredictionMessage(UUID player, int prediction) { + checkPlayer(player); + checkActivePlayer(player, MAKE_PREDICTION, CHANGE_PREDICTION); + log.info("{} predicted: {}", nameOf(player, true, false), prediction); + + boolean changed = currentInteraction != null && currentInteraction.action() == CHANGE_PREDICTION; + finishInteraction(); + + predictions.put(player, prediction); + gameScreen.addPrediction(round, player, prediction, changed); + } + + private void onTrumpMessage(Card trumpCard, Card.Suit trumpSuit) { + if (trumpCard == null) { + log.info("There is no trump in this round."); + } else { + log.info("The trump suit is {} ({}).", trumpSuit != null ? trumpSuit : "yet to be determined.", trumpCard); + } + + finishInteraction(); + + this.trumpCard = trumpCard; + this.trumpSuit = trumpSuit; + if (trumpCard == Card.WEREWOLF && trumpSuit == null) { + werewolf = true; + } else { + werewolf = false; + gameScreen.showTrumpOverlay(null, trumpCard, trumpSuit); + } + } + + private void onTrickMessage(UUID player, List cards) { + checkPlayer(player); + log.info("This trick {} goes to {}.", cards, nameOf(player)); + + this.stack.clear(); + this.tricks.computeIfAbsent(player, p -> new ArrayList<>()) + .add(cards); + gameScreen.finishTrick(player); + } + + private void onCardMessage(UUID player, Card card) { + checkPlayer(player); + checkActivePlayer(player, PLAY_CARD); + log.info("{} played {}.", nameOf(player, true, false), card); + finishInteraction(); + + this.stack.add(Pair.of(player, card)); + + var handCard = switch (card) { + 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; + }; + + var hand = this.hands.get(player); + if (hand != null) { + hand.remove(handCard); + } + + gameScreen.playCard(player, handCard); + } + + private void onScoreMessage(Map points) { + log.info("The scores are as follows: " + points); + points.forEach((player, p) -> scores.merge(player, p, Integer::sum)); + gameScreen.addScores(round, points); + gameScreen.showScoreOverlay(); + } + + private void onUserInputMessage(UUID player, UserInputMessage.Action action, long timeout) { + checkPlayer(player); + log.info( + "Waiting for input {} from {}. (times out at {})", + action, + nameOf(player), + LocalDateTime.ofInstant(Instant.ofEpochMilli(timeout), ZoneId.systemDefault()) + ); + + if (action == UserInputMessage.Action.SYNC) { + gameScreen.sync(); + } else { + currentInteraction = new Interaction(player, action, timeout); + gameScreen.setActivePlayer(player, action, timeout); + + if (werewolf && action == PICK_TRUMP) { + gameScreen.swapTrumpCard(player); + } + + if (isActive()) { + switch (action) { + case PICK_TRUMP -> currentInteraction.overlay(gameScreen.showPickTrumpOverlay(timeout, werewolf)); + case MAKE_PREDICTION -> currentInteraction.overlay(gameScreen.showMakePredictionOverlay(round, timeout)); + case CHANGE_PREDICTION -> currentInteraction.overlay(gameScreen.showChangePredictionOverlay(round, predictions.get(player), timeout)); + case PLAY_CARD -> gameScreen.setPersistentMessage(gameScreen.getMessages().get("game.message.play_card.self")); + } + } + + if (werewolf && action == PICK_TRUMP) { + werewolf = false; + } + } + } + + private void onTimeoutMessage() { + log.info("The previous interaction timed out."); + delayedFinishInteraction(); + gameScreen.timeout(); + } + + private Optional onNackMessage(Client client, ServerMessage message, NackMessage nack) { + sending.set(false); + + if (isActive() && currentInteraction.action() == JUGGLE_CARD && juggleCard != null) { + juggleCard = null; + } + + int code = nack.getCode(); + if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) { + log.error(nack.getMessage()); + gameScreen.addMessage(nack.getMessage(), true); + gameScreen.ready(false); return Optional.empty(); } else { return unexpectedMessage(client, message); } } + private void onAckMessage() { + log.info("OK"); + sending.set(false); + + if (isActive() && currentInteraction.action() == JUGGLE_CARD && juggleCard != null) { + gameScreen.setSelectedCard(juggleCard); + juggleCard = null; + } + + gameScreen.ready(true); + } + // + + // + public Optional onCardClicked(Client client, Card card) { + if (isActive()) { + if (currentInteraction.action() == PLAY_CARD) { + if (card == Card.CLOUD || card == Card.JUGGLER) { + gameScreen.showColoredCardOverlay(card, currentInteraction.timeout()); + } else { + send(client, new PlayCardMessage(card)); + } + return Optional.empty(); + } else if (currentInteraction.action() == JUGGLE_CARD) { + if (send(client, new JuggleMessage(juggleCard))) { + juggleCard = card; + } + return Optional.empty(); + } + } + + gameScreen.addMessage("You cannot do that right now.", true); + return Optional.empty(); + } + + public Optional onSuitClicked(Client client, Card.Suit suit) { + if (isActive() && currentInteraction.action() == PICK_TRUMP) { + send(client, new PickTrumpMessage(suit)); + } else { + gameScreen.addMessage("You cannot do that right now.", true); + } + return Optional.empty(); + } + + public Optional onPredictionMade(Client client, int prediction) { + if (isActive()) { + if (currentInteraction.action() == MAKE_PREDICTION || currentInteraction.action() == CHANGE_PREDICTION) { + send(client, new PredictMessage(prediction)); + return Optional.empty(); + } + } + + gameScreen.addMessage("You cannot do that right now.", true); + return Optional.empty(); + } + + public Optional sync(Client client) { + send(client, new ContinueMessage()); + return Optional.empty(); + } + // + private Optional returnToSession() { return Optional.of(new Session( new SessionData(session, sessionName, -1, null), @@ -160,88 +379,115 @@ public final class Game extends BaseState { )); } - 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()) - ); - } + /** + * Sends a message. Only one message may be sent at a time until without receiving an answer. + * @return {@code true} iff the message was sent + */ + private boolean send(Client client, PlayerMessage message) { + if (message instanceof ContinueMessage || !sending.getAndSet(true)) { + client.send(new InteractionMessage(message)); + return true; + } else { + gameScreen.addMessage("Please slow down.", true); + return false; + } + } + + /** + * Checks whether some action from the player is expected. + */ + private boolean isActive() { + return currentInteraction != null && (currentInteraction.player() == null || self.equals(currentInteraction.player())); + } + + /** + * Close the interaction overlay associated with the current interaction and reset the current interaction. + */ + private void finishInteraction() { + if (currentInteraction != null) { + var overlay = currentInteraction.overlay(); + if (overlay != null) overlay.close(); + } + + currentInteraction = null; + gameScreen.clearActivePlayer(); + } + + /** + * UI-wise equivalent to {@link #finishInteraction()} but information about the interaction is kept until after the + * next message. + */ + private void delayedFinishInteraction() { + pendingClearActivePlayer = 2; + if (currentInteraction != null) { + var overlay = currentInteraction.overlay(); + if (overlay != null) overlay.close(); + } + gameScreen.clearActivePlayer(); + } + + // + + /** + * Checks whether the given player is known and logs errors. + */ + private void checkPlayer(UUID player) { + if (player != null && !players.containsKey(player)) { + log.error("Unknown player {}.", player); + } + } + + /** + * Checks whether one of the given actions is currently expected from the given player and logs errors. + */ + private void checkActivePlayer(UUID player, UserInputMessage.Action...actions) { + if (currentInteraction != null && (currentInteraction.player() == null || currentInteraction.player().equals(player))) { + for (var action : actions) { + if (currentInteraction.action() == action) { + return; } - } 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"); } + + log.warn("Received message does not match the previous user input message."); } + /** + * Returns the name of the given player. + */ private String nameOf(UUID player) { + return nameOf(player, false, false); + } + + /** + * Returns the name of the given player, optionally with a capitalized first letter and/or a possessive suffix ("'s") + */ + private String nameOf(UUID player, boolean capitalize, boolean possessive) { if (player == null) { - return "all players"; + return (capitalize ? "A" : "a") + "ll players" + (possessive ? "'" : ""); + } else if (self.equals(player)) { + return (capitalize ? "Y" : "y") + "ou" + (possessive ? "r" : ""); } else { - return players.get(player); + return players.get(player) + (possessive ? "'s" : ""); + } + } + // + + @Data + @Accessors(fluent = true) + public static final class Interaction { + private final UUID player; + private final UserInputMessage.Action action; + private final long timeout; + private WeakReference overlay; + + public void overlay(InteractionOverlay overlay) { + this.overlay = new WeakReference<>(overlay); + } + + public InteractionOverlay overlay() { + if (overlay == null) return null; + else return overlay.get(); } } } 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 fe76d0a..509ff95 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 @@ -12,7 +12,6 @@ import lombok.Getter; import lombok.extern.log4j.Log4j2; import java.util.*; -import java.util.stream.Collectors; @Log4j2 @Getter @@ -74,10 +73,9 @@ public final class Session extends BaseState { return Optional.empty(); } else if (message instanceof StartingGameMessage) { - return Optional.of(new Game( - self, session, sessionName, secret, - players.stream().collect(Collectors.toMap(PlayerData::getUuid, PlayerData::getName)) - )); + var players = new LinkedHashMap(); + this.players.forEach(player -> players.put(player.getUuid(), player.getName())); + return Optional.of(new Game(self, session, sessionName, secret, players)); } 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/java/eu/jonahbauer/wizard/client/libgdx/util/Triple.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/util/Triple.java deleted file mode 100644 index 2fdc8c1..0000000 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/util/Triple.java +++ /dev/null @@ -1,7 +0,0 @@ -package eu.jonahbauer.wizard.client.libgdx.util; - -public record Triple(F first, S second, T third) { - public static Triple of(F first, S second, T third) { - return new Triple<>(first, second, third); - } -} diff --git a/wizard-client/wizard-client-libgdx/core/src/main/textures/game/background.png b/wizard-client/wizard-client-libgdx/core/src/main/textures/game/background.png deleted file mode 100644 index a88e44e..0000000 Binary files a/wizard-client/wizard-client-libgdx/core/src/main/textures/game/background.png and /dev/null differ diff --git a/wizard-client/wizard-client-libgdx/core/src/main/textures/menu/background.png b/wizard-client/wizard-client-libgdx/core/src/main/textures/menu/background.png index a8b6aec..2c87e99 100644 Binary files a/wizard-client/wizard-client-libgdx/core/src/main/textures/menu/background.png and b/wizard-client/wizard-client-libgdx/core/src/main/textures/menu/background.png differ diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/GameState.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/GameState.java index 5722073..23c1044 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/GameState.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/GameState.java @@ -1,6 +1,7 @@ package eu.jonahbauer.wizard.core.machine; import eu.jonahbauer.wizard.common.machine.TimeoutState; +import eu.jonahbauer.wizard.common.messages.player.ContinueMessage; import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.states.SyncState; @@ -57,6 +58,8 @@ public abstract class GameState implements TimeoutState { // public Optional onMessage(Game game, UUID player, PlayerMessage message) { + if (message instanceof ContinueMessage) return Optional.empty(); + throw new IllegalStateException("You cannot do that right now."); } diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java index b6c455d..046923a 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java @@ -35,7 +35,7 @@ public class Session implements Observer { private final Configuration configuration; @Getter(AccessLevel.NONE) - private final Map players = new HashMap<>(); + private final Map players = new LinkedHashMap<>(); private Game game; private final List> messages = new ArrayList<>();