mirror of
https://github.com/correl/mage.git
synced 2024-12-24 11:50:45 +00:00
Token's zone change counter (ZCC) improves:
* Now token's zcc uses same logic as card's zcc: enters to battlefield with +1 zcc instead +0 zcc * It can improve support of copied spells that store zcc related data (bug example: lost kicked status for copied spell/token); * Kicker abilities - improved support with copied creature spells (example: Verazol, the Split Current, #7431, #7433); * Refactor: simplified kicker code;
This commit is contained in:
parent
3eb57eade9
commit
f38639e1db
24 changed files with 262 additions and 177 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,8 @@ enum VineGeckoPredicate implements ObjectPlayerPredicate<ObjectPlayer<Card>> {
|
|||
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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<MageObject> {
|
||||
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";
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p>
|
||||
* 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<MageObject> {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public boolean apply(MageObject input, Game game) {
|
||||
return KickerAbility.getSpellKickedCount(game, input.getId()) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Kicked";
|
||||
}
|
||||
}
|
|
@ -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<GameState> {
|
||||
|
||||
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<GameState> {
|
|||
}
|
||||
|
||||
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();
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -198,51 +198,68 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
|
|||
|
||||
Token token = event.getToken();
|
||||
int amount = event.getAmount();
|
||||
|
||||
List<Permanent> permanents = new ArrayList<>();
|
||||
List<Permanent> permanentsEntered = new ArrayList<>();
|
||||
|
||||
String setCode = token instanceof TokenImpl ? ((TokenImpl) token).getSetCode(game, event.getSourceId()) : null;
|
||||
|
||||
List<Permanent> needTokens = new ArrayList<>();
|
||||
List<Permanent> 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.
|
||||
}
|
||||
|
|
|
@ -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<SpellAbility> spellAbilities = new ArrayList<>();
|
||||
private final List<Card> spellCards = new ArrayList<>();
|
||||
|
||||
|
|
|
@ -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)
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue