From 80b30d9a6ae78793e2219ed3972f1e60576c301c Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 6 Oct 2024 20:11:28 +0200 Subject: [PATCH] Room: First Spell Part (#6044) * Room: First Spell ---- Co-authored-by: tool4ever Co-authored-by: tool4EvEr Co-authored-by: TRT <> Co-authored-by: Anthony Calosa --- .../java/forge/ai/PlayerControllerAi.java | 18 ++ .../main/java/forge/ai/SpellAbilityAi.java | 13 ++ .../src/main/java/forge/ai/SpellApiToAi.java | 1 + .../main/java/forge/card/CardStateName.java | 1 + .../src/main/java/forge/game/GameAction.java | 10 +- .../java/forge/game/ability/AbilityKey.java | 1 + .../main/java/forge/game/ability/ApiType.java | 1 + .../game/ability/effects/CloneEffect.java | 1 + .../ability/effects/UnlockDoorEffect.java | 106 ++++++++++++ .../src/main/java/forge/game/card/Card.java | 162 +++++++++++++++++- .../java/forge/game/card/CardFactory.java | 16 +- .../java/forge/game/card/CardFactoryUtil.java | 6 + .../main/java/forge/game/card/CardUtil.java | 18 ++ .../main/java/forge/game/card/CardView.java | 16 +- .../main/java/forge/game/player/Player.java | 10 ++ .../forge/game/player/PlayerController.java | 2 + .../java/forge/game/trigger/TriggerType.java | 1 + .../forge/game/trigger/TriggerUnlockDoor.java | 55 ++++++ .../java/forge/trackable/TrackableObject.java | 2 +- .../main/java/forge/gui/CardDetailPanel.java | 2 +- .../toolbox/imaging/FCardImageRenderer.java | 8 +- .../java/forge/view/arcane/CardPanel.java | 7 +- .../util/PlayerControllerForTests.java | 12 ++ .../src/forge/card/CardImageRenderer.java | 4 +- .../src/forge/card/CardRenderer.java | 24 ++- .../dollmakers_shop_porcelain_gallery.txt | 16 ++ .../upcoming/ghostly_keybearer.txt | 8 + .../upcoming/glassworks_shattered_yard.txt | 16 ++ forge-gui/res/tokenscripts/w_1_1_a_toy.txt | 6 + .../forge/player/PlayerControllerHuman.java | 18 +- 30 files changed, 527 insertions(+), 34 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java create mode 100644 forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java create mode 100644 forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt create mode 100644 forge-gui/res/tokenscripts/w_1_1_a_toy.txt diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 6b60c89c64d..ec8db182168 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1530,6 +1530,24 @@ public String chooseCardName(SpellAbility sa, List faces, String mess return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces); } + @Override + public ICardFace chooseSingleCardFace(SpellAbility sa, List faces, String message) { + ApiType api = sa.getApi(); + if (null == api) { + throw new InvalidParameterException("SA is not api-based, this is not supported yet"); + } + return SpellApiToAi.Converter.get(api).chooseCardFace(player, sa, faces); + } + + @Override + public CardState chooseSingleCardState(SpellAbility sa, List states, String message, Map params) { + ApiType api = sa.getApi(); + if (null == api) { + throw new InvalidParameterException("SA is not api-based, this is not supported yet"); + } + return SpellApiToAi.Converter.get(api).chooseCardState(player, sa, states, params); + } + @Override public Card chooseDungeon(Player ai, List dungeonCards, String message) { // TODO: improve the conditions that define which dungeon is a viable option to choose diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index 1acee5072ee..5bae9afca53 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -8,6 +8,7 @@ import forge.card.mana.ManaCostParser; import forge.game.GameEntity; import forge.game.card.Card; +import forge.game.card.CardState; import forge.game.card.CounterType; import forge.game.cost.Cost; import forge.game.mana.ManaCostBeingPaid; @@ -365,6 +366,18 @@ public String chooseCardName(Player ai, SpellAbility sa, List faces) return face == null ? "" : face.getName(); } + public ICardFace chooseCardFace(Player ai, SpellAbility sa, List faces) { + System.err.println("Warning: default (ie. inherited from base class) implementation of chooseCardFace is used for " + this.getClass().getName() + ". Consider declaring an overloaded method"); + + return Iterables.getFirst(faces, null); + } + + public CardState chooseCardState(Player ai, SpellAbility sa, List faces, Map params) { + System.err.println("Warning: default (ie. inherited from base class) implementation of chooseCardState is used for " + this.getClass().getName() + ". Consider declaring an overloaded method"); + + return Iterables.getFirst(faces, null); + } + public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map params) { return max; } diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index 6c56786b187..db5e141ea87 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -190,6 +190,7 @@ public enum SpellApiToAi { .put(ApiType.TwoPiles, TwoPilesAi.class) .put(ApiType.Unattach, CannotPlayAi.class) .put(ApiType.UnattachAll, UnattachAllAi.class) + .put(ApiType.UnlockDoor, AlwaysPlayAi.class) .put(ApiType.Untap, UntapAi.class) .put(ApiType.UntapAll, UntapAllAi.class) .put(ApiType.Venture, VentureAi.class) diff --git a/forge-core/src/main/java/forge/card/CardStateName.java b/forge-core/src/main/java/forge/card/CardStateName.java index 42005ef9c9e..fdbc8fe11f3 100644 --- a/forge-core/src/main/java/forge/card/CardStateName.java +++ b/forge-core/src/main/java/forge/card/CardStateName.java @@ -12,6 +12,7 @@ public enum CardStateName { RightSplit, Adventure, Modal, + EmptyRoom, SpecializeW, SpecializeU, SpecializeB, diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index a192381de7b..df8fc772c4b 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -190,7 +190,12 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer // Make sure the card returns from the battlefield as the original card with two halves resetToOriginal = true; } - } else if (!zoneTo.is(ZoneType.Stack)) { + } else if (zoneTo.is(ZoneType.Battlefield) && c.isRoom()) { + if (c.getCastSA() == null) { + // need to set as empty room + c.updateRooms(); + } + } else if (!zoneTo.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield)) { // For regular splits, recreate the original state unless the card is going to stack as one half resetToOriginal = true; } @@ -604,6 +609,9 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer // CR 603.6b if (toBattlefield) { zoneTo.saveLKI(copied, lastKnownInfo); + if (copied.isRoom() && copied.getCastSA() != null) { + copied.unlockRoom(copied.getCastSA().getActivatingPlayer(), copied.getCastSA().getCardStateName()); + } } // only now that the LKI preserved it diff --git a/forge-game/src/main/java/forge/game/ability/AbilityKey.java b/forge-game/src/main/java/forge/game/ability/AbilityKey.java index 7eda7e340bc..24faa6069b3 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityKey.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityKey.java @@ -30,6 +30,7 @@ public enum AbilityKey { Blockers("Blockers"), CanReveal("CanReveal"), Card("Card"), + CardState("CardState"), Cards("Cards"), CardsFiltered("CardsFiltered"), CardLKI("CardLKI"), diff --git a/forge-game/src/main/java/forge/game/ability/ApiType.java b/forge-game/src/main/java/forge/game/ability/ApiType.java index 0ba67421827..0475b035cac 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -194,6 +194,7 @@ public enum ApiType { TwoPiles (TwoPilesEffect.class), Unattach (UnattachEffect.class), UnattachAll (UnattachAllEffect.class), + UnlockDoor (UnlockDoorEffect.class), Untap (UntapEffect.class), UntapAll (UntapAllEffect.class), Venture (VentureEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/effects/CloneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CloneEffect.java index f811834cf00..7b5750284d5 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CloneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CloneEffect.java @@ -144,6 +144,7 @@ public void resolve(SpellAbility sa) { final long ts = game.getNextTimestamp(); tgtCard.addCloneState(CardFactory.getCloneStates(cardToCopy, tgtCard, sa), ts); + tgtCard.updateRooms(); // set ETB tapped of clone if (sa.hasParam("IntoPlayTapped")) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java new file mode 100644 index 00000000000..7ab3831e2bb --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java @@ -0,0 +1,106 @@ +package forge.game.ability.effects; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import forge.card.CardStateName; +import forge.game.Game; +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.card.CardLists; +import forge.game.card.CardState; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; +import forge.util.Localizer; + +public class UnlockDoorEffect extends SpellAbilityEffect { + + @Override + public void resolve(SpellAbility sa) { + final Card source = sa.getHostCard(); + final Game game = source.getGame(); + final Player activator = sa.getActivatingPlayer(); + + CardCollection list; + + if (sa.hasParam("Choices")) { + Player chooser = activator; + String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChoose") + " "; + + CardCollection choices = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("Choices"), activator, source, sa); + + Card c = chooser.getController().chooseSingleEntityForEffect(choices, sa, title, Maps.newHashMap()); + if (c == null) { + return; + } + list = new CardCollection(c); + } else { + list = getTargetCards(sa); + } + + for (Card c : list) { + Map params = Maps.newHashMap(); + params.put("Object", c); + switch (sa.getParamOrDefault("Mode", "ThisDoor")) { + case "ThisDoor": + c.unlockRoom(activator, sa.getCardStateName()); + break; + case "Unlock": + List states = c.getLockedRooms().stream().map(stateName -> c.getState(stateName)).collect(Collectors.toList()); + + // need to choose Room Name + CardState chosen = activator.getController().chooseSingleCardState(sa, states, "Choose Room to unlock", params); + if (chosen == null) { + continue; + } + c.unlockRoom(activator, chosen.getStateName()); + break; + case "LockOrUnlock": + switch (c.getLockedRooms().size()) { + case 0: + // no locked, all unlocked, can only lock door + List unlockStates = c.getUnlockedRooms().stream().map(stateName -> c.getState(stateName)).collect(Collectors.toList()); + CardState chosenUnlock = activator.getController().chooseSingleCardState(sa, unlockStates, "Choose Room to lock", params); + if (chosenUnlock == null) { + continue; + } + c.lockRoom(activator, chosenUnlock.getStateName()); + break; + case 1: + // TODO check for Lock vs Unlock first? + List bothStates = Lists.newArrayList(); + bothStates.add(c.getState(CardStateName.LeftSplit)); + bothStates.add(c.getState(CardStateName.RightSplit)); + CardState chosenBoth = activator.getController().chooseSingleCardState(sa, bothStates, "Choose Room to lock or unlock", params); + if (chosenBoth == null) { + continue; + } + if (c.getLockedRooms().contains(chosenBoth.getStateName())) { + c.unlockRoom(activator, chosenBoth.getStateName()); + } else { + c.lockRoom(activator, chosenBoth.getStateName()); + } + break; + case 2: + List lockStates = c.getLockedRooms().stream().map(stateName -> c.getState(stateName)).collect(Collectors.toList()); + + // need to choose Room Name + CardState chosenLock = activator.getController().chooseSingleCardState(sa, lockStates, "Choose Room to unlock", params); + if (chosenLock == null) { + continue; + } + c.unlockRoom(activator, chosenLock.getStateName()); + break; + } + break; + } + } + } + +} diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index ab1ba9b55fd..0117e037d0c 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -215,6 +215,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr private boolean plotted; + private Set unlockedRooms = EnumSet.noneOf(CardStateName.class); + private Map unlockAbilities = Maps.newEnumMap(CardStateName.class); + private boolean specialized; private int timesCrewedThisTurn = 0; @@ -444,6 +447,9 @@ public CardState getState(final CardStateName state) { if (state == CardStateName.FaceDown) { return getFaceDownState(); } + if (state == CardStateName.EmptyRoom) { + return getEmptyRoomState(); + } CardCloneStates clStates = getLastClonedState(); if (clStates == null) { return getOriginalState(state); @@ -452,7 +458,7 @@ public CardState getState(final CardStateName state) { } public boolean hasState(final CardStateName state) { - if (state == CardStateName.FaceDown) { + if (state == CardStateName.FaceDown || state == CardStateName.EmptyRoom) { return true; } CardCloneStates clStates = getLastClonedState(); @@ -466,6 +472,9 @@ public CardState getOriginalState(final CardStateName state) { if (state == CardStateName.FaceDown) { return getFaceDownState(); } + if (state == CardStateName.EmptyRoom) { + return getEmptyRoomState(); + } return states.get(state); } @@ -492,7 +501,7 @@ public boolean setState(final CardStateName state, boolean updateView, boolean f boolean needsTransformAnimation = transform || rollback; // faceDown has higher priority over clone states // while text change states doesn't apply while the card is faceDown - if (state != CardStateName.FaceDown) { + if (state != CardStateName.FaceDown && state != CardStateName.EmptyRoom) { CardCloneStates cloneStates = getLastClonedState(); if (cloneStates != null) { if (!cloneStates.containsKey(state)) { @@ -934,6 +943,10 @@ public final String getName(CardState state, boolean alt) { return alt ? StaticData.instance().getCommonCards().getName(name, true) : name; } + public final boolean hasNameOverwrite() { + return changedCardNames.values().stream().anyMatch(CardChangedName::isOverwrite); + } + public final boolean hasNonLegendaryCreatureNames() { boolean result = false; for (CardChangedName change : this.changedCardNames.values()) { @@ -1045,7 +1058,12 @@ public final boolean isFlipCard() { } public final boolean isSplitCard() { - return getRules() != null && getRules().getSplitType() == CardSplitType.Split; + // Normal Split Cards, these need to return true before Split States are added + if (getRules() != null && getRules().getSplitType() == CardSplitType.Split) { + return true; + }; + // in case or clones or copies + return hasState(CardStateName.LeftSplit); } public final boolean isAdventureCard() { @@ -1970,7 +1988,7 @@ public final boolean hasChosenNumber() { public final Integer getChosenNumber() { return chosenNumber; } - + public final void setChosenNumber(final int i) { setChosenNumber(i, false); } public final void setChosenNumber(final int i, final boolean secret) { chosenNumber = i; @@ -3126,7 +3144,7 @@ private StringBuilder abilityTextInstantSorcery(CardState state) { } else if (keyword.startsWith("Entwine") || keyword.startsWith("Madness") || keyword.startsWith("Miracle") || keyword.startsWith("Recover") || keyword.startsWith("Escape") || keyword.startsWith("Foretell:") - || keyword.startsWith("Disturb") || keyword.startsWith("Overload") + || keyword.startsWith("Disturb") || keyword.startsWith("Overload") || keyword.startsWith("Plot")) { final String[] k = keyword.split(":"); final Cost cost = new Cost(k[1], false); @@ -5575,6 +5593,8 @@ public final boolean isSpell() { public final boolean isOutlaw() { return getType().isOutlaw(); } + public final boolean isRoom() { return getType().hasSubtype("Room"); } + /** {@inheritDoc} */ @Override public final int compareTo(final Card that) { @@ -5985,9 +6005,21 @@ public final boolean sharesNameWith(final String name) { boolean shares = getName(true).equals(name); // Split cards has extra logic to check if it does share a name with - if (isSplitCard()) { - shares |= name.equals(getState(CardStateName.LeftSplit).getName()); - shares |= name.equals(getState(CardStateName.RightSplit).getName()); + if (!shares && !hasNameOverwrite()) { + if (isInPlay()) { + // split cards in play are only rooms + for (String door : getUnlockedRoomNames()) { + shares |= name.equals(door); + } + } else { // not on the battlefield + if (hasState(CardStateName.LeftSplit)) { + shares |= name.equals(getState(CardStateName.LeftSplit).getName()); + } + if (hasState(CardStateName.RightSplit)) { + shares |= name.equals(getState(CardStateName.RightSplit).getName()); + } + } + // TODO does it need extra check for stack? } if (!shares && hasNonLegendaryCreatureNames()) { @@ -6635,7 +6667,7 @@ public final boolean setPlotted(final boolean plotted) { if (plotted == true && !isLKI()) { final Map runParams = AbilityKey.mapFromCard(this); game.getTriggerHandler().runTrigger(TriggerType.BecomesPlotted, runParams, false); - } + } return true; } @@ -7474,6 +7506,15 @@ public List getAllPossibleAbilities(final Player player, final boo } } + if (isInPlay() && !isPhasedOut() && player.canCastSorcery()) { + if (getCurrentStateName() == CardStateName.RightSplit || getCurrentStateName() == CardStateName.EmptyRoom) { + abilities.add(getUnlockAbility(CardStateName.LeftSplit)); + } + if (getCurrentStateName() == CardStateName.LeftSplit || getCurrentStateName() == CardStateName.EmptyRoom) { + abilities.add(getUnlockAbility(CardStateName.RightSplit)); + } + } + if (isInPlay() && isFaceDown() && oState.getType().isCreature() && oState.getManaCost() != null && !oState.getManaCost().isNoCost()) { if (isManifested()) { @@ -8075,4 +8116,107 @@ public boolean isWitherDamage() { } return StaticAbilityWitherDamage.isWitherDamage(this); } + + public Set getUnlockedRooms() { + return this.unlockedRooms; + } + public void setUnlockedRooms(Set set) { + this.unlockedRooms = set; + } + + public List getUnlockedRoomNames() { + List result = Lists.newArrayList(); + for (CardStateName stateName : unlockedRooms) { + if (this.hasState(stateName)) { + result.add(this.getState(stateName).getName()); + } + } + return result; + } + + public Set getLockedRooms() { + Set result = Sets.newHashSet(CardStateName.LeftSplit, CardStateName.RightSplit); + result.removeAll(this.unlockedRooms); + return result; + } + + public List getLockedRoomNames() { + List result = Lists.newArrayList(); + for (CardStateName stateName : getLockedRooms()) { + if (this.hasState(stateName)) { + result.add(this.getState(stateName).getName()); + } + } + return result; + } + + public boolean unlockRoom(Player p, CardStateName stateName) { + if (unlockedRooms.contains(stateName) || (stateName != CardStateName.LeftSplit && stateName != CardStateName.RightSplit)) { + return false; + } + unlockedRooms.add(stateName); + + updateRooms(); + + Map unlockParams = AbilityKey.mapFromPlayer(p); + unlockParams.put(AbilityKey.Card, this); + unlockParams.put(AbilityKey.CardState, getState(stateName)); + getGame().getTriggerHandler().runTrigger(TriggerType.UnlockDoor, unlockParams, true); + + // fully unlock + if (unlockedRooms.size() > 1) { + Map fullyUnlockParams = AbilityKey.mapFromPlayer(p); + fullyUnlockParams.put(AbilityKey.Card, this); + + getGame().getTriggerHandler().runTrigger(TriggerType.FullyUnlock, fullyUnlockParams, true); + } + + return true; + } + + public boolean lockRoom(Player p, CardStateName stateName) { + if (!unlockedRooms.contains(stateName) || (stateName != CardStateName.LeftSplit && stateName != CardStateName.RightSplit)) { + return false; + } + unlockedRooms.remove(stateName); + + updateRooms(); + + return true; + } + + public void updateRooms() { + if (!this.isRoom()) { + return; + } + if (this.isFaceDown()) { + return; + } + if (unlockedRooms.isEmpty()) { + this.setState(CardStateName.EmptyRoom, true); + } else if (unlockedRooms.size() > 1) { + this.setState(CardStateName.Original, true); + } else { // we already know the set is only one + for (CardStateName name : unlockedRooms) { + this.setState(name, true); + } + } + // update trigger after state change + getGame().getTriggerHandler().clearActiveTriggers(this, null); + getGame().getTriggerHandler().registerActiveTrigger(this, false); + } + + public CardState getEmptyRoomState() { + if (!states.containsKey(CardStateName.EmptyRoom)) { + states.put(CardStateName.EmptyRoom, CardUtil.getEmptyRoomCharacteristic(this)); + } + return states.get(CardStateName.EmptyRoom); + } + + public SpellAbility getUnlockAbility(CardStateName state) { + if (!unlockAbilities.containsKey(state)) { + unlockAbilities.put(state, CardFactoryUtil.abilityUnlockRoom(getState(state))); + } + return unlockAbilities.get(state); + } } diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index 45891ffedf3..577fef1e184 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -258,6 +258,16 @@ private static void buildAbilities(final Card card) { final CardState original = card.getState(CardStateName.Original); original.addNonManaAbilities(card.getCurrentState().getNonManaAbilities()); original.addIntrinsicKeywords(card.getCurrentState().getIntrinsicKeywords()); // Copy 'Fuse' to original side + for (Trigger t : card.getCurrentState().getTriggers()) { + if (t.isIntrinsic()) { + original.addTrigger(t.copy(card, false)); + } + } + for (StaticAbility st : card.getCurrentState().getStaticAbilities()) { + if (st.isIntrinsic()) { + original.addStaticAbility(st.copy(card, false)); + } + } original.getSVars().putAll(card.getCurrentState().getSVars()); // Unfortunately need to copy these to (Effect looks for sVars on execute) } else if (state != CardStateName.Original) { CardFactoryUtil.setupKeywordedAbilities(card); @@ -415,7 +425,11 @@ private static void readCardFace(Card c, ICardFace face) { c.setAttractionLights(face.getAttractionLights()); // SpellPermanent only for Original State - if (c.getCurrentStateName() == CardStateName.Original || c.getCurrentStateName() == CardStateName.Modal || c.getCurrentStateName().toString().startsWith("Specialize")) { + if (c.getCurrentStateName() == CardStateName.Original || + c.getCurrentStateName() == CardStateName.LeftSplit || + c.getCurrentStateName() == CardStateName.RightSplit || + c.getCurrentStateName() == CardStateName.Modal || + c.getCurrentStateName().toString().startsWith("Specialize")) { if (c.isLand()) { SpellAbility sa = new LandAbility(c); sa.setCardState(c.getCurrentState()); diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index ecb07b2b39b..1eee35c287a 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -119,6 +119,12 @@ public boolean canPlay() { return morphDown; } + public static SpellAbility abilityUnlockRoom(CardState cardState) { + String unlockStr = "ST$ UnlockDoor | Cost$ " + cardState.getManaCost().getShortString() + " | Unlock$ True | SpellDescription$ Unlock " + cardState.getName(); + + return AbilityFactory.getAbility(unlockStr, cardState); + } + /** *

* abilityMorphUp. diff --git a/forge-game/src/main/java/forge/game/card/CardUtil.java b/forge-game/src/main/java/forge/game/card/CardUtil.java index 3a9f4ee4314..3b85d955e93 100644 --- a/forge-game/src/main/java/forge/game/card/CardUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardUtil.java @@ -215,6 +215,24 @@ public static CardState getFaceDownCharacteristic(Card c, CardStateName state) { return ret; } + public static CardState getEmptyRoomCharacteristic(Card c) { + return getEmptyRoomCharacteristic(c, CardStateName.EmptyRoom); + } + public static CardState getEmptyRoomCharacteristic(Card c, CardStateName state) { + final CardType type = new CardType(false); + type.add("Enchantment"); + type.add("Room"); + final CardState ret = new CardState(c, state); + + ret.setName(""); + ret.setType(type); + + // find new image key for empty room + ret.setImageKey(c.getImageKey()); + + return ret; + } + // a nice entry point with minimum parameters public static Set getReflectableManaColors(final SpellAbility sa) { return getReflectableManaColors(sa, sa, Sets.newHashSet(), new CardCollection()); diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index 79d180e7a3a..d93472e6a53 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -1,6 +1,7 @@ package forge.game.card; import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; import com.google.common.collect.Sets; import forge.ImageKeys; import forge.StaticData; @@ -40,6 +41,14 @@ public static CardStateView getState(Card c, CardStateName state) { return s == null ? null : s.getView(); } + public static Map getStateMap(Iterable states) { + Map stateViewCache = Maps.newLinkedHashMap(); + for (CardState state : states) { + stateViewCache.put(state.getView(), state); + } + return stateViewCache; + } + public CardView getBackup() { if (get(TrackableProperty.PaperCardBackup) == null) return null; @@ -795,7 +804,7 @@ public String getText(CardStateView state, HashMap translationsT sb.append(getOwner().getCommanderInfo(this)).append("\r\n"); } - if (isSplitCard() && !isFaceDown() && getZone() != ZoneType.Stack) { + if (isSplitCard() && !isFaceDown() && getZone() != ZoneType.Stack && getZone() != ZoneType.Battlefield) { sb.append("(").append(getLeftSplitState().getName()).append(") "); sb.append(getLeftSplitState().getAbilityText()); sb.append("\r\n\r\n").append("(").append(getRightSplitState().getName()).append(") "); @@ -1183,6 +1192,11 @@ public String getDisplayId() { return StringUtils.EMPTY; } + @Override + public int hashCode() { + return Objects.hash(getId(), state); + } + @Override public String toString() { return (getName() + " (" + getDisplayId() + ")").trim(); diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 13815bbfd45..b8b65ff8dc4 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -64,6 +64,8 @@ import java.util.*; import java.util.Map.Entry; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; /** *

@@ -3954,4 +3956,12 @@ public Player getDeclaresBlockers() { Map.Entry e = declaresBlockers.lastEntry(); return e == null ? null : e.getValue(); } + + public List getUnlockedDoors() { + return StreamSupport.stream(getCardsIn(ZoneType.Battlefield).spliterator(), false) + .filter(Card::isRoom) + .map(Card::getUnlockedRoomNames) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } } diff --git a/forge-game/src/main/java/forge/game/player/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index 077d6c00196..909eb117925 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -243,6 +243,8 @@ public int chooseNumber(SpellAbility sa, String string, int min, int max, Map cpp, String name); + public abstract ICardFace chooseSingleCardFace(SpellAbility sa, List faces, String message); + public abstract CardState chooseSingleCardState(SpellAbility sa, List states, String message, Map params); public abstract List chooseColors(String message, SpellAbility sa, int min, int max, List options); public abstract CounterType chooseCounterType(List options, SpellAbility sa, String prompt, Map params); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerType.java b/forge-game/src/main/java/forge/game/trigger/TriggerType.java index 2db1f4c9d77..0ec832a4277 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerType.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerType.java @@ -144,6 +144,7 @@ public enum TriggerType { TurnBegin(TriggerTurnBegin.class), TurnFaceUp(TriggerTurnFaceUp.class), Unattach(TriggerUnattach.class), + UnlockDoor(TriggerUnlockDoor.class), UntapAll(TriggerUntapAll.class), Untaps(TriggerUntaps.class), VisitAttraction(TriggerVisitAttraction.class), diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java b/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java new file mode 100644 index 00000000000..9aa90973b91 --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java @@ -0,0 +1,55 @@ +package forge.game.trigger; + +import java.util.Map; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.card.CardState; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +public class TriggerUnlockDoor extends Trigger { + + public TriggerUnlockDoor(final Map params, final Card host, final boolean intrinsic) { + super(params, host, intrinsic); + } + + @Override + public boolean performTest(Map runParams) { + if (!matchesValidParam("ValidCard", runParams.get(AbilityKey.Card))) { + return false; + } + + if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) { + return false; + } + + if (hasParam("ThisDoor")) { + CardState state = (CardState) runParams.get(AbilityKey.CardState); + // This Card + if (!getHostCard().equals(state.getCard())) { + return false; + } + // This Face + if (!getCardStateName().equals(state.getStateName())) { + return false; + } + } + + return true; + } + + @Override + public void setTriggeringObjects(SpellAbility sa, Map runParams) { + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card, AbilityKey.Player); + } + + @Override + public String getImportantStackObjects(SpellAbility sa) { + StringBuilder sb = new StringBuilder(); + sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player)); + sb.append(", ").append(Localizer.getInstance().getMessage("lblCard")).append(": ").append(sa.getTriggeringObject(AbilityKey.Card)); + return sb.toString(); + } + +} diff --git a/forge-game/src/main/java/forge/trackable/TrackableObject.java b/forge-game/src/main/java/forge/trackable/TrackableObject.java index 280fe24d3d0..ac5d2729102 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableObject.java +++ b/forge-game/src/main/java/forge/trackable/TrackableObject.java @@ -47,7 +47,7 @@ public int hashCode() { @Override public final boolean equals(final Object o) { if (o == null) { return false; } - return o.hashCode() == id && o.getClass().equals(getClass()); + return o.hashCode() == hashCode() && o.getClass().equals(getClass()); } // don't know if this is really needed, but don't know a better way diff --git a/forge-gui-desktop/src/main/java/forge/gui/CardDetailPanel.java b/forge-gui-desktop/src/main/java/forge/gui/CardDetailPanel.java index 3bc4ecd7831..87fa9628d2e 100644 --- a/forge-gui-desktop/src/main/java/forge/gui/CardDetailPanel.java +++ b/forge-gui-desktop/src/main/java/forge/gui/CardDetailPanel.java @@ -198,7 +198,7 @@ public final void setCard(final CardView card, final boolean mayView, final bool nameCost = name; } else { final String manaCost; - if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack) { //only display current state's mana cost when on stack + if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { //only display current state's mana cost when on stack manaCost = card.getLeftSplitState().getManaCost() + " // " + card.getAlternateState().getManaCost(); } else { manaCost = state.getManaCost().toString(); diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/imaging/FCardImageRenderer.java b/forge-gui-desktop/src/main/java/forge/toolbox/imaging/FCardImageRenderer.java index 5186889f9dc..4a3f911309b 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/imaging/FCardImageRenderer.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/imaging/FCardImageRenderer.java @@ -23,6 +23,7 @@ import org.apache.commons.lang3.StringUtils; import forge.card.CardRarity; +import forge.card.CardStateName; import forge.card.mana.ManaCost; import forge.game.card.CardView; import forge.game.card.CardView.CardStateView; @@ -748,8 +749,11 @@ private static void drawTypeLine(Graphics2D g, CardStateView state, Color[] colo //draw type x += padding; w -= padding; - String typeLine = CardDetailUtil.formatCardType(state, true).replace(" - ", " — "); - drawVerticallyCenteredString(g, typeLine, new Rectangle(x, y, w, h), TYPE_FONT, TYPE_SIZE); + // check for shared type line + if (!state.getType().hasStringType("Room") || state.getState() != CardStateName.RightSplit) { + String typeLine = CardDetailUtil.formatCardType(state, true).replace(" - ", " — "); + drawVerticallyCenteredString(g, typeLine, new Rectangle(x, y, w, h), TYPE_FONT, TYPE_SIZE); + } } /** diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index 1adec86dd36..ebd255203b8 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java @@ -479,12 +479,15 @@ private void displayCardNameOverlay(final boolean isVisible, final Dimension img private void displayIconOverlay(final Graphics g, final boolean canShow) { if (canShow && showCardManaCostOverlay() && cardWidth < 200) { - final boolean showSplitMana = card.isSplitCard(); + final boolean showSplitMana = card.isSplitCard() && card.getZone() != ZoneType.Battlefield; if (!showSplitMana) { drawManaCost(g, card.getCurrentState().getManaCost(), 0); } else { if (!card.isFaceDown()) { // no need to draw mana symbols on face down split cards (e.g. manifested) - PaperCard pc = StaticData.instance().getCommonCards().getCard(card.getName()); + PaperCard pc = null; + if (!card.getName().isEmpty()) { + pc = StaticData.instance().getCommonCards().getCard(card.getName()); + } int ofs = pc != null && Card.getCardForUi(pc).hasKeyword(Keyword.AFTERMATH) ? -12 : 12; drawManaCost(g, card.getLeftSplitState().getManaCost(), ofs); diff --git a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java index eb44cca1733..46ed3502a92 100644 --- a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java +++ b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java @@ -727,6 +727,18 @@ public String chooseCardName(SpellAbility sa, List faces, String mess return null; } + @Override + public ICardFace chooseSingleCardFace(SpellAbility sa, List faces, String message) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CardState chooseSingleCardState(SpellAbility sa, List states, String message, Map params) { + // TODO Auto-generated method stub + return null; + } + @Override public Card chooseDungeon(Player player, List dungeonCards, String message) { // TODO Auto-generated method stub diff --git a/forge-gui-mobile/src/forge/card/CardImageRenderer.java b/forge-gui-mobile/src/forge/card/CardImageRenderer.java index b76cefdc408..faaa7a3ecb4 100644 --- a/forge-gui-mobile/src/forge/card/CardImageRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardImageRenderer.java @@ -277,7 +277,7 @@ private static void drawHeader(Graphics g, CardView card, CardStateView state, C if (!noText && state != null) { //draw mana cost for card ManaCost mainManaCost = state.getManaCost(); - if (card.isSplitCard() && card.getAlternateState() != null) { + if (card.isSplitCard() && card.getAlternateState() != null && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { //handle rendering both parts of split card mainManaCost = card.getLeftSplitState().getManaCost(); ManaCost otherManaCost = card.getRightSplitState().getManaCost(); @@ -1112,7 +1112,7 @@ private static void drawDetailsNameBox(Graphics g, CardView card, CardStateView float manaCostWidth = 0; if (canShow) { ManaCost mainManaCost = state.getManaCost(); - if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack) { //only display current state's mana cost when on stack + if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { //only display current state's mana cost when on stack //handle rendering both parts of split card mainManaCost = card.getLeftSplitState().getManaCost(); ManaCost otherManaCost = card.getAlternateState().getManaCost(); diff --git a/forge-gui-mobile/src/forge/card/CardRenderer.java b/forge-gui-mobile/src/forge/card/CardRenderer.java index f4f5f7d4b78..83fb9310633 100644 --- a/forge-gui-mobile/src/forge/card/CardRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardRenderer.java @@ -852,19 +852,17 @@ public static void drawCardWithOverlays(Graphics g, CardView card, float x, floa } if (showCardManaCostOverlay(card)) { float manaSymbolSize = w / 4.5f; - if (card.isSplitCard() && card.hasAlternateState()) { - if (!card.isFaceDown()) { // no need to draw mana symbols on face down split cards (e.g. manifested) - if (isChoiceList) { - if (card.getRightSplitState().getName().equals(details.getName())) - drawManaCost(g, card.getRightSplitState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize); - else - drawManaCost(g, card.getLeftSplitState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize); - } else { - ManaCost leftManaCost = card.getLeftSplitState().getManaCost(); - ManaCost rightManaCost = card.getRightSplitState().getManaCost(); - drawManaCost(g, leftManaCost, x - padding, y-(manaSymbolSize/1.5f), w + 2 * padding, h, manaSymbolSize); - drawManaCost(g, rightManaCost, x - padding, y+(manaSymbolSize/1.5f), w + 2 * padding, h, manaSymbolSize); - } + if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { + if (isChoiceList) { + if (card.getRightSplitState().getName().equals(details.getName())) + drawManaCost(g, card.getRightSplitState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize); + else + drawManaCost(g, card.getLeftSplitState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize); + } else { + ManaCost leftManaCost = card.getLeftSplitState().getManaCost(); + ManaCost rightManaCost = card.getRightSplitState().getManaCost(); + drawManaCost(g, leftManaCost, x - padding, y-(manaSymbolSize/1.5f), w + 2 * padding, h, manaSymbolSize); + drawManaCost(g, rightManaCost, x - padding, y+(manaSymbolSize/1.5f), w + 2 * padding, h, manaSymbolSize); } } else { drawManaCost(g, showAltState ? card.getAlternateState().getManaCost() : card.getCurrentState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize); diff --git a/forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt b/forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt new file mode 100644 index 00000000000..edaac8c507f --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt @@ -0,0 +1,16 @@ +Name:Dollmaker's Shop +ManaCost:1 W +Types:Enchantment Room +T:Mode$ AttackersDeclaredOneTarget | Execute$ TrigToken | AttackedTarget$ Player | ValidAttackers$ Creature.YouCtrl+!Toy | TriggerZones$ Battlefield | AttackingPlayer$ You | TriggerDescription$ Whenever one or more non-Toy creatures you control attack a player, create a 1/1 white Toy artifact creature token. +SVar:TrigToken:DB$ Token | TokenScript$ w_1_1_a_toy +AlternateMode:Split +Oracle:(You may cast either half. That door unlocks on the battlefield. As a sorcery, you may pay the mana cost of a locked door to unlock it.)\nWhenever one or more non-Toy creatures you control attack a player, create a 1/1 white Toy artifact creature token. + +ALTERNATE + +Name:Porcelain Gallery +ManaCost:4 W W +Types:Enchantment Room +S:Mode$ Continuous | Affected$ Creature.YouCtrl | AffectedZone$ Battlefield | SetPower$ X | SetToughness$ X | Description$ Creatures you control have base power and toughness each equal to the number of creatures you control. +SVar:X:Count$Valid Creature.YouCtrl +Oracle:(You may cast either half. That door unlocks on the battlefield. As a sorcery, you may pay the mana cost of a locked door to unlock it.)\nCreatures you control have base power and toughness each equal to the number of creatures you control. diff --git a/forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt b/forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt new file mode 100644 index 00000000000..60e74d8b49d --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt @@ -0,0 +1,8 @@ +Name:Ghostly Keybearer +ManaCost:3 U +Types:Creature Spirit +PT:3/3 +K:Flying +T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | Execute$ TrigUnlock | CombatDamage$ True | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, unlock a locked door of up to one target Room you control. +SVar:TrigUnlock:DB$ UnlockDoor | Mode$ Unlock | ValidTgts$ Room.YouCtrl | TgtPrompt$ Choose target Room you control | TargetMin$ 0 | TargetMax$ 1 +Oracle:Flying\nWhenever Ghostly Keybearer deals combat damage to a player, unlock a locked door of up to one target Room you control. diff --git a/forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt b/forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt new file mode 100644 index 00000000000..3353e2b44ed --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt @@ -0,0 +1,16 @@ +Name:Glassworks +ManaCost:2 R +Types:Enchantment Room +T:Mode$ UnlockDoor | ValidPlayer$ You | ValidCard$ Card.Self | ThisDoor$ True | Execute$ TrigDamage | TriggerDescription$ When you unlock this door, this Room deals 4 damage to target creature an opponent controls. +SVar:TrigDamage:DB$ DealDamage | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls | NumDmg$ 4 +AlternateMode:Split +Oracle:(You may cast either half. That door unlocks on the battlefield. As a sorcery, you may pay the mana cost of a locked door to unlock it.)\nWhen you unlock this door, this Room deals 4 damage to target creature an opponent controls. + +ALTERNATE + +Name:Shattered Yard +ManaCost:4 R R +Types:Enchantment Room +T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigAllDamage | TriggerDescription$ At the beginning of your end step, this Room deals 1 damage to each opponent. +SVar:TrigAllDamage:DB$ DamageAll | ValidPlayers$ Player.Opponent | NumDmg$ 1 +Oracle:(You may cast either half. That door unlocks on the battlefield. As a sorcery, you may pay the mana cost of a locked door to unlock it.)\nAt the beginning of your end step, this Room deals 1 damage to each opponent. diff --git a/forge-gui/res/tokenscripts/w_1_1_a_toy.txt b/forge-gui/res/tokenscripts/w_1_1_a_toy.txt new file mode 100644 index 00000000000..54e76aca8da --- /dev/null +++ b/forge-gui/res/tokenscripts/w_1_1_a_toy.txt @@ -0,0 +1,6 @@ +Name:Toy Token +ManaCost:no cost +Types:Artifact Creature Toy +Colors:white +PT:1/1 +Oracle: diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 78c692fe010..b5f64a0fcf9 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -19,6 +19,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; +import forge.game.card.CardView.CardStateView; import forge.game.card.token.TokenInfo; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; @@ -1829,6 +1830,11 @@ public ICardFace chooseSingleCardFace(final SpellAbility sa, final String messag return StaticData.instance().getCommonCards().getFaceByName(cardFaceView.getOracleName()); } + @Override + public ICardFace chooseSingleCardFace(SpellAbility sa, List faces, String message) { + return getGui().one(message, faces); + } + @Override public CounterType chooseCounterType(final List options, final SpellAbility sa, final String prompt, Map params) { @@ -1838,6 +1844,16 @@ public CounterType chooseCounterType(final List options, final Spel return getGui().one(prompt, options); } + @Override + public CardState chooseSingleCardState(SpellAbility sa, List states, String message, Map params) { + if (states.size() <= 1) { + return Iterables.getFirst(states, null); + } + Map cache = CardView.getStateMap(states); + CardStateView chosen = getGui().one(message, Lists.newArrayList(cache.keySet())); + return cache.get(chosen); + } + @Override public String chooseKeywordForPump(final List options, final SpellAbility sa, final String prompt, final Card tgtCard) { if (options.size() <= 1) { @@ -3216,7 +3232,7 @@ public void reorderHand(final CardView card, final int index) { @Override public String chooseCardName(SpellAbility sa, List faces, String message) { - ICardFace face = getGui().one(message, faces); + ICardFace face = chooseSingleCardFace(sa, faces, message); return face == null ? "" : face.getName(); }