Convert Kicker to costs tag system

This commit is contained in:
Steven Knipe 2023-11-16 15:07:58 -08:00
parent 1e76b59f4e
commit 77dd1711b5
6 changed files with 44 additions and 113 deletions

View file

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

View file

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

View file

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

View file

@ -1,12 +1,10 @@
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.cards.Card;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
@ -16,7 +14,7 @@ import mage.players.Player;
import mage.util.CardUtil;
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
@ -55,8 +53,6 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
protected static final String KICKER_REMINDER_COST = "You may {cost} in addition "
+ "to any other costs as you cast this spell.";
protected Map<String, Integer> activations = new ConcurrentHashMap<>(); // zoneChangeCounter, activations
protected String keywordText;
protected String reminderText;
protected List<OptionalAdditionalCost> kickerCosts = new LinkedList<>();
@ -86,7 +82,6 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
}
this.keywordText = ability.keywordText;
this.reminderText = ability.reminderText;
this.activations.putAll(ability.activations);
}
@Override
@ -119,32 +114,30 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
for (OptionalAdditionalCost cost : kickerCosts) {
cost.reset();
}
activations.clear();
}
private int getKickedCounterStrict(Game game, Ability source, String needKickerCost) {
String key;
if (needKickerCost.isEmpty()) {
// need all kickers
key = getActivationKey(source, "", game);
} else {
// need only cost related kickers
key = getActivationKey(source, needKickerCost, game);
}
private static String getActivationKey(String needKickerCost){
return "kickerActivation"+needKickerCost;
}
int totalActivations = 0;
if (kickerCosts.size() > 1) {
for (String activationKey : activations.keySet()) {
if (activationKey.startsWith(key) && activations.get(activationKey) > 0) {
totalActivations += activations.get(activationKey);
}
}
} else {
if (activations.containsKey(key) && activations.get(key) > 0) {
totalActivations += activations.get(key);
}
/**
* Return total kicker activations with the specified Cost (blank for all kickers/multikickers)
* Checks the start of the tags, to work for that blank method, which requires direct access
*
* @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.getSourceCostsTags(game, source);
if (costsTags == null) {
return 0;
}
return totalActivations;
String finalActivationKey = getActivationKey(needKickerCost);
Stream<Map.Entry<String, Object>> tagStream = costsTags.entrySet().stream()
.filter(x -> x.getKey().startsWith(finalActivationKey));
return tagStream.mapToInt(x -> (int)x.getValue()).sum();
}
/**
@ -154,7 +147,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
* @param source
* @return
*/
public int getKickedCounter(Game game, Ability source) {
public static int getKickedCounter(Game game, Ability source) {
return getKickedCounterStrict(game, source, "");
}
@ -186,47 +179,11 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
}
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()));
}
/**
* Return activation zcc key for searching spell's settings in source object
*
* @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 : "");
String activationKey = getActivationKey(kickerCost.getText(true));
Integer next = (int)source.getCostsTagOrDefault(activationKey,0)+1;
source.setCostsTag(activationKey,next);
}
@Override
@ -330,35 +287,10 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
* @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 KickerAbility.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;
return 0;
}
}

View file

@ -1677,8 +1677,17 @@ public final class CardUtil {
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
*
* @param game
* @param source
* @return the tag map (or null)
*/
//
public static Map<String, Object> getSourceCostsTags(Game game, Ability source) {
Map<String, Object> costTags;
costTags = source.getCostsTagMap();
if (costTags == null && source.getSourcePermanentOrLKI(game) != null) {
@ -1696,7 +1705,7 @@ public final class CardUtil {
* @return if the tag was found
*/
public static boolean checkSourceCostsTagExists(Game game, Ability source, String tag) {
Map<String, Object> costTags = getCostsTags(game, source);
Map<String, Object> costTags = getSourceCostsTags(game, source);
return costTags != null && costTags.containsKey(tag);
}
/**
@ -1710,7 +1719,7 @@ public final class CardUtil {
* @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){
Map<String, Object> costTags = getCostsTags(game, source);
Map<String, Object> costTags = getSourceCostsTags(game, source);
if (costTags != null) {
return costTags.getOrDefault(tag, defaultValue);
}