From 851c3a3451aeb14572280df135e16058c4cd83a7 Mon Sep 17 00:00:00 2001 From: Jonah Bauer Date: Mon, 10 Jan 2022 22:00:37 +0100 Subject: [PATCH] support for juggling and multicolored cards --- .../client/libgdx/AnimationTimings.java | 29 ++ .../libgdx/actions/ChangeParentAction.java | 7 +- .../client/libgdx/actors/game/CardStack.java | 15 +- .../client/libgdx/actors/game/CardsGroup.java | 61 +++- .../client/libgdx/actors/game/PadOfTruth.java | 12 +- .../libgdx/actors/game/overlay/Overlay.java | 5 +- .../game/overlay/PlayColoredCardOverlay.java | 78 ++++++ .../game/overlay/StartRoundOverlay.java | 5 +- .../actors/game/overlay/TrumpOverlay.java | 19 +- .../client/libgdx/screens/GameScreen.java | 265 ++++++++++++++---- .../wizard/client/libgdx/state/Game.java | 4 +- .../wizard/client/libgdx/util/Triple.java | 7 + .../main/resources/i18n/messages.properties | 5 +- .../resources/i18n/messages_de.properties | 5 +- .../jonahbauer/wizard/common/model/Card.java | 29 +- 15 files changed, 432 insertions(+), 114 deletions(-) create mode 100644 wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/AnimationTimings.java create mode 100644 wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PlayColoredCardOverlay.java create mode 100644 wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/util/Triple.java 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 new file mode 100644 index 0000000..1c85cc3 --- /dev/null +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/AnimationTimings.java @@ -0,0 +1,29 @@ +package eu.jonahbauer.wizard.client.libgdx; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class AnimationTimings { + public static final float JUGGLE = 0.25f; + + public static final float STACK_EXPAND = 0.25f; + public static final float STACK_COLLAPSE = 0.25f; + + 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 PAD_OF_TRUTH_EXPAND = 0.25f; + public static final float PAD_OF_TRUTH_COLLAPSE = 0.25f; + + public static final float HAND_LAYOUT = 0.15f; + + public static final float OVERLAY_HOLD = 3f; + public static final float OVERLAY_FADE = 0.1f; + + public static final float OVERLAY_TRUMP = .3f; + + public static final float MESSAGE_HOLD = 1.5f; + public static final float MESSAGE_FADE = 0.5f; +} diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/ChangeParentAction.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/ChangeParentAction.java index a99699b..e94ae4a 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/ChangeParentAction.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/ChangeParentAction.java @@ -7,8 +7,6 @@ import com.badlogic.gdx.utils.Pool; import com.badlogic.gdx.utils.Pools; import lombok.Setter; -/** Removes an actor from the stage. - * @author Nathan Sweet */ public class ChangeParentAction extends Action { private final Pool vectorPool = Pools.get(Vector2.class); @@ -16,11 +14,12 @@ public class ChangeParentAction extends Action { private Group parent; private boolean finished; - public boolean act (float delta) { + public boolean act(float delta) { if (!finished) { finished = true; var pos = vectorPool.obtain(); - pos.set(0, 0); + pos.set(target.getX(), target.getY()); + target.parentToLocalCoordinates(pos); target.localToStageCoordinates(pos); parent.stageToLocalCoordinates(pos); target.setPosition(pos.x, pos.y); 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 3a16444..1eb3085 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 @@ -1,6 +1,7 @@ package eu.jonahbauer.wizard.client.libgdx.actors.game; import com.badlogic.gdx.scenes.scene2d.*; +import eu.jonahbauer.wizard.client.libgdx.AnimationTimings; import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; import lombok.Data; @@ -10,9 +11,7 @@ import java.util.*; import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*; public class CardStack extends Group { - private static final float EXPAND_DURATION = 0.25f; private static final float EXPANDED_ROTATION_DEVIATION = 10; - private static final float COLLAPSE_DURATION = 0.25f; private static final float COLLAPSED_ROTATION_DEVIATION = 60; private static final float COLLAPSED_POSITION_DEVIATION = 15; @@ -126,8 +125,12 @@ public class CardStack extends Group { if (action != null) actor.removeAction(action); action = parallel( - moveTo(seat.getFrontX() - actor.getWidth() / 2, seat.getFrontY() - actor.getHeight() / 2, EXPAND_DURATION), - rotateTo(expandedRotation, EXPAND_DURATION) + moveTo( + seat.getFrontX() - actor.getWidth() / 2, + seat.getFrontY() - actor.getHeight() / 2, + AnimationTimings.STACK_EXPAND + ), + rotateTo(expandedRotation, AnimationTimings.STACK_EXPAND) ); actor.addAction(action); @@ -137,8 +140,8 @@ public class CardStack extends Group { if (action != null) actor.removeAction(action); action = parallel( - moveTo(x, y, COLLAPSE_DURATION), - rotateTo(rotation, COLLAPSE_DURATION) + moveTo(x, y, AnimationTimings.STACK_COLLAPSE), + rotateTo(rotation, AnimationTimings.STACK_COLLAPSE) ); actor.addAction(action); 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 49e9ade..5ab9101 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 @@ -3,9 +3,13 @@ package eu.jonahbauer.wizard.client.libgdx.actors.game; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.scenes.scene2d.*; +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.InputEvent; +import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.utils.Pools; +import eu.jonahbauer.wizard.client.libgdx.AnimationTimings; +import eu.jonahbauer.wizard.client.libgdx.util.Pair; import eu.jonahbauer.wizard.common.model.Card; import lombok.Getter; import lombok.Setter; @@ -13,11 +17,10 @@ import lombok.Setter; import java.util.*; import java.util.function.Consumer; -import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*; +import static com.badlogic.gdx.scenes.scene2d.actions.Actions.moveTo; public class CardsGroup extends WidgetGroup { private static final float TARGET_SPACING = -50f; - private static final float LAYOUT_DURATION = 0.15f; @Getter private final float prefWidth = 0; @@ -38,7 +41,12 @@ public class CardsGroup extends WidgetGroup { private CardActor dragTarget; private float dragStartX; + @Getter + private SelectMode selectMode = SelectMode.NONE; @Setter + @Getter + private Card selected; + private Consumer onClickListener; public CardsGroup(List cards, TextureAtlas atlas) { @@ -122,7 +130,8 @@ public class CardsGroup extends WidgetGroup { for (var child : getChildren()) { if (child instanceof CardActor card) { float height = card.getHeight(); - if (child == dragTarget || dragTarget == null && child == target) { + if (selectMode == SelectMode.NONE && (child == dragTarget || dragTarget == null && child == target) + || selectMode == SelectMode.SINGLE && card.getCard() == selected) { height += speed * delta; } else { height -= speed * delta; @@ -140,7 +149,9 @@ public class CardsGroup extends WidgetGroup { float height = getHeight(); float width = getWidth(); - cardX = new float[count]; + if (cardX == null || cardX.length != count) { + cardX = new float[count]; + } cardWidth = height / CardActor.ASPECT_RATIO; spacing = width - count * cardWidth; @@ -155,7 +166,7 @@ public class CardsGroup extends WidgetGroup { // position if (animate) { - child.addAction(moveTo(x, y, LAYOUT_DURATION)); + child.addAction(moveTo(x, y, AnimationTimings.HAND_LAYOUT)); } else { child.setPosition(x, y); } @@ -172,13 +183,27 @@ public class CardsGroup extends WidgetGroup { animate = false; } - public void update(List cards) { - clearChildren(); + public Pair, List> update(List cards) { + var added = new ArrayList<>(cards); + var removed = new ArrayList(); + + for (var child : getChildren()) { + if (child instanceof CardActor actor) { + var card = actor.getCard(); + + if (!added.remove(card)) { + removed.add(card); + } + } + } + + var removedActors = removed.stream().map(this::remove).toList(); + var addedActors = added.stream().sorted().map(card -> new CardActor(card, atlas)).toList(); + + addedActors.forEach(this::addActor); + layout(); - cards.stream() - .map(card -> new CardActor(card, atlas)) - .forEach(this::addActor); - invalidate(); + return Pair.of(removedActors, addedActors); } public CardActor remove(Card card) { @@ -216,4 +241,16 @@ public class CardsGroup extends WidgetGroup { 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 b984c11..cb707fb 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 @@ -5,18 +5,18 @@ import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Touchable; +import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.actions.ScaleToAction; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.Skin; 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; public class PadOfTruth extends Table { - private static final float EXPAND_DURATION = 0.25f; 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 = CardActor.PREF_HEIGHT / EXTENDED_HEIGHT; private final Label[] names = new Label[6]; @@ -42,9 +42,7 @@ public class PadOfTruth extends Table { public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) { if (fromActor != null && isAscendantOf(fromActor)) return; if (action != null) removeAction(action); - action = new ScaleToAction(); - action.setDuration(EXPAND_DURATION); - action.setScale(1); + action = Actions.scaleTo(1, 1, AnimationTimings.PAD_OF_TRUTH_EXPAND); addAction(action); } @@ -52,9 +50,7 @@ public class PadOfTruth extends Table { public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) { if (toActor != null && isAscendantOf(toActor)) return; if (action != null) removeAction(action); - action = new ScaleToAction(); - action.setDuration(COLLAPSE_DURATION); - action.setScale(COLLAPSED_SCALE); + action = Actions.scaleTo(COLLAPSED_SCALE, COLLAPSED_SCALE, AnimationTimings.PAD_OF_TRUTH_COLLAPSE); addAction(action); } }); 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 3d7ea1c..267c875 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 @@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.ui.Container; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; import com.badlogic.gdx.utils.I18NBundle; +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; @@ -14,8 +15,6 @@ import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; 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 I18NBundle messages; @@ -78,7 +77,7 @@ public abstract class Overlay extends Action { public void finish() { getRoot().addAction(sequence( - targeting(root, alpha(0.0f, .1f, Interpolation.pow2Out)), + targeting(root, alpha(0.0f, AnimationTimings.OVERLAY_FADE, Interpolation.pow2Out)), run(this::finishInternal) )); } 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 new file mode 100644 index 0000000..b45e55c --- /dev/null +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/overlay/PlayColoredCardOverlay.java @@ -0,0 +1,78 @@ +package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay; + +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.InputEvent; +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.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; + +public class PlayColoredCardOverlay extends Overlay implements InteractionOverlay { + + private final EnumMap actors = new EnumMap<>(Card.Suit.class); + private final EnumMap cards = new EnumMap<>(Card.Suit.class); + + private final Card card; + + public PlayColoredCardOverlay(GameScreen gameScreen, long timeout, Card card, Card red, Card green, Card blue, Card yellow) { + super(gameScreen, timeout); + this.card = card; + this.cards.put(Card.Suit.RED, red); + this.cards.put(Card.Suit.GREEN, green); + this.cards.put(Card.Suit.BLUE, blue); + this.cards.put(Card.Suit.YELLOW, yellow); + } + + @Override + public Actor createContent() { + var root = new VerticalGroup().columnCenter().space(10); + + var prompt = new Label(messages.get("game.overlay.play_colored_card.prompt"), data.skin); + var cardGroup = new HorizontalGroup().space(20); + + var card = new CardActor(this.card, atlas); + root.addActorAt(0, card); + root.padTop(- CardActor.PREF_HEIGHT); + + actors.put(Card.Suit.RED, new CardActor(Card.Suit.RED, atlas)); + actors.put(Card.Suit.GREEN, new CardActor(Card.Suit.GREEN, atlas)); + actors.put(Card.Suit.BLUE, new CardActor(Card.Suit.BLUE, atlas)); + actors.put(Card.Suit.YELLOW, new CardActor(Card.Suit.YELLOW, atlas)); + actors.values().forEach(cardGroup::addActor); + + cardGroup.addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + var target = event.getTarget(); + for (Card.Suit suit : Card.Suit.values()) { + if (actors.get(suit) == target) { + screen.send(new PlayCardMessage(cards.get(suit))); + break; + } + } + } + }); + + root.addActor(prompt); + root.addActor(cardGroup); + + var cancel = new TextButton(messages.get("game.overlay.play_colored_card.cancel"), data.skin, "simple"); + cancel.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + finishInternal(); + } + }); + root.addActor(cancel); + + return root; + } +} 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 1513a82..3b8e4e5 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 @@ -5,6 +5,7 @@ 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; @@ -36,8 +37,8 @@ public class StartRoundOverlay extends Overlay { var root = getRoot(); root.addAction(sequence( - delay(OVERLAY_TIME), - targeting(root, alpha(0.0f, .5f, Interpolation.pow2Out)), + delay(AnimationTimings.OVERLAY_HOLD), + targeting(root, alpha(0.0f, AnimationTimings.OVERLAY_FADE, Interpolation.pow2Out)), run(this::finishInternal) )); } 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 415b55c..d6ea219 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 @@ -8,6 +8,7 @@ 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.AnimationTimings; 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; @@ -72,7 +73,13 @@ public class TrumpOverlay extends Overlay { 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) + Map.entry(Card.YELLOW_13, Card.Suit.YELLOW), + Map.entry(Card.RED_JESTER, Card.Suit.NONE), + Map.entry(Card.GREEN_JESTER, Card.Suit.NONE), + Map.entry(Card.BLUE_JESTER, Card.Suit.NONE), + Map.entry(Card.YELLOW_JESTER, Card.Suit.NONE), + Map.entry(Card.BOMB, Card.Suit.NONE), + Map.entry(Card.FAIRY, Card.Suit.NONE) ); private final String player; @@ -154,7 +161,7 @@ public class TrumpOverlay extends Overlay { cardAnimation.addAction(sequence( targeting(trumpCardActor, removeActorSilently()), targeting(trumpCardActor, changeParent(parent)), - targeting(trumpCardActor, moveTo(10, 10, .3f)) + targeting(trumpCardActor, moveTo(10, 10, AnimationTimings.OVERLAY_TRUMP)) )); } @@ -164,9 +171,9 @@ public class TrumpOverlay extends Overlay { targeting(trumpSuitActor, changeParent(parent)), run(trumpCardActor::toFront), parallel( - targeting(trumpSuitActor, rotateTo(-90, .3f)), + targeting(trumpSuitActor, rotateTo(-90, AnimationTimings.OVERLAY_TRUMP)), targeting(trumpSuitActor, - moveTo(10, 10 + (trumpSuitActor.getHeight() + trumpSuitActor.getWidth()) / 2, .3f) + moveTo(10, 10 + (trumpSuitActor.getHeight() + trumpSuitActor.getWidth()) / 2, AnimationTimings.OVERLAY_TRUMP) ) ) )); @@ -174,9 +181,9 @@ public class TrumpOverlay extends Overlay { var root = getRoot(); root.addAction(sequence( - delay(OVERLAY_TIME), + delay(AnimationTimings.OVERLAY_HOLD), parallel( - targeting(root, alpha(0.0f, .3f, Interpolation.pow2Out)), + targeting(root, alpha(0.0f, AnimationTimings.OVERLAY_FADE, Interpolation.pow2Out)), cardAnimation ), run(this::finishInternal) 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 1528eb8..9c53c84 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,14 +3,17 @@ 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.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Action; import com.badlogic.gdx.scenes.scene2d.Touchable; +import com.badlogic.gdx.scenes.scene2d.actions.MoveToAction; import com.badlogic.gdx.scenes.scene2d.ui.Container; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.I18NBundle; +import eu.jonahbauer.wizard.client.libgdx.AnimationTimings; import eu.jonahbauer.wizard.client.libgdx.GameAtlas; import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor; @@ -19,10 +22,11 @@ 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.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; @@ -46,7 +50,7 @@ public class GameScreen extends MenuScreen { private final List players; - private Pair activePlayer; + private Triple activePlayer; private CardsGroup handCards; private CardStack cardStack; @@ -69,6 +73,9 @@ public class GameScreen extends MenuScreen { private final AtomicBoolean sending = new AtomicBoolean(); private final AtomicBoolean pendingSync = new AtomicBoolean(); + private boolean juggling; + private Card juggledCard; + public GameScreen(WizardGame game) { super(game); this.state = (Game) game.getClient().getState(); @@ -88,7 +95,7 @@ public class GameScreen extends MenuScreen { prepareLabels(); handCards = new CardsGroup(Collections.emptyList(), atlas); - handCards.setOnClickListener(card -> send(new PlayCardMessage(card.getCard()))); + handCards.setOnClickListener(this::onCardClicked); var container = new Container<>(handCards); container.setPosition(360, 75); container.setSize(1200, CardActor.PREF_HEIGHT); @@ -183,7 +190,7 @@ public class GameScreen extends MenuScreen { private void prepareLabels() { for (UUID player : players) { - if (state.getSelf().equals(player)) continue; + if (isSelf(player)) continue; var label = new Label("", game.data.skin); var seat = seats.get(player); label.setX(seat.getLabelX()); @@ -199,12 +206,54 @@ public class GameScreen extends MenuScreen { var player = players.get(i); var name = state.getPlayers().get(player); padOfTruth.setName(i, name); - if (!state.getSelf().equals(player)) { + if (!isSelf(player)) { nameLabels.get(player).setText(name); } } } + 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."); + } + } + + private boolean checkAction(UserInputMessage.Action action) { + return activePlayer != null && (activePlayer.first() == null || isSelf(activePlayer.first())) && activePlayer.second() == action; + } + + private boolean isSelf(UUID uuid) { + return state.getSelf().equals(uuid); + } + public void startRound(int round) { execute(parallel( run(() -> { @@ -224,35 +273,67 @@ public class GameScreen extends MenuScreen { } public void startTrick() { - setActivePlayer(null, null, 0); + clearActivePlayer(); 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); 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) - )))), + run(() -> cardStack.removeAll().forEach(card -> { + var angle = (card.getRotation() % 360 + 360) % 360; + var rotation = rotateTo(angle < 90 || angle > 270 ? 0 : 180, AnimationTimings.STACK_FINISH_ROTATE); + rotation.setUseShortestDirection(true); + + action.addAction(sequence( + targeting(card, changeParent(game.data.stage.getRoot())), + parallel( + targeting(card, rotation), + targeting(card, moveTo( + seat.getFrontX() - card.getWidth() / 2, + seat.getFrontY() - card.getHeight() / 2, + AnimationTimings.STACK_FINISH_MOVE + )) + ), + targeting(card, alpha(0, AnimationTimings.STACK_FINISH_FADE)), + removeActor(card) + )); + })), action )); } public void setHand(UUID player, List cards) { - if (state.getSelf().equals(player)) { - execute(() -> handCards.update(cards)); + if (isSelf(player)) { + var sequence = sequence(); + sequence.addAction(run(() -> { + var changes = handCards.update(cards); + + // animate card changes + if (juggling) { + setJuggling(false); + sequence.addAction(animateJuggle(changes.first(), changes.second())); + } + })); + execute(sequence); } } @@ -268,7 +349,7 @@ public class GameScreen extends MenuScreen { String player = null; if (activePlayer != null && activePlayer.second() == PICK_TRUMP) { player = state.getPlayers().get(activePlayer.first()); - setActivePlayer(null, null, 0); + clearActivePlayer(); } execute(new TrumpOverlay(this, player, trumpCard, trumpSuit)); @@ -276,12 +357,13 @@ public class GameScreen extends MenuScreen { 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; - setActivePlayer(null, null, 0); + clearActivePlayer(); } - if (state.getSelf().equals(player)) { + if (isSelf(player)) { addMessage(game.messages.format("game.action." + (changed ? "change" : "make") + "_prediction.self", prediction)); } else { var name = state.getPlayers().get(player); @@ -293,10 +375,10 @@ public class GameScreen extends MenuScreen { public void playCard(UUID player, Card card) { if (activePlayer != null && activePlayer.first().equals(player) && activePlayer.second() == PLAY_CARD) { - setActivePlayer(null, null, 0); + clearActivePlayer(); } - if (state.getSelf().equals(player)) { + if (isSelf(player)) { addMessage(game.messages.get("game.action.play_card.self")); } else { var name = state.getPlayers().get(player); @@ -307,18 +389,19 @@ public class GameScreen extends MenuScreen { var sequence = sequence(); sequence.addAction(run(() -> { - CardActor actor; - if (state.getSelf().equals(player)) { + CardActor actor = null; + if (isSelf(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); } - actor.setOrigin(actor.getWidth() / 2, actor.getHeight() / 2); + + if (actor == null) { + actor = seat.createHandCard(card, atlas); + } + cardStack.add(seat, actor); sequence.addAction(delay(actor)); - sequence.addAction(delay(0.5f)); + sequence.addAction(delay(AnimationTimings.STACK_HOLD)); })); execute(sequence); } @@ -332,12 +415,16 @@ public class GameScreen extends MenuScreen { }); } + public void clearActivePlayer() { + setActivePlayer(null, null, 0); + } + public void setActivePlayer(UUID player, UserInputMessage.Action action, long timeout) { if (action == SYNC) throw new IllegalArgumentException(); // reset label color - if (activePlayer != null && nameLabels.containsKey(activePlayer.getKey())) { - var label = nameLabels.get(activePlayer.getKey()); + if (activePlayer != null && nameLabels.containsKey(activePlayer.first())) { + var label = nameLabels.get(activePlayer.first()); execute(() -> label.setStyle(labelStyleDefault)); } @@ -347,14 +434,15 @@ public class GameScreen extends MenuScreen { return; } - activePlayer = Pair.of(player, action); + activePlayer = Triple.of(player, action, timeout); // set label color if (nameLabels.containsKey(player)) { var label = nameLabels.get(player); execute(() -> label.setStyle(labelStyleActive)); } - if (state.getSelf().equals(player)) { + boolean isSelf = state.getSelf().equals(player); + if (isSelf || player == null) { // show interface setPersistentMessage(null); switch (action) { @@ -363,8 +451,9 @@ public class GameScreen extends MenuScreen { 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")); } - // TODO do something - } else { + } + + if (!isSelf) { // show message var key = switch (action) { case CHANGE_PREDICTION -> "game.message.change_prediction."; @@ -391,8 +480,8 @@ public class GameScreen extends MenuScreen { public void addMessage(@Nls String text, boolean immediate) { var label = new Label(text, game.data.skin); label.addAction(sequence( - delay(1.5f), - alpha(0, 0.25f), + delay(AnimationTimings.MESSAGE_HOLD), + alpha(0, AnimationTimings.MESSAGE_FADE), removeActor() )); @@ -413,15 +502,18 @@ public class GameScreen extends MenuScreen { public void setPersistentMessage(@Nls String text) { execute(() -> { + if (persistentMessage != null) { + persistentMessage.addAction(sequence( + delay(AnimationTimings.MESSAGE_HOLD), + alpha(0, AnimationTimings.MESSAGE_FADE), + removeActor() + )); + persistentMessage = null; + } + if (text != null) { - if (persistentMessage == null) { - persistentMessage = new Label(text, getData().skin); - } else { - persistentMessage.setText(text); - } + persistentMessage = new Label(text, getData().skin); messages.addActor(persistentMessage); - } else if (persistentMessage != null) { - persistentMessage.remove(); } }); } @@ -442,11 +534,17 @@ public class GameScreen extends MenuScreen { if (success && currentAction instanceof Overlay overlay && overlay instanceof InteractionOverlay) { overlay.finish(); } + + if (checkAction(JUGGLE_CARD) && juggledCard != null) { + handCards.setSelected(juggledCard); + juggledCard = null; + } } public void timeout() { addMessage(game.messages.get("game.message.timeout")); ready(true); + clearActivePlayer(); } public void sync() { @@ -467,14 +565,64 @@ public class GameScreen extends MenuScreen { return game.messages; } + private Action animateJuggle(List removed, List added) { + // find left- and rightmost seat + Seat tmpLeft = null, tmpRight = null; + for (Seat seat : seats.values()) { + if (seat == Seat.BOTTOM) continue; + if (tmpLeft == null || tmpLeft.compareTo(seat) > 0) tmpLeft = seat; + if (tmpRight == null || tmpRight.compareTo(seat) < 0) tmpRight = seat; + } + if (tmpLeft == null) return null; + var left = tmpLeft; + var right = tmpRight; + + var animation = parallel(); + removed.forEach(actor -> { + getData().stage.addActor(actor); + animation.addAction(targeting(actor, left.moveToHand(AnimationTimings.JUGGLE))); + }); + + added.forEach(actor -> { + // capture old values taking consideration of the animated layout changes in CardsGroup + float x = actor.getX(), y = actor.getY(); + var actions = actor.getActions(); + for (int i = 0; i < actions.size; i ++) { + var action = actions.get(i); + if (action instanceof MoveToAction move) { + x = move.getX(); + y = move.getY(); + actions.removeIndex(i--); + } + } + var rotation = actor.getRotation(); + + // 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()))); + + // animate + animation.addAction(targeting(actor, moveTo(x, y, AnimationTimings.JUGGLE))); + animation.addAction(targeting(actor, rotateTo(rotation, AnimationTimings.JUGGLE))); + }); + + var cleanup = parallel(); + removed.forEach(actor -> cleanup.addAction(removeActor(actor))); + + return sequence(animation, cleanup); + } + @Getter public enum Seat { 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), - RIGHT(WizardGame.WIDTH, WizardGame.HEIGHT * 0.5f, WizardGame.WIDTH - 50, WizardGame.HEIGHT * 0.5f + 110f, Align.bottomRight, WizardGame.WIDTH - 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), 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); + TOP_RIGHT(WizardGame.WIDTH * 0.75f, WizardGame.HEIGHT, WizardGame.WIDTH * 0.75f, WizardGame.HEIGHT - 50, Align.top, WizardGame.WIDTH * 0.75f, WizardGame.HEIGHT - 200), + RIGHT(WizardGame.WIDTH, WizardGame.HEIGHT * 0.5f, WizardGame.WIDTH - 50, WizardGame.HEIGHT * 0.5f + 110f, Align.bottomRight, WizardGame.WIDTH - 117.5f, WizardGame.HEIGHT * 0.5f); // position of the hand, should be offscreen private final float handX; @@ -502,5 +650,20 @@ public class GameScreen extends MenuScreen { var deltaY = WizardGame.HEIGHT * 0.5f - handY; this.handAngle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX) + Math.PI / 2); } + + public CardActor createHandCard(Card card, TextureAtlas atlas) { + 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) { + return parallel( + moveTo(handX, handY, duration), + 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 6d9fc15..6cbc5da 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 @@ -84,6 +84,7 @@ public final class Game extends BaseState { case "finished", "error" -> { return returnToSession(); } + case "juggling" -> gameScreen.setJuggling(true); } } else if (observerMessage instanceof HandMessage hand) { hands.put(hand.getPlayer(), hand.getHand()); @@ -128,13 +129,14 @@ public final class Game extends BaseState { } 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()); + gameScreen.addMessage(nack.getMessage(), true); gameScreen.ready(false); return Optional.empty(); } else { 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 new file mode 100644 index 0000000..2fdc8c1 --- /dev/null +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/util/Triple.java @@ -0,0 +1,7 @@ +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/resources/i18n/messages.properties b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties index fea1b9b..2b61d31 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties +++ b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties @@ -86,4 +86,7 @@ game.overlay.trump.green.player={0} choose the trump suit [#00ff00]green[#ffffff game.overlay.trump.blue.player={0} choose the trump suit [#0000ff]blue[#ffffff] game.overlay.trump.red.player={0} choose the trump suit [#ff0000]red[#ffffff] game.overlay.trump.none.player={0} has decided there will be no trump suit this round -game.overlay.trump.unknown.player=The trump suit is yet to be determined by {0} \ No newline at end of file +game.overlay.trump.unknown.player=The trump suit is yet to be determined by {0} + +game.overlay.play_colored_card.prompt=Pick a color +game.overlay.play_colored_card.cancel=Cancel \ No newline at end of file diff --git a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties index 86f67dc..e174019 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties +++ b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties @@ -86,4 +86,7 @@ game.overlay.trump.green.player={0} hat die Trumpffarbe [#00ff00]grün[#ffffff] game.overlay.trump.blue.player={0} hat die Trumpffarbe [#0000ff]blau[#ffffff] gewählt game.overlay.trump.red.player={0} hat die Trumpffarbe [#ff0000]rot[#ffffff] gewählt game.overlay.trump.none.player={0} hat entschieden, dass es diese Runde keinen Trump geben wird -game.overlay.trump.unknown.player={0} muss die Trumpffarbe muss noch bestimmen \ No newline at end of file +game.overlay.trump.unknown.player={0} muss die Trumpffarbe muss noch bestimmen + +game.overlay.play_colored_card.prompt=Wähle eine Farbe +game.overlay.play_colored_card.cancel=Abbrechen \ No newline at end of file diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/model/Card.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/model/Card.java index d8ec4d8..8210ae5 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/model/Card.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/model/Card.java @@ -2,28 +2,19 @@ package eu.jonahbauer.wizard.common.model; public enum Card { HIDDEN, - BLUE_1, RED_1, GREEN_1, YELLOW_1, - BLUE_2, RED_2, GREEN_2, YELLOW_2, - BLUE_3, RED_3, GREEN_3, YELLOW_3, - BLUE_4, RED_4, GREEN_4, YELLOW_4, - BLUE_5, RED_5, GREEN_5, YELLOW_5, - BLUE_6, RED_6, GREEN_6, YELLOW_6, - BLUE_7, RED_7, GREEN_7, YELLOW_7, - BLUE_8, RED_8, GREEN_8, YELLOW_8, - BLUE_9, RED_9, GREEN_9, YELLOW_9, - BLUE_10, RED_10, GREEN_10, YELLOW_10, - BLUE_11, RED_11, GREEN_11, YELLOW_11, - BLUE_12, RED_12, GREEN_12, YELLOW_12, - BLUE_13, RED_13, GREEN_13, YELLOW_13, - BLUE_WIZARD, RED_WIZARD, GREEN_WIZARD, YELLOW_WIZARD, - BLUE_JESTER, RED_JESTER, GREEN_JESTER, YELLOW_JESTER, - CHANGELING, CHANGELING_WIZARD, CHANGELING_JESTER, + RED_JESTER, GREEN_JESTER, BLUE_JESTER, YELLOW_JESTER, + RED_1, RED_2, RED_3, RED_4, RED_5, RED_6, RED_7, RED_8, RED_9, RED_10, RED_11, RED_12, RED_13, + GREEN_1, GREEN_2, GREEN_3, GREEN_4, GREEN_5, GREEN_6, GREEN_7, GREEN_8, GREEN_9, GREEN_10, GREEN_11, GREEN_12, GREEN_13, + BLUE_1, BLUE_2, BLUE_3, BLUE_4, BLUE_5, BLUE_6, BLUE_7, BLUE_8, BLUE_9, BLUE_10, BLUE_11, BLUE_12, BLUE_13, + YELLOW_1, YELLOW_2, YELLOW_3, YELLOW_4, YELLOW_5, YELLOW_6, YELLOW_7, YELLOW_8, YELLOW_9, YELLOW_10, YELLOW_11, YELLOW_12, YELLOW_13, + RED_WIZARD, GREEN_WIZARD, BLUE_WIZARD, YELLOW_WIZARD, BOMB, - WEREWOLF, + CHANGELING, CHANGELING_JESTER, CHANGELING_WIZARD, + CLOUD, CLOUD_RED, CLOUD_GREEN, CLOUD_BLUE, CLOUD_YELLOW, DRAGON, FAIRY, - CLOUD, CLOUD_BLUE, CLOUD_RED, CLOUD_GREEN, CLOUD_YELLOW, - JUGGLER, JUGGLER_BLUE, JUGGLER_RED, JUGGLER_GREEN, JUGGLER_YELLOW; + JUGGLER, JUGGLER_RED, JUGGLER_GREEN, JUGGLER_BLUE, JUGGLER_YELLOW, + WEREWOLF; public enum Suit { NONE, YELLOW, RED, GREEN, BLUE