Merge branch 'master' into Attractions

# Conflicts:
#	forge-core/src/main/java/forge/deck/DeckFormat.java
This commit is contained in:
Jetz
2024-06-19 22:58:49 -04:00
941 changed files with 20173 additions and 118050 deletions

View File

@@ -50,7 +50,6 @@ import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.Visitor;
import forge.util.collect.FCollection;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
@@ -327,7 +326,9 @@ public class Game {
int plId = 0;
for (RegisteredPlayer psc : players0) {
IGameEntitiesFactory factory = (IGameEntitiesFactory)psc.getPlayer();
Player pl = factory.createIngamePlayer(this, plId++);
// If the Registered Player already has a pre-assigned ID, use that. Otherwise, assign a new one.
Integer id = psc.getId();
Player pl = factory.createIngamePlayer(this, id == null ? plId++ : id);
allPlayers.add(pl);
ingamePlayers.add(pl);

View File

@@ -248,7 +248,7 @@ public final class GameActionUtil {
// do only non intrinsic
if (iSa.isSpell() && !iSa.isIntrinsic()) {
alternatives.add(iSa);
alternatives.addAll(getMayPlaySpellOptions(iSa, source, activator, altCostOnly));
alternatives.addAll(getMayPlaySpellOptions(iSa, stackCopy, activator, altCostOnly));
// currently only AltCost get added this way
}
}

View File

@@ -84,9 +84,11 @@ public abstract class SpellAbilityEffect {
String spellDesc = CardTranslation.translateSingleDescriptionText(rawSDesc,
sa.getHostCard().getName());
int idx = spellDesc.indexOf("(");
if (idx > 0) { //trim reminder text from StackDesc
spellDesc = spellDesc.substring(0, spellDesc.indexOf("(") - 1);
//trim reminder text from StackDesc
int idxL = spellDesc.indexOf(" (");
int idxR = spellDesc.indexOf(")");
if (idxL > 0 && idxR > idxL) {
spellDesc = spellDesc.replace(spellDesc.substring(idxL, idxR + 1), "");
}
if (reps != null) {

View File

@@ -26,9 +26,10 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
sb.append(Lang.joinHomogenous(getDefinedPlayersOrTargeted(sa)));
sb.append("chooses from a list.");
final List<Player> players = getDefinedPlayersOrTargeted(sa);
sb.append(Lang.joinHomogenous(players));
sb.append(players.size() == 1 ? " chooses" : " choose").append(" from a list.");
return sb.toString();
}

View File

@@ -120,8 +120,8 @@ public class DebuffEffect extends SpellAbilityEffect {
ProtectionFromColor = true;
}
if (ProtectionFromColor) {
// Split "Protection from all colors" into extra Protection from <color>
String allColors = "Protection from all colors";
// Split "Protection from each color" into extra Protection from <color>
String allColors = "Protection from each color";
if (tgtC.hasKeyword(allColors)) {
final List<String> allColorsProtect = Lists.newArrayList();
@@ -134,7 +134,7 @@ public class DebuffEffect extends SpellAbilityEffect {
}
// Extra for Spectra Ward
allColors = "Protection:Card.nonColorless:all colors:Aura";
allColors = "Protection:Card.nonColorless:each color:Aura";
if (tgtC.hasKeyword(allColors)) {
final List<String> allColorsProtect = Lists.newArrayList();

View File

@@ -180,12 +180,8 @@ public class RepeatEachEffect extends SpellAbilityEffect {
}
}
for (final Player p : repeatPlayers) {
if (optional) {
if (!p.getController().confirmAction(repeat, null, sa.getParam("RepeatOptionalMessage"), null)) {
continue;
} else if (sa.hasParam("RememberDeciders")) {
source.addRemembered(p);
}
if (optional && !p.getController().confirmAction(repeat, null, sa.getParam("RepeatOptionalMessage"), null)) {
continue;
}
if (nextTurn) {
game.getCleanup().addUntil(p, new GameCommand() {

View File

@@ -2258,7 +2258,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// If no colon exists in Madness keyword, it must have been granted and assumed the cost from host
sbLong.append("Madness ").append(this.getManaCost()).append(" (").append(inst.getReminderText());
sbLong.append(")").append("\r\n");
} else if (keyword.startsWith("Emerge") || keyword.startsWith("Reflect")) {
} else if (keyword.startsWith("Reflect")) {
final String[] k = keyword.split(":");
sbLong.append(k[0]).append(" ").append(ManaCostParser.parse(k[1]));
sbLong.append(" (").append(inst.getReminderText()).append(")");
@@ -2325,6 +2325,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
}
sbLong.append("\r\n");
} else if (keyword.startsWith("Emerge")) {
final String[] k = keyword.split(":");
sbLong.append(k[0]);
if (k.length > 2) {
sbLong.append(" from ").append(k[2].toLowerCase());
}
sbLong.append(" ").append(ManaCostParser.parse(k[1]));
sbLong.append(" (").append(inst.getReminderText()).append(")");
sbLong.append("\r\n");
} else if (inst.getKeyword().equals(Keyword.COMPANION)) {
sbLong.append("Companion — ");
sbLong.append(((Companion)inst).getDescription());
@@ -2484,7 +2494,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|| keyword.equals("Undaunted") || keyword.startsWith("Monstrosity")
|| keyword.startsWith("Embalm") || keyword.equals("Prowess")
|| keyword.startsWith("Eternalize") || keyword.startsWith("Reinforce")
|| keyword.startsWith("Champion") || keyword.startsWith("Prowl") || keyword.startsWith("Adapt")
|| keyword.startsWith("Champion") || keyword.startsWith("Freerunning") || keyword.startsWith("Prowl") || keyword.startsWith("Adapt")
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Chapter")
|| keyword.startsWith("Transfigure") || keyword.startsWith("Aura swap")
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")
@@ -3002,7 +3012,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} else if (keyword.startsWith("Starting intensity")) {
sbAfter.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n");
} else if (keyword.startsWith("Escalate") || keyword.startsWith("Buyback")
|| keyword.startsWith("Prowl")) {
|| keyword.startsWith("Freerunning") || keyword.startsWith("Prowl")) {
final String[] k = keyword.split(":");
final String manacost = k[1];
final Cost cost = new Cost(manacost, false);
@@ -6757,7 +6767,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
pW = true;
protectKey += "W";
}
} else if (kw.contains("all colors")) {
} else if (kw.contains("each color")) {
protectKey += "allcolors:";
} else if (kw.equals("Protection from everything")) {
protectKey += "everything:";

View File

@@ -676,7 +676,7 @@ public class CardFactoryUtil {
validSource = "Green" + (damage ? "Source" : "");
} else if (protectType.equals("colorless")) {
validSource = "Colorless" + (damage ? "Source" : "");
} else if (protectType.equals("all colors")) {
} else if (protectType.equals("each color")) {
validSource = "nonColorless" + (damage ? "Source" : "");
} else if (protectType.equals("everything")) {
return "";
@@ -2830,16 +2830,22 @@ public class CardFactoryUtil {
} else if (keyword.startsWith("Emerge")) {
final String[] kw = keyword.split(":");
String costStr = kw[1];
final SpellAbility sa = card.getFirstSpellAbility();
String validStr = kw.length > 2 ? kw[2] : "Creature";
String desc = "(Emerge";
if (kw.length > 2) {
desc += " from " + kw[2].toLowerCase();
}
desc += ")";
final SpellAbility sa = card.getFirstSpellAbility();
final SpellAbility newSA = sa.copyWithDefinedCost(new Cost(costStr, false));
newSA.getRestrictions().setIsPresent("Creature.YouCtrl+CanBeSacrificedBy");
newSA.getRestrictions().setIsPresent(validStr + ".YouCtrl+CanBeSacrificedBy");
newSA.putParam("Secondary", "True");
newSA.setAlternativeCost(AlternativeCost.Emerge);
newSA.setDescription(sa.getDescription() + " (Emerge)");
newSA.putParam("AfterDescription", "(Emerge)");
newSA.setDescription(sa.getDescription() + " " + desc);
newSA.putParam("AfterDescription", desc);
newSA.setIntrinsic(intrinsic);
inst.addSpellAbility(newSA);
} else if (keyword.startsWith("Embalm")) {
@@ -3105,6 +3111,27 @@ public class CardFactoryUtil {
// instantiate attach ability
final SpellAbility sa = AbilityFactory.getAbility(abilityStr.toString(), card);
inst.addSpellAbility(sa);
} else if (keyword.startsWith("Freerunning")) {
final String[] k = keyword.split(":");
final Cost freerunningCost = new Cost(k[1], false);
final SpellAbility newSA = card.getFirstSpellAbility().copyWithDefinedCost(freerunningCost);
if (host.isInstant() || host.isSorcery()) {
newSA.putParam("Secondary", "True");
}
newSA.putParam("PrecostDesc", "Freerunning");
newSA.putParam("CostDesc", ManaCostParser.parse(k[1]));
// makes new SpellDescription
final StringBuilder sb = new StringBuilder();
sb.append(newSA.getCostDescription());
sb.append("(").append(inst.getReminderText()).append(")");
newSA.setDescription(sb.toString());
newSA.setAlternativeCost(AlternativeCost.Freerunning);
newSA.setIntrinsic(intrinsic);
inst.addSpellAbility(newSA);
} else if (keyword.startsWith("Fuse") && card.getStateName().equals(CardStateName.Original)) {
final SpellAbility sa = AbilityFactory.buildFusedAbility(card.getCard());
card.addSpellAbility(sa);
@@ -3900,7 +3927,8 @@ public class CardFactoryUtil {
final String[] k = keyword.split(":");
sbDesc.append(" from ").append(k[2]);
sbValid.append("| ValidSource$ ").append(k[1]);
final String param = k[2].contains("abilities") ? "ValidSA$ " : "ValidSource$ ";
sbValid.append("| ").append(param).append(k[1]);
}
String effect = "Mode$ CantTarget | ValidCard$ Card.Self | Secondary$ True"

View File

@@ -2120,6 +2120,14 @@ public class CardProperty {
}
List<String> nameList = Lists.newArrayList(names.split(";"));
return nameList.contains(card.getName());
} else if (property.equals("NotedNameAetherSearcher")) {
String names = sourceController.getDraftNotes().get("Aether Searcher");
if (names == null || names.isEmpty()) {
return false;
}
List<String> nameList = Lists.newArrayList(names.split(";"));
return nameList.contains(card.getName());
} else if (property.equals("NotedTypes")) {
// Should Paliano Vanguard be hardcoded here or part of the property?

View File

@@ -57,7 +57,7 @@ public final class CardUtil {
"Cycling", "Echo", "Kicker", "Flashback", "Madness", "Morph",
"Affinity", "Entwine", "Splice", "Ninjutsu",
"Transmute", "Replicate", "Recover", "Squad", "Suspend", "Aura swap",
"Fortify", "Transfigure", "Champion", "Evoke", "Prowl",
"Fortify", "Transfigure", "Champion", "Evoke", "Prowl", "Freerunning",
"Reinforce", "Unearth", "Level up", "Miracle", "Overload", "Cleave",
"Scavenge", "Encore", "Bestow", "Outlast", "Dash", "Surge", "Emerge", "Hexproof:",
"etbCounter", "Reflect", "Ward").build();

View File

@@ -326,8 +326,28 @@ public class CardView extends GameEntityView {
void updateDamage(Card c) {
set(TrackableProperty.Damage, c.getDamage());
updateLethalDamage(c);
//update CrackOverlay (currently 16 overlays)
set(TrackableProperty.CrackOverlay, c.getDamage() > 0 ? MyRandom.getRandom().nextInt(16) : 0);
//get crackoverlay by level of damage light 0, medium 1, heavy 2, max 3
int randCrackLevel = 0;
if (c.getDamage() > 0) {
switch (c.getDamage()) {
case 1:
case 2:
randCrackLevel = 0;
break;
case 3:
case 4:
randCrackLevel = 1;
break;
case 5:
case 6:
randCrackLevel = 2;
break;
default:
randCrackLevel = 3;
break;
}
}
set(TrackableProperty.CrackOverlay, randCrackLevel);
}
public int getAssignedDamage() {
@@ -1466,6 +1486,7 @@ public class CardView extends GameEntityView {
public String getKeywordKey() { return get(TrackableProperty.KeywordKey); }
public String getProtectionKey() { return get(TrackableProperty.ProtectionKey); }
public String getHexproofKey() { return get(TrackableProperty.HexproofKey); }
public boolean hasAnnihilator() { return get(TrackableProperty.HasAnnihilator); }
public boolean hasDeathtouch() { return get(TrackableProperty.HasDeathtouch); }
public boolean hasToxic() { return get(TrackableProperty.HasToxic); }
public boolean hasDevoid() { return get(TrackableProperty.HasDevoid); }
@@ -1473,6 +1494,7 @@ public class CardView extends GameEntityView {
public boolean hasDivideDamage() { return get(TrackableProperty.HasDivideDamage); }
public boolean hasDoubleStrike() { return get(TrackableProperty.HasDoubleStrike); }
public boolean hasDoubleTeam() { return get(TrackableProperty.HasDoubleTeam); }
public boolean hasExalted() { return get(TrackableProperty.HasExalted); }
public boolean hasFirstStrike() { return get(TrackableProperty.HasFirstStrike); }
public boolean hasFlying() { return get(TrackableProperty.HasFlying); }
public boolean hasFear() { return get(TrackableProperty.HasFear); }
@@ -1542,6 +1564,7 @@ public class CardView extends GameEntityView {
}
void updateKeywords(Card c, CardState state) {
c.updateKeywordsCache(state);
set(TrackableProperty.HasAnnihilator, c.hasKeyword(Keyword.ANNIHILATOR, state));
set(TrackableProperty.HasDeathtouch, c.hasKeyword(Keyword.DEATHTOUCH, state));
set(TrackableProperty.HasToxic, c.hasKeyword(Keyword.TOXIC, state));
set(TrackableProperty.HasDevoid, c.hasKeyword(Keyword.DEVOID, state));
@@ -1549,6 +1572,7 @@ public class CardView extends GameEntityView {
set(TrackableProperty.HasDivideDamage, c.hasKeyword("You may assign CARDNAME's combat damage divided as " +
"you choose among defending player and/or any number of creatures they control."));
set(TrackableProperty.HasDoubleStrike, c.hasKeyword(Keyword.DOUBLE_STRIKE, state));
set(TrackableProperty.HasExalted, c.hasKeyword(Keyword.EXALTED, state));
set(TrackableProperty.HasFirstStrike, c.hasKeyword(Keyword.FIRST_STRIKE, state));
set(TrackableProperty.HasFlying, c.hasKeyword(Keyword.FLYING, state));
set(TrackableProperty.HasFear, c.hasKeyword(Keyword.FEAR, state));

View File

@@ -223,7 +223,15 @@ public class CostAdjustment {
// Reduce cost
int sumGeneric = 0;
if (sa.hasParam("ReduceCost")) {
sumGeneric += AbilityUtils.calculateAmount(originalCard, sa.getParam("ReduceCost"), sa);
String cst = sa.getParam("ReduceCost");
String amt = sa.getParamOrDefault("ReduceAmount", cst);
int num = AbilityUtils.calculateAmount(originalCard, amt, sa);
if (sa.hasParam("ReduceAmount") && num > 0) {
cost.subtractManaCost(new ManaCost(new ManaCostParser(Strings.repeat(cst + " ", num))));
} else {
sumGeneric += num;
}
}
while (!reduceAbilities.isEmpty()) {
@@ -379,9 +387,15 @@ public class CostAdjustment {
}
private static void adjustCostByEmerge(final ManaCostBeingPaid cost, final SpellAbility sa) {
CardCollectionView canEmerge = CardLists.filter(sa.getActivatingPlayer().getCreaturesInPlay(), CardPredicates.canBeSacrificedBy(sa, false));
String kw = sa.getKeyword().getOriginal();
String k[] = kw.split(":");
String validStr = k.length > 2 ? k[2] : "Creature";
Player p = sa.getActivatingPlayer();
CardCollectionView canEmerge = CardLists.filter(p.getCardsIn(ZoneType.Battlefield),
CardPredicates.restriction(validStr, p, sa.getHostCard(), sa),
CardPredicates.canBeSacrificedBy(sa, false));
final CardCollectionView toSacList = sa.getHostCard().getController().getController().choosePermanentsToSacrifice(sa, 0, 1, canEmerge, "Creature");
final CardCollectionView toSacList = p.getController().choosePermanentsToSacrifice(sa, 0, 1, canEmerge, validStr);
if (toSacList.isEmpty()) {
return;

View File

@@ -0,0 +1,13 @@
package forge.game.keyword;
public class Emerge extends KeywordWithCostAndType {
protected void parse(String details) {
final String[] k = details.split(":");
if (k.length < 2) {
super.parse("Creature:" + k[0]);
} else {
// Flip parameters
super.parse(k[1] + ":" + k[0]);
}
}
}

View File

@@ -69,7 +69,7 @@ public enum Keyword {
DREDGE("Dredge", KeywordWithAmount.class, false, "If you would draw a card, instead you may put exactly {%d:card} from the top of your library into your graveyard. If you do, return this card from your graveyard to your hand. Otherwise, draw a card."),
ECHO("Echo", KeywordWithCost.class, false, "At the beginning of your upkeep, if this permanent came under your control since the beginning of your last upkeep, sacrifice it unless you pay %s."),
EMBALM("Embalm", KeywordWithCost.class, false, "%s, Exile this card from your graveyard: Create a token that's a copy of this card, except it's white, it has no mana cost, and it's a Zombie in addition to its other types. Embalm only as a sorcery."),
EMERGE("Emerge", KeywordWithCost.class, false, "You may cast this spell by sacrificing a creature and paying the emerge cost reduced by that creature's mana value."),
EMERGE("Emerge", Emerge.class, false, "You may cast this spell by sacrificing {1:%2$s} and paying the emerge cost reduced by that %2$s's mana value."),
ENCHANT("Enchant", KeywordWithType.class, false, "Target a %s as you cast this. This card enters the battlefield attached to that %s."),
ENCORE("Encore", KeywordWithCost.class, false, "%s, Exile this card from your graveyard: For each opponent, create a token copy that attacks that opponent this turn if able. They gain haste. Sacrifice them at the beginning of the next end step. Activate only as a sorcery."),
ENLIST("Enlist", SimpleKeyword.class, false, "As this creature attacks, you may tap a nonattacking creature you control without summoning sickness. When you do, add its power to this creatures until end of turn."),
@@ -95,6 +95,7 @@ public enum Keyword {
FOR_MIRRODIN("For Mirrodin", SimpleKeyword.class, false, "When this Equipment enters the battlefield, create a 2/2 red Rebel creature token, then attach this to it."),
FORETELL("Foretell", KeywordWithCost.class, false, "During your turn, you may pay {2} and exile this card from your hand face down. Cast it on a later turn for its foretell cost."),
FORTIFY("Fortify", KeywordWithCost.class, false, "%s: Attach to target land you control. Fortify only as a sorcery."),
FREERUNNING("Freerunning", KeywordWithCost.class, false, "You may cast this spell for its freerunning cost if you dealt combat damage to a player this turn with an Assassin or commander."),
FRENZY("Frenzy", KeywordWithAmount.class, false, "Whenever this creature attacks and isn't blocked, it gets +%d/+0 until end of turn."),
FRIENDS_FOREVER("Friends forever", Partner.class, true, "You can have two commanders if both have friends forever."),
FUSE("Fuse", SimpleKeyword.class, true, "You may cast one or both halves of this card from your hand."),

View File

@@ -2150,6 +2150,10 @@ public class Player extends GameEntity implements Comparable<Player> {
return !game.getDamageDoneThisTurn(true, true, sb.toString(), "Player", null, this, null).isEmpty();
}
public final boolean hasFreerunning() {
return !game.getDamageDoneThisTurn(true, true, "Card.Assassin+YouCtrl,Card.IsCommander+YouCtrl", "Player", null, this, null).isEmpty();
}
public final void setLibrarySearched(final int l) {
numLibrarySearchedOwn = l;
}

View File

@@ -2,7 +2,6 @@ package forge.game.player;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.game.CardTraitBase;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
@@ -15,6 +14,7 @@ import forge.util.Expressions;
import forge.util.TextUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
@@ -264,6 +264,10 @@ public class PlayerProperty {
if (source.getChosenPlayer() == null || !source.getChosenPlayer().equals(player)) {
return false;
}
} else if (property.equals("NotedDefender")) {
String tracker = player.getDraftNotes().getOrDefault("Cogwork Tracker", "");
return Iterables.contains(Arrays.asList(tracker.split(",")), String.valueOf(player));
} else if (property.startsWith("life")) {
int life = player.getLife();
int amount = AbilityUtils.calculateAmount(source, property.substring(6), spellAbility);

View File

@@ -1,13 +1,7 @@
package forge.game.player;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.LobbyPlayer;
import forge.deck.CardPool;
import forge.deck.Deck;
@@ -16,6 +10,11 @@ import forge.game.GameType;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public class RegisteredPlayer {
private final Deck originalDeck; // never return or modify this instance (it's a reference to game resources)
private Deck currentDeck;
@@ -38,6 +37,7 @@ public class RegisteredPlayer {
private List<PaperCard> vanguardAvatars = null;
private PaperCard planeswalker = null;
private int teamNumber = -1; // members of teams with negative id will play FFA.
private Integer id = null;
private boolean randomFoil = false;
private boolean enableETBCountersEffect = false;
@@ -46,6 +46,14 @@ public class RegisteredPlayer {
restoreDeck();
}
public final Integer getId() {
return id;
}
public final void setId(Integer id0) {
id = id0;
}
public final Deck getDeck() {
return currentDeck;
}

View File

@@ -11,6 +11,7 @@ public enum AlternativeCost {
Evoke,
Flashback,
Foretold,
Freerunning,
Madness,
MTMtE, // More Than Meets the Eye (Transformers Universes Beyond)
Mutate,

View File

@@ -1549,6 +1549,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return isAlternativeCost(AlternativeCost.Evoke);
}
public final boolean isFreerunning() {
return isAlternativeCost(AlternativeCost.Freerunning);
}
public final boolean isMadness() {
return isAlternativeCost(AlternativeCost.Madness);
}

View File

@@ -444,6 +444,11 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
return false;
}
}
if (sa.isFreerunning()) {
if (!activator.hasFreerunning()) {
return false;
}
}
if (this.getIsPresent() != null) {
FCollection<GameObject> list;
if (getPresentDefined() != null) {

View File

@@ -70,7 +70,8 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger {
}
/** {@inheritDoc}
* @param runParams*/
* @param runParams
**/
@Override
public final boolean performTest(final Map<AbilityKey, Object> runParams) {
final SpellAbility spellAbility = (SpellAbility) runParams.get(AbilityKey.SpellAbility);

View File

@@ -144,6 +144,7 @@ public enum TrackableProperty {
CountBasicLandTypes(TrackableTypes.IntegerType),
KeywordKey(TrackableTypes.StringType),
HasAnnihilator(TrackableTypes.BooleanType),
HasDeathtouch(TrackableTypes.BooleanType),
HasToxic(TrackableTypes.BooleanType),
HasDevoid(TrackableTypes.BooleanType),
@@ -151,6 +152,7 @@ public enum TrackableProperty {
HasDivideDamage(TrackableTypes.BooleanType),
HasDoubleStrike(TrackableTypes.BooleanType),
HasDoubleTeam(TrackableTypes.BooleanType),
HasExalted(TrackableTypes.BooleanType),
HasFirstStrike(TrackableTypes.BooleanType),
HasFlying(TrackableTypes.BooleanType),
HasFear(TrackableTypes.BooleanType),