Merge branch 'keyword_rna' into 'master'

Keyword RNA

See merge request core-developers/forge!1243
This commit is contained in:
Michael Kamensky
2018-12-24 04:05:58 +00:00
32 changed files with 386 additions and 43 deletions

View File

@@ -948,6 +948,20 @@ public class AiController {
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);
b1 += getSpellAbilityPriority(b);

View File

@@ -20,6 +20,7 @@ package forge.ai;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.*;
import forge.ai.ability.ChooseGenericEffectAi;
import forge.ai.ability.ProtectAi;
import forge.ai.ability.TokenAi;
import forge.card.CardType;
@@ -986,6 +987,11 @@ public class ComputerUtil {
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 (ai.getManaPool().willManaBeLostAtEndOfPhase()) {
boolean canUseToPayCost = false;

View File

@@ -97,9 +97,27 @@ public class ComputerUtilAbility {
final List<SpellAbility> newAbilities = Lists.newArrayList();
for (SpellAbility sa : originList) {
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.addAll(GameActionUtil.getAlternativeCosts(sa, player));
newAbilities.addAll(otherAltSa);
}
final List<SpellAbility> result = Lists.newArrayList();

View File

@@ -1052,11 +1052,15 @@ public class ComputerUtilCombat {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
}
if (ability.hasParam("Monstrosity") && blocker.isMonstrous()) {
continue;
continue;
}
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (pBonus > 0) {
@@ -1224,11 +1228,15 @@ public class ComputerUtilCombat {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
}
if (ability.hasParam("Monstrosity") && blocker.isMonstrous()) {
continue;
continue;
}
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (tBonus > 0) {
@@ -1442,11 +1450,15 @@ public class ComputerUtilCombat {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
}
if (ability.hasParam("Monstrosity") && attacker.isMonstrous()) {
continue;
continue;
}
if (ability.hasParam("Adapt") && blocker != null && blocker.getCounters(CounterType.P1P1) > 0) {
continue;
}
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (pBonus > 0) {
@@ -1675,11 +1687,15 @@ public class ComputerUtilCombat {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
}
if (ability.hasParam("Monstrosity") && attacker.isMonstrous()) {
continue;
continue;
}
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
continue;
}
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (tBonus > 0) {

View File

@@ -11,6 +11,7 @@ import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
@@ -74,7 +75,7 @@ public class ComputerUtilCost {
final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) {
if (type.name().equals("P1P1")) {
if (CounterType.P1P1.equals(type)) {
return false;
}
continue;
@@ -105,7 +106,8 @@ public class ComputerUtilCost {
}
//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;
}
}

View File

@@ -6,6 +6,8 @@ import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
@@ -24,6 +26,8 @@ import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
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) {
if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) {
return true;
} else if (aiLogic.startsWith("Fabricate")) {
} else if (aiLogic.startsWith("Fabricate") || "Riot".equals(aiLogic)) {
return true;
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
@@ -344,7 +348,56 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
if (!filtered.isEmpty()) {
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
}
}
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;
}
}

View File

@@ -192,7 +192,7 @@ public class CountersMoveAi extends SpellAbilityAi {
}
// 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) {
return false;
}
@@ -333,11 +333,12 @@ public class CountersMoveAi extends SpellAbilityAi {
// try to remove P1P1 from undying or evolve
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;
}
}
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
return true;
}
@@ -392,10 +393,10 @@ public class CountersMoveAi extends SpellAbilityAi {
}
if (cType != null) {
if (CounterType.P1P1.equals(cType) && card.hasKeyword("Undying")) {
if (CounterType.P1P1.equals(cType) && card.hasKeyword(Keyword.UNDYING)) {
return false;
}
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
return false;
}

View File

@@ -311,6 +311,10 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
if (sa.hasParam("Adapt") && source.getCounters(CounterType.P1P1) > 0) {
return false;
}
// TODO handle proper calculation of X values based on Cost
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);

View File

@@ -308,6 +308,30 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
}
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));
return true;
}

View File

