From d034e242e039a08e71e34747396a348789809f7e Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Tue, 3 Sep 2024 07:23:35 +0200 Subject: [PATCH 01/28] Room: First Spell Part --- .../src/main/java/forge/game/GameAction.java | 2 +- .../main/java/forge/game/card/CardFactory.java | 6 +++++- .../dollmakers_shop_porcelain_gallery.txt | 17 +++++++++++++++++ forge-gui/res/tokenscripts/w_1_1_a_toy.txt | 6 ++++++ 4 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt create mode 100644 forge-gui/res/tokenscripts/w_1_1_a_toy.txt diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index a192381de7b..33423b003ad 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -190,7 +190,7 @@ 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.Stack) && !zoneTo.is(ZoneType.Battlefield)) { // For regular splits, recreate the original state unless the card is going to stack as one half resetToOriginal = true; } 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..410ae827c90 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -415,7 +415,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-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..d2e7f403511 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt @@ -0,0 +1,17 @@ +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.Other+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/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: From b77b57eddb5e5f5fd1f667f7f507215a4747903a Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 8 Sep 2024 16:38:52 +0200 Subject: [PATCH 02/28] Add Unlock Static Action --- .../main/java/forge/card/CardStateName.java | 1 + .../src/main/java/forge/game/GameAction.java | 8 ++ .../java/forge/game/ability/AbilityKey.java | 1 + .../src/main/java/forge/game/card/Card.java | 106 +++++++++++++++++- .../java/forge/game/card/CardFactory.java | 10 ++ .../java/forge/game/card/CardFactoryUtil.java | 24 ++++ .../main/java/forge/game/card/CardUtil.java | 18 +++ .../main/java/forge/game/player/Player.java | 10 ++ .../java/forge/game/trigger/TriggerType.java | 1 + .../forge/game/trigger/TriggerUnlockDoor.java | 50 +++++++++ .../upcoming/glassworks_shattered_yard.txt | 16 +++ 11 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java create mode 100644 forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt 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 33423b003ad..df8fc772c4b 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -190,6 +190,11 @@ 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.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/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 0d989ad6f4a..7cb3c793e77 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)) { @@ -1966,7 +1975,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; @@ -3122,7 +3131,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); @@ -5577,6 +5586,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) { @@ -6637,7 +6648,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; } @@ -7476,6 +7487,15 @@ public List getAllPossibleAbilities(final Player player, final boo } } + if (isInPlay() && isRoom()) { + 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()) { @@ -8083,4 +8103,80 @@ 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 boolean unlockRoom(Player p, CardStateName stateName) { + if (unlockedRooms.contains(stateName)) { + 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 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 410ae827c90..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); 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..83c8d5473c5 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,30 @@ public boolean canPlay() { return morphDown; } + public static SpellAbility abilityUnlockRoom(CardState cardState) { + final AbilityStatic unlock = new AbilityStatic(cardState.getCard(), cardState.getManaCost()) { + + @Override + public void resolve() { + // TODO Auto-generated method stub + hostCard.unlockRoom(getActivatingPlayer(), getCardStateName()); + } + + @Override + public boolean canPlay() { + if (!hostCard.isInPlay()) { + return false; // cut short if already on the battlefield, avoids side effects when checking statics + } + return true; + } + }; + + unlock.setDescription("Unlock " + cardState.getName()); + unlock.setCardState(cardState); + + return unlock; + } + /** *

* 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/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/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..433c74307e9 --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java @@ -0,0 +1,50 @@ +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); + if (!getCardState().equals(state)) { + 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-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt b/forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt new file mode 100644 index 00000000000..68aa5eabbd9 --- /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 | 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. From 82f1c6243df95fbdb7a291be94dbf65e6a0b89ce Mon Sep 17 00:00:00 2001 From: tool4ever Date: Sun, 8 Sep 2024 16:14:28 +0000 Subject: [PATCH 03/28] Update dollmakers_shop_porcelain_gallery.txt --- .../cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt b/forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt index d2e7f403511..edaac8c507f 100644 --- a/forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt +++ b/forge-gui/res/cardsfolder/upcoming/dollmakers_shop_porcelain_gallery.txt @@ -1,7 +1,6 @@ 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 @@ -12,6 +11,6 @@ ALTERNATE Name:Porcelain Gallery ManaCost:4 W W Types:Enchantment Room -S:Mode$ Continuous | Affected$ Creature.Other+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. +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. From e6406c6fdbdcbd20323aa7a10e6ced5732793b32 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 8 Sep 2024 21:25:37 +0200 Subject: [PATCH 04/28] ~ fix sorcery speed --- forge-game/src/main/java/forge/game/card/Card.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7cb3c793e77..165362c54ad 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -7487,7 +7487,7 @@ public List getAllPossibleAbilities(final Player player, final boo } } - if (isInPlay() && isRoom()) { + if (isInPlay() && isRoom() && !isPhasedOut() && player.canCastSorcery()) { if (getCurrentStateName() == CardStateName.RightSplit || getCurrentStateName() == CardStateName.EmptyRoom) { abilities.add(getUnlockAbility(CardStateName.LeftSplit)); } From 353621feaf27a3df309d9ab26eb6a6f57402a4cd Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 15 Sep 2024 22:35:02 +0200 Subject: [PATCH 05/28] Fix unlock original state with Copy Enchantment --- .../src/main/java/forge/game/ability/effects/CloneEffect.java | 1 + forge-game/src/main/java/forge/game/card/Card.java | 2 +- forge-game/src/main/java/forge/game/card/CardFactoryUtil.java | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) 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/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 165362c54ad..db98ef3ea10 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -8122,7 +8122,7 @@ public List getUnlockedRoomNames() { } public boolean unlockRoom(Player p, CardStateName stateName) { - if (unlockedRooms.contains(stateName)) { + if (unlockedRooms.contains(stateName) || (stateName != CardStateName.LeftSplit && stateName != CardStateName.RightSplit)) { return false; } unlockedRooms.add(stateName); 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 83c8d5473c5..e6b944540a4 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -124,7 +124,6 @@ public static SpellAbility abilityUnlockRoom(CardState cardState) { @Override public void resolve() { - // TODO Auto-generated method stub hostCard.unlockRoom(getActivatingPlayer(), getCardStateName()); } From 5c816ea1e3d13dbe886a9a5deab6698f3715fb95 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 15 Sep 2024 22:35:48 +0200 Subject: [PATCH 06/28] Fix noname room crash --- .../src/main/java/forge/view/arcane/CardPanel.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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..7d247db93d9 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 @@ -484,7 +484,10 @@ private void displayIconOverlay(final Graphics g, final boolean canShow) { 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); From 700adfa5ec60cd0310eb4099ff0b1169f2412611 Mon Sep 17 00:00:00 2001 From: TRT <> Date: Mon, 16 Sep 2024 10:34:41 +0200 Subject: [PATCH 07/28] Fix script --- .../res/cardsfolder/upcoming/glassworks_shattered_yard.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt b/forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt index 68aa5eabbd9..3353e2b44ed 100644 --- a/forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt +++ b/forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt @@ -1,7 +1,7 @@ Name:Glassworks ManaCost:2 R Types:Enchantment Room -T:Mode$ UnlockDoor | ValidPlayer$ You | ThisDoor$ True | Execute$ TrigDamage | TriggerDescription$ When you unlock this door, this Room deals 4 damage to target creature an opponent controls. +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. From 217e854d533d9b0b53896bb6ee150168e259b9c8 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Mon, 16 Sep 2024 11:20:18 +0200 Subject: [PATCH 08/28] Update TriggerUnlockDoor.java Update ThisDoor Check --- .../main/java/forge/game/trigger/TriggerUnlockDoor.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java b/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java index 433c74307e9..999b4502a5c 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java @@ -26,7 +26,12 @@ public boolean performTest(Map runParams) { if (hasParam("ThisDoor")) { CardState state = (CardState) runParams.get(AbilityKey.CardState); - if (!getCardState().equals(state)) { + // This Card + if (!getHostCard().equals(state.getCard())) { + return false; + } + // This Face + if (!getCardStateName().equals(state.getCardStateName())) { return false; } } From ad90c1644ed9ffbc314ec3ca215e7fa58424cea1 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Mon, 16 Sep 2024 11:23:36 +0200 Subject: [PATCH 09/28] Update TriggerUnlockDoor.java --- .../src/main/java/forge/game/trigger/TriggerUnlockDoor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java b/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java index 999b4502a5c..9aa90973b91 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java @@ -31,7 +31,7 @@ public boolean performTest(Map runParams) { return false; } // This Face - if (!getCardStateName().equals(state.getCardStateName())) { + if (!getCardStateName().equals(state.getStateName())) { return false; } } From d9454a900cc5affd23d8f61aa52304a37dab93c7 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Wed, 18 Sep 2024 07:25:07 +0200 Subject: [PATCH 10/28] Card: add lockRoom --- forge-game/src/main/java/forge/game/card/Card.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 db98ef3ea10..1a6d099abd9 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -8145,6 +8145,17 @@ public boolean unlockRoom(Player p, CardStateName stateName) { 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; From 394d624a0bcf18c9840e91ade616e9238f532e99 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 28 Sep 2024 01:09:38 +0200 Subject: [PATCH 11/28] Use ApiType for UnlockDoor --- .../java/forge/ai/PlayerControllerAi.java | 9 +++ .../main/java/forge/ai/SpellAbilityAi.java | 6 ++ .../main/java/forge/game/ability/ApiType.java | 1 + .../ability/effects/UnlockDoorEffect.java | 76 +++++++++++++++++++ .../src/main/java/forge/game/card/Card.java | 18 +++++ .../java/forge/game/card/CardFactoryUtil.java | 21 +---- .../forge/game/player/PlayerController.java | 1 + .../util/PlayerControllerForTests.java | 6 ++ .../forge/player/PlayerControllerHuman.java | 7 +- 9 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 6b60c89c64d..45eae9c2e50 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1530,6 +1530,15 @@ 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 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..290a1019d2e 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -365,6 +365,12 @@ 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 int chooseNumber(Player player, SpellAbility sa, int min, int max, Map params) { return max; } 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/UnlockDoorEffect.java b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java new file mode 100644 index 00000000000..ee907a53fb5 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java @@ -0,0 +1,76 @@ +package forge.game.ability.effects; + +import java.util.Map; + +import org.testng.collections.Lists; +import org.testng.collections.Maps; + +import forge.StaticData; +import forge.card.CardStateName; +import forge.card.ICardFace; +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.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) { + switch (sa.getParamOrDefault("Mode", "ThisDoor")) { + case "ThisDoor": + c.unlockRoom(activator, sa.getCardStateName()); + break; + case "Unlock": + //List faces = c.getLockedRoomNames().stream().map(face -> StaticData.instance().getCommonCards().getFaceByName(face)).collect(Collectors.toList()); + + Map map = Maps.newHashMap(); + + for (CardStateName faceStateName : c.getLockedRooms()) { + if (!c.hasState(faceStateName)) { + continue; + } + String faceName = c.getState(faceStateName).getName(); + ICardFace face = StaticData.instance().getCommonCards().getFaceByName(faceName); + map.put(face, faceStateName); + } + + // need to choose Room Name + ICardFace chosen = activator.getController().chooseSingleCardFace(sa, Lists.newArrayList(map.keySet()), "Choose Room to unlock"); + if (chosen == null) { + continue; + } + c.unlockRoom(activator, map.get(chosen)); + 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 1a6d099abd9..80862a84e82 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -8121,6 +8121,24 @@ public List getUnlockedRoomNames() { return result; } + public Set getLockedRooms() { + if (!this.isSplitCard()) + return ImmutableSet.of(); + 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; 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 e6b944540a4..1eee35c287a 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -120,26 +120,9 @@ public boolean canPlay() { } public static SpellAbility abilityUnlockRoom(CardState cardState) { - final AbilityStatic unlock = new AbilityStatic(cardState.getCard(), cardState.getManaCost()) { + String unlockStr = "ST$ UnlockDoor | Cost$ " + cardState.getManaCost().getShortString() + " | Unlock$ True | SpellDescription$ Unlock " + cardState.getName(); - @Override - public void resolve() { - hostCard.unlockRoom(getActivatingPlayer(), getCardStateName()); - } - - @Override - public boolean canPlay() { - if (!hostCard.isInPlay()) { - return false; // cut short if already on the battlefield, avoids side effects when checking statics - } - return true; - } - }; - - unlock.setDescription("Unlock " + cardState.getName()); - unlock.setCardState(cardState); - - return unlock; + return AbilityFactory.getAbility(unlockStr, cardState); } /** 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..01bbfa923c2 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,7 @@ 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 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-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java index eb44cca1733..604c53f9a7a 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,12 @@ 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 Card chooseDungeon(Player player, List dungeonCards, String message) { // TODO Auto-generated method stub diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 78c692fe010..4264d7cf08c 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -1829,6 +1829,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) { @@ -3216,7 +3221,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(); } From 670c74c8c8b2502a38e019d4b1654d617de8ac96 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 28 Sep 2024 01:10:02 +0200 Subject: [PATCH 12/28] ~ add ghostly keybearer --- forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt 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..780a0a76f4a --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt @@ -0,0 +1,9 @@ +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 Ghostly Keybearer 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. + From 15e1859f9a1cc7edd8c079daa37feba092e68812 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 28 Sep 2024 01:12:28 +0200 Subject: [PATCH 13/28] ~ fix import --- .../java/forge/game/ability/effects/UnlockDoorEffect.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index ee907a53fb5..94fc83da7d6 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java @@ -2,8 +2,8 @@ import java.util.Map; -import org.testng.collections.Lists; -import org.testng.collections.Maps; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import forge.StaticData; import forge.card.CardStateName; From b86bba94d333a69a223c0350a9aecaa66c521ced Mon Sep 17 00:00:00 2001 From: tool4ever Date: Sat, 28 Sep 2024 07:36:54 +0000 Subject: [PATCH 14/28] Update ghostly_keybearer.txt --- forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt b/forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt index 780a0a76f4a..60e74d8b49d 100644 --- a/forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt +++ b/forge-gui/res/cardsfolder/upcoming/ghostly_keybearer.txt @@ -3,7 +3,6 @@ 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 Ghostly Keybearer deals combat damage to a player, unlock a locked door of up to one target Room you control. +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. - From 51daf48fcb1179c2d8e51a90180e53998d06b0fe Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Thu, 3 Oct 2024 14:44:35 +0200 Subject: [PATCH 15/28] PlayerController: add chooseSingleCardState --- .../java/forge/ai/PlayerControllerAi.java | 9 +++++++ .../main/java/forge/ai/SpellAbilityAi.java | 7 ++++++ .../ability/effects/UnlockDoorEffect.java | 25 +++++-------------- .../main/java/forge/game/card/CardView.java | 9 +++++++ .../forge/game/player/PlayerController.java | 1 + .../util/PlayerControllerForTests.java | 6 +++++ .../forge/player/PlayerControllerHuman.java | 11 ++++++++ 7 files changed, 49 insertions(+), 19 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 45eae9c2e50..997859fd090 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1539,6 +1539,15 @@ public ICardFace chooseSingleCardFace(SpellAbility sa, List faces, St return SpellApiToAi.Converter.get(api).chooseCardFace(player, sa, faces); } + @Override + public CardState chooseSingleCardState(SpellAbility sa, List states, 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).chooseCardState(player, sa, states); + } + @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 290a1019d2e..25267b08d94 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; @@ -371,6 +372,12 @@ public ICardFace chooseCardFace(Player ai, SpellAbility sa, List face return Iterables.getFirst(faces, null); } + public CardState chooseCardState(Player ai, SpellAbility sa, List faces) { + 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-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java index 94fc83da7d6..7d27f5f92af 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java @@ -1,18 +1,16 @@ package forge.game.ability.effects; -import java.util.Map; +import java.util.List; +import java.util.stream.Collectors; -import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import forge.StaticData; -import forge.card.CardStateName; -import forge.card.ICardFace; 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; @@ -49,25 +47,14 @@ public void resolve(SpellAbility sa) { c.unlockRoom(activator, sa.getCardStateName()); break; case "Unlock": - //List faces = c.getLockedRoomNames().stream().map(face -> StaticData.instance().getCommonCards().getFaceByName(face)).collect(Collectors.toList()); - - Map map = Maps.newHashMap(); - - for (CardStateName faceStateName : c.getLockedRooms()) { - if (!c.hasState(faceStateName)) { - continue; - } - String faceName = c.getState(faceStateName).getName(); - ICardFace face = StaticData.instance().getCommonCards().getFaceByName(faceName); - map.put(face, faceStateName); - } + List states = c.getLockedRooms().stream().map(stateName -> c.getState(stateName)).collect(Collectors.toList()); // need to choose Room Name - ICardFace chosen = activator.getController().chooseSingleCardFace(sa, Lists.newArrayList(map.keySet()), "Choose Room to unlock"); + CardState chosen = activator.getController().chooseSingleCardState(sa, states, "Choose Room to unlock"); if (chosen == null) { continue; } - c.unlockRoom(activator, map.get(chosen)); + c.unlockRoom(activator, chosen.getStateName()); break; } } 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..0fb12d6d170 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; 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 01bbfa923c2..b2b31050bff 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -244,6 +244,7 @@ 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); 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-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java index 604c53f9a7a..188703ed47b 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 @@ -733,6 +733,12 @@ public ICardFace chooseSingleCardFace(SpellAbility sa, List faces, St return null; } + @Override + public CardState chooseSingleCardState(SpellAbility sa, List states, String message) { + // 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/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 4264d7cf08c..0ac1d6b8569 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; @@ -1843,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) { + 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) { From 76114584bec9a097ea158b9c40646f1f5f2b640d Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 5 Oct 2024 08:28:53 +0200 Subject: [PATCH 16/28] UnlockEffect: LockOrUnlock mode --- .../ability/effects/UnlockDoorEffect.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) 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 index 7d27f5f92af..4a334c27f64 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java @@ -3,8 +3,11 @@ import java.util.List; import java.util.stream.Collectors; +import org.testng.collections.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; @@ -56,6 +59,44 @@ public void resolve(SpellAbility sa) { } 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"); + 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"); + 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"); + if (chosenLock == null) { + continue; + } + c.unlockRoom(activator, chosenLock.getStateName()); + break; + } + break; } } } From 7476b5f4d5e908ef15c39d8b7dc7f0b21e3975a7 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 5 Oct 2024 08:38:23 +0200 Subject: [PATCH 17/28] ~ fix import --- .../main/java/forge/game/ability/effects/UnlockDoorEffect.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 4a334c27f64..4a5fe44ca36 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java @@ -3,8 +3,7 @@ import java.util.List; import java.util.stream.Collectors; -import org.testng.collections.Lists; - +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.card.CardStateName; From 46421bd245e24c866a3f3b63d30c1bbd76c96ff5 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sat, 5 Oct 2024 12:43:46 +0200 Subject: [PATCH 18/28] View room fixes --- forge-game/src/main/java/forge/game/card/CardView.java | 2 +- forge-gui-desktop/src/main/java/forge/gui/CardDetailPanel.java | 2 +- .../src/main/java/forge/view/arcane/CardPanel.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 0fb12d6d170..a3709afe1f3 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -804,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(") "); 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/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index 7d247db93d9..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,7 +479,7 @@ 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 { From 18e9f6f2617bef734bfb2d0b41ba5fe7a36226a1 Mon Sep 17 00:00:00 2001 From: Anthony Calosa Date: Sat, 5 Oct 2024 19:09:06 +0800 Subject: [PATCH 19/28] update manacost for room --- forge-gui-mobile/src/forge/card/CardImageRenderer.java | 4 ++-- forge-gui-mobile/src/forge/card/CardRenderer.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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..0acc741a3ce 100644 --- a/forge-gui-mobile/src/forge/card/CardRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardRenderer.java @@ -852,7 +852,7 @@ 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.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { 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())) From 045868b6f6f23cf706de37606f5ef1c475085eef Mon Sep 17 00:00:00 2001 From: Anthony Calosa Date: Sat, 5 Oct 2024 19:11:11 +0800 Subject: [PATCH 20/28] remove redundant check --- .../src/forge/card/CardRenderer.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/forge-gui-mobile/src/forge/card/CardRenderer.java b/forge-gui-mobile/src/forge/card/CardRenderer.java index 0acc741a3ce..83fb9310633 100644 --- a/forge-gui-mobile/src/forge/card/CardRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardRenderer.java @@ -853,18 +853,16 @@ public static void drawCardWithOverlays(Graphics g, CardView card, float x, floa if (showCardManaCostOverlay(card)) { float manaSymbolSize = w / 4.5f; if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { - 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 (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); From 5e476642322d5f53cee473b903080e25e3606d61 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 5 Oct 2024 19:43:14 +0200 Subject: [PATCH 21/28] fix CardStateView hashCode and params for chooseCardState --- .../src/main/java/forge/ai/PlayerControllerAi.java | 4 ++-- forge-ai/src/main/java/forge/ai/SpellAbilityAi.java | 2 +- .../forge/game/ability/effects/UnlockDoorEffect.java | 11 +++++++---- .../src/main/java/forge/game/card/CardView.java | 5 +++++ .../main/java/forge/game/player/PlayerController.java | 2 +- .../main/java/forge/trackable/TrackableObject.java | 2 +- .../util/PlayerControllerForTests.java | 2 +- .../main/java/forge/player/PlayerControllerHuman.java | 2 +- 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 997859fd090..ec8db182168 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1540,12 +1540,12 @@ public ICardFace chooseSingleCardFace(SpellAbility sa, List faces, St } @Override - public CardState chooseSingleCardState(SpellAbility sa, List states, String message) { + 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); + return SpellApiToAi.Converter.get(api).chooseCardState(player, sa, states, params); } @Override diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index 25267b08d94..5bae9afca53 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -372,7 +372,7 @@ public ICardFace chooseCardFace(Player ai, SpellAbility sa, List face return Iterables.getFirst(faces, null); } - public CardState chooseCardState(Player ai, SpellAbility sa, List faces) { + 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); 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 index 4a5fe44ca36..7ab3831e2bb 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/UnlockDoorEffect.java @@ -1,6 +1,7 @@ package forge.game.ability.effects; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import com.google.common.collect.Lists; @@ -44,6 +45,8 @@ public void resolve(SpellAbility 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()); @@ -52,7 +55,7 @@ public void resolve(SpellAbility sa) { 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"); + CardState chosen = activator.getController().chooseSingleCardState(sa, states, "Choose Room to unlock", params); if (chosen == null) { continue; } @@ -63,7 +66,7 @@ public void resolve(SpellAbility sa) { 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"); + CardState chosenUnlock = activator.getController().chooseSingleCardState(sa, unlockStates, "Choose Room to lock", params); if (chosenUnlock == null) { continue; } @@ -74,7 +77,7 @@ public void resolve(SpellAbility sa) { 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"); + CardState chosenBoth = activator.getController().chooseSingleCardState(sa, bothStates, "Choose Room to lock or unlock", params); if (chosenBoth == null) { continue; } @@ -88,7 +91,7 @@ public void resolve(SpellAbility sa) { 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"); + CardState chosenLock = activator.getController().chooseSingleCardState(sa, lockStates, "Choose Room to unlock", params); if (chosenLock == null) { continue; } 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 a3709afe1f3..d93472e6a53 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -1192,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/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index b2b31050bff..909eb117925 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -244,7 +244,7 @@ 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); + 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/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/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java index 188703ed47b..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 @@ -734,7 +734,7 @@ public ICardFace chooseSingleCardFace(SpellAbility sa, List faces, St } @Override - public CardState chooseSingleCardState(SpellAbility sa, List states, String message) { + public CardState chooseSingleCardState(SpellAbility sa, List states, String message, Map params) { // TODO Auto-generated method stub return null; } diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 0ac1d6b8569..b5f64a0fcf9 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -1845,7 +1845,7 @@ public CounterType chooseCounterType(final List options, final Spel } @Override - public CardState chooseSingleCardState(SpellAbility sa, List states, String message) { + public CardState chooseSingleCardState(SpellAbility sa, List states, String message, Map params) { if (states.size() <= 1) { return Iterables.getFirst(states, null); } From d43e37aa5066d793d0b07f31ed59f6180b2dbbfd Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 5 Oct 2024 20:00:55 +0200 Subject: [PATCH 22/28] ~ remove isSplitCard for getLockedRooms --- forge-game/src/main/java/forge/game/card/Card.java | 2 -- 1 file changed, 2 deletions(-) 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 80862a84e82..a5804b0c975 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -8122,8 +8122,6 @@ public List getUnlockedRoomNames() { } public Set getLockedRooms() { - if (!this.isSplitCard()) - return ImmutableSet.of(); Set result = Sets.newHashSet(CardStateName.LeftSplit, CardStateName.RightSplit); result.removeAll(this.unlockedRooms); return result; From 0f70374251d70471d69ce8d1dc62c7129cdf530c Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sat, 5 Oct 2024 21:09:11 +0200 Subject: [PATCH 23/28] Fix doubled type for rooms --- .../java/forge/toolbox/imaging/FCardImageRenderer.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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); + } } /** From 5c8bcc9185a532cfd4717c596c7275bd36337615 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 6 Oct 2024 12:23:04 +0200 Subject: [PATCH 24/28] ~ fix sharesNameWith for Split/Room --- .../src/main/java/forge/game/card/Card.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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 a5804b0c975..63350cf9ac9 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -1050,7 +1050,9 @@ public final boolean isFlipCard() { } public final boolean isSplitCard() { - return getRules() != null && getRules().getSplitType() == CardSplitType.Split; + //return getRules() != null && getRules().getSplitType() == CardSplitType.Split; + // in case or clones or copies + return hasState(CardStateName.LeftSplit); } public final boolean isAdventureCard() { @@ -5998,9 +6000,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) { + 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()) { From 9abe505d57d63c6aada31971b0b7ecb1c27f0218 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 6 Oct 2024 13:54:18 +0200 Subject: [PATCH 25/28] Always unlocking better than never for AI --- forge-ai/src/main/java/forge/ai/SpellApiToAi.java | 1 + 1 file changed, 1 insertion(+) 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) From 66b6880942d6d322d10de0d57d4abb7ec319ab56 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 6 Oct 2024 17:31:23 +0200 Subject: [PATCH 26/28] Unlock doesn't need to be a Room --- forge-game/src/main/java/forge/game/card/Card.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 63350cf9ac9..3a54d836012 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -7501,7 +7501,7 @@ public List getAllPossibleAbilities(final Player player, final boo } } - if (isInPlay() && isRoom() && !isPhasedOut() && player.canCastSorcery()) { + if (isInPlay() && !isPhasedOut() && player.canCastSorcery()) { if (getCurrentStateName() == CardStateName.RightSplit || getCurrentStateName() == CardStateName.EmptyRoom) { abilities.add(getUnlockAbility(CardStateName.LeftSplit)); } From f21871bdfc7a1ab541b5d33568afc9269fbd65c8 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 6 Oct 2024 17:33:14 +0200 Subject: [PATCH 27/28] fix isSplitCard for normal Splits --- forge-game/src/main/java/forge/game/card/Card.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 3a54d836012..41cf0d14375 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -1050,7 +1050,10 @@ 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); } From 4e6c11077ad256772e6e16de925426dd097ac226 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 6 Oct 2024 19:31:06 +0200 Subject: [PATCH 28/28] check left and right state only if no Name Overwrite --- forge-game/src/main/java/forge/game/card/Card.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 41cf0d14375..1aeedb66800 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -939,6 +939,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()) { @@ -6003,7 +6007,7 @@ 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 (!shares) { + if (!shares && !hasNameOverwrite()) { if (isInPlay()) { // split cards in play are only rooms for (String door : getUnlockedRoomNames()) {