Merge pull request #11417 from ssk97/TagTracking3_KeywordAbilities

Costs Tag Tracking part 3: Most keyword abilities
This commit is contained in:
xenohedron 2023-11-20 21:28:40 -05:00 committed by GitHub
commit 4977fea307
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 510 additions and 404 deletions

View file

@ -6,7 +6,7 @@ import mage.abilities.Ability;
import mage.abilities.common.CantBeCounteredSourceAbility; import mage.abilities.common.CantBeCounteredSourceAbility;
import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.effects.common.CopyPermanentEffect; import mage.abilities.effects.common.CopyPermanentEffect;
import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
@ -14,6 +14,7 @@ import mage.constants.SubType;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
import mage.util.functions.CopyApplier; import mage.util.functions.CopyApplier;
import java.util.UUID; import java.util.UUID;
@ -65,11 +66,21 @@ class AlteredEgoCopyApplier extends CopyApplier {
// effect is applied to that object after applying the copy effect with that exception, the // effect is applied to that object after applying the copy effect with that exception, the
// exceptions effect doesnt happen. // exceptions effect doesnt happen.
if (!isCopyOfCopy(source, blueprint, copyToObjectId)) { if (!isCopyOfCopy(source, blueprint, copyToObjectId) && CardUtil.checkSourceCostsTagExists(game, source, "X")) {
// except it enters with an additional X +1/+1 counters on it // except it enters with an additional X +1/+1 counters on it
blueprint.getAbilities().add( blueprint.getAbilities().add(
new EntersBattlefieldAbility(new EntersBattlefieldWithXCountersEffect(CounterType.P1P1.createInstance())) new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(
CardUtil.getSourceCostsTag(game, source, "X", 0)
)))
); );
/*
* If the chosen creature has {X} in its mana cost, that X is considered to be 0.
* The value of X in Altered Ego's last ability will be whatever value was chosen for X while casting Altered Ego.
* (2016-04-08)
* So the X value of Altered Ego must be separate from the copied creature's X value
*/
CardUtil.getSourceCostsTagsMap(game, source).remove("X");
} }
return true; return true;

View file

@ -73,7 +73,7 @@ class StrongholdArenaGainLifeEffect extends OneShotEffect {
if (controller == null) { if (controller == null) {
return false; return false;
} }
controller.gainLife(KickerAbility.getSourceObjectKickedCount(game, source) * 3, game, source); controller.gainLife(KickerAbility.getKickedCounter(game, source) * 3, game, source);
return true; return true;
} }
} }

View file

@ -67,6 +67,48 @@ public class BlitzTest extends CardTestPlayerBase {
assertHandCount(playerA, 1); assertHandCount(playerA, 1);
} }
@Test
public void testBlitzCopy() {
//Copying the spell on the stack must include the Blitz ability activation
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 6);
addCard(Zone.HAND, playerA, decoy);
addCard(Zone.HAND, playerA, "Double Major");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, decoy + withBlitz);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major",decoy);
setChoice(playerA, ""); //stack triggers
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, decoy, 0);
assertGraveyardCount(playerA, decoy, 1);
assertGraveyardCount(playerA, "Double Major", 1);
assertHandCount(playerA, 2);
}
@Test
public void testBlitzClone() {
//Copying the creature permanent must not include Blitz activation
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 8);
addCard(Zone.HAND, playerA, decoy);
addCard(Zone.HAND, playerA, "Clone");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, decoy + withBlitz);
waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Clone");
setChoice(playerA,true);
setChoice(playerA,decoy);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, decoy, 1);
assertGraveyardCount(playerA, decoy, 1);
assertHandCount(playerA, 1);
}
@Test @Test
public void testNoBlitz() { public void testNoBlitz() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);

View file

@ -70,5 +70,41 @@ public class EchoTest extends CardTestPlayerBase {
assertTappedCount("Mountain", true, 0); assertTappedCount("Mountain", true, 0);
} }
//Deranged Hermit has been cloned with Phantasmal Image.
//The Phantasmal Image version of the Deranged Hermit had to pay the echo cost multiple times.
@Test
public void testEchoTriggerClone() {
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 15);
// Deranged Hermit {3}{G}{G}
// Echo
addCard(Zone.HAND, playerA, "Deranged Hermit");
addCard(Zone.HAND, playerA, "Phantasmal Image");
addCard(Zone.HAND, playerA, "Double Major");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Deranged Hermit");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Deranged Hermit");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Phantasmal Image");
setChoice(playerA, true);
setChoice(playerA, "Deranged Hermit");
setChoice(playerA, ""); //stack triggers
setChoice(playerA, "");
setChoice(playerA, true); //Pay echo costs
setChoice(playerA, true);
setChoice(playerA, true);
setStrictChooseMode(true);
setStopAt(3, PhaseStep.PRECOMBAT_MAIN);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertPermanentCount(playerA, "Deranged Hermit", 3);
assertTappedCount("Tropical Island", true, 15);
}
} }

View file

@ -0,0 +1,150 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author notgreat
*/
public class SpectacleTest extends CardTestPlayerBase {
@Test
public void testWithoutSpectacleBasic() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1+4);
addCard(Zone.HAND, playerA, "Lightning Bolt"); // {R}
addCard(Zone.HAND, playerA, "Spikewheel Acrobat"); // {3}{R}, Spectacle {2}{R}
checkPlayableAbility("Can't cast with Spectacle yet", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Spikewheel Acrobat with spectacle", false);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN);
checkPlayableAbility("Can cast with Spectacle", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Spikewheel Acrobat with spectacle", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spikewheel Acrobat");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA,"Spikewheel Acrobat",1);
assertTappedCount("Mountain",true,1+4);
}
@Test
public void testWithoutSpectacleTriggerAfterDamage() {
// Rafter Demon {2}{B}{R}
// Spectacle {3}{B}{R}
// When Rafter Demon enters the battlefield, if its spectacle cost was paid, each opponent discards a card.
addCard(Zone.BATTLEFIELD, playerA, "Badlands", 6);
addCard(Zone.HAND, playerA, "Lightning Bolt"); // {R}
addCard(Zone.HAND, playerA, "Rafter Demon"); // {2}{B}{R}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rafter Demon");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA,"Rafter Demon",1);
assertTappedCount("Badlands",true,5);
assertGraveyardCount(playerA, "Lightning Bolt", 1);
assertLife(playerB, 17);
assertGraveyardCount(playerB, 0);
}
@Test
public void testWithSpectacle() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.HAND, playerA, "Lightning Bolt"); // {R}
addCard(Zone.HAND, playerA, "Spikewheel Acrobat"); // Spectacle {2}{R}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt",playerB);
waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spikewheel Acrobat with spectacle");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA,"Spikewheel Acrobat",1);
assertTappedCount("Mountain",true,4);
assertLife(playerB, 17);
}
@Test
public void testRafterDemonCopyClone() {
addCard(Zone.BATTLEFIELD, playerA, "Badlands", 3);
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 4);
addCard(Zone.BATTLEFIELD, playerA, "Wastes", 5);
// Rafter Demon {2}{B}{R}
// Spectacle {3}{B}{R}
// When Rafter Demon enters the battlefield, if its spectacle cost was paid, each opponent discards a card.
addCard(Zone.HAND, playerA, "Rafter Demon");
addCard(Zone.HAND, playerA, "Lightning Bolt");
addCard(Zone.HAND, playerB, "Darksteel Relic",5);
addCard(Zone.HAND, playerA, "Double Major");
addCard(Zone.HAND, playerA, "Clone");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rafter Demon with spectacle");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major");
addTarget(playerA, "Rafter Demon");
addTarget(playerB, "Darksteel Relic",2);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkGraveyardCount("Discard x2", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Darksteel Relic", 2);
checkPermanentCount("Rafter Demon x2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rafter Demon", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Clone");
setChoice(playerA, true); // copy
setChoice(playerA, "Rafter Demon");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
setStrictChooseMode(true);
execute();
assertGraveyardCount(playerA, "Lightning Bolt", 1);
assertGraveyardCount(playerB, 2);
assertHandCount(playerB, "Darksteel Relic", 3);
assertPermanentCount(playerA, "Rafter Demon", 3);
assertLife(playerB, 17);
}
@Test
public void SnapcasterMageWithSpectacle() {
//Should not be castable with Spectacle on flashback, since that's two alternative casts at once
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 3+2+2);
addCard(Zone.HAND, playerA, "Snapcaster Mage", 1);
addCard(Zone.HAND, playerA, "Skewer the Critics", 1);
addCard(Zone.HAND, playerA, "Snapcaster Mage", 1);
addCard(Zone.HAND, playerA, "Pyretic Ritual", 1);
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Skewer the Critics", playerB);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snapcaster Mage");
addTarget(playerA, "Skewer the Critics");
checkPlayableAbility("No flashback with Spectacle available", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Flashback", false );
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Pyretic Ritual", true);
activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Flashback");
addTarget(playerA, playerB);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerB, 20-3-3);
assertTappedCount("Volcanic Island", true, 3+2+2);
assertExileCount("Skewer the Critics", 1);
}
}

