From 2f7a30f920161a4b3449878e2a9c1ab76c5c78fd Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Wed, 14 Apr 2021 20:30:17 +0200 Subject: [PATCH 1/7] Fix Multiplayer LTB triggers --- .../src/main/java/forge/ai/PlayerControllerAi.java | 2 +- forge-game/src/main/java/forge/game/Game.java | 2 +- forge-game/src/main/java/forge/game/card/Card.java | 14 +++++++++----- .../src/main/java/forge/game/player/Player.java | 2 +- .../java/forge/game/spellability/SpellAbility.java | 2 +- .../src/main/java/forge/game/zone/MagicStack.java | 2 +- .../java/forge/player/PlayerControllerHuman.java | 2 +- 7 files changed, 15 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 334fd63fd30..4902eebdcd9 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1029,7 +1029,7 @@ public class PlayerControllerAi extends PlayerController { */ if (sa.isMayChooseNewTargets() && !sa.setupTargets()) { if (sa.isSpell()) { - sa.getHostCard().ceaseToExist(); + sa.getHostCard().ceaseToExist(false); } continue; } diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 1a3659d3d2d..527d7ee523d 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -783,7 +783,7 @@ public class Game { cc.removeEncodedCard(c); cc.removeRemembered(c); } - c.ceaseToExist(); + c.ceaseToExist(false); } else { // return stolen permanents if (c.getController().equals(p) && c.isInZone(ZoneType.Battlefield)) { 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 1aaf6a1dba9..63de2504845 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -6756,11 +6756,15 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } } - public void ceaseToExist() { - getGame().getTriggerHandler().suppressMode(TriggerType.ChangesZone); - getZone().remove(this); - setZone(null); - getGame().getTriggerHandler().clearSuppression(TriggerType.ChangesZone); + public void ceaseToExist(boolean skipTrig) { + // CR 800.4d if card is controlled by opponent, LTB should trigger + if (skipTrig && getOwner().equals(getController())) { + getGame().getTriggerHandler().suppressMode(TriggerType.ChangesZone); + } + game.getAction().moveTo(ZoneType.None, this, null); + if (skipTrig && getOwner().equals(getController())) { + getGame().getTriggerHandler().clearSuppression(TriggerType.ChangesZone); + } } public void forceTurnFaceUp() { 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 b5dd3af1353..bc8f479f3ef 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -133,7 +133,7 @@ import forge.util.collect.FCollectionView; public class Player extends GameEntity implements Comparable { public static final List ALL_ZONES = Collections.unmodifiableList(Arrays.asList(ZoneType.Battlefield, ZoneType.Library, ZoneType.Graveyard, ZoneType.Hand, ZoneType.Exile, ZoneType.Command, ZoneType.Ante, - ZoneType.Sideboard, ZoneType.PlanarDeck, ZoneType.SchemeDeck, ZoneType.Merged, ZoneType.Subgame)); + ZoneType.Sideboard, ZoneType.PlanarDeck, ZoneType.SchemeDeck, ZoneType.Merged, ZoneType.Subgame, ZoneType.None)); private final Map commanderDamage = Maps.newHashMap(); diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 6245c3fe132..145c9fe8413 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -2361,7 +2361,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void rollback() { for (Card c : rollbackEffects) { - c.ceaseToExist(); + c.ceaseToExist(true); } rollbackEffects.clear(); } diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index 0c0e5ea7500..434d854065a 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -586,7 +586,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable Date: Wed, 14 Apr 2021 20:37:11 +0200 Subject: [PATCH 2/7] Fix logic --- forge-game/src/main/java/forge/game/card/Card.java | 4 ++-- 1 file changed, 2 insertions(+), 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 63de2504845..03c17d5808d 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -6758,11 +6758,11 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public void ceaseToExist(boolean skipTrig) { // CR 800.4d if card is controlled by opponent, LTB should trigger - if (skipTrig && getOwner().equals(getController())) { + if (skipTrig || getOwner().equals(getController())) { getGame().getTriggerHandler().suppressMode(TriggerType.ChangesZone); } game.getAction().moveTo(ZoneType.None, this, null); - if (skipTrig && getOwner().equals(getController())) { + if (skipTrig || getOwner().equals(getController())) { getGame().getTriggerHandler().clearSuppression(TriggerType.ChangesZone); } } From 5aaf39f40b2f67184bcc66f1a4e9a6af25a502bf Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Thu, 15 Apr 2021 20:20:52 +0200 Subject: [PATCH 3/7] LTB triggers for opponents --- forge-game/src/main/java/forge/game/Game.java | 13 +++++++++++-- forge-game/src/main/java/forge/game/card/Card.java | 12 ++++++------ .../java/forge/game/trigger/TriggerHandler.java | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 527d7ee523d..cf68f0b4cb7 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -768,13 +768,20 @@ public class Game { } else { p.revealFaceDownCards(); } + + for (Card c : cards) { + // CR 800.4d if card is controlled by opponent, LTB should trigger + if (c.getOwner().equals(p) && c.getController().equals(p)) { + c.getCurrentState().clearTriggers(); + } + } - for(Card c : cards) { + for (Card c : cards) { if (c.getController().equals(p) && (c.isPlane() || c.isPhenomenon())) { planarControllerLost = true; } - if(isMultiplayer) { + if (isMultiplayer) { // unattach all "Enchant Player" c.removeAttachedTo(p); if (c.getOwner().equals(p)) { @@ -784,6 +791,8 @@ public class Game { cc.removeRemembered(c); } c.ceaseToExist(false); + // CR 603.2f owner of trigger source lost game + triggerHandler.clearDelayedTrigger(c); } else { // return stolen permanents if (c.getController().equals(p) && c.isInZone(ZoneType.Battlefield)) { 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 03c17d5808d..2e590f74a3c 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -6757,13 +6757,13 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } public void ceaseToExist(boolean skipTrig) { - // CR 800.4d if card is controlled by opponent, LTB should trigger - if (skipTrig || getOwner().equals(getController())) { - getGame().getTriggerHandler().suppressMode(TriggerType.ChangesZone); + // CR 603.6c other players LTB triggers should work + if (skipTrig) { + getZone().remove(this); + setZone(getOwner().getZone(ZoneType.None)); } - game.getAction().moveTo(ZoneType.None, this, null); - if (skipTrig || getOwner().equals(getController())) { - getGame().getTriggerHandler().clearSuppression(TriggerType.ChangesZone); + else { + game.getAction().moveTo(ZoneType.None, this, null); } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java index 7ddf5b28cff..d990767b3ab 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java @@ -633,8 +633,8 @@ public class TriggerHandler { public void onPlayerLost(Player p) { List lost = new ArrayList<>(); for (Trigger t : delayedTriggers) { - // CR 603.2f owner of trigger source lost game || 800.4d trigger controller lost game - if (game.getCardState(t.getHostCard(), null) == null || t.getHostCard().getOwner().equals(p)) { + // CR 800.4d trigger controller lost game + if (t.getHostCard().getOwner().equals(p)) { lost.add(t); } } From 237b0324c1bf2a37098043b5f588b431c197fdc9 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Thu, 15 Apr 2021 20:23:07 +0200 Subject: [PATCH 4/7] Clean up --- forge-game/src/main/java/forge/game/Game.java | 2 +- .../src/main/java/forge/game/trigger/TriggerHandler.java | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index cf68f0b4cb7..0527a1e0063 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -768,7 +768,7 @@ public class Game { } else { p.revealFaceDownCards(); } - + for (Card c : cards) { // CR 800.4d if card is controlled by opponent, LTB should trigger if (c.getOwner().equals(p) && c.getController().equals(p)) { diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java index d990767b3ab..656829d12b1 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java @@ -631,13 +631,12 @@ public class TriggerHandler { } public void onPlayerLost(Player p) { - List lost = new ArrayList<>(); - for (Trigger t : delayedTriggers) { + List lost = new ArrayList<>(delayedTriggers); + for (Trigger t : lost) { // CR 800.4d trigger controller lost game if (t.getHostCard().getOwner().equals(p)) { - lost.add(t); + delayedTriggers.remove(t); } } - delayedTriggers.removeAll(lost); } } From 337065271e4c812517bd9bf21d1dd123082b6320 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Thu, 15 Apr 2021 21:27:31 +0200 Subject: [PATCH 5/7] ChangeZoneTable for Triplicate Titan --- forge-gui/res/cardsfolder/upcoming/triplicate_titan.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/forge-gui/res/cardsfolder/upcoming/triplicate_titan.txt b/forge-gui/res/cardsfolder/upcoming/triplicate_titan.txt index 30f1e4db5ef..4ae7b601aa2 100644 --- a/forge-gui/res/cardsfolder/upcoming/triplicate_titan.txt +++ b/forge-gui/res/cardsfolder/upcoming/triplicate_titan.txt @@ -6,8 +6,9 @@ K:Flying K:Vigilance K:Trample T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigTokenFly | TriggerController$ TriggeredCardController | TriggerDescription$ When CARDNAME dies, create a 3/3 colorless Golem artifact creature token with flying, a 3/3 colorless Golem artifact creature token with vigilance, and a 3/3 colorless Golem artifact creature token with trample. -SVar:TrigTokenFly:DB$Token | TokenAmount$ 1 | TokenScript$ c_3_3_a_golem_flying | SubAbility$ DBTokenVig +SVar:TrigTokenFly:DB$Token | TokenAmount$ 1 | TokenScript$ c_3_3_a_golem_flying | ChangeZoneTable$ True | SubAbility$ DBTokenVig SVar:DBTokenVig:DB$Token | TokenAmount$ 1 | TokenScript$ c_3_3_a_golem_vigilance | SubAbility$ DBTokenTra -SVar:DBTokenTra:DB$Token | TokenAmount$ 1 | TokenScript$ c_3_3_a_golem_trample +SVar:DBTokenTra:DB$Token | TokenAmount$ 1 | TokenScript$ c_3_3_a_golem_trample | SubAbility$ DBResolve +SVar:DBResolve:DB$ ChangeZoneResolve DeckHas:Ability$Token Oracle:Flying, vigilance, trample\nWhen Triplicate Titan dies, create a 3/3 colorless Golem artifact creature token with flying, a 3/3 colorless Golem artifact creature token with vigilance, and a 3/3 colorless Golem artifact creature token with trample. From 8174b883a012691927138461fc99f70ec6c2c9cd Mon Sep 17 00:00:00 2001 From: Andreas Bendel Date: Thu, 15 Apr 2021 19:38:32 +0000 Subject: [PATCH 6/7] Update de-DE.properties added line and translation lblRandomHistoricArchetypeDecks translated lblInvalidTargetSpecification --- forge-gui/res/languages/de-DE.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index beabfc97cf9..8963885d492 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -580,6 +580,7 @@ lblQuestOpponentDecks=Quest-Gegner-Deck lblRandomColorDecks=Decks - zufällige Farben lblRandomStandardArchetypeDecks=Decks - zufälliger Standard-Archetyp lblRandomPioneerArchetypeDecks=Decks - zufälliger Pioneer-Archetyp +lblRandomHistoricArchetypeDecks=Decks - zufälliger Historisch-Archetyp lblRandomModernArchetypeDecks=Decks - zufälliger Modern-Archetyp lblRandomLegacyArchetypeDecks=Decks - zufälliger Legacy-Archetyp lblRandomVintageArchetypeDecks=Decks - zufälliger Vintage-Archetyp @@ -2674,4 +2675,4 @@ lblWildOpponentNumberError=Anzahl der Wild-Gegner kann nur 0 bis 3 sein #GauntletWinLose.java lblGauntletProgress=Spießrutenlauf-Fortschritt #SpellAbility.java -lblInvalidTargetSpecification=Not all target requirements are met. +lblInvalidTargetSpecification=Nicht alle Zielbedingungen sind erfüllt. From a1f69f0732e2963a54ec8d0e39626fef26273956 Mon Sep 17 00:00:00 2001 From: Lyu Zong-Hong Date: Fri, 16 Apr 2021 08:22:37 +0900 Subject: [PATCH 7/7] Add Shaman's Trance --- .../StaticAbilityContinuous.java | 30 +++++++++++++++++++ .../res/cardsfolder/s/shamans_trance.txt | 10 +++++++ .../forge/player/PlayerControllerHuman.java | 5 +++- 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 forge-gui/res/cardsfolder/s/shamans_trance.txt diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java index 406a61fad43..629b54081a2 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -882,6 +882,17 @@ public final class StaticAbilityContinuous { affectedCard.setMayPlay(mayPlayController, mayPlayWithoutManaCost, mayPlayAltCost != null ? new Cost(mayPlayAltCost, false) : null, mayPlayWithFlash, mayPlayGrantZonePermissions, stAb); + + // If the MayPlay effect only affected itself, check if it is in graveyard and give other player who cast Shaman's Trance MayPlay + if (stAb.getParam("Affected").equals("Card.Self") && affectedCard.isInZone(ZoneType.Graveyard)) { + for (final Player p : game.getPlayers()) { + if (p.hasKeyword("Shaman's Trance") && mayPlayController != p) { + affectedCard.setMayPlay(p, mayPlayWithoutManaCost, + mayPlayAltCost != null ? new Cost(mayPlayAltCost, false) : null, + mayPlayWithFlash, mayPlayGrantZonePermissions, stAb); + } + } + } } } @@ -979,7 +990,26 @@ public final class StaticAbilityContinuous { affectedCards.addAll(game.getCardsIn(ZoneType.Battlefield)); } if (stAb.hasParam("Affected")) { + // Handle Shaman's Trance + CardCollection affectedCardsOriginal = null; + if (controller.hasKeyword("Shaman's Trance") && stAb.hasParam("MayPlay")) { + affectedCardsOriginal = new CardCollection(affectedCards); + } + affectedCards = CardLists.getValidCards(affectedCards, stAb.getParam("Affected").split(","), controller, hostCard, stAb); + + // Add back all cards that are in other player's graveyard, and meet the restrictions without YouOwn/YouCtrl (treat it as in your graveyard) + if (affectedCardsOriginal != null) { + String affectedParam = stAb.getParam("Affected"); + affectedParam = affectedParam.replaceAll("[\\.\\+]YouOwn", ""); + affectedParam = affectedParam.replaceAll("[\\.\\+]YouCtrl", ""); + String[] restrictions = affectedParam.split(","); + for (final Card card : affectedCardsOriginal) { + if (card.isInZone(ZoneType.Graveyard) && card.getController() != controller && card.isValid(restrictions, controller, hostCard, stAb)) { + affectedCards.add(card); + } + } + } } affectedCards.removeAll(stAb.getIgnoreEffectCards()); diff --git a/forge-gui/res/cardsfolder/s/shamans_trance.txt b/forge-gui/res/cardsfolder/s/shamans_trance.txt new file mode 100644 index 00000000000..187593e172b --- /dev/null +++ b/forge-gui/res/cardsfolder/s/shamans_trance.txt @@ -0,0 +1,10 @@ +Name:Shaman's Trance +ManaCost:2 R +Types:Instant +A:SP$ Effect | Cost$ 2 R | StaticAbilities$ STCantPlayLand,STCantCastSpell,STTrance | SpellDescription$ Other players can't play lands or cast spells from their graveyards this turn. You may play lands and cast spells from other players' graveyards this turn as though those cards were in your graveyard. +SVar:STCantPlayLand:Mode$ CantPlayLand | ValidCard$ Land | Origin$ Graveyard | Player$ Player.Other | EffectZone$ Command | Description$ Other players can't play lands from their graveyards this turn. +SVar:STCantCastSpell:Mode$ CantBeCast | ValidCard$ Card | Origin$ Graveyard | Caster$ Player.Other | EffectZone$ Command | Description$ Other players cast spells from their graveyards this turn. +SVar:STTrance:Mode$ Continuous | Affected$ You | AddKeyword$ Shaman's Trance | Description$ You may play lands and cast spells from other players' graveyards this turn as though those cards were in your graveyard. +AI:RemoveDeck:All +AI:RemoveDeck:Random +Oracle:Other players can't play lands or cast spells from their graveyards this turn. You may play lands and cast spells from other players' graveyards this turn as though those cards were in your graveyard. diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index b425ebb3112..d960a62ceab 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -265,7 +265,10 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont public SpellAbility getAbilityToPlay(final Card hostCard, final List abilities, final ITriggerEvent triggerEvent) { // make sure another human player can't choose opponents cards just because he might see them - if (triggerEvent != null && !hostCard.isInZone(ZoneType.Battlefield) && !hostCard.getOwner().equals(player) && !hostCard.getController().equals(player) && hostCard.mayPlay(player).size() == 0) { + if (triggerEvent != null && !hostCard.isInZone(ZoneType.Battlefield) && !hostCard.getOwner().equals(player) && + !hostCard.getController().equals(player) && hostCard.mayPlay(player).size() == 0 && + // If player cast Shaman's Trance, they can play spells from any Graveyard (if other effects allow it to be cast) + (!player.hasKeyword("Shaman's Trance") || !hostCard.isInZone(ZoneType.Graveyard))) { return null; } spellViewCache = SpellAbilityView.getMap(abilities);