Merge branch 'master' of https://github.com/jjayers99/forge-updates into master-local

This commit is contained in:
jjayers99
2024-01-02 20:51:49 -05:00
50 changed files with 388 additions and 250 deletions

View File

@@ -30,7 +30,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import forge.ai.AiCardMemory.MemorySet; import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.ChooseGenericEffectAi;
import forge.ai.ability.ProtectAi; import forge.ai.ability.ProtectAi;
import forge.ai.ability.TokenAi; import forge.ai.ability.TokenAi;
import forge.card.CardStateName; import forge.card.CardStateName;
@@ -290,7 +289,7 @@ public class ComputerUtil {
SpellAbility newSA = sa.copyWithNoManaCost(); SpellAbility newSA = sa.copyWithNoManaCost();
newSA.setActivatingPlayer(ai, true); newSA.setActivatingPlayer(ai, true);
if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) { if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA, false) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) {
return false; return false;
} }
@@ -1176,7 +1175,7 @@ public class ComputerUtil {
return true; return true;
} }
if (cardState.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) { if (cardState.hasKeyword(Keyword.RIOT) && SpecialAiLogic.preferHasteForRiot(sa, ai)) {
// Planning to choose Haste for Riot, so do this in Main 1 // Planning to choose Haste for Riot, so do this in Main 1
return true; return true;
} }

View File

@@ -255,10 +255,25 @@ public class ComputerUtilAbility {
} }
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True // deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { if (ApiType.RollPlanarDice == a.getApi() || ApiType.RollPlanarDice == b.getApi()) {
return 1; Card hostCardForGame = a.getHostCard();
} else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { if (hostCardForGame == null) {
return -1; if (b.getHostCard() != null) {
hostCardForGame = b.getHostCard();
} else {
return 0; // fallback if neither SA have a host card somehow
}
}
Game game = hostCardForGame.getGame();
for (Card c : game.getActivePlanes()) {
if (c.hasSVar("AIRollPlanarDieParams") && c.getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
if (ApiType.RollPlanarDice == a.getApi()) {
return 1;
} else {
return -1;
}
}
}
} }
// deprioritize pump spells with pure energy cost (can be activated last, // deprioritize pump spells with pure energy cost (can be activated last,

View File

@@ -1257,7 +1257,7 @@ public class ComputerUtilCombat {
sa.setActivatingPlayer(source.getController(), true); sa.setActivatingPlayer(source.getController(), true);
if (sa.hasParam("Cost")) { if (sa.hasParam("Cost")) {
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) {
continue; continue;
} }
} }
@@ -1454,7 +1454,7 @@ public class ComputerUtilCombat {
continue; continue;
} }
if (sa.hasParam("Cost")) { if (sa.hasParam("Cost")) {
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) {
continue; continue;
} }
} }
@@ -1488,7 +1488,7 @@ public class ComputerUtilCombat {
continue; continue;
} }
if (sa.hasParam("Cost")) { if (sa.hasParam("Cost")) {
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) {
continue; continue;
} }
} }

View File

@@ -636,7 +636,7 @@ public class ComputerUtilCost {
} }
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect) return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa); && CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, effect);
} }
public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) { public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
@@ -760,6 +760,8 @@ public class ComputerUtilCost {
int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList); int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList);
return evalToken < evalCounter; return evalToken < evalCounter;
} else if ("Riot".equals(aiLogic)) {
return !SpecialAiLogic.preferHasteForRiot(sa, payer);
} }
// Check for shocklands and similar ETB replacement effects // Check for shocklands and similar ETB replacement effects

View File

@@ -976,7 +976,7 @@ public class ComputerUtilMana {
// Check if AI can still play this mana ability // Check if AI can still play this mana ability
ma.setActivatingPlayer(ai, true); ma.setActivatingPlayer(ai, true);
// if the AI can't pay the additional costs skip the mana ability // if the AI can't pay the additional costs skip the mana ability
if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) { if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma, false)) {
return false; return false;
} else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) { } else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) {
return false; return false;
@@ -1517,7 +1517,7 @@ public class ComputerUtilMana {
if (cost != null) { if (cost != null) {
// if the AI can't pay the additional costs skip the mana ability // if the AI can't pay the additional costs skip the mana ability
m.setActivatingPlayer(ai, true); m.setActivatingPlayer(ai, true);
if (!CostPayment.canPayAdditionalCosts(m.getPayCosts(), m)) { if (!CostPayment.canPayAdditionalCosts(m.getPayCosts(), m, false)) {
continue; continue;
} }

View File

@@ -1063,6 +1063,9 @@ public class PlayerControllerAi extends PlayerController {
final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } }; final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } };
emptyAbility.setActivatingPlayer(player, true); emptyAbility.setActivatingPlayer(player, true);
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects()); emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
emptyAbility.setReplacingObjects(sa.getReplacingObjects());
emptyAbility.setTrigger(sa.getTrigger());
emptyAbility.setReplacementEffect(sa.getReplacementEffect());
emptyAbility.setSVars(sa.getSVars()); emptyAbility.setSVars(sa.getSVars());
emptyAbility.setCardState(sa.getCardState()); emptyAbility.setCardState(sa.getCardState());
emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid()); emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid());

View File

@@ -2,6 +2,8 @@ package forge.ai;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import forge.ai.ability.TokenAi; import forge.ai.ability.TokenAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
@@ -13,6 +15,7 @@ import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.Expressions; import forge.util.Expressions;
@@ -398,4 +401,48 @@ public class SpecialAiLogic {
} }
return willPlay; return willPlay;
} }
public static boolean preferHasteForRiot(SpellAbility sa, Player player) {
// returning true means preferring Haste, returning false means preferring a +1/+1 counter
final Card host = sa.getHostCard();
final Game game = host.getGame();
final Card copy = CardUtil.getLKICopy(host);
copy.setLastKnownZone(player.getZone(ZoneType.Battlefield));
// check state it would have on the battlefield
CardCollection preList = new CardCollection(copy);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList);
// reset again?
game.getAction().checkStaticAbilities(false);
// can't gain counters, use Haste
if (!copy.canReceiveCounters(CounterEnumType.P1P1)) {
return true;
}
// already has Haste, use counter
if (copy.hasKeyword(Keyword.HASTE)) {
return false;
}
// not AI turn
if (!game.getPhaseHandler().isPlayerTurn(player)) {
return false;
}
// not before Combat
if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
}
// TODO check other opponents too if able
final Player opp = player.getWeakestOpponent();
if (opp != null) {
// TODO add predict Combat Damage?
return opp.getLife() < copy.getNetPower();
}
// haste might not be good enough?
return false;
}
} }

View File

@@ -3,7 +3,6 @@ package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -11,9 +10,7 @@ import forge.ai.SpellApiToAi;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.game.Game; import forge.game.Game;
import forge.game.card.*; import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -33,8 +30,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) { if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) {
return true; return true;
} else if ("Riot".equals(aiLogic)) {
return true;
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) { } else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) { for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
if (SpellApiToAi.Converter.get(sb.getApi()).canPlayAIWithSubs(ai, sb)) { if (SpellApiToAi.Converter.get(sb.getApi()).canPlayAIWithSubs(ai, sb)) {
@@ -86,9 +81,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) { Map<String, Object> params) {
Card host = sa.getHostCard(); Card host = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Game game = host.getGame(); final Game game = host.getGame();
final Combat combat = game.getCombat();
final String logic = sa.getParam("AILogic"); final String logic = sa.getParam("AILogic");
if (logic == null) { if (logic == null) {
return spells.get(0); return spells.get(0);
@@ -287,54 +280,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
if (!filtered.isEmpty()) { if (!filtered.isEmpty()) {
return filtered.get(0); return filtered.get(0);
} }
} else if ("Riot".equals(logic)) {
SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1);
return preferHasteForRiot(sa, player) ? hasteSA : counterSA;
} }
return spells.get(0); // return first choice if no logic found return spells.get(0); // return first choice if no logic found
} }
public static boolean preferHasteForRiot(SpellAbility sa, Player player) {
// returning true means preferring Haste, returning false means preferring a +1/+1 counter
final Card host = sa.getHostCard();
final Game game = host.getGame();
final Card copy = CardUtil.getLKICopy(host);
copy.setLastKnownZone(player.getZone(ZoneType.Battlefield));
// check state it would have on the battlefield
CardCollection preList = new CardCollection(copy);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList);
// reset again?
game.getAction().checkStaticAbilities(false);
// can't gain counters, use Haste
if (!copy.canReceiveCounters(CounterEnumType.P1P1)) {
return true;
}
// already has Haste, use counter
if (copy.hasKeyword(Keyword.HASTE)) {
return false;
}
// not AI turn
if (!game.getPhaseHandler().isPlayerTurn(player)) {
return false;
}
// not before Combat
if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
}
// TODO check other opponents too if able
final Player opp = player.getWeakestOpponent();
if (opp != null) {
// TODO add predict Combat Damage?
return opp.getLife() < copy.getNetPower();
}
// haste might not be good enough?
return false;
}
} }

View File

@@ -19,9 +19,16 @@ public class RollPlanarDiceAi extends SpellAbilityAi {
*/ */
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); for (Card c : ai.getGame().getActivePlanes()) {
Card plane = sa.getHostCard(); if (willRollOnPlane(ai, c)) {
return true;
}
}
return false;
}
private boolean willRollOnPlane(Player ai, Card plane) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
boolean decideToRoll = false; boolean decideToRoll = false;
boolean rollInMain1 = false; boolean rollInMain1 = false;
String modeName = "never"; String modeName = "never";