View file

@ -280,9 +280,7 @@ public class SquadTest extends CardTestPlayerBase {
assertPermanentCount(playerA, flagellant, 3); // One original + its squad buddy + the squad buddy from the additional trigger assertPermanentCount(playerA, flagellant, 3); // One original + its squad buddy + the squad buddy from the additional trigger
} }
@Ignore
@Test @Test
//TODO: Enable after fixing clones activating it if they have the same zcc. Also affects Kicker
public void test_CloneMustNotCopySquad() { public void test_CloneMustNotCopySquad() {
addCard(Zone.BATTLEFIELD, playerA, swamp, 8); // 3 + 2 + 3 addCard(Zone.BATTLEFIELD, playerA, swamp, 8); // 3 + 2 + 3
addCard(Zone.BATTLEFIELD, playerA, "Island", 1); addCard(Zone.BATTLEFIELD, playerA, "Island", 1);

View file

@ -53,4 +53,34 @@ public class AlteredEgoTest extends CardTestPlayerBase {
assertGraveyardCount(playerA, "Altered Ego", 1); assertGraveyardCount(playerA, "Altered Ego", 1);
} }
/**
* If the chosen creature has {X} in its mana cost, that X is considered to be 0.
* The value of X in Altered Ego's last ability will be whatever value was chosen for X while casting Altered Ego.
* (2016-04-08)
*/
@Test
public void copyXCreature() {
addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2);
addCard(Zone.BATTLEFIELD, playerB, "Tropical Island", 7);
addCard(Zone.HAND, playerA, "Endless One"); // {X}, ETB with X +1/+1 counters, 0/0
addCard(Zone.HAND, playerB, "Altered Ego"); // {X}{2}{G}{U}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Endless One");
setChoice(playerA, "X=2");
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Altered Ego");
setChoice(playerB, "X=3");
setChoice(playerB, true); // use copy
setChoice(playerB, "Endless One"); // copy target
setChoice(playerB, ""); // Order place counters effects
setStrictChooseMode(true);
setStopAt(2, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Endless One", 1);
assertPowerToughness(playerA, "Endless One", 2, 2);
assertPermanentCount(playerB, "Endless One", 1);
assertPowerToughness(playerB, "Endless One", 3, 3); //The X should not be copied
}
} }

View file

@ -113,7 +113,6 @@ public class CopyPermanentSpellTest extends CardTestPlayerBase {
assertPowerToughness(playerA, "Aether Figment", 3, 3, Filter.ComparisonScope.All); assertPowerToughness(playerA, "Aether Figment", 3, 3, Filter.ComparisonScope.All);
} }
@Ignore // currently fails
@Test @Test
public void testSurgeTrigger() { public void testSurgeTrigger() {
makeTester(); makeTester();
@ -121,7 +120,7 @@ public class CopyPermanentSpellTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Memnite"); addCard(Zone.HAND, playerA, "Memnite");
addCard(Zone.HAND, playerA, "Reckless Bushwhacker"); addCard(Zone.HAND, playerA, "Reckless Bushwhacker");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Memnite"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Memnite", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reckless Bushwhacker with surge"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reckless Bushwhacker with surge");
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);

View file

@ -0,0 +1,108 @@
package org.mage.test.cards.copy;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author notgreat
*/
public class CostTagCopyCloneTests extends CardTestPlayerBase {
@Test
public void KickerETBCountersClone() {
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 11);
addCard(Zone.HAND, playerA, "Aether Figment");
addCard(Zone.HAND, playerA, "Clone");
addCard(Zone.HAND, playerA, "Shrivel");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aether Figment");
setChoice(playerA, true); // with Kicker
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, false);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Clone");
setChoice(playerA, true); // use copy
setChoice(playerA, "Aether Figment"); // copy target
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, false);
// since Clone wasn't kicked, it's a 1/1. Cast Shrivel to easily separate
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shrivel");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, false);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Aether Figment", 1);
assertGraveyardCount(playerA, "Clone", 1);
}
@Test
public void XCountersCopyClone() {
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 7);
addCard(Zone.HAND, playerA, "Endless One");
addCard(Zone.HAND, playerA, "Clone");
addCard(Zone.HAND, playerA, "Double Major");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Endless One");
setChoice(playerA, "X=1");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major");
addTarget(playerA, "Endless One");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Clone");
setChoice(playerA, true);
setChoice(playerA, "Endless One"); // since Clone doesn't copy X, it's a 0/0 and dies
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Endless One", 2);
assertGraveyardCount(playerA, "Clone", 1);
}
@Test
public void ETBXClone() {
addCard(Zone.BATTLEFIELD, playerA, "Tundra", 4+4);
addCard(Zone.HAND, playerA, "Defenders of Humanity");
addCard(Zone.HAND, playerA, "Clever Impersonator");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Defenders of Humanity");
setChoice(playerA, "X=1");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, false);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Clever Impersonator");
setChoice(playerA, true);
setChoice(playerA, "Defenders of Humanity"); //clones don't copy X
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, false);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA,"Defenders of Humanity",2);
assertPermanentCount(playerA,"Astartes Warrior Token",1);
}
@Test
public void ETBXCopy() {
addCard(Zone.BATTLEFIELD, playerA, "Tundra", 4+1);
addCard(Zone.HAND, playerA, "Defenders of Humanity");
addCard(Zone.BATTLEFIELD, playerA, "Overloaded Mage-Ring");
assertPermanentCount(playerA,"Astartes Warrior Token",0);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Defenders of Humanity");
setChoice(playerA, "X=1");
activateAbility(1,PhaseStep.PRECOMBAT_MAIN,playerA,
"{1}, {T}, Sacrifice {this}: Copy target spell you control.",
"Defenders of Humanity");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA,"Defenders of Humanity",2);
assertPermanentCount(playerA,"Astartes Warrior Token",2);
}
}