@@ -96,7 +96,9 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
build.add(keyword);
}
build.add(edition.getCode());
if (edition != null) {
build.add(edition.getCode());
}
// Should future image file names be all lower case? Instead of Up case sets?
return StringUtils.join(build, "_").toLowerCase();
@@ -121,7 +123,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
@Override public String getName() { 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 boolean isFoil() { return false; }
@Override public CardRules getRules() { return card; }

View File

@@ -47,7 +47,7 @@ public class TokenDb implements ITokenDatabase {
tokensByName.put(fullName, pt);
return pt;
} catch(Exception e) {
return null;
throw e;
}
}

View File

@@ -152,7 +152,15 @@ public class ForgeScript {
public static boolean spellAbilityHasProperty(SpellAbility sa, String property, Player sourceController,
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()) {
return false;
}

View File

@@ -326,7 +326,7 @@ public class Game {
* Gets the players who participated in match (regardless of outcome).
* <i>Use this in UI and after match calculations</i>
*/
public final List<Player> getRegisteredPlayers() {
public final PlayerCollection getRegisteredPlayers() {
return allPlayers;
}

View File

@@ -207,6 +207,8 @@ public class TokenEffect extends SpellAbilityEffect {
if (result != null) {
tokenName = result.getName();
} else {
throw new RuntimeException("don't find Token for TokenScript: " + sa.getParam("TokenScript"));
}
return result;

View File

@@ -1613,7 +1613,7 @@ public class Card extends GameEntity implements Comparable<Card> {
|| keyword.equals("Suspend") // for the ones without amounnt
|| keyword.equals("Hideaway") || keyword.equals("Ascend")
|| keyword.equals("Totem armor") || keyword.equals("Battle cry")
|| keyword.equals("Devoid")){
|| keyword.equals("Devoid") || keyword.equals("Riot")){
sbLong.append(keyword + " (" + inst.getReminderText() + ")");
} else if (keyword.startsWith("Partner:")) {
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("Crew") || keyword.startsWith("Tribute") || keyword.startsWith("Absorb")
|| 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")) {
final String[] k = keyword.split(":");
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.getInstance("Offering:"+ offeringType).getReminderText() + ")");
} 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("Surge") || keyword.startsWith("Transmute") || keyword.startsWith("Suspend")
|| keyword.equals("Undaunted") || keyword.startsWith("Monstrosity") || keyword.startsWith("Embalm")
|| keyword.startsWith("Level up") || keyword.equals("Prowess") || keyword.startsWith("Eternalize")
|| 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 parsing takes care of adding a proper description
} else if (keyword.startsWith("CantBeBlockedByAmount")) {

View File

@@ -985,11 +985,7 @@ public class CardFactoryUtil {
return doXMath(cc.getLifeGainedByTeamThisTurn(), m, c);
}
if (sq[0].contains("LifeOppsLostThisTurn")) {
int lost = 0;
for (Player opp : cc.getOpponents()) {
lost += opp.getLifeLostThisTurn();
}
return doXMath(lost, m, c);
return doXMath(cc.getOpponentLostLifeThisTurn(), m, c);
}
if (sq[0].equals("TotalDamageDoneByThisTurn")) {
return doXMath(c.getTotalDamageDoneBy(), m, c);
@@ -2047,6 +2043,12 @@ public class CardFactoryUtil {
final String effect, final boolean optional, final boolean secondary,
final boolean intrinsic, final String valid, final String zone) {
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();
setupETBReplacementAbility(repAb);
if (!intrinsic) {
@@ -2137,6 +2139,20 @@ public class CardFactoryUtil {
afflictTrigger.setOverridingAbility(AbilityFactory.getAbility(abStringAfflict, card));
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")) {
final String[] k = keyword.split(":");
final String n = k[1];
@@ -2462,8 +2478,13 @@ public class CardFactoryUtil {
inst.addTrigger(trigger);
} else if (keyword.startsWith("Graft")) {
final String abStr = "DB$ MoveCounter | Source$ Self | "
+ "Defined$ TriggeredCardLKICopy | CounterType$ P1P1 | CounterNum$ 1";
final StringBuilder sb = new StringBuilder();
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"
+ "| Origin$ Any | Destination$ Battlefield "
@@ -2474,7 +2495,7 @@ public class CardFactoryUtil {
+ "may move a +1/+1 counter from this creature onto it.";
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
trigger.setOverridingAbility(AbilityFactory.getAbility(abStr, card));
trigger.setOverridingAbility(AbilityFactory.getAbility(sb.toString(), card));
inst.addTrigger(trigger);
} else if (keyword.startsWith("Haunt")) {
@@ -3471,6 +3492,23 @@ public class CardFactoryUtil {
re.setOverridingAbility(saExile);
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")) {
String sb = "etbCounter:LORE:1:no Condition:no desc";
final ReplacementEffect re = makeEtbCounter(sb, card, intrinsic);
@@ -3512,7 +3550,7 @@ public class CardFactoryUtil {
inst.addReplacement(cardre);
} 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", "");
@@ -3662,6 +3700,24 @@ public class CardFactoryUtil {
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)) {
// Aftermath does modify existing SA, and does not add new one
@@ -4138,6 +4194,23 @@ public class CardFactoryUtil {
sa.setTemporary(!intrinsic);
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) {
final GameCommand sunburstCIP = new GameCommand() {
private static final long serialVersionUID = 1489845860231758299L;

View File

@@ -1632,6 +1632,11 @@ public class CardProperty {
return false;
}
return card.getCastSA().isProwl();
} else if (property.startsWith("spectacle")) {
if (card.getCastSA() == null) {
return false;
}
return card.getCastSA().isSpectacle();
} else if (property.equals("HasDevoured")) {
if (card.getDevouredCards().isEmpty()) {
return false;

View File

@@ -98,6 +98,8 @@ public class Cost implements Serializable {
return true;
}
@SuppressWarnings("unchecked")
public <T extends CostPart> T getCostPartByType(Class<T> costType) {
for (CostPart p : getCostParts()) {
if (costType.isInstance(p)) {

View File

@@ -13,8 +13,10 @@ import forge.util.TextUtil;
public enum Keyword {
UNDEFINED(SimpleKeyword.class, false, ""),
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."),
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."),
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}."),
@@ -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."),
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."),
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."),
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."),
@@ -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."),
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."),
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."),
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."),

View File

@@ -260,6 +260,10 @@ public class Player extends GameEntity implements Comparable<Player> {
return game.getPlayers().filter(PlayerPredicates.isOpponentOf(this));
}
public final PlayerCollection getRegisteredOpponents() {
return game.getRegisteredPlayers().filter(PlayerPredicates.isOpponentOf(this));
}
public void updateOpponentsForView() {
view.updateOpponents(this);
}
@@ -2037,8 +2041,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
public final int getBloodthirstAmount() {
return Aggregates.sum(Iterables.filter(
game.getRegisteredPlayers(), PlayerPredicates.isOpponentOf(this)), Accessors.FN_GET_ASSIGNED_DAMAGE);
return Aggregates.sum(getRegisteredOpponents(), Accessors.FN_GET_ASSIGNED_DAMAGE);
}
public final boolean hasSurge() {
@@ -2047,6 +2050,14 @@ public class Player extends GameEntity implements Comparable<Player> {
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) {
if (prowl.contains("AllCreatureTypes")) {
return true;

View File

@@ -96,6 +96,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private int sourceTrigger = -1;
private List<Object> triggerRemembered = Lists.newArrayList();
// TODO use enum for the flags
private boolean flashBackAbility = false;
private boolean aftermath = false;
private boolean cycling = false;
@@ -103,6 +104,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private boolean evoke = false;
private boolean prowl = false;
private boolean surge = false;
private boolean spectacle = false;
private boolean offering = false;
private boolean emerge = false;
private boolean morphup = false;
@@ -1121,6 +1123,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
surge = isSurge;
}
public final boolean isSpectacle() {
return spectacle;
}
public final void setSpectacle(final boolean isSpectacle) {
spectacle = isSpectacle;
}
public CardCollection getTappedForConvoke() {
return tappedForConvoke;
}

View File

@@ -419,6 +419,11 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
return false;
}
}
if (sa.isSpectacle()) {
if (activator.getOpponentLostLifeThisTurn() <= 0) {
return false;
}
}
if (isDesert()) {
if (!activator.hasDesert()) {
return false;

View 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.)

View 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.

View File

@@ -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.

View File

@@ -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.)

View 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.

View 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.

View 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.

View 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.)

View File

@@ -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.

View File

@@ -0,0 +1,7 @@
Name:Spirit
ManaCost:no cost
Types:Creature Spirit
Colors:white,black
PT:1/1
K:Flying
Oracle:Flying