View File

@@ -253,7 +253,6 @@ public class Game {
public List<Card> getLeftBattlefieldThisTurn() { public List<Card> getLeftBattlefieldThisTurn() {
return leftBattlefieldThisTurn; return leftBattlefieldThisTurn;
} }
public List<Card> getLeftGraveyardThisTurn() { public List<Card> getLeftGraveyardThisTurn() {
return leftGraveyardThisTurn; return leftGraveyardThisTurn;
} }
@@ -261,7 +260,6 @@ public class Game {
public void clearLeftBattlefieldThisTurn() { public void clearLeftBattlefieldThisTurn() {
leftBattlefieldThisTurn.clear(); leftBattlefieldThisTurn.clear();
} }
public void clearLeftGraveyardThisTurn() { public void clearLeftGraveyardThisTurn() {
leftGraveyardThisTurn.clear(); leftGraveyardThisTurn.clear();
} }

View File

@@ -583,14 +583,19 @@ public class GameAction {
} }
} }
// Need to apply any static effects to produce correct triggers if (!table.isEmpty()) {
checkStaticAbilities(); // we don't want always trigger before counters are placed
game.getTriggerHandler().suppressMode(TriggerType.Always);
if (table.replaceCounterEffect(game, null, true, true, params)) { // Need to apply any static effects to produce correct triggers
// update static abilities after etb counters have been placed
checkStaticAbilities(); checkStaticAbilities();
} }
table.replaceCounterEffect(game, null, true, true, params);
// update static abilities after etb counters have been placed
game.getTriggerHandler().clearSuppression(TriggerType.Always);
checkStaticAbilities();
// 400.7g try adding keyword back into card if it doesn't already have it // 400.7g try adding keyword back into card if it doesn't already have it
if (zoneTo.is(ZoneType.Stack) && cause != null && cause.isSpell() && !cause.isIntrinsic() && c.equals(cause.getHostCard())) { if (zoneTo.is(ZoneType.Stack) && cause != null && cause.isSpell() && !cause.isIntrinsic() && c.equals(cause.getHostCard())) {
if (cause.getKeyword() != null && !copied.getKeywords().contains(cause.getKeyword())) { if (cause.getKeyword() != null && !copied.getKeywords().contains(cause.getKeyword())) {
@@ -2085,6 +2090,9 @@ public class GameAction {
//<THIS CODE WILL WORK WITH PHASE = NULL> //<THIS CODE WILL WORK WITH PHASE = NULL>
if (game.getRules().hasAppliedVariant(GameType.Planechase)) { if (game.getRules().hasAppliedVariant(GameType.Planechase)) {
first.initPlane(); first.initPlane();
for (final Player p1 : game.getPlayers()) {
p1.createPlanechaseEffects(game);
}
} }
first = runOpeningHandActions(first); first = runOpeningHandActions(first);

View File

@@ -87,6 +87,11 @@ public enum PlanarDice {
runParams.put(AbilityKey.Result, Arrays.asList(0)); runParams.put(AbilityKey.Result, Arrays.asList(0));
roller.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDieOnce, runParams, false); roller.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDieOnce, runParams, false);
if (res == Chaos) {
runParams = AbilityKey.mapFromPlayer(roller);
roller.getGame().getTriggerHandler().runTrigger(TriggerType.ChaosEnsues, runParams, false);
}
return res; return res;
} }

View File

@@ -1691,9 +1691,6 @@ public class AbilityUtils {
if (root.isTrigger()) { if (root.isTrigger()) {
Trigger t = root.getTrigger(); Trigger t = root.getTrigger();
if (t == null) {
return doXMath(0, expr, c, ctb);
}
// ImmediateTrigger should check for the Ability which created the trigger // ImmediateTrigger should check for the Ability which created the trigger
if (t.getSpawningAbility() != null) { if (t.getSpawningAbility() != null) {
@@ -2083,7 +2080,6 @@ public class AbilityUtils {
for (Mana m : paidMana) { for (Mana m : paidMana) {
if (m.toString().equals(type)) { if (m.toString().equals(type)) {
count++; count++;
} }
} }
return doXMath(count, expr, c, ctb); return doXMath(count, expr, c, ctb);
@@ -2168,11 +2164,9 @@ public class AbilityUtils {
if (sq[0].startsWith("RememberedSize")) { if (sq[0].startsWith("RememberedSize")) {
return doXMath(c.getRememberedCount(), expr, c, ctb); return doXMath(c.getRememberedCount(), expr, c, ctb);
} }
if (sq[0].startsWith("ChosenSize")) { if (sq[0].startsWith("ChosenSize")) {
return doXMath(c.getChosenCards().size(), expr, c, ctb); return doXMath(c.getChosenCards().size(), expr, c, ctb);
} }
if (sq[0].startsWith("ImprintedSize")) { if (sq[0].startsWith("ImprintedSize")) {
return doXMath(c.getImprintedCards().size(), expr, c, ctb); return doXMath(c.getImprintedCards().size(), expr, c, ctb);
} }
@@ -2714,54 +2708,35 @@ public class AbilityUtils {
return MyRandom.getRandom().nextInt(1+max-min) + min; return MyRandom.getRandom().nextInt(1+max-min) + min;
} }
String[] paidparts = l[0].split("\\$", 2);
Iterable<Card> someCards = null;
// Count$ThisTurnCast <Valid> // Count$ThisTurnCast <Valid>
// Count$LastTurnCast <Valid> // Count$LastTurnCast <Valid>
if (sq[0].startsWith("ThisTurnCast") || sq[0].startsWith("LastTurnCast")) { if (sq[0].startsWith("ThisTurnCast") || sq[0].startsWith("LastTurnCast")) {
String[] paidparts = l[0].split("\\$", 2);
final String[] workingCopy = paidparts[0].split("_"); final String[] workingCopy = paidparts[0].split("_");
final String validFilter = workingCopy[1]; final String validFilter = workingCopy[1];
List<Card> res;
if (workingCopy[0].contains("This")) { if (workingCopy[0].contains("This")) {
res = CardUtil.getThisTurnCast(validFilter, c, ctb, player); someCards = CardUtil.getThisTurnCast(validFilter, c, ctb, player);
} else { } else {
res = CardUtil.getLastTurnCast(validFilter, c, ctb, player); someCards = CardUtil.getLastTurnCast(validFilter, c, ctb, player);
} }
int num;
if (paidparts.length > 1) {
num = handlePaid(res, paidparts[1], c, ctb);
} else {
num = res.size();
}
return doXMath(num, expr, c, ctb);
} }
// Count$ThisTurnEntered <ZoneDestination> [from <ZoneOrigin>] <Valid> // Count$ThisTurnEntered <ZoneDestination> [from <ZoneOrigin>] <Valid>
if (sq[0].startsWith("ThisTurnEntered")) { if (sq[0].startsWith("ThisTurnEntered") || sq[0].startsWith("LastTurnEntered")) {
final String[] workingCopy = l[0].split("_", 5); final String[] workingCopy = paidparts[0].split("_");
ZoneType destination = ZoneType.smartValueOf(workingCopy[1]); ZoneType destination = ZoneType.smartValueOf(workingCopy[1]);
final boolean hasFrom = workingCopy[2].equals("from"); final boolean hasFrom = workingCopy[2].equals("from");
ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null; ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null;
String validFilter = workingCopy[hasFrom ? 4 : 2]; String validFilter = workingCopy[hasFrom ? 4 : 2];
final List<Card> res = CardUtil.getThisTurnEntered(destination, origin, validFilter, c, ctb, player); if (workingCopy[0].contains("This")) {
return doXMath(res.size(), expr, c, ctb); someCards = CardUtil.getThisTurnEntered(destination, origin, validFilter, c, ctb, player);
} } else {
someCards = CardUtil.getLastTurnEntered(destination, origin, validFilter, c, ctb, player);
// Count$LastTurnEntered <ZoneDestination> [from <ZoneOrigin>] <Valid> }
if (sq[0].startsWith("LastTurnEntered")) {
final String[] workingCopy = l[0].split("_", 5);
ZoneType destination = ZoneType.smartValueOf(workingCopy[1]);
final boolean hasFrom = workingCopy[2].equals("from");
ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null;
String validFilter = workingCopy[hasFrom ? 4 : 2];
final List<Card> res = CardUtil.getLastTurnEntered(destination, origin, validFilter, c, ctb, player);
return doXMath(res.size(), expr, c, ctb);
} }
if (sq[0].startsWith("CountersAddedThisTurn")) { if (sq[0].startsWith("CountersAddedThisTurn")) {
@@ -2779,7 +2754,6 @@ public class AbilityUtils {
// count valid cards in any specified zone/s // count valid cards in any specified zone/s
if (sq[0].startsWith("Valid")) { if (sq[0].startsWith("Valid")) {
String[] paidparts = l[0].split("\\$", 2);
String[] lparts = paidparts[0].split(" ", 2); String[] lparts = paidparts[0].split(" ", 2);
CardCollectionView cardsInZones = null; CardCollectionView cardsInZones = null;
@@ -2805,13 +2779,7 @@ public class AbilityUtils {
} }
} }
int cnt; someCards = CardLists.getValidCards(cardsInZones, lparts[1], player, c, ctb);
if (paidparts.length > 1) {
cnt = handlePaid(CardLists.getValidCards(cardsInZones, lparts[1], player, c, ctb), paidparts[1], c, ctb);
} else {
cnt = CardLists.getValidCardCount(cardsInZones, lparts[1], player, c, ctb);
}
return doXMath(cnt, expr, c, ctb);
} }
if (sq[0].startsWith("MostCardName")) { if (sq[0].startsWith("MostCardName")) {
@@ -2891,23 +2859,27 @@ public class AbilityUtils {
return doXMath(Iterables.size(powers), expr, c, ctb); return doXMath(Iterables.size(powers), expr, c, ctb);
} }
if (sq[0].startsWith("DifferentCounterKinds_")) { if (sq[0].startsWith("DifferentCounterKinds_")) {
final List<CounterType> kinds = Lists.newArrayList(); final Set<CounterType> kinds = Sets.newHashSet();
final String rest = l[0].substring(22); final String rest = l[0].substring(22);
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb); CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
for (final Card card : list) { for (final Card card : list) {
for (final Map.Entry<CounterType, Integer> map : card.getCounters().entrySet()) { kinds.addAll(card.getCounters().keySet());
if (!kinds.contains(map.getKey())) {
kinds.add(map.getKey());
}
}
} }
return doXMath(kinds.size(), expr, c, ctb); return doXMath(kinds.size(), expr, c, ctb);
} }
// Complex counting methods // Complex counting methods
CardCollectionView someCards = getCardListForXCount(c, player, sq, ctb); Integer num = null;
if (someCards == null) {
someCards = getCardListForXCount(c, player, sq, ctb);
} else if (paidparts.length > 1) {
num = handlePaid(someCards, paidparts[1], c, ctb);
}
if (num == null) {
num = Iterables.size(someCards);
}
return doXMath(someCards.size(), expr, c, ctb); return doXMath(num, expr, c, ctb);
} }
public static final void applyManaColorConversion(ManaConversionMatrix matrix, String conversion) { public static final void applyManaColorConversion(ManaConversionMatrix matrix, String conversion) {

View File

@@ -192,7 +192,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
tgtC.addChangedCardKeywords(kws, Lists.newArrayList(), false, tStamp, 0); tgtC.addChangedCardKeywords(kws, Lists.newArrayList(), false, tStamp, 0);
game.fireEvent(new GameEventCardStatsChanged(tgtC)); game.fireEvent(new GameEventCardStatsChanged(tgtC));
} }
if (hiddenKws.isEmpty()) { if (!hiddenKws.isEmpty()) {
tgtC.addHiddenExtrinsicKeywords(tStamp, 0, hiddenKws); tgtC.addHiddenExtrinsicKeywords(tStamp, 0, hiddenKws);
} }

View File

@@ -5,9 +5,12 @@ import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.GameCommand; import forge.GameCommand;
import forge.card.CardType;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
@@ -129,8 +132,12 @@ public class ProtectEffect extends SpellAbilityEffect {
} }
List<String> gainsKWList = Lists.newArrayList(); List<String> gainsKWList = Lists.newArrayList();
for (String color : gains) { for (String type : gains) {
gainsKWList.add(TextUtil.concatWithSpace("Protection from", color)); if (isChoice && sa.getParam("Choices").equals("CardType")) {
gainsKWList.add("Protection:" + type);
} else {
gainsKWList.add(TextUtil.concatWithSpace("Protection from", type));
}
} }
tgtCards.addAll(CardUtil.getRadiance(sa)); tgtCards.addAll(CardUtil.getRadiance(sa));
@@ -176,6 +183,8 @@ public class ProtectEffect extends SpellAbilityEffect {
if (choices.contains("AnyColor")) { if (choices.contains("AnyColor")) {
gains.addAll(MagicColor.Constant.ONLY_COLORS); gains.addAll(MagicColor.Constant.ONLY_COLORS);
choices = choices.replaceAll("AnyColor,?", ""); choices = choices.replaceAll("AnyColor,?", "");
} else if (choices.contains("CardType")) {
choices = StringUtils.join(CardType.getAllCardTypes(), ",");
} }
// Add any remaining choices // Add any remaining choices
if (choices.length() > 0) { if (choices.length() > 0) {

View File

@@ -5532,6 +5532,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// KeywordCollection#getAmount // KeywordCollection#getAmount
final String[] parse = kw.contains(":") ? kw.split(":") : kw.split(" "); final String[] parse = kw.contains(":") ? kw.split(":") : kw.split(" ");
if (parse.length < 2) {
count++;
continue;
}
final String s = parse[1]; final String s = parse[1];
if (StringUtils.isNumeric(s)) { if (StringUtils.isNumeric(s)) {
count += Integer.parseInt(s); count += Integer.parseInt(s);

View File

@@ -318,42 +318,11 @@ public class CardFactory {
// ****************************************************************** // ******************************************************************
// ************** Link to different CardFactories ******************* // ************** Link to different CardFactories *******************
if (card.isPlane()) {
buildPlaneAbilities(card);
}
buildBattleAbilities(card); buildBattleAbilities(card);
CardFactoryUtil.setupKeywordedAbilities(card); // Should happen AFTER setting left/right split abilities to set Fuse ability to both sides CardFactoryUtil.setupKeywordedAbilities(card); // Should happen AFTER setting left/right split abilities to set Fuse ability to both sides
card.updateStateForView(); card.updateStateForView();
} }
private static void buildPlaneAbilities(Card card) {
String trigger = "Mode$ PlanarDice | Result$ Planeswalk | TriggerZones$ Command | Secondary$ True | " +
"TriggerDescription$ Whenever you roll the Planeswalker symbol on the planar die, planeswalk.";
String rolledWalk = "DB$ Planeswalk | Cause$ PlanarDie";
Trigger planesWalkTrigger = TriggerHandler.parseTrigger(trigger, card, true);
planesWalkTrigger.setOverridingAbility(AbilityFactory.getAbility(rolledWalk, card));
card.addTrigger(planesWalkTrigger);
String chaosTrig = "Mode$ PlanarDice | Result$ Chaos | TriggerZones$ Command | Static$ True";
String rolledChaos = "DB$ ChaosEnsues";
Trigger chaosTrigger = TriggerHandler.parseTrigger(chaosTrig, card, true);
chaosTrigger.setOverridingAbility(AbilityFactory.getAbility(rolledChaos, card));
card.addTrigger(chaosTrigger);
String specialA = "ST$ RollPlanarDice | Cost$ X | SorcerySpeed$ True | Activator$ Player | SpecialAction$ True" +
" | ActivationZone$ Command | SpellDescription$ Roll the planar dice. X is equal to the number of " +
"times you have previously taken this action this turn. | CostDesc$ {X}: ";
SpellAbility planarRoll = AbilityFactory.getAbility(specialA, card);
planarRoll.setSVar("X", "Count$PlanarDiceSpecialActionThisTurn");
card.addSpellAbility(planarRoll);
}
private static void buildBattleAbilities(Card card) { private static void buildBattleAbilities(Card card) {
if (!card.isBattle()) { if (!card.isBattle()) {
return; return;

View File

@@ -2445,21 +2445,11 @@ public class CardFactoryUtil {
ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, repeatSA, false, true, intrinsic, "Card.Self", ""); ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, repeatSA, false, true, intrinsic, "Card.Self", "");
inst.addReplacement(cardre); inst.addReplacement(cardre);
} else if (keyword.startsWith("Riot")) { } else if (keyword.equals("Riot")) {
final String choose = "DB$ GenericChoice | AILogic$ Riot | SpellDescription$ Riot"; final String hasteStr = "DB$ Animate | Defined$ Self | Keywords$ Haste | Duration$ Permanent | UnlessCost$ AddCounter<1/P1P1> | UnlessPayer$ You | UnlessAI$ Riot | SpellDescription$ Riot";
final String counter = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | ETB$ True | CounterNum$ 1" + final SpellAbility hasteSa = AbilityFactory.getAbility(hasteStr, card);
" | SpellDescription$ Put a +1/+1 counter on it."; ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, hasteSa, false, true, intrinsic, "Card.Self", "");
final String haste = "DB$ Animate | Defined$ Self | Keywords$ Haste | Duration$ Permanent | SpellDescription$ Haste";
SpellAbility saChoose = AbilityFactory.getAbility(choose, card);
List<AbilitySub> list = Lists.newArrayList();
list.add((AbilitySub)AbilityFactory.getAbility(counter, card));
list.add((AbilitySub)AbilityFactory.getAbility(haste, card));
saChoose.setAdditionalAbilityList("Choices", list);
ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, saChoose, false, true, intrinsic, "Card.Self", "");
inst.addReplacement(cardre); inst.addReplacement(cardre);
} else if (keyword.equals("Sunburst")) { } else if (keyword.equals("Sunburst")) {

View File

@@ -297,7 +297,7 @@ public class Cost implements Serializable {
final String[] splitStr = abCostParse(parse, 5); final String[] splitStr = abCostParse(parse, 5);
final String type = splitStr.length > 2 ? splitStr[2] : "CARDNAME"; final String type = splitStr.length > 2 ? splitStr[2] : "CARDNAME";
final String description = splitStr.length > 3 ? splitStr[3] : null; final String description = splitStr.length > 3 ? splitStr[3] : null;
final ZoneType zone = splitStr.length > 4 ? ZoneType.smartValueOf(splitStr[4]) : ZoneType.Battlefield; final List<ZoneType> zone = splitStr.length > 4 ? ZoneType.listValueOf(splitStr[4]) : Lists.newArrayList(ZoneType.Battlefield);
boolean oneOrMore = false; boolean oneOrMore = false;
if (splitStr[0].equals("X1+")) { if (splitStr[0].equals("X1+")) {
oneOrMore = true; oneOrMore = true;
@@ -979,7 +979,7 @@ public class Cost implements Serializable {
if (counters < 0) { if (counters < 0) {
costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription())); costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription()));
} else { } else {
costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription(), ZoneType.Battlefield, false)); costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription(), Lists.newArrayList(ZoneType.Battlefield) , false));
} }
} else { } else {
continue; continue;

View File

@@ -95,13 +95,13 @@ public class CostPayment extends ManaConversionMatrix {
* a {@link forge.game.spellability.SpellAbility} object. * a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean. * @return a boolean.
*/ */
public static boolean canPayAdditionalCosts(Cost cost, final SpellAbility ability) { public static boolean canPayAdditionalCosts(Cost cost, final SpellAbility ability, final boolean effect) {
if (cost == null) { if (cost == null) {
return true; return true;
} }
cost = CostAdjustment.adjust(cost, ability); cost = CostAdjustment.adjust(cost, ability);
return cost.canPay(ability, false); return cost.canPay(ability, effect);
} }
/** /**

View File

@@ -19,13 +19,21 @@ package forge.game.cost;
import java.util.List; import java.util.List;
import com.google.common.collect.Sets;
import forge.game.Game;
import forge.game.GameEntityCounterTable; import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityKey;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -140,15 +148,36 @@ public class CostPutCounter extends CostPartWithList {
@Override @Override
public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) { public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) {
final Card source = ability.getHostCard(); final Card source = ability.getHostCard();
final Game game = source.getGame();
if (this.payCostFromSource()) { if (this.payCostFromSource()) {
return source.isInPlay() && (getAbilityAmount(ability) == 0 || source.canReceiveCounters(this.counter)); if (isETBReplacement(ability, effect)) {
final Card copy = CardUtil.getLKICopy(source);
copy.setLastKnownZone(payer.getZone(ZoneType.Battlefield));
// check state it would have on the battlefield
CardCollection preList = new CardCollection(copy);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList);
// reset again?
game.getAction().checkStaticAbilities(false);
if (copy.canReceiveCounters(getCounter())) {
return true;
}
} else {
if (!source.isInPlay()) {
return false;
}
if (source.canReceiveCounters(getCounter())) {
return true;
}
}
return getAbilityAmount(ability) == 0;
} }
// 3 Cards have Put a -1/-1 Counter on a Creature you control. // 3 Cards have Put a -1/-1 Counter on a Creature you control.
List<Card> typeList = CardLists.getValidCards(source.getGame().getCardsIn(ZoneType.Battlefield), List<Card> typeList = CardLists.getValidCards(source.getGame().getCardsIn(ZoneType.Battlefield),
this.getType().split(";"), payer, source, ability); this.getType().split(";"), payer, source, ability);
typeList = CardLists.filter(typeList, CardPredicates.canReceiveCounters(this.counter)); typeList = CardLists.filter(typeList, CardPredicates.canReceiveCounters(getCounter()));
return !typeList.isEmpty(); return !typeList.isEmpty();
} }
@@ -172,8 +201,12 @@ public class CostPutCounter extends CostPartWithList {
@Override @Override
protected Card doPayment(Player payer, SpellAbility ability, Card targetCard, final boolean effect) { protected Card doPayment(Player payer, SpellAbility ability, Card targetCard, final boolean effect) {
final int i = this.getAbilityAmount(ability); final int i = getAbilityAmount(ability);
targetCard.addCounter(this.getCounter(), i, payer, counterTable); if (isETBReplacement(ability, effect)) {
targetCard.addEtbCounter(getCounter(), i, payer);
} else {
targetCard.addCounter(getCounter(), i, payer, counterTable);
}
return targetCard; return targetCard;
} }
@@ -208,4 +241,27 @@ public class CostPutCounter extends CostPartWithList {
counterTable.clear(); counterTable.clear();
} }
protected boolean isETBReplacement(final SpellAbility ability, final boolean effect) {
if (!effect) {
return false;
}
// only for itself?
if (!payCostFromSource()) {
return false;
}
if (ability == null) {
return false;
}
if (!ability.isReplacementAbility()) {
return false;
}
ReplacementEffect re = ability.getReplacementEffect();
if (re.getMode() != ReplacementType.Moved) {
return false;
}
if (!ability.getHostCard().equals(ability.getReplacingObject(AbilityKey.Card))) {
return false;
}
return true;
}
} }

View File

@@ -41,7 +41,7 @@ public class CostRemoveCounter extends CostPart {
*/ */
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public final CounterType counter; public final CounterType counter;
public final ZoneType zone; public final List<ZoneType> zone;
public final Boolean oneOrMore; public final Boolean oneOrMore;
/** /**
@@ -57,7 +57,7 @@ public class CostRemoveCounter extends CostPart {
* the description * the description
* @param zone the zone. * @param zone the zone.
*/ */
public CostRemoveCounter(final String amount, final CounterType counter, final String type, final String description, final ZoneType zone, final boolean oneOrMore) { public CostRemoveCounter(final String amount, final CounterType counter, final String type, final String description, final List<ZoneType> zone, final boolean oneOrMore) {
super(amount, type, description); super(amount, type, description);
this.counter = counter; this.counter = counter;
@@ -73,23 +73,24 @@ public class CostRemoveCounter extends CostPart {
final CounterType cntrs = this.counter; final CounterType cntrs = this.counter;
final Card source = ability.getHostCard(); final Card source = ability.getHostCard();
final String type = this.getType(); final String type = this.getType();
if (this.payCostFromSource()) { if (this.payCostFromSource()) {
return source.getCounters(cntrs); return source.getCounters(cntrs);
} else {
List<Card> typeList;
if (type.equals("OriginalHost")) {
typeList = Lists.newArrayList(ability.getOriginalHost());
} else {
typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability);
}
// Single Target
int maxcount = 0;
for (Card c : typeList) {
maxcount = Math.max(maxcount, c.getCounters(cntrs));
}
return maxcount;
} }
List<Card> typeList;
if (type.equals("OriginalHost")) {
typeList = Lists.newArrayList(ability.getOriginalHost());
} else {
typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability);
}
// Single Target
int maxcount = 0;
for (Card c : typeList) {
maxcount = Math.max(maxcount, c.getCounters(cntrs));
}
return maxcount;
} }
/* /*
@@ -146,26 +147,24 @@ public class CostRemoveCounter extends CostPart {
final int amount; final int amount;
if (getAmount().equals("All")) { if (getAmount().equals("All")) {
amount = source.getCounters(cntrs); amount = source.getCounters(cntrs);
} } else {
else {
amount = getAbilityAmount(ability); amount = getAbilityAmount(ability);
} }
if (this.payCostFromSource()) { if (this.payCostFromSource()) {
return !source.isPhasedOut() && (source.getCounters(cntrs) - amount) >= 0; return !source.isPhasedOut() && (source.getCounters(cntrs) - amount) >= 0;
} }
else {
List<Card> typeList;
if (type.equals("OriginalHost")) {
typeList = Lists.newArrayList(ability.getOriginalHost());
} else {
typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability);
}
// (default logic) remove X counters from a single permanent List<Card> typeList;
for (Card c : typeList) { if (type.equals("OriginalHost")) {
if (c.getCounters(cntrs) - amount >= 0) { typeList = Lists.newArrayList(ability.getOriginalHost());
return true; } else {
} typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability);
}
// (default logic) remove X counters from a single permanent
for (Card c : typeList) {
if (c.getCounters(cntrs) - amount >= 0) {
return true;
} }
} }

View File

@@ -175,7 +175,7 @@ public enum Keyword {
STRIVE("Strive", KeywordWithCost.class, false, "CARDNAME costs %s more to cast for each target beyond the first."), STRIVE("Strive", KeywordWithCost.class, false, "CARDNAME costs %s more to cast for each target beyond the first."),
SUNBURST("Sunburst", SimpleKeyword.class, false, "This enters the battlefield with either a +1/+1 or charge counter on it for each color of mana spent to cast it based on whether it's a creature."), SUNBURST("Sunburst", SimpleKeyword.class, false, "This enters the battlefield with either a +1/+1 or charge counter on it for each color of mana spent to cast it based on whether it's a creature."),
SURGE("Surge", KeywordWithCost.class, false, "You may cast this spell for its surge cost if you or a teammate has cast another spell this turn."), SURGE("Surge", KeywordWithCost.class, false, "You may cast this spell for its surge cost if you or a teammate has cast another spell this turn."),
SUSPEND("Suspend", Suspend.class, false, "Rather than cast this card from your hand, you may pay %s and exile it with {%d:time counter} on it. At the beginning of your upkeep, remove a time counter. When the last is removed, cast it without paying its mana cost."), SUSPEND("Suspend", Suspend.class, false, "If you could begin to cast this card by putting it onto the stack from your hand, you may pay %s and exile it with {%d:time counter} on it. At the beginning of your upkeep, remove a time counter. When the last is removed, play it without paying its mana cost. If you cast a creature spell this way, it gains haste until you lose control of the spell or the permanent it becomes."),
TOTEM_ARMOR("Totem armor", SimpleKeyword.class, true, "If enchanted permanent would be destroyed, instead remove all damage marked on it and destroy this Aura."), TOTEM_ARMOR("Totem armor", SimpleKeyword.class, true, "If enchanted permanent would be destroyed, instead remove all damage marked on it and destroy this Aura."),
TOXIC("Toxic", KeywordWithAmount.class, false, "Players dealt combat damage by this creature also get {%d:poison counter}."), TOXIC("Toxic", KeywordWithAmount.class, false, "Players dealt combat damage by this creature also get {%d:poison counter}."),
TRAINING("Training", SimpleKeyword.class, false, "Whenever this creature attacks with another creature with greater power, put a +1/+1 counter on this creature."), TRAINING("Training", SimpleKeyword.class, false, "Whenever this creature attacks with another creature with greater power, put a +1/+1 counter on this creature."),

View File

@@ -3176,6 +3176,36 @@ public class Player extends GameEntity implements Comparable<Player> {
return eff; return eff;
} }
public void createPlanechaseEffects(Game game) {
final PlayerZone com = getZone(ZoneType.Command);
final String name = "Planar Dice";
final Card eff = new Card(game.nextCardId(), game);
eff.setTimestamp(game.getNextTimestamp());
eff.setName(name);
eff.setOwner(this);
eff.setImmutable(true);
String image = ImageKeys.getTokenKey("planechase");
eff.setImageKey(image);
String trigger = "Mode$ PlanarDice | Result$ Planeswalk | TriggerZones$ Command | ValidPlayer$ You | Secondary$ True | " +
"TriggerDescription$ Whenever you roll the Planeswalker symbol on the planar die, planeswalk.";
String rolledWalk = "DB$ Planeswalk | Cause$ PlanarDie";
Trigger planesWalkTrigger = TriggerHandler.parseTrigger(trigger, eff, true);
planesWalkTrigger.setOverridingAbility(AbilityFactory.getAbility(rolledWalk, eff));
eff.addTrigger(planesWalkTrigger);
String specialA = "ST$ RollPlanarDice | Cost$ X | SorcerySpeed$ True | Activator$ Player | SpecialAction$ True" +
" | ActivationZone$ Command | SpellDescription$ Roll the planar dice. X is equal to the number of " +
"times you have previously taken this action this turn. | CostDesc$ {X}: ";
SpellAbility planarRoll = AbilityFactory.getAbility(specialA, eff);
planarRoll.setSVar("X", "Count$PlanarDiceSpecialActionThisTurn");
eff.addSpellAbility(planarRoll);
eff.updateStateForView();
com.add(eff);
this.updateZoneForView(com);
}
public void createTheRing(Card host) { public void createTheRing(Card host) {
final PlayerZone com = getZone(ZoneType.Command); final PlayerZone com = getZone(ZoneType.Command);
if (theRing == null) { if (theRing == null) {

View File

@@ -90,7 +90,7 @@ public abstract class AbilityActivated extends SpellAbility implements Cloneable
return false; return false;
} }
return CostPayment.canPayAdditionalCosts(this.getPayCosts(), this); return CostPayment.canPayAdditionalCosts(this.getPayCosts(), this, false);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */

View File

@@ -112,7 +112,7 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
return false; return false;
} }
if (!CostPayment.canPayAdditionalCosts(this.getPayCosts(), this)) { if (!CostPayment.canPayAdditionalCosts(this.getPayCosts(), this, false)) {
return false; return false;
} }

View File

@@ -827,6 +827,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public void setReplacingObject(final AbilityKey type, final Object o) { public void setReplacingObject(final AbilityKey type, final Object o) {
replacingObjects.put(type, o); replacingObjects.put(type, o);
} }
public void setReplacingObjects(final Map<AbilityKey, Object> repParams) {
replacingObjects = AbilityKey.newMap(repParams);
}
public void setReplacingObjectsFrom(final Map<AbilityKey, Object> repParams, final AbilityKey... types) { public void setReplacingObjectsFrom(final Map<AbilityKey, Object> repParams, final AbilityKey... types) {
int typesLength = types.length; int typesLength = types.length;
for (int i = 0; i < typesLength; i += 1) { for (int i = 0; i < typesLength; i += 1) {

View File

@@ -373,8 +373,14 @@ public class AdventureEventData implements Serializable {
} }
} }
for (String restricted : Config.instance().getConfigData().restrictedEditions) { ConfigData configData = Config.instance().getConfigData();
legalBlocks.removeIf(q -> q.getName().equals(restricted)); if (configData.allowedEditions != null) {
List<String> allowed = Arrays.asList(configData.allowedEditions);
legalBlocks.removeIf(q -> !allowed.contains(q.getName()));
} else {
for (String restricted : configData.restrictedEditions) {
legalBlocks.removeIf(q -> q.getName().equals(restricted));
}
} }
return legalBlocks.isEmpty() ? null : Aggregates.random(legalBlocks); return legalBlocks.isEmpty() ? null : Aggregates.random(legalBlocks);
} }
@@ -388,8 +394,14 @@ public class AdventureEventData implements Serializable {
legalBlocks.add(b); legalBlocks.add(b);
} }
} }
for (String restricted : Config.instance().getConfigData().restrictedEditions) { ConfigData configData = Config.instance().getConfigData();
legalBlocks.removeIf(q -> q.getName().equals(restricted)); if (configData.allowedEditions != null) {
List<String> allowed = Arrays.asList(configData.allowedEditions);
legalBlocks.removeIf(q -> !allowed.contains(q.getName()));
} else {
for (String restricted : configData.restrictedEditions) {
legalBlocks.removeIf(q -> q.getName().equals(restricted));
}
} }
return legalBlocks.isEmpty()?null:Aggregates.random(legalBlocks); return legalBlocks.isEmpty()?null:Aggregates.random(legalBlocks);
} }

