mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 20:28:00 +00:00
Merge branch 'keyword_rna' into 'master'
Keyword RNA See merge request core-developers/forge!1243
This commit is contained in:
@@ -948,6 +948,20 @@ public class AiController {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()
|
||||||
|
&& a.getPayCosts() != null && b.getPayCosts() != null) {
|
||||||
|
// Cheaper Spectacle costs should be preferred
|
||||||
|
// FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not?
|
||||||
|
// (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs)
|
||||||
|
if (a.isSpectacle() && !b.isSpectacle()
|
||||||
|
&& a.getPayCosts().getTotalMana().getCMC() < b.getPayCosts().getTotalMana().getCMC()) {
|
||||||
|
return 1;
|
||||||
|
} else if (b.isSpectacle() && !a.isSpectacle()
|
||||||
|
&& b.getPayCosts().getTotalMana().getCMC() < a.getPayCosts().getTotalMana().getCMC()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a1 += getSpellAbilityPriority(a);
|
a1 += getSpellAbilityPriority(a);
|
||||||
b1 += getSpellAbilityPriority(b);
|
b1 += getSpellAbilityPriority(b);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package forge.ai;
|
|||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.*;
|
import com.google.common.collect.*;
|
||||||
|
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.CardType;
|
import forge.card.CardType;
|
||||||
@@ -986,6 +987,11 @@ public class ComputerUtil {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (card.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) {
|
||||||
|
// Planning to choose Haste for Riot, so do this in Main 1
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// if we have non-persistent mana in our pool, would be good to try to use it and not waste it
|
// if we have non-persistent mana in our pool, would be good to try to use it and not waste it
|
||||||
if (ai.getManaPool().willManaBeLostAtEndOfPhase()) {
|
if (ai.getManaPool().willManaBeLostAtEndOfPhase()) {
|
||||||
boolean canUseToPayCost = false;
|
boolean canUseToPayCost = false;
|
||||||
|
|||||||
@@ -97,9 +97,27 @@ public class ComputerUtilAbility {
|
|||||||
final List<SpellAbility> newAbilities = Lists.newArrayList();
|
final List<SpellAbility> newAbilities = Lists.newArrayList();
|
||||||
for (SpellAbility sa : originList) {
|
for (SpellAbility sa : originList) {
|
||||||
sa.setActivatingPlayer(player);
|
sa.setActivatingPlayer(player);
|
||||||
//add alternative costs as additional spell abilities
|
|
||||||
|
// determine which alternative costs are cheaper than the original and prioritize them
|
||||||
|
List<SpellAbility> saAltCosts = GameActionUtil.getAlternativeCosts(sa, player);
|
||||||
|
List<SpellAbility> priorityAltSa = Lists.newArrayList();
|
||||||
|
List<SpellAbility> otherAltSa = Lists.newArrayList();
|
||||||
|
for (SpellAbility altSa : saAltCosts) {
|
||||||
|
if (altSa.getPayCosts() == null || sa.getPayCosts() == null) {
|
||||||
|
otherAltSa.add(altSa);
|
||||||
|
} else if (sa.getPayCosts().isOnlyManaCost()
|
||||||
|
&& altSa.getPayCosts().isOnlyManaCost() && sa.getPayCosts().getTotalMana().compareTo(altSa.getPayCosts().getTotalMana()) == 1) {
|
||||||
|
// the alternative cost is strictly cheaper, so why not? (e.g. Omniscience etc.)
|
||||||
|
priorityAltSa.add(altSa);
|
||||||
|
} else {
|
||||||
|
otherAltSa.add(altSa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add alternative costs as additional spell abilities
|
||||||
|
newAbilities.addAll(priorityAltSa);
|
||||||
newAbilities.add(sa);
|
newAbilities.add(sa);
|
||||||
newAbilities.addAll(GameActionUtil.getAlternativeCosts(sa, player));
|
newAbilities.addAll(otherAltSa);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<SpellAbility> result = Lists.newArrayList();
|
final List<SpellAbility> result = Lists.newArrayList();
|
||||||
|
|||||||
@@ -1057,6 +1057,10 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
|
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
|
||||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
||||||
if (pBonus > 0) {
|
if (pBonus > 0) {
|
||||||
@@ -1229,6 +1233,10 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
|
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
|
||||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
||||||
if (tBonus > 0) {
|
if (tBonus > 0) {
|
||||||
@@ -1447,6 +1455,10 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ability.hasParam("Adapt") && blocker != null && blocker.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
|
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
|
||||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
||||||
if (pBonus > 0) {
|
if (pBonus > 0) {
|
||||||
@@ -1680,6 +1692,10 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
|
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
|
||||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
||||||
if (tBonus > 0) {
|
if (tBonus > 0) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import forge.game.card.*;
|
|||||||
import forge.game.card.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.cost.*;
|
import forge.game.cost.*;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.Spell;
|
import forge.game.spellability.Spell;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -74,7 +75,7 @@ public class ComputerUtilCost {
|
|||||||
|
|
||||||
final CounterType type = remCounter.counter;
|
final CounterType type = remCounter.counter;
|
||||||
if (!part.payCostFromSource()) {
|
if (!part.payCostFromSource()) {
|
||||||
if (type.name().equals("P1P1")) {
|
if (CounterType.P1P1.equals(type)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -105,7 +106,8 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//don't kill the creature
|
//don't kill the creature
|
||||||
if (type.name().equals("P1P1") && source.getLethalDamage() <= 1) {
|
if (CounterType.P1P1.equals(type) && source.getLethalDamage() <= 1
|
||||||
|
&& !source.hasKeyword(Keyword.UNDYING)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import java.util.Map;
|
|||||||
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.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
@@ -24,6 +26,8 @@ import forge.game.card.CounterType;
|
|||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.AbilitySub;
|
import forge.game.spellability.AbilitySub;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -38,7 +42,7 @@ 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 (aiLogic.startsWith("Fabricate")) {
|
} else if (aiLogic.startsWith("Fabricate") || "Riot".equals(aiLogic)) {
|
||||||
return true;
|
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")) {
|
||||||
@@ -344,7 +348,56 @@ 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(CounterType.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?
|
||||||
|
if (opp.getLife() < copy.getNetPower()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// haste might not be good enough?
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check for some specific AI preferences
|
// check for some specific AI preferences
|
||||||
if (src.hasStartOfKeyword("Graft") && "DontMoveCounterIfLethal".equals(src.getSVar("AIGraftPreference"))) {
|
if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) {
|
||||||
if (cType == CounterType.P1P1 && src.getNetToughness() - src.getTempToughnessBoost() - 1 <= 0) {
|
if (cType == CounterType.P1P1 && src.getNetToughness() - src.getTempToughnessBoost() - 1 <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -333,11 +333,12 @@ public class CountersMoveAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// try to remove P1P1 from undying or evolve
|
// try to remove P1P1 from undying or evolve
|
||||||
if (CounterType.P1P1.equals(cType)) {
|
if (CounterType.P1P1.equals(cType)) {
|
||||||
if (card.hasKeyword("Undying") || card.hasKeyword("Evolve")) {
|
if (card.hasKeyword(Keyword.UNDYING) || card.hasKeyword(Keyword.EVOLVE)
|
||||||
|
|| card.hasKeyword(Keyword.ADAPT)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
|
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,10 +393,10 @@ public class CountersMoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cType != null) {
|
if (cType != null) {
|
||||||
if (CounterType.P1P1.equals(cType) && card.hasKeyword("Undying")) {
|
if (CounterType.P1P1.equals(cType) && card.hasKeyword(Keyword.UNDYING)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
|
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -311,6 +311,10 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sa.hasParam("Adapt") && source.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO handle proper calculation of X values based on Cost
|
// TODO handle proper calculation of X values based on Cost
|
||||||
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||||
|
|
||||||
|
|||||||
@@ -308,6 +308,30 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mandatory) {
|
if (mandatory) {
|
||||||
|
if (type.equals("P1P1")) {
|
||||||
|
// Try to target creatures with Adapt or similar
|
||||||
|
CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT));
|
||||||
|
if (!adaptCreats.isEmpty()) {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outlast nice target
|
||||||
|
CardCollection outlastCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.OUTLAST));
|
||||||
|
if (!outlastCreats.isEmpty()) {
|
||||||
|
// outlast cards often benefit from having +1/+1 counters, try not to remove last one
|
||||||
|
CardCollection betterTargets = CardLists.filter(outlastCreats, CardPredicates.hasCounter(CounterType.P1P1, 2));
|
||||||
|
|
||||||
|
if (!betterTargets.isEmpty()) {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
|
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
|||||||
build.add(keyword);
|
build.add(keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (edition != null) {
|
||||||
build.add(edition.getCode());
|
build.add(edition.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
// Should future image file names be all lower case? Instead of Up case sets?
|
// Should future image file names be all lower case? Instead of Up case sets?
|
||||||
return StringUtils.join(build, "_").toLowerCase();
|
return StringUtils.join(build, "_").toLowerCase();
|
||||||
@@ -121,7 +123,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
|||||||
@Override public String getName() { return name; }
|
@Override public String getName() { return name; }
|
||||||
|
|
||||||
@Override public String toString() { return name; }
|
@Override public String toString() { return name; }
|
||||||
@Override public String getEdition() { return edition.getCode(); }
|
@Override public String getEdition() { return edition != null ? edition.getCode() : "???"; }
|
||||||
@Override public int getArtIndex() { return 0; } // This might change however
|
@Override public int getArtIndex() { return 0; } // This might change however
|
||||||
@Override public boolean isFoil() { return false; }
|
@Override public boolean isFoil() { return false; }
|
||||||
@Override public CardRules getRules() { return card; }
|
@Override public CardRules getRules() { return card; }
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class TokenDb implements ITokenDatabase {
|
|||||||
tokensByName.put(fullName, pt);
|
tokensByName.put(fullName, pt);
|
||||||
return pt;
|
return pt;
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
return null;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,15 @@ public class ForgeScript {
|
|||||||
|
|
||||||
public static boolean spellAbilityHasProperty(SpellAbility sa, String property, Player sourceController,
|
public static boolean spellAbilityHasProperty(SpellAbility sa, String property, Player sourceController,
|
||||||
Card source, SpellAbility spellAbility) {
|
Card source, SpellAbility spellAbility) {
|
||||||
if (property.equals("Buyback")) {
|
if (property.equals("ManaAbility")) {
|
||||||
|
if (!sa.isManaAbility()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (property.equals("nonManaAbility")) {
|
||||||
|
if (sa.isManaAbility()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (property.equals("Buyback")) {
|
||||||
if (!sa.isBuyBackAbility()) {
|
if (!sa.isBuyBackAbility()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ public class Game {
|
|||||||
* Gets the players who participated in match (regardless of outcome).
|
* Gets the players who participated in match (regardless of outcome).
|
||||||
* <i>Use this in UI and after match calculations</i>
|
* <i>Use this in UI and after match calculations</i>
|
||||||
*/
|
*/
|
||||||
public final List<Player> getRegisteredPlayers() {
|
public final PlayerCollection getRegisteredPlayers() {
|
||||||
return allPlayers;
|
return allPlayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,8 @@ public class TokenEffect extends SpellAbilityEffect {
|
|||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
tokenName = result.getName();
|
tokenName = result.getName();
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("don't find Token for TokenScript: " + sa.getParam("TokenScript"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1613,7 +1613,7 @@ public class Card extends GameEntity implements Comparable<Card> {
|
|||||||
|| keyword.equals("Suspend") // for the ones without amounnt
|
|| keyword.equals("Suspend") // for the ones without amounnt
|
||||||
|| keyword.equals("Hideaway") || keyword.equals("Ascend")
|
|| keyword.equals("Hideaway") || keyword.equals("Ascend")
|
||||||
|| keyword.equals("Totem armor") || keyword.equals("Battle cry")
|
|| keyword.equals("Totem armor") || keyword.equals("Battle cry")
|
||||||
|| keyword.equals("Devoid")){
|
|| keyword.equals("Devoid") || keyword.equals("Riot")){
|
||||||
sbLong.append(keyword + " (" + inst.getReminderText() + ")");
|
sbLong.append(keyword + " (" + inst.getReminderText() + ")");
|
||||||
} else if (keyword.startsWith("Partner:")) {
|
} else if (keyword.startsWith("Partner:")) {
|
||||||
final String[] k = keyword.split(":");
|
final String[] k = keyword.split(":");
|
||||||
@@ -1622,7 +1622,8 @@ public class Card extends GameEntity implements Comparable<Card> {
|
|||||||
|| keyword.startsWith("Fabricate") || keyword.startsWith("Soulshift") || keyword.startsWith("Bushido")
|
|| keyword.startsWith("Fabricate") || keyword.startsWith("Soulshift") || keyword.startsWith("Bushido")
|
||||||
|| keyword.startsWith("Crew") || keyword.startsWith("Tribute") || keyword.startsWith("Absorb")
|
|| keyword.startsWith("Crew") || keyword.startsWith("Tribute") || keyword.startsWith("Absorb")
|
||||||
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing")
|
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing")
|
||||||
|| keyword.startsWith ("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|
|| keyword.startsWith("Afterlife")
|
||||||
|
|| keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|
||||||
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")) {
|
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")) {
|
||||||
final String[] k = keyword.split(":");
|
final String[] k = keyword.split(":");
|
||||||
sbLong.append(k[0] + " " + k[1] + " (" + inst.getReminderText() + ")");
|
sbLong.append(k[0] + " " + k[1] + " (" + inst.getReminderText() + ")");
|
||||||
@@ -1655,13 +1656,13 @@ public class Card extends GameEntity implements Comparable<Card> {
|
|||||||
sbLong.append(keyword);
|
sbLong.append(keyword);
|
||||||
sbLong.append(" (" + Keyword.getInstance("Offering:"+ offeringType).getReminderText() + ")");
|
sbLong.append(" (" + Keyword.getInstance("Offering:"+ offeringType).getReminderText() + ")");
|
||||||
} else if (keyword.startsWith("Equip") || keyword.startsWith("Fortify") || keyword.startsWith("Outlast")
|
} else if (keyword.startsWith("Equip") || keyword.startsWith("Fortify") || keyword.startsWith("Outlast")
|
||||||
|| keyword.startsWith("Unearth") || keyword.startsWith("Scavenge")
|
|| keyword.startsWith("Unearth") || keyword.startsWith("Scavenge") || keyword.startsWith("Spectacle")
|
||||||
|| keyword.startsWith("Evoke") || keyword.startsWith("Bestow") || keyword.startsWith("Dash")
|
|| keyword.startsWith("Evoke") || keyword.startsWith("Bestow") || keyword.startsWith("Dash")
|
||||||
|| keyword.startsWith("Surge") || keyword.startsWith("Transmute") || keyword.startsWith("Suspend")
|
|| keyword.startsWith("Surge") || keyword.startsWith("Transmute") || keyword.startsWith("Suspend")
|
||||||
|| keyword.equals("Undaunted") || keyword.startsWith("Monstrosity") || keyword.startsWith("Embalm")
|
|| keyword.equals("Undaunted") || keyword.startsWith("Monstrosity") || keyword.startsWith("Embalm")
|
||||||
|| keyword.startsWith("Level up") || keyword.equals("Prowess") || keyword.startsWith("Eternalize")
|
|| keyword.startsWith("Level up") || keyword.equals("Prowess") || keyword.startsWith("Eternalize")
|
||||||
|| keyword.startsWith("Reinforce") || keyword.startsWith("Champion") || keyword.startsWith("Prowl")
|
|| keyword.startsWith("Reinforce") || keyword.startsWith("Champion") || keyword.startsWith("Prowl")
|
||||||
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu")
|
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt")
|
||||||
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")) {
|
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")) {
|
||||||
// keyword parsing takes care of adding a proper description
|
// keyword parsing takes care of adding a proper description
|
||||||
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
|
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
|
||||||
|
|||||||
@@ -985,11 +985,7 @@ public class CardFactoryUtil {
|
|||||||
return doXMath(cc.getLifeGainedByTeamThisTurn(), m, c);
|
return doXMath(cc.getLifeGainedByTeamThisTurn(), m, c);
|
||||||
}
|
}
|
||||||
if (sq[0].contains("LifeOppsLostThisTurn")) {
|
if (sq[0].contains("LifeOppsLostThisTurn")) {
|
||||||
int lost = 0;
|
return doXMath(cc.getOpponentLostLifeThisTurn(), m, c);
|
||||||
for (Player opp : cc.getOpponents()) {
|
|
||||||
lost += opp.getLifeLostThisTurn();
|
|
||||||
}
|
|
||||||
return doXMath(lost, m, c);
|
|
||||||
}
|
}
|
||||||
if (sq[0].equals("TotalDamageDoneByThisTurn")) {
|
if (sq[0].equals("TotalDamageDoneByThisTurn")) {
|
||||||
return doXMath(c.getTotalDamageDoneBy(), m, c);
|
return doXMath(c.getTotalDamageDoneBy(), m, c);
|
||||||
@@ -2047,6 +2043,12 @@ public class CardFactoryUtil {
|
|||||||
final String effect, final boolean optional, final boolean secondary,
|
final String effect, final boolean optional, final boolean secondary,
|
||||||
final boolean intrinsic, final String valid, final String zone) {
|
final boolean intrinsic, final String valid, final String zone) {
|
||||||
SpellAbility repAb = AbilityFactory.getAbility(effect, card);
|
SpellAbility repAb = AbilityFactory.getAbility(effect, card);
|
||||||
|
return createETBReplacement(card, layer, repAb, optional, secondary, intrinsic, valid, zone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReplacementEffect createETBReplacement(final Card card, ReplacementLayer layer,
|
||||||
|
final SpellAbility repAb, final boolean optional, final boolean secondary,
|
||||||
|
final boolean intrinsic, final String valid, final String zone) {
|
||||||
String desc = repAb.getDescription();
|
String desc = repAb.getDescription();
|
||||||
setupETBReplacementAbility(repAb);
|
setupETBReplacementAbility(repAb);
|
||||||
if (!intrinsic) {
|
if (!intrinsic) {
|
||||||
@@ -2137,6 +2139,20 @@ public class CardFactoryUtil {
|
|||||||
afflictTrigger.setOverridingAbility(AbilityFactory.getAbility(abStringAfflict, card));
|
afflictTrigger.setOverridingAbility(AbilityFactory.getAbility(abStringAfflict, card));
|
||||||
|
|
||||||
inst.addTrigger(afflictTrigger);
|
inst.addTrigger(afflictTrigger);
|
||||||
|
} else if (keyword.startsWith("Afterlife")) {
|
||||||
|
final String k[] = keyword.split(":");
|
||||||
|
final String name = StringUtils.join(k, " ");
|
||||||
|
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self ");
|
||||||
|
sb.append("| Secondary$ True | TriggerDescription$ ").append(name);
|
||||||
|
sb.append(" (").append(inst.getReminderText()).append(")");
|
||||||
|
final String effect = "DB$ Token | TokenAmount$ " + k[1] + " | TokenScript$ wb_1_1_spirit_flying";
|
||||||
|
|
||||||
|
final Trigger trigger = TriggerHandler.parseTrigger(sb.toString(), card, intrinsic);
|
||||||
|
|
||||||
|
trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
|
||||||
|
inst.addTrigger(trigger);
|
||||||
} else if (keyword.startsWith("Annihilator")) {
|
} else if (keyword.startsWith("Annihilator")) {
|
||||||
final String[] k = keyword.split(":");
|
final String[] k = keyword.split(":");
|
||||||
final String n = k[1];
|
final String n = k[1];
|
||||||
@@ -2462,8 +2478,13 @@ public class CardFactoryUtil {
|
|||||||
|
|
||||||
inst.addTrigger(trigger);
|
inst.addTrigger(trigger);
|
||||||
} else if (keyword.startsWith("Graft")) {
|
} else if (keyword.startsWith("Graft")) {
|
||||||
final String abStr = "DB$ MoveCounter | Source$ Self | "
|
final StringBuilder sb = new StringBuilder();
|
||||||
+ "Defined$ TriggeredCardLKICopy | CounterType$ P1P1 | CounterNum$ 1";
|
sb.append("DB$ MoveCounter | Source$ Self | Defined$ TriggeredCardLKICopy");
|
||||||
|
sb.append(" | CounterType$ P1P1 | CounterNum$ 1");
|
||||||
|
|
||||||
|
if (card.hasSVar("AIGraftPreference")) {
|
||||||
|
sb.append(" | AILogic$ ").append(card.getSVar("AIGraftPreference"));
|
||||||
|
}
|
||||||
|
|
||||||
String trigStr = "Mode$ ChangesZone | ValidCard$ Creature.Other"
|
String trigStr = "Mode$ ChangesZone | ValidCard$ Creature.Other"
|
||||||
+ "| Origin$ Any | Destination$ Battlefield "
|
+ "| Origin$ Any | Destination$ Battlefield "
|
||||||
@@ -2474,7 +2495,7 @@ public class CardFactoryUtil {
|
|||||||
+ "may move a +1/+1 counter from this creature onto it.";
|
+ "may move a +1/+1 counter from this creature onto it.";
|
||||||
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
|
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
|
||||||
|
|
||||||
trigger.setOverridingAbility(AbilityFactory.getAbility(abStr, card));
|
trigger.setOverridingAbility(AbilityFactory.getAbility(sb.toString(), card));
|
||||||
|
|
||||||
inst.addTrigger(trigger);
|
inst.addTrigger(trigger);
|
||||||
} else if (keyword.startsWith("Haunt")) {
|
} else if (keyword.startsWith("Haunt")) {
|
||||||
@@ -3471,6 +3492,23 @@ public class CardFactoryUtil {
|
|||||||
re.setOverridingAbility(saExile);
|
re.setOverridingAbility(saExile);
|
||||||
|
|
||||||
inst.addReplacement(re);
|
inst.addReplacement(re);
|
||||||
|
} else if (keyword.startsWith("Riot")) {
|
||||||
|
final String choose = "DB$ GenericChoice | AILogic$ Riot | SpellDescription$ Riot";
|
||||||
|
|
||||||
|
final String counter = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | ETB$ True | CounterNum$ 1" +
|
||||||
|
" | SpellDescription$ Put a +1/+1 counter on it.";
|
||||||
|
final String haste = "DB$ Animate | Defined$ Self | Keywords$ Haste | Permanent$ True | 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);
|
||||||
} else if (keyword.startsWith("Saga")) {
|
} else if (keyword.startsWith("Saga")) {
|
||||||
String sb = "etbCounter:LORE:1:no Condition:no desc";
|
String sb = "etbCounter:LORE:1:no Condition:no desc";
|
||||||
final ReplacementEffect re = makeEtbCounter(sb, card, intrinsic);
|
final ReplacementEffect re = makeEtbCounter(sb, card, intrinsic);
|
||||||
@@ -3512,7 +3550,7 @@ public class CardFactoryUtil {
|
|||||||
|
|
||||||
inst.addReplacement(cardre);
|
inst.addReplacement(cardre);
|
||||||
} else if (keyword.equals("Unleash")) {
|
} else if (keyword.equals("Unleash")) {
|
||||||
String effect = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Unleash (" + inst.getReminderText() + ")";
|
String effect = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | ETB$ True | CounterNum$ 1 | SpellDescription$ Unleash (" + inst.getReminderText() + ")";
|
||||||
|
|
||||||
ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, effect, true, true, intrinsic, "Card.Self", "");
|
ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, effect, true, true, intrinsic, "Card.Self", "");
|
||||||
|
|
||||||
@@ -3662,6 +3700,24 @@ public class CardFactoryUtil {
|
|||||||
inst.addSpellAbility(newSA);
|
inst.addSpellAbility(newSA);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
} else if (keyword.startsWith("Adapt")) {
|
||||||
|
final String[] k = keyword.split(":");
|
||||||
|
final String magnitude = k[1];
|
||||||
|
final String manacost = k[2];
|
||||||
|
|
||||||
|
String desc = "Adapt " + magnitude;
|
||||||
|
|
||||||
|
String effect = "AB$ PutCounter | Cost$ " + manacost + " | ConditionPresent$ "
|
||||||
|
+ "Card.Self+counters_EQ0_P1P1 | Adapt$ True | CounterNum$ " + magnitude
|
||||||
|
+ " | CounterType$ P1P1 | StackDescription$ SpellDescription";
|
||||||
|
|
||||||
|
effect += "| SpellDescription$ " + desc + " (" + inst.getReminderText() + ")";
|
||||||
|
|
||||||
|
final SpellAbility sa = AbilityFactory.getAbility(effect, card);
|
||||||
|
sa.setIntrinsic(intrinsic);
|
||||||
|
|
||||||
|
sa.setTemporary(!intrinsic);
|
||||||
|
inst.addSpellAbility(sa);
|
||||||
} else if (keyword.equals("Aftermath") && card.getCurrentStateName().equals(CardStateName.RightSplit)) {
|
} else if (keyword.equals("Aftermath") && card.getCurrentStateName().equals(CardStateName.RightSplit)) {
|
||||||
// Aftermath does modify existing SA, and does not add new one
|
// Aftermath does modify existing SA, and does not add new one
|
||||||
|
|
||||||
@@ -4138,6 +4194,23 @@ public class CardFactoryUtil {
|
|||||||
sa.setTemporary(!intrinsic);
|
sa.setTemporary(!intrinsic);
|
||||||
inst.addSpellAbility(sa);
|
inst.addSpellAbility(sa);
|
||||||
|
|
||||||
|
} else if (keyword.startsWith("Spectacle")) {
|
||||||
|
final String[] k = keyword.split(":");
|
||||||
|
final Cost cost = new Cost(k[1], false);
|
||||||
|
final SpellAbility newSA = card.getFirstSpellAbility().copyWithDefinedCost(cost);
|
||||||
|
|
||||||
|
newSA.setBasicSpell(false);
|
||||||
|
newSA.setSpectacle(true);
|
||||||
|
|
||||||
|
String desc = "Spectacle " + cost.toSimpleString() + " (" + inst.getReminderText()
|
||||||
|
+ ")";
|
||||||
|
newSA.setDescription(desc);
|
||||||
|
|
||||||
|
newSA.setIntrinsic(intrinsic);
|
||||||
|
|
||||||
|
newSA.setTemporary(!intrinsic);
|
||||||
|
inst.addSpellAbility(newSA);
|
||||||
|
|
||||||
} else if (keyword.equals("Sunburst") && intrinsic) {
|
} else if (keyword.equals("Sunburst") && intrinsic) {
|
||||||
final GameCommand sunburstCIP = new GameCommand() {
|
final GameCommand sunburstCIP = new GameCommand() {
|
||||||
private static final long serialVersionUID = 1489845860231758299L;
|
private static final long serialVersionUID = 1489845860231758299L;
|
||||||
|
|||||||
@@ -1632,6 +1632,11 @@ public class CardProperty {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return card.getCastSA().isProwl();
|
return card.getCastSA().isProwl();
|
||||||
|
} else if (property.startsWith("spectacle")) {
|
||||||
|
if (card.getCastSA() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return card.getCastSA().isSpectacle();
|
||||||
} else if (property.equals("HasDevoured")) {
|
} else if (property.equals("HasDevoured")) {
|
||||||
if (card.getDevouredCards().isEmpty()) {
|
if (card.getDevouredCards().isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ public class Cost implements Serializable {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public <T extends CostPart> T getCostPartByType(Class<T> costType) {
|
public <T extends CostPart> T getCostPartByType(Class<T> costType) {
|
||||||
for (CostPart p : getCostParts()) {
|
for (CostPart p : getCostParts()) {
|
||||||
if (costType.isInstance(p)) {
|
if (costType.isInstance(p)) {
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import forge.util.TextUtil;
|
|||||||
public enum Keyword {
|
public enum Keyword {
|
||||||
UNDEFINED(SimpleKeyword.class, false, ""),
|
UNDEFINED(SimpleKeyword.class, false, ""),
|
||||||
ABSORB(KeywordWithAmount.class, false, "If a source would deal damage to this creature, prevent %d of that damage."),
|
ABSORB(KeywordWithAmount.class, false, "If a source would deal damage to this creature, prevent %d of that damage."),
|
||||||
|
ADAPT(KeywordWithCostAndAmount.class, false, "If this creature has no +1/+1 counters on it, put {%2$d:+1/+1 counter} on it."),
|
||||||
AFFINITY(KeywordWithType.class, false, "This spell costs you {1} less to cast for each %s you control."),
|
AFFINITY(KeywordWithType.class, false, "This spell costs you {1} less to cast for each %s you control."),
|
||||||
AFFLICT(KeywordWithAmount.class, false, "Whenever this creature becomes blocked, defending player loses %d life."),
|
AFFLICT(KeywordWithAmount.class, false, "Whenever this creature becomes blocked, defending player loses %d life."),
|
||||||
|
AFTERLIFE(KeywordWithAmount.class, false, "When this creature dies, create {%1$d:1/1 white and black Spirit creature token} with flying."),
|
||||||
AFTERMATH(SimpleKeyword.class, false, "Cast this spell only from your graveyard. Then exile it."),
|
AFTERMATH(SimpleKeyword.class, false, "Cast this spell only from your graveyard. Then exile it."),
|
||||||
AMPLIFY(KeywordWithAmountAndType.class, false, "As this creature enters the battlefield, put {%d:+1/+1 counter} on it for each %s card you reveal in your hand."),
|
AMPLIFY(KeywordWithAmountAndType.class, false, "As this creature enters the battlefield, put {%d:+1/+1 counter} on it for each %s card you reveal in your hand."),
|
||||||
ANNIHILATOR(KeywordWithAmount.class, false, "Whenever this creature attacks, defending player sacrifices {%d:permanent}."),
|
ANNIHILATOR(KeywordWithAmount.class, false, "Whenever this creature attacks, defending player sacrifices {%d:permanent}."),
|
||||||
@@ -122,6 +124,7 @@ public enum Keyword {
|
|||||||
RENOWN(KeywordWithAmount.class, true, "When this creature deals combat damage to a player, if it isn't renowned, put {%d:+1/+1 counter} on it and it becomes renowned."),
|
RENOWN(KeywordWithAmount.class, true, "When this creature deals combat damage to a player, if it isn't renowned, put {%d:+1/+1 counter} on it and it becomes renowned."),
|
||||||
REPLICATE(KeywordWithCost.class, false, "As an additional cost to cast this spell, you may pay %s any number of times. If you do, copy it that many times. You may choose new targets for the copies."),
|
REPLICATE(KeywordWithCost.class, false, "As an additional cost to cast this spell, you may pay %s any number of times. If you do, copy it that many times. You may choose new targets for the copies."),
|
||||||
RETRACE(SimpleKeyword.class, true, "You may cast this card from your graveyard by discarding a land card in addition to paying its other costs."),
|
RETRACE(SimpleKeyword.class, true, "You may cast this card from your graveyard by discarding a land card in addition to paying its other costs."),
|
||||||
|
RIOT(SimpleKeyword.class, false, "This creature enters the battlefield with your choice of a +1/+1 counter or haste."),
|
||||||
RIPPLE(KeywordWithAmount.class, false, "When you cast this spell, you may reveal the top {%d:card} of your library. You may cast any of those cards with the same name as this spell without paying their mana costs. Put the rest on the bottom of your library in any order."),
|
RIPPLE(KeywordWithAmount.class, false, "When you cast this spell, you may reveal the top {%d:card} of your library. You may cast any of those cards with the same name as this spell without paying their mana costs. Put the rest on the bottom of your library in any order."),
|
||||||
SHADOW(SimpleKeyword.class, true, "This creature can block or be blocked by only creatures with shadow."),
|
SHADOW(SimpleKeyword.class, true, "This creature can block or be blocked by only creatures with shadow."),
|
||||||
SHROUD(SimpleKeyword.class, true, "This can't be the target of spells or abilities."),
|
SHROUD(SimpleKeyword.class, true, "This can't be the target of spells or abilities."),
|
||||||
@@ -129,6 +132,7 @@ public enum Keyword {
|
|||||||
SCAVENGE(KeywordWithCost.class, false, "%s, Exile this card from your graveyard: Put a number of +1/+1 counters equal to this card's power on target creature. Scavenge only as a sorcery."),
|
SCAVENGE(KeywordWithCost.class, false, "%s, Exile this card from your graveyard: Put a number of +1/+1 counters equal to this card's power on target creature. Scavenge only as a sorcery."),
|
||||||
SOULBOND(SimpleKeyword.class, true, "You may pair this creature with another unpaired creature when either enters the battlefield. They remain paired for as long as you control both of them"),
|
SOULBOND(SimpleKeyword.class, true, "You may pair this creature with another unpaired creature when either enters the battlefield. They remain paired for as long as you control both of them"),
|
||||||
SOULSHIFT(KeywordWithAmount.class, false, "When this creature dies, you may return target Spirit card with converted mana cost %d or less from your graveyard to your hand."),
|
SOULSHIFT(KeywordWithAmount.class, false, "When this creature dies, you may return target Spirit card with converted mana cost %d or less from your graveyard to your hand."),
|
||||||
|
SPECTACLE(KeywordWithCost.class, true, "You may cast this spell for its spectacle cost rather than its mana cost if an opponent lost life this turn."),
|
||||||
SPLICE(KeywordWithCostAndType.class, false, "As you cast an %2$s spell, you may reveal this card from your hand and pay its splice cost. If you do, add this card's effects to that spell."),
|
SPLICE(KeywordWithCostAndType.class, false, "As you cast an %2$s spell, you may reveal this card from your hand and pay its splice cost. If you do, add this card's effects to that spell."),
|
||||||
SPLIT_SECOND(SimpleKeyword.class, true, "As long as this spell is on the stack, players can't cast other spells or activate abilities that aren't mana abilities."),
|
SPLIT_SECOND(SimpleKeyword.class, true, "As long as this spell is on the stack, players can't cast other spells or activate abilities that aren't mana abilities."),
|
||||||
STORM(SimpleKeyword.class, false, "When you cast this spell, copy it for each other spell that was cast before it this turn. You may choose new targets for the copies."),
|
STORM(SimpleKeyword.class, false, "When you cast this spell, copy it for each other spell that was cast before it this turn. You may choose new targets for the copies."),
|
||||||
|
|||||||
@@ -260,6 +260,10 @@ public class Player extends GameEntity implements Comparable<Player> {
|
|||||||
return game.getPlayers().filter(PlayerPredicates.isOpponentOf(this));
|
return game.getPlayers().filter(PlayerPredicates.isOpponentOf(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final PlayerCollection getRegisteredOpponents() {
|
||||||
|
return game.getRegisteredPlayers().filter(PlayerPredicates.isOpponentOf(this));
|
||||||
|
}
|
||||||
|
|
||||||
public void updateOpponentsForView() {
|
public void updateOpponentsForView() {
|
||||||
view.updateOpponents(this);
|
view.updateOpponents(this);
|
||||||
}
|
}
|
||||||
@@ -2037,8 +2041,7 @@ public class Player extends GameEntity implements Comparable<Player> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final int getBloodthirstAmount() {
|
public final int getBloodthirstAmount() {
|
||||||
return Aggregates.sum(Iterables.filter(
|
return Aggregates.sum(getRegisteredOpponents(), Accessors.FN_GET_ASSIGNED_DAMAGE);
|
||||||
game.getRegisteredPlayers(), PlayerPredicates.isOpponentOf(this)), Accessors.FN_GET_ASSIGNED_DAMAGE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final boolean hasSurge() {
|
public final boolean hasSurge() {
|
||||||
@@ -2047,6 +2050,14 @@ public class Player extends GameEntity implements Comparable<Player> {
|
|||||||
return !CardLists.filterControlledBy(game.getStack().getSpellsCastThisTurn(), list).isEmpty();
|
return !CardLists.filterControlledBy(game.getStack().getSpellsCastThisTurn(), list).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final int getOpponentLostLifeThisTurn() {
|
||||||
|
int lost = 0;
|
||||||
|
for (Player opp : getRegisteredOpponents()) {
|
||||||
|
lost += opp.getLifeLostThisTurn();
|
||||||
|
}
|
||||||
|
return lost;
|
||||||
|
}
|
||||||
|
|
||||||
public final boolean hasProwl(final String type) {
|
public final boolean hasProwl(final String type) {
|
||||||
if (prowl.contains("AllCreatureTypes")) {
|
if (prowl.contains("AllCreatureTypes")) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
|||||||
private int sourceTrigger = -1;
|
private int sourceTrigger = -1;
|
||||||
private List<Object> triggerRemembered = Lists.newArrayList();
|
private List<Object> triggerRemembered = Lists.newArrayList();
|
||||||
|
|
||||||
|
// TODO use enum for the flags
|
||||||
private boolean flashBackAbility = false;
|
private boolean flashBackAbility = false;
|
||||||
private boolean aftermath = false;
|
private boolean aftermath = false;
|
||||||
private boolean cycling = false;
|
private boolean cycling = false;
|
||||||
@@ -103,6 +104,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
|||||||
private boolean evoke = false;
|
private boolean evoke = false;
|
||||||
private boolean prowl = false;
|
private boolean prowl = false;
|
||||||
private boolean surge = false;
|
private boolean surge = false;
|
||||||
|
private boolean spectacle = false;
|
||||||
private boolean offering = false;
|
private boolean offering = false;
|
||||||
private boolean emerge = false;
|
private boolean emerge = false;
|
||||||
private boolean morphup = false;
|
private boolean morphup = false;
|
||||||
@@ -1121,6 +1123,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
|||||||
surge = isSurge;
|
surge = isSurge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final boolean isSpectacle() {
|
||||||
|
return spectacle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setSpectacle(final boolean isSpectacle) {
|
||||||
|
spectacle = isSpectacle;
|
||||||
|
}
|
||||||
|
|
||||||
public CardCollection getTappedForConvoke() {
|
public CardCollection getTappedForConvoke() {
|
||||||
return tappedForConvoke;
|
return tappedForConvoke;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,6 +419,11 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (sa.isSpectacle()) {
|
||||||
|
if (activator.getOpponentLostLifeThisTurn() <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isDesert()) {
|
if (isDesert()) {
|
||||||
if (!activator.hasDesert()) {
|
if (!activator.hasDesert()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
8
forge-gui/res/cardsfolder/upcoming/aeromunculus.txt
Normal file
8
forge-gui/res/cardsfolder/upcoming/aeromunculus.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Name:Aeromunculus
|
||||||
|
ManaCost:1 G U
|
||||||
|
Types:Creature Homunculus Mutant
|
||||||
|
PT:2/3
|
||||||
|
K:Flying
|
||||||
|
K:Adapt:1:2 G U
|
||||||
|
DeckHas:Ability$Counters
|
||||||
|
Oracle:Flying\n{2}{G}{U}: Adapt 1. (If this creature has no +1/+1 counters on it, put a +1/+1 counter on it.)
|
||||||
8
forge-gui/res/cardsfolder/upcoming/frenzied_arynx.txt
Normal file
8
forge-gui/res/cardsfolder/upcoming/frenzied_arynx.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Name:Frenzied Arynx
|
||||||
|
ManaCost:2 R G
|
||||||
|
Types:Creature Cat Beast
|
||||||
|
PT:3/3
|
||||||
|
K:Riot
|
||||||
|
K:Trample
|
||||||
|
A:AB$ Pump | Cost$ 4 R G | NumAtt$ +3 | SpellDescription$ CARDNAME gets +3/+0 until end of turn.
|
||||||
|
Oracle:Riot (This creature enters the battlefield with your choice of a +1/+1 counter or haste.)\nTrample\n{4}{R}{G}: Frenzied Arynx gets +3/+0 until end of turn.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Name:Gruul Spellbreaker
|
||||||
|
ManaCost:1 R G
|
||||||
|
Types:Creature Ogre Warrior
|
||||||
|
PT:3/3
|
||||||
|
K:Riot
|
||||||
|
K:Trample
|
||||||
|
S:Mode$ Continuous | Affected$ You,Self | AddKeyword$ Hexproof | Condition$ PlayerTurn | Description$ As long as it's your turn, you and CARDNAME have hexproof.
|
||||||
|
Oracle:Riot (This creature enters the battlefield with your choice of a +1/+1 counter or haste.)\nTrample\nAs long as it's your turn, you and Gruul Spellbreaker have hexproof.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Name:Imperious Oligarch
|
||||||
|
ManaCost:W B
|
||||||
|
Types:Creature Human Cleric
|
||||||
|
PT:2/1
|
||||||
|
K:Vigilance
|
||||||
|
K:Afterlife:1
|
||||||
|
Oracle:Vigilance\nAfterlife 1 (When this creature dies, create a 1/1 white and black Spirit creature token with flying.)
|
||||||
8
forge-gui/res/cardsfolder/upcoming/rafter_demon.txt
Normal file
8
forge-gui/res/cardsfolder/upcoming/rafter_demon.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Name:Rafter Demon
|
||||||
|
ManaCost:2 B R
|
||||||
|
Types:Creature Demon
|
||||||
|
PT:4/2
|
||||||
|
K:Spectacle:3 B R
|
||||||
|
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self+spectacle | Execute$ TrigDiscard | TriggerDescription$ When CARDNAME enters the battlefield, if its spectacle cost was paid, each opponent discards a card.
|
||||||
|
SVar:TrigDiscard:DB$ Discard | Defined$ Player.Opponent | NumCards$ 1 | Mode$ TgtChoose
|
||||||
|
Oracle:Spectacle {3}{B}{R} (You may cast this spell for its spectacle cost rather than its mana cost if an opponent lost life this turn.)\nWhen Rafter Demon enters the battlefield, if its spectacle cost was paid, each opponent discards a card.
|
||||||
12
forge-gui/res/cardsfolder/upcoming/simic_ascendancy.txt
Normal file
12
forge-gui/res/cardsfolder/upcoming/simic_ascendancy.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Name:Simic Ascendancy
|
||||||
|
ManaCost:G U
|
||||||
|
Types:Enchantment
|
||||||
|
A:AB$ PutCounter | Cost$ 1 G U | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SpellDescription$ Put a +1/+1 counter on target creature you control.
|
||||||
|
T:Mode$ CounterAddedOnce | ValidCard$ Creature.YouCtrl | TriggerZones$ Battlefield | CounterType$ P1P1 | Execute$ TrigPutCounter | TriggerDescription$ Whenever one or more +1/+1 counters are put on a creature you control, put that many growth counters on CARDNAME.
|
||||||
|
SVar:TrigPutCounter:DB$ PutCounter | CounterType$ GROWTH | CounterNum$ X | References$ X
|
||||||
|
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | IsPresent$ Card.Self+counters_GE20_GROWTH | Execute$ TrigWinGame | TriggerDescription$ At the beginning of your upkeep, if CARDNAME has twenty or more growth counters on it, you win the game.
|
||||||
|
SVar:TrigWinGame:DB$WinsGame | Defined$ You
|
||||||
|
SVar:X:TriggerCount$Amount
|
||||||
|
DeckHints:Ability$Counters
|
||||||
|
DeckHas:Ability$Counters
|
||||||
|
Oracle:{1}{G}{U}: Put a +1/+1 counter on target creature you control.\nWhenever one or more +1/+1 counters are put on a creature you control, put that many growth counters on Simic Ascendancy.\nAt the beginning of your upkeep, if Simic Ascendancy has twenty or more growth counters on it, you win the game.
|
||||||
6
forge-gui/res/cardsfolder/upcoming/sphinxs_insight.txt
Normal file
6
forge-gui/res/cardsfolder/upcoming/sphinxs_insight.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Name:Sphinx's Insight
|
||||||
|
ManaCost:2 W U
|
||||||
|
Types:Instant
|
||||||
|
A:SP$ Draw | Cost$ 2 W U | NumCards$ 2 | SubAbility$ DBLife | StackDescription$ SpellDescription | SpellDescription$ Draw two cards.
|
||||||
|
SVar:DBLife:DB$ GainLife | LifeAmount$ 2 | ConditionPlayerTurn$ True | ConditionPhases$ Main1,Main2 | SpellDescription$ Addendum - If you cast this spell during your main phase, you gain 2 life.
|
||||||
|
Oracle:Draw two cards.\nAddendum - If you cast this spell during your main phase, you gain 2 life.
|
||||||
7
forge-gui/res/cardsfolder/upcoming/tithe_taker.txt
Normal file
7
forge-gui/res/cardsfolder/upcoming/tithe_taker.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Name:Tithe Taker
|
||||||
|
ManaCost:1 W
|
||||||
|
Types:Creature Human Soldier
|
||||||
|
PT:2/1
|
||||||
|
K:Afterlife:1
|
||||||
|
S:Mode$ RaiseCost | ValidCard$ Card | Activator$ Opponent | ValidSpell$ Spell,Activated.nonManaAbility | Amount$ 1 | Condition$ PlayerTurn | Description$ During your turn, spells your opponents cast cost {1} more to cast and abilities your opponents activate cost {1} more to activate unless they're mana abilities.
|
||||||
|
Oracle:During your turn, spells your opponents cast cost {1} more to cast and abilities your opponents activate cost {1} more to activate unless they're mana abilities.\nAfterlife 1 (When this creature dies, create a 1/1 white and black Spirit creature token with flying.)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Name:Zegana, Utopian Speaker
|
||||||
|
ManaCost:2 G U
|
||||||
|
Types:Legendary Creature Merfolk Wizard
|
||||||
|
PT:4/4
|
||||||
|
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | IsPresent$ Creature.Other+YouCtrl+counters_GE1_P1P1 | Execute$ TrigDraw | TriggerDescription$ When CARDNAME enters the battlefield, if you control another creature with a +1/+1 counter on it, draw a card.
|
||||||
|
SVar:TrigDraw:DB$ Draw | NumCards$ 1
|
||||||
|
K:Adapt:4:4 G U
|
||||||
|
S:Mode$ Continuous | Affected$ Creature.YouCtrl+counters_GE1_P1P1 | AddKeyword$ Trample | Description$ Each creature you control with a +1/+1 counter on it has trample.
|
||||||
|
DeckHas:Ability$Counters
|
||||||
|
DeckHints:Ability$Counters
|
||||||
|
Oracle:When Zegana, Utopian Speaker enters the battlefield, if you control another creature with a +1/+1 counter on it, draw a card.\n{4}{G}{U}: Adapt 4. (If this creature has no +1/+1 counters on it, put four +1/+1 counters on it.)\nEach creature you control with a +1/+1 counter on it has trample.
|
||||||
7
forge-gui/res/tokenscripts/wb_1_1_spirit_flying.txt
Normal file
7
forge-gui/res/tokenscripts/wb_1_1_spirit_flying.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Name:Spirit
|
||||||
|
ManaCost:no cost
|
||||||
|
Types:Creature Spirit
|
||||||
|
Colors:white,black
|
||||||
|
PT:1/1
|
||||||
|
K:Flying
|
||||||
|
Oracle:Flying
|
||||||
Reference in New Issue
Block a user