diff --git a/Mage.Sets/src/mage/cards/b/BloodstoneGoblin.java b/Mage.Sets/src/mage/cards/b/BloodstoneGoblin.java index 7f837ffb7d..32c6df312d 100644 --- a/Mage.Sets/src/mage/cards/b/BloodstoneGoblin.java +++ b/Mage.Sets/src/mage/cards/b/BloodstoneGoblin.java @@ -1,9 +1,6 @@ - package mage.cards.b; -import java.util.UUID; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; @@ -17,10 +14,10 @@ import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.stack.Spell; + +import java.util.UUID; /** - * * @author TheElk801 */ public final class BloodstoneGoblin extends CardImpl { @@ -70,15 +67,7 @@ class BloodstoneGoblinTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - Spell spell = game.getStack().getSpell(event.getTargetId()); - if (spell != null && spell.isControlledBy(controllerId)) { - for (Ability ability : spell.getAbilities()) { - if (ability instanceof KickerAbility && ((KickerAbility) ability).getKickedCounter(game, spell.getSpellAbility()) > 0) { - return true; - } - } - } - return false; + return KickerAbility.getSpellKickedCount(game, event.getTargetId()) > 0; } @Override diff --git a/Mage.Sets/src/mage/cards/d/DeathforgeShaman.java b/Mage.Sets/src/mage/cards/d/DeathforgeShaman.java index 5871b516b5..fc59997e06 100644 --- a/Mage.Sets/src/mage/cards/d/DeathforgeShaman.java +++ b/Mage.Sets/src/mage/cards/d/DeathforgeShaman.java @@ -1,11 +1,8 @@ - package mage.cards.d; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.MultikickerCount; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DamageTargetEffect; @@ -13,13 +10,14 @@ import mage.abilities.keyword.MultikickerAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.game.Game; import mage.target.common.TargetPlayerOrPlaneswalker; +import java.util.UUID; + /** - * * @author jeffwadsworth */ public final class DeathforgeShaman extends CardImpl { @@ -69,8 +67,7 @@ class DeathforgeShamanEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - DynamicValue value = MultikickerCount.instance; - int damage = value.calculate(game, source, this) * 2; + int damage = MultikickerCount.instance.calculate(game, source, this) * 2; return new DamageTargetEffect(damage).apply(game, source); } diff --git a/Mage.Sets/src/mage/cards/h/HallarTheFirefletcher.java b/Mage.Sets/src/mage/cards/h/HallarTheFirefletcher.java index e6906c8130..f8b13fcca4 100644 --- a/Mage.Sets/src/mage/cards/h/HallarTheFirefletcher.java +++ b/Mage.Sets/src/mage/cards/h/HallarTheFirefletcher.java @@ -1,30 +1,22 @@ - package mage.cards.h; -import java.util.UUID; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.dynamicvalue.common.CountersSourceCount; import mage.abilities.effects.common.DamagePlayersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.keyword.KickerAbility; -import mage.constants.SubType; -import mage.constants.SuperType; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.TargetController; -import mage.constants.Zone; +import mage.constants.*; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.stack.Spell; + +import java.util.UUID; /** - * * @author TheElk801 */ public final class HallarTheFirefletcher extends CardImpl { @@ -80,15 +72,10 @@ class HallarTheFirefletcherTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - Spell spell = game.getStack().getSpell(event.getTargetId()); - if (spell != null && spell.isControlledBy(controllerId)) { - for (Ability ability : spell.getAbilities()) { - if (ability instanceof KickerAbility && ((KickerAbility) ability).getKickedCounter(game, spell.getSpellAbility()) > 0) { - return true; - } - } + if (!isControlledBy(event.getPlayerId())) { + return false; } - return false; + return KickerAbility.getSpellKickedCount(game, event.getTargetId()) > 0; } @Override diff --git a/Mage.Sets/src/mage/cards/r/RumblingAftershocks.java b/Mage.Sets/src/mage/cards/r/RumblingAftershocks.java index 4cfb8a106d..543848bfe0 100644 --- a/Mage.Sets/src/mage/cards/r/RumblingAftershocks.java +++ b/Mage.Sets/src/mage/cards/r/RumblingAftershocks.java @@ -13,9 +13,7 @@ import mage.constants.Zone; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.game.permanent.Permanent; -import mage.game.stack.Spell; import mage.players.Player; import mage.target.common.TargetAnyTarget; @@ -75,18 +73,10 @@ class RumblingAftershocksTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - Spell spell = game.getStack().getSpell(event.getTargetId()); - if (spell != null && spell.isControlledBy(controllerId)) { - int damageAmount = 0; - for (Ability ability : spell.getAbilities()) { - if (ability instanceof KickerAbility) { - damageAmount += ((KickerAbility) ability).getKickedCounter(game, spell.getSpellAbility()); - } - } - if (damageAmount > 0) { - this.getEffects().get(0).setValue("damageAmount", damageAmount); - return true; - } + int kickedCount = KickerAbility.getSpellKickedCount(game, event.getTargetId()); + if (kickedCount > 0) { + this.getEffects().get(0).setValue("damageAmount", kickedCount); + return true; } return false; } diff --git a/Mage.Sets/src/mage/cards/s/ScourgeOfTheSkyclaves.java b/Mage.Sets/src/mage/cards/s/ScourgeOfTheSkyclaves.java index 492050a695..7fec7c857a 100644 --- a/Mage.Sets/src/mage/cards/s/ScourgeOfTheSkyclaves.java +++ b/Mage.Sets/src/mage/cards/s/ScourgeOfTheSkyclaves.java @@ -15,7 +15,6 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; -import mage.game.stack.Spell; import mage.players.Player; import java.util.Objects; @@ -64,17 +63,7 @@ enum ScourgeOfTheSkyclavesCondition implements Condition { @Override public boolean apply(Game game, Ability source) { - Spell spell = game.getSpell(source.getSourceId()); - if (spell == null) { - return false; - } - for (Ability ability : spell.getAbilities()) { - if (ability instanceof KickerAbility - && ((KickerAbility) ability).getKickedCounter(game, spell.getSpellAbility()) > 0) { - return true; - } - } - return false; + return KickerAbility.getSpellKickedCount(game, source.getSourceId()) > 0; } } diff --git a/Mage.Sets/src/mage/cards/v/VineGecko.java b/Mage.Sets/src/mage/cards/v/VineGecko.java index efaba3bec7..3b0fc089a3 100644 --- a/Mage.Sets/src/mage/cards/v/VineGecko.java +++ b/Mage.Sets/src/mage/cards/v/VineGecko.java @@ -76,7 +76,8 @@ enum VineGeckoPredicate implements ObjectPlayerPredicate> { if (watcher == null || watcher.checkPlayer(input.getPlayerId())) { return false; } - for (Ability ability : input.getObject().getAbilities()) { + + for (Ability ability : input.getObject().getAbilities(game)) { if (ability instanceof KickerAbility && ((KickerAbility) ability).getKickedCounter(game, input.getObject().getSpellAbility()) > 0) { return true; diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java index 7d51a71f6c..e91ace7894 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java @@ -508,6 +508,51 @@ public class KickerTest extends CardTestPlayerBase { assertCounterCount(playerA, "Academy Drake", CounterType.P1P1, 2); } + @Test + public void test_ZCC_CopiedCreaturesSpellMustWork() { + // bug: + // Lost kicker status after creature spell copy by Verazol, the Split Current + + // Verazol, the Split Current enters the battlefield with a +1/+1 counter on it for each mana spent to cast it. + // Whenever you cast a kicked spell, you may remove two +1/+1 counters from Verazol, the Split Current. If you + // do, copy that spell. You may choose new targets for that copy. + addCard(Zone.HAND, playerA, "Verazol, the Split Current", 1); // {X}{G}{U} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + // + // Multikicker {R} + // When Deathforge Shaman enters the battlefield, it deals damage to target player equal to twice the number of times it was kicked. + addCard(Zone.HAND, playerA, "Deathforge Shaman", 1); // {4}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5 + 2 * 2); // for 2x kicker + + // prepare varazol with 4x counters + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 3); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Verazol, the Split Current"); + setChoice(playerA, "X=2"); // add 2 + 2 counters + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Verazol, the Split Current", 1); + checkPermanentCounters("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Verazol, the Split Current", CounterType.P1P1, 4); + + // cast 2x kicked spell for 4x damage + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Deathforge Shaman"); + setChoice(playerA, "Yes"); // 1x kick + setChoice(playerA, "Yes"); // 2x kick + setChoice(playerA, "No"); // stop multikicker + setChoice(playerA, "Yes"); // remove counters and activate verazol's copy + addTarget(playerA, playerA); // on resolve: target for copied spell + addTarget(playerA, playerB); // on resolve: target for original spell + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + // + checkLife("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 20 - 4); + checkLife("after", 1, PhaseStep.PRECOMBAT_MAIN, playerB, 20 - 4); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + } + @Test public void test_Single_OrimsChants() { // bug: diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/MimicVatTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/MimicVatTest.java index cd6bb2717d..1391b9472a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/copy/MimicVatTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/MimicVatTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.copy; import mage.constants.PhaseStep; @@ -7,7 +6,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class MimicVatTest extends CardTestPlayerBase { @@ -25,31 +23,40 @@ public class MimicVatTest extends CardTestPlayerBase { public void TestClone() { addCard(Zone.BATTLEFIELD, playerA, "Island", 6); // Imprint - Whenever a nontoken creature dies, you may exile that card. If you do, return each other card exiled with Mimic Vat to its owner's graveyard. - // {3}, {T}: Create a tokenonto the battlefield that's a copy of the exiled card. It gains haste. Exile it at the beginning of the next end step. + // {3}, {T}: Create a token that's a copy of the exiled card. It gains haste. Exile it at the beginning of the next end step. addCard(Zone.BATTLEFIELD, playerA, "Mimic Vat", 1); // Artifact {3} // {2}, {T}, Sacrifice a creature: Draw a card. addCard(Zone.BATTLEFIELD, playerA, "Phyrexian Vault", 1); // You may have Clone enter the battlefield as a copy of any creature on the battlefield. addCard(Zone.HAND, playerA, "Clone", 1);// Creature {3}{U} - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + // clone the opponent's creature castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Clone"); + setChoice(playerA, "Yes"); // use clone on etb setChoice(playerA, "Silvercoat Lion"); + + // kill clone and exile it (imprint into vat) activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}, Sacrifice a creature"); - setChoice(playerA, "Yes"); + setChoice(playerA, "Silvercoat Lion"); + setChoice(playerA, "Yes"); // exile killed card by vat + // turn 3 + + // create a token from exile (imprinted card: clone) activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{3}, {T}: Create a token"); + setChoice(playerA, "Yes"); // use clone on etb setChoice(playerA, "Silvercoat Lion"); + setStrictChooseMode(true); setStopAt(3, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertExileCount("Clone", 1); assertPermanentCount(playerB, "Silvercoat Lion", 1); assertPermanentCount(playerA, "Silvercoat Lion", 1); - } @Test diff --git a/Mage/src/main/java/mage/MageObjectImpl.java b/Mage/src/main/java/mage/MageObjectImpl.java index 9b3fa7d8d2..d6d56a068d 100644 --- a/Mage/src/main/java/mage/MageObjectImpl.java +++ b/Mage/src/main/java/mage/MageObjectImpl.java @@ -20,11 +20,14 @@ import mage.game.MageObjectAttribute; import mage.game.events.ZoneChangeEvent; import mage.util.GameLog; import mage.util.SubTypes; +import org.apache.log4j.Logger; import java.util.*; public abstract class MageObjectImpl implements MageObject { + private static final Logger logger = Logger.getLogger(MageObjectImpl.class); + protected UUID objectId; protected String name; diff --git a/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java index 2213538d4f..171f8d3d42 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java @@ -3,7 +3,6 @@ package mage.abilities.condition.common; import mage.abilities.Ability; import mage.abilities.condition.Condition; import mage.abilities.keyword.KickerAbility; -import mage.cards.Card; import mage.game.Game; @@ -18,22 +17,7 @@ public enum KickedCondition implements Condition { @Override public boolean apply(Game game, Ability source) { - Card card = game.getCard(source.getSourceId()); - if (card == null) { - // if permanent spell was copied then it enters with sourceId = PermanentToken instead Card (example: Lithoform Engine) - card = game.getPermanentEntering(source.getSourceId()); - } - - if (card != null) { - for (Ability ability : card.getAbilities()) { - if (ability instanceof KickerAbility) { - if (((KickerAbility) ability).isKicked(game, source)) { - return true; - } - } - } - } - return false; + return KickerAbility.getSourceObjectKickedCount(game, source) > 0; } @Override diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetKickerXValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetKickerXValue.java index dfac960ff9..e4ab0453cc 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetKickerXValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetKickerXValue.java @@ -10,6 +10,8 @@ import mage.game.stack.Spell; /** + * Kicker {X} + * * @author JayDi85 */ public enum GetKickerXValue implements DynamicValue { @@ -23,7 +25,7 @@ public enum GetKickerXValue implements DynamicValue { // only one X value per card possible // kicker can be calls multiple times (use getKickedCounter) - int finalValue = 0; + int countX = 0; Spell spell = game.getSpellOrLKIStack(sourceAbility.getSourceId()); if (spell != null && spell.getSpellAbility() != null) { int xValue = spell.getSpellAbility().getManaCostsToPay().getX(); @@ -39,13 +41,13 @@ public enum GetKickerXValue implements DynamicValue { if (haveVarCost) { int kickedCount = ((KickerAbility) ability).getKickedCounter(game, sourceAbility); if (kickedCount > 0) { - finalValue += kickedCount * xValue; + countX += kickedCount * xValue; } } } } } - return finalValue; + return countX; } @Override diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/MultikickerCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/MultikickerCount.java index 25de5d6457..5766208290 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/MultikickerCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/MultikickerCount.java @@ -1,14 +1,14 @@ - package mage.abilities.dynamicvalue.common; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.abilities.keyword.KickerAbility; -import mage.cards.Card; import mage.game.Game; /** + * Find permanent/spell kicked stats, can be used in ETB effects. + * * @author LevelX2 */ public enum MultikickerCount implements DynamicValue { @@ -16,16 +16,7 @@ public enum MultikickerCount implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - int count = 0; - Card card = game.getCard(sourceAbility.getSourceId()); - if (card != null) { - for (Ability ability : card.getAbilities(game)) { - if (ability instanceof KickerAbility) { - count += ((KickerAbility) ability).getKickedCounter(game, sourceAbility); - } - } - } - return count; + return KickerAbility.getSourceObjectKickedCount(game, sourceAbility); } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/common/AttachEffect.java b/Mage/src/main/java/mage/abilities/effects/common/AttachEffect.java index 677f102bb8..1918bc1c6c 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/AttachEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/AttachEffect.java @@ -8,9 +8,9 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.TargetCard; +import mage.util.CardUtil; /** - * * @author BetaSteward_at_googlemail.com */ public class AttachEffect extends OneShotEffect { @@ -37,10 +37,10 @@ public class AttachEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Permanent sourcePermanent = game.getPermanent(source.getSourceId()); if (sourcePermanent != null) { + // if it activating on the stack then allow +1 zcc int zcc = game.getState().getZoneChangeCounter(sourcePermanent.getId()); - if (zcc == source.getSourceObjectZoneChangeCounter() - || zcc == source.getSourceObjectZoneChangeCounter() + 1 - || zcc == source.getSourceObjectZoneChangeCounter() + 2) { + if (zcc == CardUtil.getActualSourceObjectZoneChangeCounter(game, source) + || zcc == CardUtil.getActualSourceObjectZoneChangeCounter(game, source) + 1) { Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); if (permanent != null) { return permanent.addAttachment(source.getSourceId(), source, game); diff --git a/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java b/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java index 0d7468fe56..ae06330536 100644 --- a/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java @@ -1,21 +1,21 @@ package mage.abilities.keyword; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.abilities.StaticAbility; import mage.abilities.costs.*; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.constants.AbilityType; +import mage.cards.Card; import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.stack.Spell; import mage.players.Player; +import mage.util.CardUtil; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -45,7 +45,7 @@ import java.util.concurrent.ConcurrentHashMap; * Otherwise, the spell is cast as if it did not have those targets. See rule * 601.2c. * - * @author LevelX2 + * @author LevelX2, JayDi85 */ public class KickerAbility extends StaticAbility implements OptionalAdditionalSourceCosts { @@ -203,17 +203,25 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo * @return */ public static String getActivationKey(Ability source, Game game) { - // must use ZCC from the moment of spell's ability activation - int zcc = source.getSourceObjectZoneChangeCounter(); - if (zcc == 0) { - // if ability is not activated yet (example: triggered ability checking the kicker conditional) - zcc = game.getState().getZoneChangeCounter(source.getSourceId()); - } + // Kicker activates in STACK zone so all zcc must be from "stack moment" + // Use cases: + // * resolving spell have same zcc (example: check kicker status in sorcery/instant); + // * copied spell have same zcc as source spell (see Spell.copySpell and zcc sync); + // * creature/token from resolved spell have +1 zcc after moved to battlefield (example: check kicker status in ETB triggers/effects); - // triggers or activated abilities moves to stack and card's ZCC is changed -- so you must use workaround to find spell's zcc - if (source.getAbilityType() == AbilityType.TRIGGERED || source.getAbilityType() == AbilityType.ACTIVATED) { + // find object info from the source ability (it can be a permanent or a spell on stack, on the moment of trigger/resolve) + MageObject sourceObject = source.getSourceObject(game); + Zone sourceObjectZone = game.getState().getZone(sourceObject.getId()); + int zcc = CardUtil.getActualSourceObjectZoneChangeCounter(game, source); + + // find "stack moment" zcc: + // * permanent cards enters from STACK to BATTLEFIELD (+1 zcc) + // * permanent tokens enters from OUTSIDE to BATTLEFIELD (+1 zcc, see prepare code in TokenImpl.putOntoBattlefieldHelper) + // * spells and copied spells resolves on STACK (zcc not changes) + if (sourceObjectZone != Zone.STACK) { --zcc; } + return zcc + ""; } @@ -308,4 +316,44 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo } return sb.toString(); } + + /** + * Find spell's kicked stats. Must be used on stack only, e.g. for SPELL_CAST events + * + * @param game + * @param spellId + * @return + */ + public static int getSpellKickedCount(Game game, UUID spellId) { + int count = 0; + Spell spell = game.getSpellOrLKIStack(spellId); + if (spell != null) { + for (Ability ability : spell.getAbilities(game)) { + if (ability instanceof KickerAbility) { + count += ((KickerAbility) ability).getKickedCounter(game, spell.getSpellAbility()); + } + } + } + return count; + } + + /** + * Find source object's kicked stats. Can be used in any places, e.g. in ETB effects + * + * @param game + * @param abilityToCheck + * @return + */ + public static int getSourceObjectKickedCount(Game game, Ability abilityToCheck) { + MageObject sourceObject = abilityToCheck.getSourceObject(game); + int count = 0; + if (sourceObject instanceof Card) { + for (Ability ability : ((Card) sourceObject).getAbilities(game)) { + if (ability instanceof KickerAbility) { + count += ((KickerAbility) ability).getKickedCounter(game, abilityToCheck); + } + } + } + return count; + } } diff --git a/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java b/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java index b47231adca..cac84c61da 100644 --- a/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java +++ b/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java @@ -115,7 +115,7 @@ public abstract class ModalDoubleFacesCard extends CardImpl { @Override public Counters getCounters(Game game) { - return leftHalfCard.getCounters(game.getState()); + return getCounters(game.getState()); } @Override diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index 0254debdfa..566db4eac9 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -6,7 +6,7 @@ import mage.constants.SuperType; import mage.constants.TargetController; import mage.filter.common.*; import mage.filter.predicate.Predicates; -import mage.filter.predicate.mageobject.KickedPredicate; +import mage.filter.predicate.mageobject.KickedSpellPredicate; import mage.filter.predicate.mageobject.MulticoloredPredicate; import mage.filter.predicate.permanent.AnotherPredicate; import mage.filter.predicate.permanent.AttackingPredicate; @@ -646,7 +646,7 @@ public final class StaticFilters { public static final FilterSpell FILTER_SPELL_KICKED_A = new FilterSpell("a kicked spell"); static { - FILTER_SPELL_KICKED_A.add(KickedPredicate.instance); + FILTER_SPELL_KICKED_A.add(KickedSpellPredicate.instance); FILTER_SPELL_KICKED_A.setLockedFilter(true); } diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/KickedPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/KickedPredicate.java deleted file mode 100644 index 9c7d45f1f2..0000000000 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/KickedPredicate.java +++ /dev/null @@ -1,35 +0,0 @@ -package mage.filter.predicate.mageobject; - -import mage.MageObject; -import mage.abilities.Ability; -import mage.abilities.keyword.KickerAbility; -import mage.filter.predicate.Predicate; -import mage.game.Game; -import mage.game.stack.Spell; - -/** - * @author TheElk801 - */ -public enum KickedPredicate implements Predicate { - instance; - - @Override - public boolean apply(MageObject input, Game game) { - Spell spell = game.getSpell(input.getId()); - if (spell == null) { - return false; - } - for (Ability ability : spell.getAbilities()) { - if (ability instanceof KickerAbility - && ((KickerAbility) ability).getKickedCounter(game, spell.getSpellAbility()) > 0) { - return true; - } - } - return false; - } - - @Override - public String toString() { - return "Kicked"; - } -} diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/KickedSpellPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/KickedSpellPredicate.java new file mode 100644 index 0000000000..4d91f8ef7b --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/KickedSpellPredicate.java @@ -0,0 +1,29 @@ +package mage.filter.predicate.mageobject; + +import mage.MageObject; +import mage.abilities.keyword.KickerAbility; +import mage.filter.predicate.Predicate; +import mage.game.Game; + +/** + * Find spell's kicked stats. + *

+ * Warning, must be used for SPELL_CAST events only + * (if you need kicked stats in ETB effects then search object's abilities instead spell, + * see MultikickerCount as example) + * + * @author TheElk801 + */ +public enum KickedSpellPredicate implements Predicate { + instance; + + @Override + public boolean apply(MageObject input, Game game) { + return KickerAbility.getSpellKickedCount(game, input.getId()) > 0; + } + + @Override + public String toString() { + return "Kicked"; + } +} diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index d675cb4d44..fc76413697 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -35,6 +35,7 @@ import mage.util.Copyable; import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import mage.watchers.Watchers; +import org.apache.log4j.Logger; import java.io.Serializable; import java.util.*; @@ -52,6 +53,7 @@ import static java.util.Collections.emptyList; */ public class GameState implements Serializable, Copyable { + private static final Logger logger = Logger.getLogger(GameState.class); private static final ThreadLocalStringBuilder threadLocalBuilder = new ThreadLocalStringBuilder(1024); public static final String COPIED_FROM_CARD_KEY = "CopiedFromCard"; @@ -1225,9 +1227,10 @@ public class GameState implements Serializable, Copyable { } public void updateZoneChangeCounter(UUID objectId) { - Integer value = getZoneChangeCounter(objectId); + int value = getZoneChangeCounter(objectId); value++; - this.zoneChangeCounter.put(objectId, value); + setZoneChangeCounter(objectId, value); + // card is changing zone so clear state if (cardState.containsKey(objectId)) { this.cardState.get(objectId).clear(); diff --git a/Mage/src/main/java/mage/game/ZonesHandler.java b/Mage/src/main/java/mage/game/ZonesHandler.java index 9338b334af..dc9927b02e 100644 --- a/Mage/src/main/java/mage/game/ZonesHandler.java +++ b/Mage/src/main/java/mage/game/ZonesHandler.java @@ -328,6 +328,7 @@ public final class ZonesHandler { return false; } } + if (!game.replaceEvent(event)) { Zone fromZone = event.getFromZone(); if (event.getToZone() == Zone.BATTLEFIELD) { @@ -382,6 +383,11 @@ public final class ZonesHandler { } } if (success) { + // change ZCC on real enter + // warning, tokens creation code uses same zcc logic as cards (+1 zcc on enter to battlefield) + // so if you want to change zcc logic here (but I know you don't) then change token code + // too in TokenImpl.putOntoBattlefieldHelper + // KickerTest do many tests for token's zcc if (event.getToZone() == Zone.BATTLEFIELD && event.getTarget() != null) { event.getTarget().updateZoneChangeCounter(game, event); } else if (!(card instanceof Permanent)) { diff --git a/Mage/src/main/java/mage/game/permanent/PermanentToken.java b/Mage/src/main/java/mage/game/permanent/PermanentToken.java index f18118fbe9..a0d755bec9 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentToken.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentToken.java @@ -6,6 +6,7 @@ import mage.abilities.costs.mana.ManaCost; import mage.cards.Card; import mage.constants.EmptyNames; import mage.game.Game; +import mage.game.events.ZoneChangeEvent; import mage.game.permanent.token.Token; import java.util.UUID; @@ -119,6 +120,14 @@ public class PermanentToken extends PermanentImpl { } } + @Override + public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) { + // token must change zcc on enters to battlefield (like cards do with stack), + // so it can keep abilities settings synced with copied spell/card + // example: kicker ability of copied creature spell + super.updateZoneChangeCounter(game, event); + } + @Override public Card getMainCard() { // token don't have game card, so return itself diff --git a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java index f61dfc50f1..8b78ea3b22 100644 --- a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java +++ b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java @@ -198,51 +198,68 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { Token token = event.getToken(); int amount = event.getAmount(); - - List permanents = new ArrayList<>(); - List permanentsEntered = new ArrayList<>(); - String setCode = token instanceof TokenImpl ? ((TokenImpl) token).getSetCode(game, event.getSourceId()) : null; + List needTokens = new ArrayList<>(); + List allowedTokens = new ArrayList<>(); + + // prepare tokens to enter for (int i = 0; i < amount; i++) { - PermanentToken newToken = new PermanentToken(token, event.getPlayerId(), setCode, game); // use event.getPlayerId() because it can be replaced by replacement effect - game.getState().addCard(newToken); - permanents.add(newToken); - game.getPermanentsEntering().put(newToken.getId(), newToken); - newToken.setTapped(tapped); + // use event.getPlayerId() as controller cause it can be replaced by replacement effect + PermanentToken newPermanent = new PermanentToken(token, event.getPlayerId(), setCode, game); + game.getState().addCard(newPermanent); + needTokens.add(newPermanent); + game.getPermanentsEntering().put(newPermanent.getId(), newPermanent); + newPermanent.setTapped(tapped); + + ZoneChangeEvent emptyEvent = new ZoneChangeEvent(newPermanent, newPermanent.getControllerId(), Zone.OUTSIDE, Zone.BATTLEFIELD); + // tokens zcc must simulate card's zcc too keep copied card/spell settings + // (example: etb's kicker ability of copied creature spell, see tests with Deathforge Shaman) + newPermanent.updateZoneChangeCounter(game, emptyEvent); } + + // check ETB effects game.setScopeRelevant(true); - for (Permanent permanent : permanents) { + for (Permanent permanent : needTokens) { if (permanent.entersBattlefield(source, game, Zone.OUTSIDE, true)) { - permanentsEntered.add(permanent); + allowedTokens.add(permanent); } else { game.getPermanentsEntering().remove(permanent.getId()); } } game.setScopeRelevant(false); + + // put allowed tokens to play int createOrder = game.getState().getNextPermanentOrderNumber(); - for (Permanent permanent : permanentsEntered) { + for (Permanent permanent : allowedTokens) { game.addPermanent(permanent, createOrder); permanent.setZone(Zone.BATTLEFIELD, game); game.getPermanentsEntering().remove(permanent.getId()); + // keep tokens ids if (token instanceof TokenImpl) { ((TokenImpl) token).lastAddedTokenIds.add(permanent.getId()); ((TokenImpl) token).lastAddedTokenId = permanent.getId(); } - game.addSimultaneousEvent(new ZoneChangeEvent(permanent, permanent.getControllerId(), Zone.OUTSIDE, Zone.BATTLEFIELD)); + + // created token events + ZoneChangeEvent zccEvent = new ZoneChangeEvent(permanent, permanent.getControllerId(), Zone.OUTSIDE, Zone.BATTLEFIELD); + game.addSimultaneousEvent(zccEvent); if (permanent instanceof PermanentToken && created) { game.addSimultaneousEvent(new CreatedTokenEvent(source, (PermanentToken) permanent)); } + + // must attack if (attacking && game.getCombat() != null && game.getActivePlayerId().equals(permanent.getControllerId())) { game.getCombat().addAttackingCreature(permanent.getId(), game, attackedPlayer); } + + // game logs if (created) { game.informPlayers(controller.getLogName() + " creates a " + permanent.getLogName() + " token"); } else { game.informPlayers(permanent.getLogName() + " enters the battlefield as a token under " + controller.getLogName() + "'s control'"); } - } game.getState().applyEffects(game); // Needed to do it here without LKIReset i.e. do get SwordOfTheMeekTest running correctly. } diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index a8a645f5d2..25b13a9485 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -35,6 +35,7 @@ import mage.util.CardUtil; import mage.util.GameLog; import mage.util.ManaUtil; import mage.util.SubTypes; +import org.apache.log4j.Logger; import java.util.*; @@ -43,6 +44,8 @@ import java.util.*; */ public class Spell extends StackObjImpl implements Card { + private static final Logger logger = Logger.getLogger(Spell.class); + private final List spellAbilities = new ArrayList<>(); private final List spellCards = new ArrayList<>(); diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 74f777ec55..6325dbce46 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1149,4 +1149,24 @@ public final class CardUtil { } return CardUtil.getSourceLogName(game, " (source: ", source, ")", ""); } + + /** + * Find actual ZCC of source object, works in any moment (even before source ability activated) + *

+ * Use case for usage: if you want to get actual object ZCC before ability resolve + * (ability gets zcc after resolve/activate/trigger only -- ?wtf workaround to targets setup I think?) + * + * @param game + * @param source + * @return + */ + public static int getActualSourceObjectZoneChangeCounter(Game game, Ability source) { + // current object zcc, find from source object (it can be permanent or spell on stack) + int zcc = source.getSourceObjectZoneChangeCounter(); + if (zcc == 0) { + // if ability is not activated yet then use current object's zcc (example: triggered etb ability checking the kicker conditional) + zcc = game.getState().getZoneChangeCounter(source.getSourceId()); + } + return zcc; + } }