View File

@@ -22,4 +22,5 @@ public class ConfigData {
public RewardData legalCards; public RewardData legalCards;
public String[] restrictedCards; public String[] restrictedCards;
public String[] restrictedEditions; public String[] restrictedEditions;
public String[] allowedEditions;
} }

View File

@@ -86,7 +86,8 @@ public class RewardData implements Serializable {
private static Iterable<PaperCard> allEnemyCards; private static Iterable<PaperCard> allEnemyCards;
static private void initializeAllCards(){ static private void initializeAllCards(){
RewardData legals = Config.instance().getConfigData().legalCards; ConfigData configData = Config.instance().getConfigData();
RewardData legals = configData.legalCards;
if(legals==null) if(legals==null)
allCards = CardUtil.getFullCardPool(false); // we need unique cards only here, so that a unique card can be chosen before a set variant is determined allCards = CardUtil.getFullCardPool(false); // we need unique cards only here, so that a unique card can be chosen before a set variant is determined
@@ -100,11 +101,14 @@ public class RewardData implements Serializable {
return false; return false;
if(input.getRules().getAiHints().getRemNonCommanderDecks()) if(input.getRules().getAiHints().getRemNonCommanderDecks())
return false; return false;
if(Arrays.asList(Config.instance().getConfigData().restrictedEditions).contains(input.getEdition())) if(configData.allowedEditions != null) {
if (!Arrays.asList(configData.allowedEditions).contains(input.getEdition()))
return false;
} else if(Arrays.asList(configData.restrictedEditions).contains(input.getEdition()))
return false; return false;
if(input.getRules().isCustom()) if(input.getRules().isCustom())
return false; return false;
return !Arrays.asList(Config.instance().getConfigData().restrictedCards).contains(input.getName()); return !Arrays.asList(configData.restrictedCards).contains(input.getName());
}); });
//Filter AI cards for enemies. //Filter AI cards for enemies.
allEnemyCards=Iterables.filter(allCards, input -> { allEnemyCards=Iterables.filter(allCards, input -> {

View File

@@ -11,6 +11,7 @@ import com.github.tommyettinger.textra.TextraButton;
import com.github.tommyettinger.textra.TextraLabel; import com.github.tommyettinger.textra.TextraLabel;
import forge.Forge; import forge.Forge;
import forge.StaticData; import forge.StaticData;
import forge.adventure.data.ConfigData;
import forge.adventure.data.RewardData; import forge.adventure.data.RewardData;
import forge.adventure.util.*; import forge.adventure.util.*;
import forge.card.CardEdition; import forge.card.CardEdition;
@@ -151,7 +152,10 @@ public class SpellSmithScene extends UIScene {
.filter(input2 -> input2.getEdition().equals(input.getCode())).collect(Collectors.toList()); .filter(input2 -> input2.getEdition().equals(input.getCode())).collect(Collectors.toList());
if (it.size() == 0) if (it.size() == 0)
return false; return false;
return (!Arrays.asList(Config.instance().getConfigData().restrictedEditions).contains(input.getCode())); ConfigData configData = Config.instance().getConfigData();
if (configData.allowedEditions != null)
return Arrays.asList(configData.allowedEditions).contains(input.getCode());
return (!Arrays.asList(configData.restrictedEditions).contains(input.getCode()));
}).sorted(new Comparator<CardEdition>() { }).sorted(new Comparator<CardEdition>() {
@Override @Override
public int compare(CardEdition e1, CardEdition e2) { public int compare(CardEdition e1, CardEdition e2) {

View File

@@ -480,8 +480,11 @@ public class MapStage extends GameStage {
if (enemy != null && !enemy.toString().isEmpty()) { if (enemy != null && !enemy.toString().isEmpty()) {
EnemyData EN = WorldData.getEnemy(enemy.toString()); EnemyData EN = WorldData.getEnemy(enemy.toString());
if (EN == null) { if (EN == null) {
System.err.printf("Enemy \"%s\" not found.", enemy); System.err.printf("Enemy \"%s\" not found, choosing a random one for current biome\n", enemy);
break; forge.adventure.world.World world = Current.world();
Vector2 poiPos = AdventureQuestController.instance().mostRecentPOI.getPosition();
int currentBiome = forge.adventure.world.World.highestBiome(world.getBiome((int) poiPos.x / world.getTileSize(), (int) poiPos.y / world.getTileSize()));
EN = world.getData().GetBiomes().get(currentBiome).getEnemy(1.0f);
} }
EnemySprite mob = new EnemySprite(id, EN); EnemySprite mob = new EnemySprite(id, EN);
Object dialogObject = prop.get("dialog"); //Check if the enemy has a dialogue attached to it. Object dialogObject = prop.get("dialog"); //Check if the enemy has a dialogue attached to it.

View File

@@ -6,6 +6,7 @@ import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.StaticData; import forge.StaticData;
import forge.adventure.data.ConfigData;
import forge.adventure.data.GeneratedDeckData; import forge.adventure.data.GeneratedDeckData;
import forge.adventure.data.GeneratedDeckTemplateData; import forge.adventure.data.GeneratedDeckTemplateData;
import forge.adventure.data.RewardData; import forge.adventure.data.RewardData;
@@ -23,7 +24,6 @@ import forge.item.generation.UnOpenedProduct;
import forge.model.FModel; import forge.model.FModel;
import forge.util.Aggregates; import forge.util.Aggregates;
import java.io.File;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -714,7 +714,7 @@ public class CardUtil {
public static Deck getDeck(String path, boolean forAI, boolean isFantasyMode, String colors, boolean isTheme, boolean useGeneticAI, CardEdition starterEdition, boolean discourageDuplicates) public static Deck getDeck(String path, boolean forAI, boolean isFantasyMode, String colors, boolean isTheme, boolean useGeneticAI, CardEdition starterEdition, boolean discourageDuplicates)
{ {
if(path.endsWith(".dck")) if(path.endsWith(".dck"))
return DeckSerializer.fromFile(new File(Config.instance().getCommonFilePath(path))); return DeckSerializer.fromFile(Config.instance().getFile(path).file());
if(forAI && (isFantasyMode||useGeneticAI)) { if(forAI && (isFantasyMode||useGeneticAI)) {
Deck deck = DeckgenUtil.getRandomOrPreconOrThemeDeck(colors, forAI, isTheme, useGeneticAI); Deck deck = DeckgenUtil.getRandomOrPreconOrThemeDeck(colors, forAI, isTheme, useGeneticAI);
@@ -755,8 +755,12 @@ public class CardUtil {
} }
public static Deck generateBoosterPackAsDeck(String code){ public static Deck generateBoosterPackAsDeck(String code){
ConfigData configData = Config.instance().getConfigData();
if (Arrays.asList(Config.instance().getConfigData().restrictedEditions).contains(code)){ if (configData.allowedEditions != null) {
if (!Arrays.asList(configData.allowedEditions).contains(code)){
System.err.println("Cannot generate booster pack, '" + code + "' is not an allowed edition");
}
} else if (Arrays.asList(configData.restrictedEditions).contains(code)){
System.err.println("Cannot generate booster pack, '" + code + "' is a restricted edition"); System.err.println("Cannot generate booster pack, '" + code + "' is a restricted edition");
} }

View File

@@ -5,6 +5,6 @@ T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigPutCounter | Tri
SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Artifact.nonToken+YouCtrl+Other,Creature.YouCtrl+Other+nonToken | CounterType$ OIL | CounterNum$ 1 SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Artifact.nonToken+YouCtrl+Other,Creature.YouCtrl+Other+nonToken | CounterType$ OIL | CounterNum$ 1
A:AB$ Token | Cost$ 2 T | TokenScript$ r_1_1_phyrexian_goblin | CheckSVar$ CountCountersRemoved | CheckSVarCompare$ GE1 | SpellDescription$ Create a 1/1 red Phyrexian Goblin creature token. Activate only if an oil counter was removed from a permanent you controlled this turn or a permanent with an oil counter on it was put into a graveyard this turn. A:AB$ Token | Cost$ 2 T | TokenScript$ r_1_1_phyrexian_goblin | CheckSVar$ CountCountersRemoved | CheckSVarCompare$ GE1 | SpellDescription$ Create a 1/1 red Phyrexian Goblin creature token. Activate only if an oil counter was removed from a permanent you controlled this turn or a permanent with an oil counter on it was put into a graveyard this turn.
SVar:CountCountersRemoved:Count$CountersRemovedThisTurn OIL Card.YouCtrl+inRealZoneBattlefield/Plus.X SVar:CountCountersRemoved:Count$CountersRemovedThisTurn OIL Card.YouCtrl+inRealZoneBattlefield/Plus.X
SVar:X:Count$ThisTurnEntered_Graveyard_from_Battlefield_Permanent.YouCtrl+counters_GE1_OIL SVar:X:Count$ThisTurnEntered_Graveyard_from_Battlefield_Permanent.counters_GE1_OIL
DeckHas:Ability$Token/Counters & Type$Goblin|Phyrexian DeckHas:Ability$Token/Counters & Type$Goblin|Phyrexian
Oracle:At the beginning of your upkeep, put an oil counter on another target nontoken artifact or creature you control.\n{2}, {T}: Create a 1/1 red Phyrexian Goblin creature token. Activate only if an oil counter was removed from a permanent you controlled this turn or a permanent with an oil counter on it was put into a graveyard this turn. Oracle:At the beginning of your upkeep, put an oil counter on another target nontoken artifact or creature you control.\n{2}, {T}: Create a 1/1 red Phyrexian Goblin creature token. Activate only if an oil counter was removed from a permanent you controlled this turn or a permanent with an oil counter on it was put into a graveyard this turn.

View File

@@ -3,11 +3,8 @@ ManaCost:2 B
Types:Creature Rat Types:Creature Rat
PT:1/1 PT:1/1
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigPutCounter | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, put an infection counter on it. T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigPutCounter | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, put an infection counter on it.
SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ INFECTION | CounterNum$ 1 | SubAbility$ DBRemember SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ INFECTION | CounterNum$ 1
SVar:DBRemember:DB$ Pump | RememberObjects$ Opponent | StackDescription$ None
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ DBDisease | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, Diseased Vermin deals X damage to target opponent previously dealt damage by it, where X is the number of infection counters on it. T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ DBDisease | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, Diseased Vermin deals X damage to target opponent previously dealt damage by it, where X is the number of infection counters on it.
SVar:DBDisease:DB$ DealDamage | ValidTgts$ Opponent.IsRemembered | NumDmg$ X SVar:DBDisease:DB$ DealDamage | ValidTgts$ Opponent.wasDealtDamageThisGameBy Self | NumDmg$ X
SVar:X:Count$CardCounters.INFECTION SVar:X:Count$CardCounters.INFECTION
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ DBCleanup | Static$ True
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
Oracle:Whenever Diseased Vermin deals combat damage to a player, put an infection counter on it.\nAt the beginning of your upkeep, Diseased Vermin deals X damage to target opponent previously dealt damage by it, where X is the number of infection counters on it. Oracle:Whenever Diseased Vermin deals combat damage to a player, put an infection counter on it.\nAt the beginning of your upkeep, Diseased Vermin deals X damage to target opponent previously dealt damage by it, where X is the number of infection counters on it.

View File

@@ -6,7 +6,7 @@ T:Mode$ SpellCast | ValidCard$ Instant,Sorcery | ValidActivatingPlayer$ You | Tr
SVar:TrigCharm:DB$ Charm | Choices$ DBTapUntap,DBDamage,DBCopy,DBChangeZone | ChoiceRestriction$ ThisGame | CharmNum$ 1 SVar:TrigCharm:DB$ Charm | Choices$ DBTapUntap,DBDamage,DBCopy,DBChangeZone | ChoiceRestriction$ ThisGame | CharmNum$ 1
SVar:DBTapUntap:DB$ TapOrUntap | ValidTgts$ Permanent | TgtPrompt$ Select target permanent to tap or untap | SpellDescription$ You may tap or untap target permanent. SVar:DBTapUntap:DB$ TapOrUntap | ValidTgts$ Permanent | TgtPrompt$ Select target permanent to tap or untap | SpellDescription$ You may tap or untap target permanent.
SVar:DBDamage:DB$ DealDamage | Defined$ Opponent | NumDmg$ 3 | SpellDescription$ CARDNAME deals 3 damage to each opponent. SVar:DBDamage:DB$ DealDamage | Defined$ Opponent | NumDmg$ 3 | SpellDescription$ CARDNAME deals 3 damage to each opponent.
SVar:DBCopy:DB$ CopySpellAbility | ValidTgts$ Instant.YouCtrl,Sorcery.YouCtrl | TgtPrompt$ Select target instant or sorcery spell you control | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell you control. You may choose new targets for the copy. SVar:DBCopy:DB$ CopySpellAbility | ValidTgts$ Instant.YouCtrl,Sorcery.YouCtrl | TargetType$ Spell | TgtPrompt$ Select target instant or sorcery spell you control | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell you control. You may choose new targets for the copy.
SVar:DBChangeZone:DB$ ChangeZone | Origin$ Battlefield | Destination$ Library | LibraryPosition$ 0 | SpellDescription$ Put NICKNAME on top of its owner's library. SVar:DBChangeZone:DB$ ChangeZone | Origin$ Battlefield | Destination$ Library | LibraryPosition$ 0 | SpellDescription$ Put NICKNAME on top of its owner's library.
DeckNeeds:Type$Instant|Sorcery DeckNeeds:Type$Instant|Sorcery
Oracle:Whenever you cast an instant or sorcery spell, choose one that hasn't been chosen —\n• You may tap or untap target permanent.\n• Gandalf the Grey deals 3 damage to each opponent.\n• Copy target instant or sorcery spell you control. You may choose new targets for the copy.\n• Put Gandalf on top of its owner's library. Oracle:Whenever you cast an instant or sorcery spell, choose one that hasn't been chosen —\n• You may tap or untap target permanent.\n• Gandalf the Grey deals 3 damage to each opponent.\n• Copy target instant or sorcery spell you control. You may choose new targets for the copy.\n• Put Gandalf on top of its owner's library.

View File

@@ -4,7 +4,7 @@ Types:Artifact Vehicle
PT:5/5 PT:5/5
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDamage | TriggerDescription$ When CARDNAME enters the battlefield, it deals 5 damage to target creature an opponent controls. T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDamage | TriggerDescription$ When CARDNAME enters the battlefield, it deals 5 damage to target creature an opponent controls.
SVar:TrigDamage:DB$ DealDamage | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls | NumDmg$ 5 SVar:TrigDamage:DB$ DealDamage | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls | NumDmg$ 5
T:Mode$ ExcessDamageAll | ValidTarget$ Creature.OppCtrl | CombatDamage$ False | Execute$ TrigTreasure | TriggerDescription$ Whenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token. T:Mode$ ExcessDamageAll | ValidTarget$ Creature.OppCtrl | CombatDamage$ False | TriggerZones$ Battlefield | Execute$ TrigTreasure | TriggerDescription$ Whenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token.
SVar:TrigTreasure:DB$ Token | TokenScript$ c_a_treasure_sac SVar:TrigTreasure:DB$ Token | TokenScript$ c_a_treasure_sac
K:Crew:2 K:Crew:2
Oracle:When Magmatic Galleon enters the battlefield, it deals 5 damage to target creature an opponent controls.\nWhenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token.\nCrew 2 Oracle:When Magmatic Galleon enters the battlefield, it deals 5 damage to target creature an opponent controls.\nWhenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token.\nCrew 2

View File

@@ -3,7 +3,7 @@ ManaCost:2 G U R
Types:Legendary Creature Human Rogue Types:Legendary Creature Human Rogue
PT:3/3 PT:3/3
T:Mode$ Phase | Phase$ BeginCombat | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigCounter | TriggerDescription$ At the beginning of combat on your turn, put your choice of a +1/+1, first strike, vigilance, or menace counter on CARDNAME. T:Mode$ Phase | Phase$ BeginCombat | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigCounter | TriggerDescription$ At the beginning of combat on your turn, put your choice of a +1/+1, first strike, vigilance, or menace counter on CARDNAME.
SVar:TrigCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1,First strike,Vigilance,Menace | CounterNum$ 1 SVar:TrigCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1,First Strike,Vigilance,Menace | CounterNum$ 1
K:Counters remain on CARDNAME as it moves to any zone other than a player's hand or library. K:Counters remain on CARDNAME as it moves to any zone other than a player's hand or library.
SVar:AltCost:Cost$ 2 G U R Discard<2/Card> | ActivationZone$ Graveyard | Description$ You may cast NICKNAME from your graveyard by discarding two cards in addition to paying its other costs. SVar:AltCost:Cost$ 2 G U R Discard<2/Card> | ActivationZone$ Graveyard | Description$ You may cast NICKNAME from your graveyard by discarding two cards in addition to paying its other costs.
DeckHas:Ability$Counters|Discard DeckHas:Ability$Counters|Discard

View File

@@ -4,5 +4,5 @@ Types:Legendary Creature Halfling Soldier
PT:2/2 PT:2/2
K:Vigilance K:Vigilance
K:Ward:1 K:Ward:1
A:AB$ Protection | Cost$ T | ValidTgts$ Creature.YouCtrl+Other | TgtPrompt$ Select another target creature you control | Gains$ Choice | Choices$ artifacts,enchantments,creatures,battles,instants,sorceries,planeswalkers,lands | SpellDescription$ Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.) A:AB$ Protection | Cost$ T | ValidTgts$ Creature.YouCtrl+Other | TgtPrompt$ Select another target creature you control | Gains$ Choice | Choices$ CardType | SpellDescription$ Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.)
Oracle:Vigilance, ward {1}\n{T}: Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.) Oracle:Vigilance, ward {1}\n{T}: Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.)

View File

@@ -2,7 +2,7 @@ Name:Raven's Run
ManaCost:no cost ManaCost:no cost
Types:Plane Shadowmoor Types:Plane Shadowmoor
S:Mode$ Continuous | EffectZone$ Command | Affected$ Creature | AddKeyword$ Wither | Description$ All Creatures have Wither (They deal damage to creatures in the form of -1/-1 counters.) S:Mode$ Continuous | EffectZone$ Command | Affected$ Creature | AddKeyword$ Wither | Description$ All Creatures have Wither (They deal damage to creatures in the form of -1/-1 counters.)
T:Mode$ ChaosEnsues | OptionalDecider$ You | TriggerZones$ Command | Execute$ TrigPutCounter | TriggerDescription$ Whenever chaos ensues, put a -1/-1 counter on target creature, two -1/-1 counters on another target creature, and three -1/-1 counters on a third target creature. T:Mode$ ChaosEnsues | TriggerZones$ Command | Execute$ TrigPutCounter | TriggerDescription$ Whenever chaos ensues, put a -1/-1 counter on target creature, two -1/-1 counters on another target creature, and three -1/-1 counters on a third target creature.
SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Creature | TargetUnique$ True | CounterType$ M1M1 | SubAbility$ DBPutTwo SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Creature | TargetUnique$ True | CounterType$ M1M1 | SubAbility$ DBPutTwo
SVar:DBPutTwo:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select another target creature | TargetUnique$ True | CounterType$ M1M1 | CounterNum$ 2 | SubAbility$ DBPutThree SVar:DBPutTwo:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select another target creature | TargetUnique$ True | CounterType$ M1M1 | CounterNum$ 2 | SubAbility$ DBPutThree
SVar:DBPutThree:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select a third target creature | TargetUnique$ True | CounterType$ M1M1 | CounterNum$ 3 SVar:DBPutThree:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select a third target creature | TargetUnique$ True | CounterType$ M1M1 | CounterNum$ 3

View File

@@ -4,8 +4,8 @@ Types:Legendary Creature Goblin Rebel
PT:2/2 PT:2/2
K:Trample K:Trample
K:Haste K:Haste
T:Mode$ Attacks | ValidCard$ Creature.equipped+Other | Execute$ TrigAttach | TriggerZones$ Battlefield | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. T:Mode$ Attacks | ValidCard$ Creature.equipped+YouCtrl+Other | Execute$ TrigAttach | TriggerZones$ Battlefield | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME.
T:Mode$ ChangesZone | ValidCard$ Creature.equipped+Other | Origin$ Battlefield | Destination$ Graveyard | Execute$ TrigAttach | TriggerZones$ Battlefield | OptionalDecider$ You | Secondary$ True | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. T:Mode$ ChangesZone | ValidCard$ Creature.equipped+YouCtrl+Other | Origin$ Battlefield | Destination$ Graveyard | Execute$ TrigAttach | TriggerZones$ Battlefield | OptionalDecider$ You | Secondary$ True | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME.
SVar:TrigAttach:DB$ Attach | Object$ AttachedTo TriggeredCard.Equipment | Defined$ Self SVar:TrigAttach:DB$ Attach | Object$ AttachedTo TriggeredCard.Equipment | Defined$ Self
DeckNeeds:Type$Equipment DeckNeeds:Type$Equipment
Oracle:Trample, haste\nWhenever an equipped creature you control other than Rhuk, Hexgold Nabber attacks or dies, you may attach all Equipment attached to that creature to Rhuk. Oracle:Trample, haste\nWhenever an equipped creature you control other than Rhuk, Hexgold Nabber attacks or dies, you may attach all Equipment attached to that creature to Rhuk.

View File

@@ -2,7 +2,6 @@ Name:Rift Elemental
ManaCost:R ManaCost:R
Types:Creature Elemental Types:Creature Elemental
PT:1/1 PT:1/1
A:AB$ Pump | Cost$ 1 R SubCounter<1/TIME/Permanent/permanent you control> | Defined$ Self | NumAtt$ 2 | SpellDescription$ CARDNAME gets +2/+0 until end of turn. A:AB$ Pump | Cost$ 1 R SubCounter<1/TIME/Permanent.inZoneBattlefield;Card.suspended/a permanent you control or suspended card you own/Battlefield,Exile> | Defined$ Self | NumAtt$ 2 | SpellDescription$ CARDNAME gets +2/+0 until end of turn.
A:AB$ Pump | Cost$ 1 R SubCounter<1/TIME/Card.suspended/suspended card you own/Exile> | Defined$ Self | NumAtt$ 2 | SpellDescription$ CARDNAME gets +2/+0 until end of turn.
AI:RemoveDeck:All AI:RemoveDeck:All
Oracle:{1}{R}, Remove a time counter from a permanent you control or suspended card you own: Rift Elemental gets +2/+0 until end of turn. Oracle:{1}{R}, Remove a time counter from a permanent you control or suspended card you own: Rift Elemental gets +2/+0 until end of turn.

View File

@@ -0,0 +1,16 @@
Name:Sycorax Commander
ManaCost:2 B R
Types:Creature Alien Soldier
PT:4/2
K:First Strike
K:Haste
T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigChoice | TriggerDescription$ Sanctified Rules of Combat — When CARDNAME enters the battlefield, each opponent faces a villainous choice — That opponent discards all the cards in their hand, then draws that many cards minus one, or CARDNAME deals damage to that player equal to the number of cards in their hand.
SVar:TrigChoice:DB$ VillainousChoice | Defined$ Opponent | Choices$ DBDiscard,DBDamage
SVar:DBDiscard:DB$ Discard | Defined$ Player.IsRemembered | Mode$ Hand | RememberDiscarded$ True | SubAbility$ DBDraw | SpellDescription$ Opponent discards their hand, then draws that many cards minus one.
SVar:DBDraw:DB$ Draw | NumCards$ X | Defined$ Player.IsRemembered | SubAbility$ CleanDrawn
SVar:CleanDrawn:DB$ Cleanup | ClearRemembered$ True
# This calculation isn't considering the remembered player, only the remembered cards
SVar:X:Remembered$Amount/Minus.1
SVar:DBDamage:DB$ DealDamage | Defined$ Player.IsRemembered | NumDmg$ Y | SpellDescription$ CARDNAME deals damage to opponent equal to the number of cards in their hand.
SVar:Y:Count$ValidHand Card.OwnedBy Player.IsRemembered
Oracle:First strike, haste\nSanctified Rules of Combat — When Sycorax Commander enters the battlefield, each opponent faces a villainous choice — That opponent discards all the cards in their hand, then draws that many cards minus one, or Sycorax Commander deals damage to that player equal to the number of cards in their hand.

View File

@@ -0,0 +1,13 @@
Name:The Dalek Emperor
ManaCost:5 B R
Types:Legendary Artifact Creature Dalek
PT:6/6
K:Affinity:Dalek
S:Mode$ Continuous | Affected$ Dalek.Other+YouCtrl | AddKeyword$ Haste | Description$ Other Daleks you control have haste.
T:Mode$ Phase | Phase$ BeginCombat | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigChoice | TriggerDescription$ At the beginning of combat on your turn, each opponent faces a villainous choice — That player sacrifices a creature they control, or you create a 3/3 black Dalek artifact creature token with menace.
SVar:TrigChoice:DB$ VillainousChoice | Defined$ Opponent | Choices$ DBSacrifice,DBToken
SVar:DBSacrifice:DB$ Sacrifice | Defined$ Remembered | SacValid$ Creature | SacMessage$ Creature | SpellDescription$ Opponent sacrifices a creature.
SVar:DBToken:DB$ Token | TokenAmount$ 1 | TokenScript$ b_3_3_a_dalek_menace | TokenOwner$ You | SpellDescription$ You create a 3/3 black Dalek artifact creature token with menace.
DeckHints:Type$Dalek
DeckHas:Ability$Token
Oracle:Affinity for Daleks (This spell costs {1} less to cast for each Dalek you control.)\nOther Daleks you control have haste.\nAt the beginning of combat on your turn, each opponent faces a villainous choice — That player sacrifices a creature they control, or you create a 3/3 black Dalek artifact creature token with menace.

View File

@@ -0,0 +1,15 @@
Name:The Master, Gallifrey's End
ManaCost:2 B R
Types:Legendary Creature Time Lord Rogue
PT:4/3
T:Mode$ ChangesZone | ValidCard$ Creature.Artifact+nonToken+YouCtrl | Origin$ Battlefield | Destination$ Graveyard | OptionalDecider$ You | Execute$ TrigExile | TriggerZones$ Battlefield | TriggerDescription$ Make Them Pay — Whenever a nontoken artifact creature you control dies, you may exile it. If you do, choose an opponent with the most life among your opponents. That player faces a villainous choice — They lose 4 life, or you create a token that's a copy of that card.
SVar:TrigExile:DB$ ChangeZone | RememberChanged$ True | Origin$ Graveyard | Destination$ Exile | Defined$ TriggeredNewCardLKICopy | SubAbility$ DBChoosePlayer
SVar:DBChoosePlayer:DB$ ChoosePlayer | Defined$ You | Choices$ Player.Opponent+lifeEQX | SubAbility$ DBChoice
SVar:X:Count$OppGreatestLifeTotal
SVar:DBChoice:DB$ VillainousChoice | Defined$ ChosenPlayer | Choices$ DBLoseLife,DBCopy | SubAbility$ DBCleanup
SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 4 | Defined$ Player.IsRemembered | SpellDescription$ Opponent loses 4 life.
SVar:DBCopy:DB$ CopyPermanent | Defined$ Remembered | SubAbility$ DBCleanup | SpellDescription$ You create a token that's a copy of the exiled card.
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
DeckHints:Type$Artifact
DeckHas:Ability$Token
Oracle:Make Them Pay — Whenever a nontoken artifact creature you control dies, you may exile it. If you do, choose an opponent with the most life among your opponents. That player faces a villainous choice — They lose 4 life, or you create a token that's a copy of that card.

View File

@@ -4,6 +4,6 @@ Types:Enchantment Saga
K:Chapter:2:DBDestroyAll,DBChangeZone K:Chapter:2:DBDestroyAll,DBChangeZone
SVar:DBDestroyAll:DB$ DestroyAll | ValidCards$ Creature | SpellDescription$ Destroy all creatures. SVar:DBDestroyAll:DB$ DestroyAll | ValidCards$ Creature | SpellDescription$ Destroy all creatures.
SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.Legendary+YouCtrl | TgtPrompt$ Choose target legendary creature card in your graveyard | SubAbility$ DBPutCounter | RememberChanged$ True | SpellDescription$ Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it. SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.Legendary+YouCtrl | TgtPrompt$ Choose target legendary creature card in your graveyard | SubAbility$ DBPutCounter | RememberChanged$ True | SpellDescription$ Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it.
SVar:DBPutCounter:DB$ PutCounter | Choices$ Creature.IsRemembered | CounterType$ First strike,Vigilance,Lifelink | CounterNum$ 1| SubAbility$ DBCleanup SVar:DBPutCounter:DB$ PutCounter | Choices$ Creature.IsRemembered | CounterType$ First Strike,Vigilance,Lifelink | CounterNum$ 1| SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after II.)\nI — Destroy all creatures.\nII — Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it. Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after II.)\nI — Destroy all creatures.\nII — Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it.

View File

@@ -0,0 +1,8 @@
Name:This Is How It Ends
ManaCost:3 B
Types:Instant
A:SP$ ChangeZone | ValidTgts$ Creature | TgtPrompt$ Select target creature | Origin$ Battlefield | Destination$ Library | Shuffle$ True | SubAbility$ DBChoice | SpellDescription$ The owner of target creature shuffles it into their library,
SVar:DBChoice:DB$ VillainousChoice | Defined$ TargetedOwner | Choices$ DBLoseLife,DBShuffleAnother | SpellDescription$ then faces a villainous choice — They lose 5 life, or they shuffle another creature they own into their library.
SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 5 | Defined$ Remembered | SpellDescription$ Targeted creature's owner loses 5 life.
SVar:DBShuffleAnother:DB$ ChangeZone | ChangeType$ Creature.OwnedBy Remembered | DefinedPlayer$ Remembered | Chooser$ Remembered | ChangeNum$ 1 | Mandatory$ True | Origin$ Battlefield | Destination$ Library | Shuffle$ True | Hidden$ True | SpellDescription$ Targeted creature's owner shuffles another creature they own into their library.
Oracle:Target creature's owner shuffles it into their library, then faces a villainous choice — They lose 5 life, or they shuffle another creature they own into their library.

View File

@@ -3,7 +3,7 @@ ManaCost:1 U
Types:Instant Arcane Types:Instant Arcane
K:Splice:Arcane:Return<1/Creature.Blue/blue creature> K:Splice:Arcane:Return<1/Creature.Blue/blue creature>
A:SP$ Pump | ValidTgts$ Creature | KW$ Shroud | SubAbility$ DBUnblockable | StackDescription$ REP Target creature_{c:Targeted} | SpellDescription$ Target creature gains shroud until end of turn and can't be blocked this turn. (A creature with shroud can't be the target of spells or abilities.) A:SP$ Pump | ValidTgts$ Creature | KW$ Shroud | SubAbility$ DBUnblockable | StackDescription$ REP Target creature_{c:Targeted} | SpellDescription$ Target creature gains shroud until end of turn and can't be blocked this turn. (A creature with shroud can't be the target of spells or abilities.)
SVar:DBUnblockable:DB$ Effect | RememberObjects$ Self | ExileOnMoved$ Battlefield | StaticAbilities$ Unblockable SVar:DBUnblockable:DB$ Effect | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | StaticAbilities$ Unblockable
SVar:Unblockable:Mode$ CantBlockBy | ValidAttacker$ Card.IsRemembered | Description$ This creature can't be blocked this turn. SVar:Unblockable:Mode$ CantBlockBy | ValidAttacker$ Card.IsRemembered | Description$ This creature can't be blocked this turn.
AI:RemoveDeck:All AI:RemoveDeck:All
DeckHints:Type$Arcane DeckHints:Type$Arcane

View File

@@ -1036,7 +1036,7 @@ public class HumanCostDecision extends CostDecisionMakerBase {
} }
final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, validCards, ability); final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, validCards, ability);
inp.setMessage(Localizer.getInstance().getMessage("lblRemoveCountersFromAInZoneCard", cost.zone.getTranslatedName())); inp.setMessage(Localizer.getInstance().getMessage("lblRemoveCountersFromAInZoneCard", Lang.joinHomogenous(cost.zone, ZoneType.Accessors.GET_TRANSLATED_NAME)));
inp.setCancelAllowed(true); inp.setCancelAllowed(true);
inp.showAndWait(); inp.showAndWait();