View file

@ -159,24 +159,21 @@ public interface Ability extends Controllable, Serializable {
void addManaCostsToPay(ManaCost manaCost); void addManaCostsToPay(ManaCost manaCost);
/** /**
* Gets a map of the cost tags (set while casting/activating) of this ability, can be null if no tags have been set yet * Gets a map of the cost tags (set while casting/activating) of this ability, can be null if no tags have been set yet.
* does NOT return the source permanent's tags * Does NOT return the source permanent's tags.
* You should not be using this function in implementation of cards,
* this is a backing data structure used for internal storage.
* Use CardUtil {@link mage.util.CardUtil#getSourceCostsTag getSourceCostsTag} or {@link mage.util.CardUtil#checkSourceCostsTagExists checkSourceCostsTagExists} instead
* *
* @return The map of tags and corresponding objects * @return The map of tags and corresponding objects
*/ */
Map<String, Object> getCostsTagMap(); Map<String, Object> getCostsTagMap();
/** /**
* Set tag to the value, initializes this ability's tags map if it is null * Set tag for this ability to the value, initializes this ability's tags map if needed.
* Should only be used from an {@link ActivatedAbility} (including {@link SpellAbility})
*/ */
void setCostsTag(String tag, Object value); void setCostsTag(String tag, Object value);
/**
* Returns the value of the tag or defaultValue if the tag is not found in this ability's tag map
* does NOT check the source permanent's tags, use CardUtil.getSourceCostsTag for that
*
* @return The given tag value (or the default if not found)
*/
Object getCostsTagOrDefault(String tag, Object defaultValue);
/** /**
* Retrieves the effects that are put into the place by the resolution of * Retrieves the effects that are put into the place by the resolution of

View file

@ -706,11 +706,6 @@ public abstract class AbilityImpl implements Ability {
return manaCostsToPay; return manaCostsToPay;
} }
/**
* Accessed to see what was optional/variable costs were paid
*
* @return
*/
@Override @Override
public Map<String, Object> getCostsTagMap() { public Map<String, Object> getCostsTagMap() {
return costsTagMap; return costsTagMap;
@ -721,12 +716,6 @@ public abstract class AbilityImpl implements Ability {
} }
costsTagMap.put(tag, value); costsTagMap.put(tag, value);
} }
public Object getCostsTagOrDefault(String tag, Object defaultValue){
if (costsTagMap != null && costsTagMap.containsKey(tag)){
return costsTagMap.get(tag);
}
return defaultValue;
}
@Override @Override
public Effects getEffects() { public Effects getEffects() {

View file

@ -1,11 +1,10 @@
package mage.abilities.condition.common; package mage.abilities.condition.common;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.keyword.BargainAbility; import mage.abilities.keyword.BargainAbility;
import mage.cards.Card;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
/** /**
* Checks if the spell was cast with the alternate Bargain cost * Checks if the spell was cast with the alternate Bargain cost
@ -18,16 +17,7 @@ public enum BargainedCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
// TODO: replace by Tag Cost Tracking. return CardUtil.checkSourceCostsTagExists(game, source, BargainAbility.BARGAIN_ACTIVATION_VALUE_KEY);
MageObject sourceObject = source.getSourceObject(game);
if (sourceObject instanceof Card) {
for (Ability ability : ((Card) sourceObject).getAbilities(game)) {
if (ability instanceof BargainAbility) {
return ((BargainAbility) ability).wasBargained(game, source);
}
}
}
return false;
} }
@Override @Override

View file

@ -4,8 +4,7 @@ import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.keyword.BlitzAbility; import mage.abilities.keyword.BlitzAbility;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
import java.util.List;
/** /**
* @author TheElk801 * @author TheElk801
@ -15,7 +14,6 @@ public enum BlitzedCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
List<Integer> blitzActivations = (List<Integer>) game.getState().getValue(BlitzAbility.BLITZ_ACTIVATION_VALUE_KEY + source.getSourceId()); return CardUtil.checkSourceCostsTagExists(game, source, BlitzAbility.BLITZ_ACTIVATION_VALUE_KEY);
return blitzActivations != null && blitzActivations.contains(game.getState().getZoneChangeCounter(source.getSourceId()));
} }
} }

View file

@ -3,7 +3,6 @@ package mage.abilities.condition.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.keyword.DashAbility; import mage.abilities.keyword.DashAbility;
import mage.cards.Card;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil; import mage.util.CardUtil;
@ -15,11 +14,6 @@ public enum DashedCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Card card = game.getCard(source.getSourceId()); return CardUtil.checkSourceCostsTagExists(game, source, DashAbility.getActivationKey());
return card != null
&& CardUtil.castStream(card
.getAbilities(game)
.stream(), DashAbility.class)
.anyMatch(ability -> ability.isActivated(source, game));
} }
} }

View file

@ -5,8 +5,8 @@ package mage.abilities.condition.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.keyword.EvokeAbility; import mage.abilities.keyword.EvokeAbility;
import mage.cards.Card;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
/** /**
* Checks if a the spell was cast with the alternate evoke costs * Checks if a the spell was cast with the alternate evoke costs
@ -20,12 +20,6 @@ public enum EvokedCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Card card = game.getCard(source.getSourceId()); return CardUtil.checkSourceCostsTagExists(game, source, EvokeAbility.getActivationKey());
if (card != null) {
return card.getAbilities(game).stream()
.filter(EvokeAbility.class::isInstance)
.anyMatch(evoke -> ((EvokeAbility) evoke).isActivated(source, game));
}
return false;
} }
} }

View file

@ -24,7 +24,7 @@ public enum KickedCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
return KickerAbility.getSourceObjectKickedCount(game, source) >= kickedCount; return KickerAbility.getKickedCounter(game, source) >= kickedCount;
} }
@Override @Override

View file

@ -1,10 +1,8 @@
package mage.abilities.condition.common; package mage.abilities.condition.common;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.keyword.KickerAbility; import mage.abilities.keyword.KickerAbility;
import mage.cards.Card;
import mage.game.Game; import mage.game.Game;
/** /**
@ -22,14 +20,6 @@ public class KickedCostCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
MageObject sourceObject = source.getSourceObject(game); return KickerAbility.getKickedCounterStrict(game, source, kickerCostText) > 0;
if (sourceObject instanceof Card) {
for (Ability ability : ((Card) sourceObject).getAbilities(game)) {
if (ability instanceof KickerAbility) {
return ((KickerAbility) ability).isKicked(game, source, kickerCostText);
}
}
}
return false;
} }
} }

View file

@ -3,8 +3,8 @@ package mage.abilities.condition.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.keyword.ProwlAbility; import mage.abilities.keyword.ProwlAbility;
import mage.cards.Card;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
/** /**
* Checks if a the spell was cast with the alternate prowl costs * Checks if a the spell was cast with the alternate prowl costs
@ -17,17 +17,7 @@ public enum ProwlCostWasPaidCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Card card = game.getCard(source.getSourceId()); return CardUtil.checkSourceCostsTagExists(game, source, ProwlAbility.getActivationKey());
if (card != null) {
for (Ability ability : card.getAbilities(game)) {
if (ability instanceof ProwlAbility) {
if (((ProwlAbility) ability).isActivated(source, game)) {
return true;
}
}
}
}
return false;
} }
@Override @Override

View file

@ -4,11 +4,8 @@ package mage.abilities.condition.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.keyword.SpectacleAbility; import mage.abilities.keyword.SpectacleAbility;
import mage.constants.AbilityType;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.List;
/** /**
* @author TheElk801 * @author TheElk801
@ -19,15 +16,6 @@ public enum SpectacleCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
if (source.getAbilityType() == AbilityType.TRIGGERED) { return CardUtil.checkSourceCostsTagExists(game, source, SpectacleAbility.SPECTACLE_ACTIVATION_VALUE_KEY);
@SuppressWarnings("unchecked")
List<Integer> spectacleActivations = (ArrayList) game.getState().getValue(SpectacleAbility.SPECTACLE_ACTIVATION_VALUE_KEY + source.getSourceId());
if (spectacleActivations != null) {
return spectacleActivations.contains(game.getState().getZoneChangeCounter(source.getSourceId()) - 1);
}
return false;
} else {
return source instanceof SpectacleAbility;
}
} }
} }

View file

@ -4,11 +4,8 @@ package mage.abilities.condition.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.keyword.SurgeAbility; import mage.abilities.keyword.SurgeAbility;
import mage.constants.AbilityType;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.List;
/** /**
* *
@ -20,15 +17,6 @@ public enum SurgedCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
if (source.getAbilityType() == AbilityType.TRIGGERED) { return CardUtil.checkSourceCostsTagExists(game, source, SurgeAbility.SURGE_ACTIVATION_VALUE_KEY);
@SuppressWarnings("unchecked")
List<Integer> surgeActivations = (ArrayList) game.getState().getValue(SurgeAbility.SURGE_ACTIVATION_VALUE_KEY + source.getSourceId());
if (surgeActivations != null) {
return surgeActivations.contains(game.getState().getZoneChangeCounter(source.getSourceId()) - 1);
}
return false;
} else {
return source instanceof SurgeAbility;
}
} }
} }

View file

@ -5,11 +5,11 @@ import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility; import mage.abilities.StaticAbility;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil;
import java.util.Iterator; import java.util.Iterator;
@ -20,7 +20,10 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement
protected final AlternativeCost alternativeCost; protected final AlternativeCost alternativeCost;
protected final String reminderText; protected final String reminderText;
private int zoneChangeCounter = 0; protected final String activationKey;
protected static String getActivationKey(String name){
return name+"ActivationKey";
}
protected AlternativeSourceCostsImpl(String name, String reminderText, String manaString) { protected AlternativeSourceCostsImpl(String name, String reminderText, String manaString) {
this(name, reminderText, new ManaCostsImpl<>(manaString)); this(name, reminderText, new ManaCostsImpl<>(manaString));
@ -31,13 +34,14 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement
this.name = name; this.name = name;
this.reminderText = reminderText; this.reminderText = reminderText;
this.alternativeCost = new AlternativeCostImpl<>(name, reminderText, cost); this.alternativeCost = new AlternativeCostImpl<>(name, reminderText, cost);
this.activationKey = getActivationKey(name);
} }
protected AlternativeSourceCostsImpl(final AlternativeSourceCostsImpl ability) { protected AlternativeSourceCostsImpl(final AlternativeSourceCostsImpl ability) {
super(ability); super(ability);
this.alternativeCost = ability.alternativeCost.copy(); this.alternativeCost = ability.alternativeCost.copy();
this.reminderText = ability.reminderText; this.reminderText = ability.reminderText;
this.zoneChangeCounter = ability.zoneChangeCounter; this.activationKey = ability.activationKey;
} }
@Override @Override
@ -58,15 +62,9 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement
|| !player.chooseUse(Outcome.Benefit, "Cast this for its " + this.name + " cost? (" + alternativeCost.getText(true) + ')', ability, game)) { || !player.chooseUse(Outcome.Benefit, "Cast this for its " + this.name + " cost? (" + alternativeCost.getText(true) + ')', ability, game)) {
return false; return false;
} }
ability.setCostsTag(activationKey, null);
alternativeCost.activate(); alternativeCost.activate();
if (zoneChangeCounter == 0) {
Card card = game.getCard(getSourceId());
if (card != null) {
zoneChangeCounter = card.getZoneChangeCounter(game);
} else {
throw new IllegalArgumentException("source card not found");
}
}
ability.clearManaCostsToPay(); ability.clearManaCostsToPay();
ability.clearCosts(); ability.clearCosts();
for (Iterator<Cost> it = ((Costs<Cost>) alternativeCost).iterator(); it.hasNext(); ) { for (Iterator<Cost> it = ((Costs<Cost>) alternativeCost).iterator(); it.hasNext(); ) {
@ -82,11 +80,7 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement
@Override @Override
public boolean isActivated(Ability ability, Game game) { public boolean isActivated(Ability ability, Game game) {
Card card = game.getCard(sourceId); return CardUtil.checkSourceCostsTagExists(game, ability, activationKey);
if (card != null && card.getZoneChangeCounter(game) <= zoneChangeCounter + 1) {
return alternativeCost.isActivated(game);
}
return false;
} }
@Override @Override
@ -102,7 +96,6 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement
@Override @Override
public void resetCost() { public void resetCost() {
alternativeCost.reset(); alternativeCost.reset();
this.zoneChangeCounter = 0;
} }
@Override @Override

View file

@ -19,7 +19,7 @@ public enum GetKickerXValue implements DynamicValue {
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
// Currently identical logic to the Manacost X value // Currently identical logic to the Manacost X value
// which should be fine since you can only have one X at a time // which should be fine since you can only have one X at a time
return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0); return CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
} }
@Override @Override

View file

@ -14,7 +14,7 @@ public enum GetXLoyaltyValue implements DynamicValue {
@Override @Override
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0); return CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
} }
@Override @Override

View file

@ -14,7 +14,7 @@ public enum GetXValue implements DynamicValue {
@Override @Override
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0); return CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
} }
@Override @Override

View file

@ -16,7 +16,7 @@ public enum ManacostVariableValue implements DynamicValue {
@Override @Override
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0); return CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
} }
@Override @Override

View file

@ -16,7 +16,7 @@ public enum MultikickerCount implements DynamicValue {
@Override @Override
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
return KickerAbility.getSourceObjectKickedCount(game, sourceAbility); return KickerAbility.getKickedCounter(game, sourceAbility);
} }
@Override @Override

View file

@ -57,7 +57,7 @@ public class EntersBattlefieldWithXCountersEffect extends OneShotEffect {
} }
} }
if (permanent != null) { if (permanent != null) {
int amount = ((int) CardUtil.getSourceCostsTag(game, source, "X", 0)) * multiplier; int amount = CardUtil.getSourceCostsTag(game, source, "X", 0) * multiplier;
if (amount > 0) { if (amount > 0) {
Counter counterToAdd = counter.copy(); Counter counterToAdd = counter.copy();
counterToAdd.add(amount - counter.getCount()); counterToAdd.add(amount - counter.getCount());

View file

@ -1,7 +1,6 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility; import mage.abilities.StaticAbility;
@ -16,7 +15,6 @@ import mage.filter.predicate.Predicates;
import mage.filter.predicate.permanent.TokenPredicate; import mage.filter.predicate.permanent.TokenPredicate;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil;
/** /**
* Written before ruling was clarified. Feel free to put the ruling once it gets there. * Written before ruling was clarified. Feel free to put the ruling once it gets there.
@ -37,7 +35,7 @@ public class BargainAbility extends StaticAbility implements OptionalAdditionalS
private static final String reminderText = "You may sacrifice an artifact, enchantment, or token as you cast this spell."; private static final String reminderText = "You may sacrifice an artifact, enchantment, or token as you cast this spell.";
private final String rule; private final String rule;
private String activationKey; // TODO: replace by Tag Cost Tracking. public static final String BARGAIN_ACTIVATION_VALUE_KEY = "bargainActivation";
protected OptionalAdditionalCost additionalCost; protected OptionalAdditionalCost additionalCost;
@ -61,14 +59,12 @@ public class BargainAbility extends StaticAbility implements OptionalAdditionalS
this.rule = additionalCost.getName() + ' ' + additionalCost.getReminderText(); this.rule = additionalCost.getName() + ' ' + additionalCost.getReminderText();
this.setRuleAtTheTop(true); this.setRuleAtTheTop(true);
this.addHint(BargainCostWasPaidHint.instance); this.addHint(BargainCostWasPaidHint.instance);
this.activationKey = null;
} }
private BargainAbility(final BargainAbility ability) { private BargainAbility(final BargainAbility ability) {
super(ability); super(ability);
this.rule = ability.rule; this.rule = ability.rule;
this.additionalCost = ability.additionalCost.copy(); this.additionalCost = ability.additionalCost.copy();
this.activationKey = ability.activationKey;
} }
@Override @Override
@ -80,7 +76,6 @@ public class BargainAbility extends StaticAbility implements OptionalAdditionalS
if (additionalCost != null) { if (additionalCost != null) {
additionalCost.reset(); additionalCost.reset();
} }
this.activationKey = null;
} }
@Override @Override
@ -104,7 +99,7 @@ public class BargainAbility extends StaticAbility implements OptionalAdditionalS
for (Cost cost : ((Costs<Cost>) additionalCost)) { for (Cost cost : ((Costs<Cost>) additionalCost)) {
ability.getCosts().add(cost.copy()); ability.getCosts().add(cost.copy());
} }
this.activationKey = getActivationKey(ability, game); ability.setCostsTag(BARGAIN_ACTIVATION_VALUE_KEY, null);
} }
@Override @Override
@ -112,40 +107,6 @@ public class BargainAbility extends StaticAbility implements OptionalAdditionalS
return additionalCost.getCastSuffixMessage(0); return additionalCost.getCastSuffixMessage(0);
} }
public boolean wasBargained(Game game, Ability source) {
return activationKey != null && getActivationKey(source, game).equalsIgnoreCase(activationKey);
}
/**
* TODO: remove with Tag Cost Tracking.
* Return activation zcc key for searching spell's settings in source object
*
* @param source
* @param game
*/
public static String getActivationKey(Ability source, Game game) {
// Bargain 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);
// 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 + "";
}
@Override @Override
public String getRule() { public String getRule() {
return rule; return rule;

View file

@ -19,9 +19,6 @@ import mage.constants.TimingRule;
import mage.game.Game; import mage.game.Game;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import java.util.ArrayList;
import java.util.List;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -80,15 +77,7 @@ public class BlitzAbility extends SpellAbility {
if (!super.activate(game, noMana)) { if (!super.activate(game, noMana)) {
return false; return false;
} }
Object obj = game.getState().getValue(BLITZ_ACTIVATION_VALUE_KEY + getSourceId()); this.setCostsTag(BLITZ_ACTIVATION_VALUE_KEY, null);
List<Integer> blitzActivations;
if (obj != null) {
blitzActivations = (List<Integer>) obj;
} else {
blitzActivations = new ArrayList<>();
game.getState().setValue(BLITZ_ACTIVATION_VALUE_KEY + getSourceId(), blitzActivations);
}
blitzActivations.add(game.getState().getZoneChangeCounter(getSourceId()));
return true; return true;
} }
} }

View file

@ -41,6 +41,9 @@ public class DashAbility extends AlternativeSourceCostsImpl {
public DashAbility copy() { public DashAbility copy() {
return new DashAbility(this); return new DashAbility(this);
} }
public static String getActivationKey(){
return getActivationKey(KEYWORD);
}
} }
class DashAddDelayedTriggeredAbilityEffect extends OneShotEffect { class DashAddDelayedTriggeredAbilityEffect extends OneShotEffect {

View file

@ -9,10 +9,9 @@ import mage.constants.Outcome;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Set;
/** /**
* 702.40. Entwine * 702.40. Entwine
@ -32,9 +31,9 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
private static final String keywordText = "Entwine"; private static final String keywordText = "Entwine";
protected static final String reminderText = "You may {cost} in addition to any other costs to use all modes."; protected static final String reminderText = "You may {cost} in addition to any other costs to use all modes.";
protected static final String ENTWINE_ACTIVATION_VALUE_KEY = "entwineActivation";
protected OptionalAdditionalCost entwineCost; protected OptionalAdditionalCost entwineCost;
protected Set<String> activations = new HashSet<>(); // same logic as KickerAbility: activations per zoneChangeCounter
public EntwineAbility(String manaString) { public EntwineAbility(String manaString) {
super(Zone.STACK, null); super(Zone.STACK, null);
@ -62,7 +61,6 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
if (ability.entwineCost != null) { if (ability.entwineCost != null) {
this.entwineCost = ability.entwineCost.copy(); this.entwineCost = ability.entwineCost.copy();
} }
this.activations.addAll(ability.activations);
} }
@Override @Override
@ -97,7 +95,7 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
ability.addCost(cost.copy()); ability.addCost(cost.copy());
} }
} }
activateEntwine(game, ability); ability.setCostsTag(ENTWINE_ACTIVATION_VALUE_KEY, null);
} }
} }
@ -135,23 +133,9 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
if (entwineCost != null) { if (entwineCost != null) {
entwineCost.reset(); entwineCost.reset();
} }
String key = getActivationKey(source, game);
this.activations.remove(key);
}
private void activateEntwine(Game game, Ability source) {
String key = getActivationKey(source, game);
this.activations.add(key);
} }
public boolean costWasActivated(Ability ability, Game game) { public boolean costWasActivated(Ability ability, Game game) {
String key = getActivationKey(ability, game); return CardUtil.checkSourceCostsTagExists(game, ability, ENTWINE_ACTIVATION_VALUE_KEY);
return this.activations.contains(key);
}
private String getActivationKey(Ability source, Game game) {
// same logic as KickerAbility
return KickerAbility.getActivationKey(source, game);
} }
} }

View file

@ -39,4 +39,8 @@ public class EvokeAbility extends AlternativeSourceCostsImpl {
public EvokeAbility copy() { public EvokeAbility copy() {
return new EvokeAbility(this); return new EvokeAbility(this);
} }
public static String getActivationKey(){
return getActivationKey(EVOKE_KEYWORD);
}
} }

View file

@ -1,12 +1,10 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility; import mage.abilities.StaticAbility;
import mage.abilities.costs.*; import mage.abilities.costs.*;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
@ -16,7 +14,7 @@ import mage.players.Player;
import mage.util.CardUtil; import mage.util.CardUtil;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream;
/** /**
* 20121001 702.31. Kicker 702.31a Kicker is a static ability that functions * 20121001 702.31. Kicker 702.31a Kicker is a static ability that functions
@ -55,8 +53,6 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
protected static final String KICKER_REMINDER_COST = "You may {cost} in addition " protected static final String KICKER_REMINDER_COST = "You may {cost} in addition "
+ "to any other costs as you cast this spell."; + "to any other costs as you cast this spell.";
protected Map<String, Integer> activations = new ConcurrentHashMap<>(); // zoneChangeCounter, activations
protected String keywordText; protected String keywordText;
protected String reminderText; protected String reminderText;
protected List<OptionalAdditionalCost> kickerCosts = new LinkedList<>(); protected List<OptionalAdditionalCost> kickerCosts = new LinkedList<>();
@ -86,7 +82,6 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
} }
this.keywordText = ability.keywordText; this.keywordText = ability.keywordText;
this.reminderText = ability.reminderText; this.reminderText = ability.reminderText;
this.activations.putAll(ability.activations);
} }
@Override @Override
@ -119,32 +114,36 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
for (OptionalAdditionalCost cost : kickerCosts) { for (OptionalAdditionalCost cost : kickerCosts) {
cost.reset(); cost.reset();
} }
activations.clear();
} }
private int getKickedCounterStrict(Game game, Ability source, String needKickerCost) { private static String getActivationKey(String needKickerCost){
String key; return "kickerActivation"+needKickerCost;
if (needKickerCost.isEmpty()) {
// need all kickers
key = getActivationKey(source, "", game);
} else {
// need only cost related kickers
key = getActivationKey(source, needKickerCost, game);
} }
int totalActivations = 0; /**
if (kickerCosts.size() > 1) { * Return total kicker activations with the specified Cost (blank for all kickers/multikickers)
for (String activationKey : activations.keySet()) { * Checks the start of the tags, to work for that blank method, which requires direct access
if (activationKey.startsWith(key) && activations.get(activationKey) > 0) { *
totalActivations += activations.get(activationKey); * @param game
* @param source
* @param needKickerCost use cost.getText(true)
* @return
*/
public static int getKickedCounterStrict(Game game, Ability source, String needKickerCost) {
Map<String, Object> costsTags = CardUtil.getSourceCostsTagsMap(game, source);
if (costsTags == null) {
return 0;
} }
String finalActivationKey = getActivationKey(needKickerCost);
Stream<Map.Entry<String, Object>> tagStream = costsTags.entrySet().stream()
.filter(x -> x.getKey().startsWith(finalActivationKey));
return tagStream.mapToInt(x -> {
Object value = x.getValue();
if (!(value instanceof Integer)){
throw new IllegalStateException("Wrong code usage: Kicker tag "+x.getKey()+" needs Integer but has "+(value==null?"null":value.getClass().getName()));
} }
} else { return (int) value;
if (activations.containsKey(key) && activations.get(key) > 0) { }).sum();
totalActivations += activations.get(key);
}
}
return totalActivations;
} }
/** /**
@ -154,7 +153,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
* @param source * @param source
* @return * @return
*/ */
public int getKickedCounter(Game game, Ability source) { public static int getKickedCounter(Game game, Ability source) {
return getKickedCounterStrict(game, source, ""); return getKickedCounterStrict(game, source, "");
} }
@ -186,47 +185,11 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
} }
private void activateKicker(OptionalAdditionalCost kickerCost, Ability source, Game game) { private void activateKicker(OptionalAdditionalCost kickerCost, Ability source, Game game) {
int amount = 1;
String key = getActivationKey(source, kickerCost.getText(true), game);
if (activations.containsKey(key)) {
amount += activations.get(key);
}
activations.put(key, amount);
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.KICKED, source.getSourceId(), source, source.getControllerId())); game.fireEvent(GameEvent.getEvent(GameEvent.EventType.KICKED, source.getSourceId(), source, source.getControllerId()));
}
/** String activationKey = getActivationKey(kickerCost.getText(true));
* Return activation zcc key for searching spell's settings in source object Integer next = CardUtil.getSourceCostsTag(game, source, activationKey,0)+1;
* source.setCostsTag(activationKey,next);
* @param source
* @param game
* @return
*/
public static String getActivationKey(Ability source, Game game) {
// 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);
// 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 + "";
}
private String getActivationKey(Ability source, String costText, Game game) {
return getActivationKey(source, game) + ((kickerCosts.size() > 1) ? costText : "");
} }
@Override @Override
@ -330,35 +293,10 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
* @return * @return
*/ */
public static int getSpellKickedCount(Game game, UUID spellId) { public static int getSpellKickedCount(Game game, UUID spellId) {
int count = 0;
Spell spell = game.getSpellOrLKIStack(spellId); Spell spell = game.getSpellOrLKIStack(spellId);
if (spell != null) { if (spell != null) {
for (Ability ability : spell.getAbilities(game)) { return KickerAbility.getKickedCounter(game, spell.getSpellAbility());
if (ability instanceof KickerAbility) {
count += ((KickerAbility) ability).getKickedCounter(game, spell.getSpellAbility());
} }
} return 0;
}
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;
} }
} }

View file

@ -46,4 +46,8 @@ public class ProwlAbility extends AlternativeSourceCostsImpl {
public boolean isAvailable(Ability source, Game game) { public boolean isAvailable(Ability source, Game game) {
return ProwlCondition.instance.apply(game, source); return ProwlCondition.instance.apply(game, source);
} }
public static String getActivationKey(){
return getActivationKey(PROWL_KEYWORD);
}
} }

View file

@ -10,8 +10,6 @@ import mage.constants.SpellAbilityType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID; import java.util.UUID;
/** /**
@ -55,15 +53,9 @@ public class SpectacleAbility extends SpellAbility {
} }
@Override @Override
@SuppressWarnings("unchecked")
public boolean activate(Game game, boolean noMana) { public boolean activate(Game game, boolean noMana) {
if (super.activate(game, noMana)) { if (super.activate(game, noMana)) {
List<Integer> spectacleActivations = (List<Integer>) game.getState().getValue(SPECTACLE_ACTIVATION_VALUE_KEY + getSourceId()); this.setCostsTag(SPECTACLE_ACTIVATION_VALUE_KEY,null);
if (spectacleActivations == null) {
spectacleActivations = new ArrayList<>(); // zoneChangeCounter
game.getState().setValue(SPECTACLE_ACTIVATION_VALUE_KEY + getSourceId(), spectacleActivations);
}
spectacleActivations.add(game.getState().getZoneChangeCounter(getSourceId()));
return true; return true;
} }
return false; return false;

View file

@ -1,6 +1,5 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility; import mage.abilities.StaticAbility;
@ -9,7 +8,6 @@ import mage.abilities.costs.*;
import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.CreateTokenCopySourceEffect; import mage.abilities.effects.CreateTokenCopySourceEffect;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.Zone; import mage.constants.Zone;
@ -24,6 +22,7 @@ import mage.util.CardUtil;
public class SquadAbility extends StaticAbility implements OptionalAdditionalSourceCosts { public class SquadAbility extends StaticAbility implements OptionalAdditionalSourceCosts {
protected OptionalAdditionalCost cost; protected OptionalAdditionalCost cost;
protected static final String SQUAD_KEYWORD = "Squad"; protected static final String SQUAD_KEYWORD = "Squad";
protected static final String SQUAD_ACTIVATION_VALUE_KEY = "squadActivationCount";
protected static final String SQUAD_REMINDER = "You may pay an additional " protected static final String SQUAD_REMINDER = "You may pay an additional "
+ "{cost} any number of times as you cast this spell."; + "{cost} any number of times as you cast this spell.";
public SquadAbility() { public SquadAbility() {
@ -34,7 +33,6 @@ public class SquadAbility extends StaticAbility implements OptionalAdditionalSou
super(Zone.STACK, null); super(Zone.STACK, null);
setSquadCost(cost); setSquadCost(cost);
addSubAbility(new SquadTriggerAbility()); addSubAbility(new SquadTriggerAbility());
//Note that I get subabilities list's position 0 to modify the zcc/count references
} }
private SquadAbility(final SquadAbility ability) { private SquadAbility(final SquadAbility ability) {
@ -47,7 +45,7 @@ public class SquadAbility extends StaticAbility implements OptionalAdditionalSou
return new SquadAbility(this); return new SquadAbility(this);
} }
public final void setSquadCost(Cost cost) { private void setSquadCost(Cost cost) {
OptionalAdditionalCost newCost = new OptionalAdditionalCostImpl( OptionalAdditionalCost newCost = new OptionalAdditionalCostImpl(
SQUAD_KEYWORD, SQUAD_REMINDER, cost); SQUAD_KEYWORD, SQUAD_REMINDER, cost);
newCost.setRepeatable(true); newCost.setRepeatable(true);
@ -59,28 +57,6 @@ public class SquadAbility extends StaticAbility implements OptionalAdditionalSou
cost.reset(); cost.reset();
} }
protected static int get_zcc(Ability source, Game game) {
// Squad/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);
// 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;
}
@Override @Override
public void addOptionalAdditionalCosts(Ability ability, Game game) { public void addOptionalAdditionalCosts(Ability ability, Game game) {
if (!(ability instanceof SpellAbility)) { if (!(ability instanceof SpellAbility)) {
@ -94,7 +70,7 @@ public class SquadAbility extends StaticAbility implements OptionalAdditionalSou
boolean again = true; boolean again = true;
while (player.canRespond() && again) { while (player.canRespond() && again) {
String times = ""; String times = "";
int activatedCount = getSquadCount(); int activatedCount = cost.getActivateCount();
times = (activatedCount + 1) + (activatedCount == 0 ? " time " : " times "); times = (activatedCount + 1) + (activatedCount == 0 ? " time " : " times ");
// TODO: add AI support to find max number of possible activations (from available mana) // TODO: add AI support to find max number of possible activations (from available mana)
// canPay checks only single mana available, not total mana usage // canPay checks only single mana available, not total mana usage
@ -111,10 +87,7 @@ public class SquadAbility extends StaticAbility implements OptionalAdditionalSou
again = false; again = false;
} }
} }
SquadTriggerAbility squadETB = (SquadTriggerAbility)getSubAbilities().get(0); ability.setCostsTag(SQUAD_ACTIVATION_VALUE_KEY,cost.getActivateCount());
squadETB.zcc = get_zcc(ability, game);
SquadEffectETB squadEffect = (SquadEffectETB)squadETB.getEffects().get(0);
squadEffect.activationCount = cost.getActivateCount();
} }
@Override @Override
@ -131,18 +104,8 @@ public class SquadAbility extends StaticAbility implements OptionalAdditionalSou
cost.getText()+"any number of times. When this creature enters the battlefield, "+ cost.getText()+"any number of times. When this creature enters the battlefield, "+
"create that many tokens that are copies of it.)</i>"; "create that many tokens that are copies of it.)</i>";
} }
/**
* Number of times squad cost was paid
*
* @return int activation count
*/
public int getSquadCount() {
return cost.getActivateCount();
}
} }
class SquadTriggerAbility extends EntersBattlefieldTriggeredAbility { class SquadTriggerAbility extends EntersBattlefieldTriggeredAbility {
protected Integer zcc;
public SquadTriggerAbility() { public SquadTriggerAbility() {
super(new SquadEffectETB()); super(new SquadEffectETB());
this.setRuleVisible(false); this.setRuleVisible(false);
@ -150,7 +113,6 @@ class SquadTriggerAbility extends EntersBattlefieldTriggeredAbility {
private SquadTriggerAbility(final SquadTriggerAbility ability) { private SquadTriggerAbility(final SquadTriggerAbility ability) {
super(ability); super(ability);
this.zcc = ability.zcc;
} }
@Override @Override
public SquadTriggerAbility copy() { public SquadTriggerAbility copy() {
@ -159,21 +121,17 @@ class SquadTriggerAbility extends EntersBattlefieldTriggeredAbility {
@Override @Override
public boolean checkInterveningIfClause(Game game) { public boolean checkInterveningIfClause(Game game) {
if (zcc != null && zcc == SquadAbility.get_zcc(this, game)){ int squadCount = CardUtil.getSourceCostsTag(game, this, SquadAbility.SQUAD_ACTIVATION_VALUE_KEY,0);
SquadEffectETB effect = (SquadEffectETB)getEffects().get(0); return (squadCount > 0);
return effect.activationCount > 0;
}
return false;
} }
@Override @Override
public String getRule() { public String getRule() {
return "Squad <i>(When this creature enters the battlefield, if its squad cost was paid, " return "Squad <i>(When this creature enters the battlefield, if its squad cost was paid, "
+ "create a token thats a copy of it for each time its squad cost was paid.)</i>"; + "create a token that's a copy of it for each time its squad cost was paid.)</i>";
} }
} }
class SquadEffectETB extends OneShotEffect { class SquadEffectETB extends OneShotEffect {
protected Integer activationCount;
SquadEffectETB() { SquadEffectETB() {
super(Outcome.Benefit); super(Outcome.Benefit);
@ -181,7 +139,6 @@ class SquadEffectETB extends OneShotEffect {
private SquadEffectETB(final SquadEffectETB effect) { private SquadEffectETB(final SquadEffectETB effect) {
super(effect); super(effect);
this.activationCount = effect.activationCount;
} }
@Override @Override
@ -191,10 +148,8 @@ class SquadEffectETB extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
if (activationCount != null) { int squadCount = CardUtil.getSourceCostsTag(game, source, SquadAbility.SQUAD_ACTIVATION_VALUE_KEY,0);
CreateTokenCopySourceEffect effect = new CreateTokenCopySourceEffect(activationCount); CreateTokenCopySourceEffect effect = new CreateTokenCopySourceEffect(squadCount);
return effect.apply(game, source); return effect.apply(game, source);
} }
return true;
}
} }

View file

@ -10,8 +10,6 @@ import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.watchers.common.CastSpellLastTurnWatcher; import mage.watchers.common.CastSpellLastTurnWatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID; import java.util.UUID;
/** /**
@ -65,15 +63,9 @@ public class SurgeAbility extends SpellAbility {
} }
@Override @Override
@SuppressWarnings("unchecked")
public boolean activate(Game game, boolean noMana) { public boolean activate(Game game, boolean noMana) {
if (super.activate(game, noMana)) { if (super.activate(game, noMana)) {
List<Integer> surgeActivations = (ArrayList) game.getState().getValue(SURGE_ACTIVATION_VALUE_KEY + getSourceId()); this.setCostsTag(SURGE_ACTIVATION_VALUE_KEY, null);
if (surgeActivations == null) {
surgeActivations = new ArrayList<>(); // zoneChangeCounter
game.getState().setValue(SURGE_ACTIVATION_VALUE_KEY + getSourceId(), surgeActivations);
}
surgeActivations.add(game.getState().getZoneChangeCounter(getSourceId()));
return true; return true;
} }
return false; return false;

View file

@ -408,10 +408,6 @@ public class StackAbility extends StackObjectImpl implements Ability {
ability.setCostsTag(tag, value); ability.setCostsTag(tag, value);
} }
@Override @Override
public Object getCostsTagOrDefault(String tag, Object defaultValue){
return ability.getCostsTagOrDefault(tag, defaultValue);
}
@Override
public AbilityType getAbilityType() { public AbilityType getAbilityType() {
return ability.getAbilityType(); return ability.getAbilityType();
} }

View file

@ -282,27 +282,13 @@ public abstract class PlayerImpl implements Player, Serializable {
this.bufferTimeLeft = player.getBufferTimeLeft(); this.bufferTimeLeft = player.getBufferTimeLeft();
this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving;
for (Entry<UUID, Set<MageIdentifier>> entry : player.getCastSourceIdWithAlternateMana().entrySet()) { this.castSourceIdWithAlternateMana = CardUtil.deepCopyObject(player.castSourceIdWithAlternateMana);
this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue()))); this.castSourceIdManaCosts = CardUtil.deepCopyObject(player.castSourceIdManaCosts);
} this.castSourceIdCosts = CardUtil.deepCopyObject(player.castSourceIdCosts);
for (Entry<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>());
for (Entry<MageIdentifier, ManaCosts<ManaCost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
for (Entry<UUID, Map<MageIdentifier, Costs<Cost>>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), new HashMap<>());
for (Entry<MageIdentifier, Costs<Cost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
this.payManaMode = player.payManaMode; this.payManaMode = player.payManaMode;
this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null;
for (Designation object : player.designations) { this.designations = CardUtil.deepCopyObject(player.designations);
this.designations.add(object.copy());
}
} }
@Override @Override

View file

@ -1677,8 +1677,17 @@ public final class CardUtil {
return new MageObjectReference(ability.getSourceId(), zcc, game); return new MageObjectReference(ability.getSourceId(), zcc, game);
} }
//Use the two other functions below to access the tags, this is just the shared logic for them /**
private static Map<String, Object> getCostsTags(Game game, Ability source) { * Returns the entire cost tags map of either the source ability, or the permanent source of the ability. May be null.
* Works in any moment (even before source ability activated)
* Usually you should use one of the single tag functions instead: getSourceCostsTag() or checkSourceCostsTagExists()
* Use this function with caution, as it directly exposes the backing data structure.
*
* @param game
* @param source
* @return the tag map (or null)
*/
public static Map<String, Object> getSourceCostsTagsMap(Game game, Ability source) {
Map<String, Object> costTags; Map<String, Object> costTags;
costTags = source.getCostsTagMap(); costTags = source.getCostsTagMap();
if (costTags == null && source.getSourcePermanentOrLKI(game) != null) { if (costTags == null && source.getSourcePermanentOrLKI(game) != null) {
@ -1696,12 +1705,13 @@ public final class CardUtil {
* @return if the tag was found * @return if the tag was found
*/ */
public static boolean checkSourceCostsTagExists(Game game, Ability source, String tag) { public static boolean checkSourceCostsTagExists(Game game, Ability source, String tag) {
Map<String, Object> costTags = getCostsTags(game, source); Map<String, Object> costTags = getSourceCostsTagsMap(game, source);
return costTags != null && costTags.containsKey(tag); return costTags != null && costTags.containsKey(tag);
} }
/** /**
* Find a specific tag in the cost tags of either the source ability, or the permanent source of the ability. * Find a specific tag in the cost tags of either the source ability, or the permanent source of the ability.
* Works in any moment (even before source ability activated) * Works in any moment (even before source ability activated)
* Do not use with null values, use checkSourceCostsTagExists instead
* *
* @param game * @param game
* @param source * @param source
@ -1709,10 +1719,17 @@ public final class CardUtil {
* @param defaultValue A default value to return if the tag is not found * @param defaultValue A default value to return if the tag is not found
* @return The object stored by the tag if found, the default if not * @return The object stored by the tag if found, the default if not
*/ */
public static Object getSourceCostsTag(Game game, Ability source, String tag, Object defaultValue){ public static <T> T getSourceCostsTag(Game game, Ability source, String tag, T defaultValue){
Map<String, Object> costTags = getCostsTags(game, source); Map<String, Object> costTags = getSourceCostsTagsMap(game, source);
if (costTags != null) { if (costTags != null) {
return costTags.getOrDefault(tag, defaultValue); Object value = costTags.getOrDefault(tag, defaultValue);
if (value == null) {
throw new IllegalStateException("Wrong code usage: Costs tag " + tag + " has value stored of type null but is trying to be read. Use checkSourceCostsTagExists");
}
if (value.getClass() != defaultValue.getClass()) {
throw new IllegalStateException("Wrong code usage: Costs tag " + tag + " has value stored of type " + value.getClass().getName() + " different from default of type " + defaultValue.getClass().getName());
}
return (T) value;
} }
return defaultValue; return defaultValue;
} }