- DelayedTriggerAi: port over AILogic NarsetRebound and SpellCopy (the latter doesn't quite work yet and the spell somehow magically fizzles with no trace).

This commit is contained in:
Hans Mackowiak
2020-11-04 08:56:41 +00:00
committed by Michael Kamensky
parent d5639f5395
commit 9b040b063b
109 changed files with 642 additions and 540 deletions

View File

@@ -103,7 +103,7 @@ public class GameAction {
boolean wasFacedown = c.isFaceDown();
//Rule 110.5g: A token that has left the battlefield can't move to another zone
if (c.isToken() && zoneFrom != null && !fromBattlefield) {
if (c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Stack)) {
return c;
}

View File

@@ -1660,12 +1660,8 @@ public class AbilityUtils {
// Count$Kicked.<numHB>.<numNotHB>
if (sq[0].startsWith("Kicked")) {
if (((SpellAbility)ctb).isKicked()) {
return CardFactoryUtil.doXMath(Integer.parseInt(sq[1]), expr, c); // Kicked
}
else {
return CardFactoryUtil.doXMath(Integer.parseInt(sq[2]), expr, c); // not Kicked
}
boolean kicked = ((SpellAbility)ctb).isKicked() || c.getKickerMagnitude() > 0;
return CardFactoryUtil.doXMath(Integer.parseInt(kicked ? sq[1] : sq[2]), expr, c);
}
//Count$SearchedLibrary.<DefinedPlayer>

View File

@@ -24,14 +24,23 @@ import java.util.List;
public class AttachEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
if (sa.getHostCard().isAura() && sa.isSpell()) {
final Card host = sa.getHostCard();
if (host.isAura() && sa.isSpell()) {
final Player ap = sa.getActivatingPlayer();
// The Spell_Permanent (Auras) version of this AF needs to
// move the card into play before Attaching
sa.getHostCard().setController(ap, 0);
final Card c = ap.getGame().getAction().moveTo(ap.getZone(ZoneType.Battlefield), sa.getHostCard(), sa);
host.setController(ap, 0);
// 111.11. A copy of a permanent spell becomes a token as it resolves.
// The token has the characteristics of the spell that became that token.
// The token is not “created” for the purposes of any replacement effects or triggered abilities that refer to creating a token.
if (host.isCopiedSpell()) {
host.setCopiedSpell(false);
host.setToken(true);
}
final Card c = ap.getGame().getAction().moveToPlay(host, ap, sa);
sa.setHostCard(c);
}

View File

@@ -4,7 +4,9 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -12,15 +14,16 @@ import forge.game.card.CardCollection;
import forge.game.card.CardFactory;
import forge.game.card.CardLists;
import forge.game.player.Player;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.util.Lang;
import forge.util.Localizer;
import forge.util.CardTranslation;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class CopySpellAbilityEffect extends SpellAbilityEffect {
@@ -56,7 +59,8 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
Player controller = sa.getActivatingPlayer();
final Game game = card.getGame();
List<Player> controllers = Lists.newArrayList(sa.getActivatingPlayer());
int amount = 1;
if (sa.hasParam("Amount")) {
@@ -64,13 +68,9 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
}
if (sa.hasParam("Controller")) {
controller = AbilityUtils.getDefinedPlayers(card, sa.getParam("Controller"), sa).get(0);
controllers = AbilityUtils.getDefinedPlayers(card, sa.getParam("Controller"), sa);
}
boolean isOptional = sa.hasParam("Optional");
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantCopyTheSpell", CardTranslation.getTranslatedName(card.getName())))) {
return;
}
final List<SpellAbility> tgtSpells = getTargetSpells(sa);
@@ -79,123 +79,143 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
return;
}
boolean mayChooseNewTargets = true;
List<SpellAbility> copies = new ArrayList<>();
if (sa.hasParam("CopyMultipleSpells")) {
final int spellCount = Integer.parseInt(sa.getParam("CopyMultipleSpells"));
boolean isOptional = sa.hasParam("Optional");
for (int multi = 0; multi < spellCount && !tgtSpells.isEmpty(); multi++) {
String prompt = Localizer.getInstance().getMessage("lblSelectMultiSpellCopyToStack", Lang.getOrdinal(multi + 1));
SpellAbility chosen = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa, prompt,
ImmutableMap.of());
SpellAbility copiedSpell = CardFactory.copySpellAbilityAndPossiblyHost(card, chosen.getHostCard(), chosen, true);
copiedSpell.getHostCard().setController(card.getController(), card.getGame().getNextTimestamp());
copiedSpell.setActivatingPlayer(controller);
copies.add(copiedSpell);
tgtSpells.remove(chosen);
for (Player controller : controllers) {
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantCopyTheSpell", CardTranslation.getTranslatedName(card.getName())))) {
continue;
}
}
else if (sa.hasParam("CopyForEachCanTarget")) {
List<SpellAbility> copies = Lists.newArrayList();
SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa,
Localizer.getInstance().getMessage("lblSelectASpellCopy"), ImmutableMap.of());
chosenSA.setActivatingPlayer(controller);
// Find subability or rootability that has targets
SpellAbility targetedSA = chosenSA;
while (targetedSA != null) {
if (targetedSA.usesTargeting() && targetedSA.getTargets().getNumTargeted() != 0) {
break;
if (sa.hasParam("CopyForEachCanTarget")) {
// Find subability or rootability that has targets
SpellAbility targetedSA = chosenSA;
while (targetedSA != null) {
if (targetedSA.usesTargeting() && targetedSA.getTargets().getNumTargeted() != 0) {
break;
}
targetedSA = targetedSA.getSubAbility();
}
targetedSA = targetedSA.getSubAbility();
}
if (targetedSA == null) {
return;
}
final List<GameEntity> candidates = targetedSA.getTargetRestrictions().getAllCandidates(targetedSA, true);
if (sa.hasParam("CanTargetPlayer")) {
// Radiate
// Remove targeted players because getAllCandidates include all the valid players
for(Player p : targetedSA.getTargets().getTargetPlayers())
candidates.remove(p);
mayChooseNewTargets = false;
for (GameEntity o : candidates) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(card, chosenSA.getHostCard(), chosenSA, true);
resetFirstTargetOnCopy(copy, o, targetedSA);
copies.add(copy);
if (targetedSA == null) {
continue;
}
} else {// Precursor Golem, Ink-Treader Nephilim
final String type = sa.getParam("CopyForEachCanTarget");
CardCollection valid = new CardCollection();
List<Player> players = Lists.newArrayList();
Player originalTargetPlayer = Iterables.getFirst(getTargetPlayers(chosenSA), null);
for (final GameEntity o : candidates) {
if (o instanceof Card) {
valid.add((Card) o);
} else if (o instanceof Player) {
final Player p = (Player) o;
if (p.equals(originalTargetPlayer))
continue;
if (p.isValid(type.split(","), chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa)) {
players.add(p);
final List<GameEntity> candidates = targetedSA.getTargetRestrictions().getAllCandidates(targetedSA, true);
if (sa.hasParam("CanTargetPlayer")) {
// Radiate
// Remove targeted players because getAllCandidates include all the valid players
for(Player p : targetedSA.getTargets().getTargetPlayers())
candidates.remove(p);
for (GameEntity o : candidates) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA);
resetFirstTargetOnCopy(copy, o, targetedSA);
copies.add(copy);
}
} else {// Precursor Golem, Ink-Treader Nephilim
final String type = sa.getParam("CopyForEachCanTarget");
CardCollection valid = new CardCollection();
List<Player> players = Lists.newArrayList();
Player originalTargetPlayer = Iterables.getFirst(getTargetPlayers(chosenSA), null);
for (final GameEntity o : candidates) {
if (o instanceof Card) {
valid.add((Card) o);
} else if (o instanceof Player) {
final Player p = (Player) o;
if (p.equals(originalTargetPlayer))
continue;
if (p.isValid(type.split(","), chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa)) {
players.add(p);
}
}
}
}
valid = CardLists.getValidCards(valid, type.split(","), chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa);
Card originalTarget = Iterables.getFirst(getTargetCards(chosenSA), null);
valid.remove(originalTarget);
mayChooseNewTargets = false;
if (sa.hasParam("ChooseOnlyOne")) {
Card choice = controller.getController().chooseSingleEntityForEffect(valid, sa, Localizer.getInstance().getMessage("lblChooseOne"), null);
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(card, chosenSA.getHostCard(), chosenSA, true);
resetFirstTargetOnCopy(copy, choice, targetedSA);
copies.add(copy);
} else {
for (final Card c : valid) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(card, chosenSA.getHostCard(), chosenSA, true);
valid = CardLists.getValidCards(valid, type.split(","), chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa);
Card originalTarget = Iterables.getFirst(getTargetCards(chosenSA), null);
valid.remove(originalTarget);
if (sa.hasParam("ChooseOnlyOne")) {
Card choice = controller.getController().chooseSingleEntityForEffect(valid, sa, Localizer.getInstance().getMessage("lblChooseOne"), null);
if (choice != null) {
valid = new CardCollection(choice);
}
}
for (final Card c : valid) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA);
resetFirstTargetOnCopy(copy, c, targetedSA);
copies.add(copy);
}
}
for (final Player p : players) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA);
resetFirstTargetOnCopy(copy, p, targetedSA);
copies.add(copy);
}
}
for (final Player p : players) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(card, chosenSA.getHostCard(), chosenSA, true);
resetFirstTargetOnCopy(copy, p, targetedSA);
}
else {
for (int i = 0; i < amount; i++) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA);
if (sa.hasParam("MayChooseTarget")) {
copy.setMayChooseNewTargets(true);
if (copy.usesTargeting()) {
copy.getTargetRestrictions().setMandatory(true);
}
}
// extra case for Epic to remove the keyword and the last part of the SpellAbility
if (sa.hasParam("Epic")) {
copy.getHostCard().removeIntrinsicKeyword("Epic");
SpellAbility sub = copy;
while (sub.getSubAbility() != null && !sub.hasParam("Epic")) {
sub = sub.getSubAbility();
}
if (sub != null) {
sub.getParent().setSubAbility(sub.getSubAbility());
}
}
copies.add(copy);
}
}
}
else {
SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa,
Localizer.getInstance().getMessage("lblSelectASpellCopy"), ImmutableMap.of());
chosenSA.setActivatingPlayer(controller);
for (int i = 0; i < amount; i++) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(
card, chosenSA.getHostCard(), chosenSA, true);
// extra case for Epic to remove the keyword and the last part of the SpellAbility
if (sa.hasParam("Epic")) {
copy.getHostCard().removeIntrinsicKeyword("Epic");
SpellAbility sub = copy;
while (sub.getSubAbility() != null && !sub.hasParam("Epic")) {
sub = sub.getSubAbility();
}
if (sub != null) {
sub.getParent().setSubAbility(sub.getSubAbility());
}
if (copies.isEmpty()) {
continue;
}
int addAmount = copies.size();
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(controller);
repParams.put(AbilityKey.SpellAbility, chosenSA);
repParams.put(AbilityKey.Amount, addAmount);
switch (game.getReplacementHandler().run(ReplacementType.CopySpell, repParams)) {
case NotReplaced:
break;
case Updated: {
addAmount = (int) repParams.get(AbilityKey.Amount);
break;
}
default:
addAmount = 0;
}
if (addAmount <= 0) {
continue;
}
int extraAmount = addAmount - copies.size();
for (int i = 0; i < extraAmount; i++) {
SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA);
// extra copies added with CopySpellReplacenment currently always has new choose targets
copy.setMayChooseNewTargets(true);
if (copy.usesTargeting()) {
copy.getTargetRestrictions().setMandatory(true);
}
copies.add(copy);
}
}
for(SpellAbility copySA : copies) {
if (mayChooseNewTargets && copySA.usesTargeting()) {
// TODO: ideally this should be implemented by way of allowing the player to cancel targeting
// but in that case preserving whatever target was specified for the original spell (since
// "changing targets" is the optional part).
copySA.getTargetRestrictions().setMandatory(true);
}
controller.getController().playSpellAbilityForFree(copySA, mayChooseNewTargets);
controller.getController().orderAndPlaySimultaneousSa(copies);
}
} // end resolve

View File

@@ -5,7 +5,10 @@ import com.google.common.collect.Maps;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardUtil;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
@@ -21,6 +24,8 @@ public class DelayedTriggerEffect extends SpellAbilityEffect {
protected String getStackDescription(SpellAbility sa) {
if (sa.hasParam("TriggerDescription")) {
return sa.getParam("TriggerDescription");
} else if (sa.hasParam("SpellDescription")) {
return sa.getParam("SpellDescription");
}
return "";
@@ -44,7 +49,8 @@ public class DelayedTriggerEffect extends SpellAbilityEffect {
triggerRemembered = sa.getParam("RememberObjects");
}
final Trigger delTrig = TriggerHandler.parseTrigger(mapParams, sa.getHostCard(), true);
Card lki = CardUtil.getLKICopy(sa.getHostCard());
final Trigger delTrig = TriggerHandler.parseTrigger(mapParams, lki, true);
if (triggerRemembered != null) {
for (final String rem : triggerRemembered.split(",")) {
@@ -67,7 +73,9 @@ public class DelayedTriggerEffect extends SpellAbilityEffect {
}
if (mapParams.containsKey("Execute") || sa.hasAdditionalAbility("Execute")) {
SpellAbility overridingSA = sa.getAdditionalAbility("Execute");
AbilitySub overridingSA = (AbilitySub)sa.getAdditionalAbility("Execute").copy(lki, false);
// need to reset the parent, additionalAbility does set it to this
overridingSA.setParent(null);
overridingSA.setActivatingPlayer(sa.getActivatingPlayer());
overridingSA.setDeltrigActivatingPlayer(sa.getActivatingPlayer()); // ensure that the original activator can be restored later
// Set Transform timestamp when the delayed trigger is created

View File

@@ -22,6 +22,14 @@ public class PermanentEffect extends SpellAbilityEffect {
sa.getHostCard().setController(p, 0);
final Card host = sa.getHostCard();
// 111.11. A copy of a permanent spell becomes a token as it resolves.
// The token has the characteristics of the spell that became that token.
// The token is not “created” for the purposes of any replacement effects or triggered abilities that refer to creating a token.
if (host.isCopiedSpell()) {
host.setCopiedSpell(false);
host.setToken(true);
}
final Card c = p.getGame().getAction().moveToPlay(host, p, sa);
sa.setHostCard(c);

View File

@@ -3612,8 +3612,8 @@ public class Card extends GameEntity implements Comparable<Card> {
if (multiKickerMagnitude > 0) {
return multiKickerMagnitude;
}
boolean hasK1 = costsPaid.contains(OptionalCost.Kicker1);
return hasK1 == costsPaid.contains(OptionalCost.Kicker2) ? (hasK1 ? 2 : 0) : 1;
boolean hasK1 = isOptionalCostPaid(OptionalCost.Kicker1);
return hasK1 == isOptionalCostPaid(OptionalCost.Kicker2) ? (hasK1 ? 2 : 0) : 1;
}
private int pseudoKickerMagnitude = 0;

View File

@@ -125,12 +125,13 @@ public class CardFactory {
* which wouldn't ordinarily get set during a simple Card.copy() call.
* </p>
* */
private final static Card copySpellHost(final Card source, final Card original, final SpellAbility sa, final boolean bCopyDetails){
Player controller = sa.getActivatingPlayer();
private final static Card copySpellHost(final SpellAbility sourceSA, final SpellAbility targetSA){
final Card source = sourceSA.getHostCard();
final Card original = targetSA.getHostCard();
Player controller = sourceSA.getActivatingPlayer();
final Card c = copyCard(original, true);
// change the color of the copy (eg: Fork)
final SpellAbility sourceSA = source.getFirstSpellAbility();
if (null != sourceSA && sourceSA.hasParam("CopyIsColor")) {
String tmp = "";
final String newColor = sourceSA.getParam("CopyIsColor");
@@ -148,13 +149,14 @@ public class CardFactory {
c.setOwner(controller);
c.setCopiedSpell(true);
if (bCopyDetails) {
c.setXManaCostPaidByColor(original.getXManaCostPaidByColor());
c.setKickerMagnitude(original.getKickerMagnitude());
c.setXManaCostPaidByColor(original.getXManaCostPaidByColor());
c.setKickerMagnitude(original.getKickerMagnitude());
for (OptionalCost cost : original.getOptionalCostsPaid()) {
c.addOptionalCostPaid(cost);
}
for (OptionalCost cost : original.getOptionalCostsPaid()) {
c.addOptionalCostPaid(cost);
}
if (targetSA.isBestow()) {
c.animateBestow();
}
return c;
}
@@ -174,44 +176,33 @@ public class CardFactory {
* @param bCopyDetails
* a boolean.
*/
public final static SpellAbility copySpellAbilityAndPossiblyHost(final Card source, final Card original, final SpellAbility sa, final boolean bCopyDetails) {
Player controller = sa.getActivatingPlayer();
public final static SpellAbility copySpellAbilityAndPossiblyHost(final SpellAbility sourceSA, final SpellAbility targetSA) {
Player controller = sourceSA.getActivatingPlayer();
//it is only necessary to copy the host card if the SpellAbility is a spell, not an ability
final Card c;
if (sa.isSpell()){
c = copySpellHost(source, original, sa, bCopyDetails);
}
else {
c = original;
}
final Card c = targetSA.isSpell() ? copySpellHost(sourceSA, targetSA) : targetSA.getHostCard();
final SpellAbility copySA;
if (sa.isTrigger() && sa.isWrapper()) {
copySA = getCopiedTriggeredAbility((WrappedAbility)sa, c);
if (targetSA.isTrigger() && targetSA.isWrapper()) {
copySA = getCopiedTriggeredAbility((WrappedAbility)targetSA, c);
} else {
copySA = sa.copy(c, false);
}
if (sa.isSpell()){
//only update c's abilities if c is a copy.
//(it would be nice to move this into `copySpellHost`,
// so all the c-mutating code is together in one place.
// but copySA doesn't exist until after `copySpellHost` finishes executing,
// so it's hard to resolve that dependency.)
c.getCurrentState().setNonManaAbilities(copySA);
copySA = targetSA.copy(c, false);
}
copySA.setCopied(true);
if (targetSA.usesTargeting()) {
// do for SubAbilities too?
copySA.setTargets(targetSA.getTargets().clone());
}
//remove all costs
if (!copySA.isTrigger()) {
copySA.setPayCosts(new Cost("", sa.isAbility()));
copySA.setPayCosts(new Cost("", targetSA.isAbility()));
}
copySA.setActivatingPlayer(controller);
if (bCopyDetails) {
copySA.setPaidHash(sa.getPaidHash());
}
copySA.setPaidHash(targetSA.getPaidHash());
return copySA;
}

View File

@@ -2434,7 +2434,7 @@ public class CardFactoryUtil {
inst.addTrigger(parsedTrigReturn);
} else if (keyword.equals("Conspire")) {
final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ Conspire | Secondary$ True | TriggerDescription$ Copy CARDNAME if its conspire cost was paid";
final String abString = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ 1";
final String abString = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ 1 | MayChooseTarget$ True";
final Trigger conspireTrigger = TriggerHandler.parseTrigger(trigScript, card, intrinsic);
conspireTrigger.setOverridingAbility(AbilityFactory.getAbility(abString, card));
@@ -2630,6 +2630,18 @@ public class CardFactoryUtil {
trigger.setOverridingAbility(AbilityFactory.getAbility(sb.toString(), card));
inst.addTrigger(trigger);
} else if (keyword.startsWith("Gravestorm")) {
String trigStr = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerDescription$ Gravestorm (" + inst.getReminderText() + ")";
String copyStr = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ GravestormCount | MayChooseTarget$ True";
SpellAbility copySa = AbilityFactory.getAbility(copyStr, card);
copySa.setSVar("GravestormCount", "Count$ThisTurnEntered_Graveyard_from_Battlefield_Permanent");
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
trigger.setOverridingAbility(copySa);
inst.addTrigger(trigger);
} else if (keyword.startsWith("Haunt")) {
final String[] k = keyword.split(":");
@@ -3069,7 +3081,7 @@ public class CardFactoryUtil {
inst.addTrigger(myTrigger);
} else if (keyword.startsWith("Replicate")) {
final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ ReplicateAmount | Secondary$ True | TriggerDescription$ Copy CARDNAME for each time you paid its replicate cost";
final String abString = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ ReplicateAmount";
final String abString = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ ReplicateAmount | MayChooseTarget$ True";
final Trigger replicateTrigger = TriggerHandler.parseTrigger(trigScript, card, intrinsic);
final SpellAbility replicateAbility = AbilityFactory.getAbility(abString, card);
@@ -3197,7 +3209,7 @@ public class CardFactoryUtil {
final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | Secondary$ True"
+ "| TriggerDescription$ Storm (" + inst.getReminderText() + ")";
String effect = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ StormCount";
String effect = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ StormCount | MayChooseTarget$ True";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, card, intrinsic);

View File

@@ -74,7 +74,7 @@ public enum Keyword {
FORTIFY("Fortify", KeywordWithCost.class, false, "%s: Attach to target land you control. Fortify only as a sorcery."),
FRENZY("Frenzy", KeywordWithAmount.class, false, "Whenever this creature attacks and isn't blocked, it gets +%d/+0 until end of turn."),
GRAFT("Graft", KeywordWithAmount.class, false, "This permanent enters the battlefield with {%d:+1/+1 counter} on it. Whenever another creature enters the battlefield, you may move a +1/+1 counter from this permanent onto it."),
GRAVESTORM("Gravestorm", SimpleKeyword.class, false, "When you cast this spell, copy it for each permanent that was put into a graveyard from the battlefield this turn. You may choose new targets for the copies."),
GRAVESTORM("Gravestorm", SimpleKeyword.class, false, "When you cast this spell, copy it for each permanent that was put into a graveyard from the battlefield this turn. If the spell has any targets, you may choose new targets for any of the copies."),
HASTE("Haste", SimpleKeyword.class, true, "This creature can attack and {T} as soon as it comes under your control."),
HAUNT("Haunt", SimpleKeyword.class, false, "When this is put into a graveyard, exile it haunting target creature."),
HEXPROOF("Hexproof", Hexproof.class, false, "This can't be the target of %s spells or abilities your opponents control."),

View File

@@ -0,0 +1,43 @@
package forge.game.replacement;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
public class ReplaceCopySpell extends ReplacementEffect {
public ReplaceCopySpell(Map<String, String> map, Card host, boolean intrinsic) {
super(map, host, intrinsic);
}
/* (non-Javadoc)
* @see forge.card.replacement.ReplacementEffect#canReplace(java.util.Map)
*/
@Override
public boolean canReplace(Map<AbilityKey, Object> runParams) {
if (((int) runParams.get(AbilityKey.Amount)) <= 0) {
return false;
}
if (hasParam("ValidPlayer")) {
if (!matchesValid(runParams.get(AbilityKey.Affected), getParam("ValidPlayer").split(","), getHostCard())) {
return false;
}
}
if (hasParam("ValidSpell")) {
if (!matchesValid(runParams.get(AbilityKey.SpellAbility), getParam("ValidSpell").split(","), getHostCard())) {
return false;
}
}
return true;
}
/* (non-Javadoc)
* @see forge.card.replacement.ReplacementEffect#setReplacingObjects(java.util.HashMap, forge.card.spellability.SpellAbility)
*/
@Override
public void setReplacingObjects(Map<AbilityKey, Object> runParams, SpellAbility sa) {
sa.setReplacingObject(AbilityKey.Amount, runParams.get(AbilityKey.Amount));
}
}

View File

@@ -14,6 +14,7 @@ public enum ReplacementType {
AddCounter(ReplaceAddCounter.class),
Attached(ReplaceAttached.class),
Counter(ReplaceCounter.class),
CopySpell(ReplaceCopySpell.class),
CreateToken(ReplaceToken.class),
DamageDone(ReplaceDamage.class),
Destroy(ReplaceDestroy.class),

View File

@@ -43,6 +43,7 @@ import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
import forge.game.mana.Mana;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.replacement.ReplacementEffect;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
@@ -52,6 +53,7 @@ import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
@@ -148,6 +150,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private boolean undoable;
private boolean isCopied = false;
private boolean mayChooseNewTargets = false;
private EnumSet<OptionalCost> optionalCosts = EnumSet.noneOf(OptionalCost.class);
private TargetRestrictions targetRestrictions = null;
@@ -863,6 +866,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
copyHelper(clone, host);
// always set this to false, it is only set in CopyEffect
clone.mayChooseNewTargets = false;
clone.triggeringObjects = AbilityKey.newMap(this.triggeringObjects);
clone.setPayCosts(getPayCosts().copy());
@@ -884,6 +890,11 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
clone.setPaidHash(Maps.newHashMap(getPaidHash()));
if (usesTargeting()) {
// the targets need to be cloned, otherwise they might be cleared
clone.targetChosen = getTargets().clone();
}
// clear maps for copy, the values will be added later
clone.additionalAbilities = Maps.newHashMap();
clone.additionalAbilityLists = Maps.newHashMap();
@@ -1287,6 +1298,13 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
}
public boolean isMayChooseNewTargets() {
return mayChooseNewTargets;
}
public void setMayChooseNewTargets(boolean value) {
mayChooseNewTargets = value;
}
/**
* Returns whether variable was present in the announce list.
*/
@@ -1358,6 +1376,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
public void setTargets(TargetChoices targets) {
// TODO should copy the target choices?
targetChosen = targets;
}
@@ -1656,6 +1675,48 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return p != null && p.isTargeting(o);
}
public boolean setupTargets() {
// Skip to paying if parent ability doesn't target and has no subAbilities.
// (or trigger case where its already targeted)
SpellAbility currentAbility = this;
final Card source = getHostCard();
do {
final TargetRestrictions tgt = currentAbility.getTargetRestrictions();
if (tgt != null && tgt.doesTarget()) {
currentAbility.clearTargets();
Player targetingPlayer;
if (currentAbility.hasParam("TargetingPlayer")) {
final PlayerCollection candidates = AbilityUtils.getDefinedPlayers(source, currentAbility.getParam("TargetingPlayer"), currentAbility);
// activator chooses targeting player
targetingPlayer = getActivatingPlayer().getController().chooseSingleEntityForEffect(
candidates, currentAbility, "Choose the targeting player", null);
} else {
targetingPlayer = getActivatingPlayer();
}
currentAbility.setTargetingPlayer(targetingPlayer);
if (!targetingPlayer.getController().chooseTargetsFor(currentAbility)) {
return false;
}
}
final AbilitySub subAbility = currentAbility.getSubAbility();
if (subAbility != null) {
// This is necessary for "TargetsWithDefinedController$ ParentTarget"
subAbility.setParent(currentAbility);
}
currentAbility = subAbility;
} while (currentAbility != null);
return true;
}
public final void clearTargets() {
final TargetRestrictions tg = getTargetRestrictions();
if (tg != null) {
resetTargets();
if (hasParam("DividedAsYouChoose")) {
tg.calculateStillToDivide(getParam("DividedAsYouChoose"), getHostCard(), this);
}
}
}
// Takes one argument like Permanent.Blue+withFlying
@Override
public final boolean isValid(final String restriction, final Player sourceController, final Card source, SpellAbility spellAbility) {

View File

@@ -25,6 +25,7 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import java.util.ArrayList;
import java.util.List;
@@ -42,7 +43,7 @@ public class TargetChoices implements Cloneable {
// Card or Player are legal targets.
private final CardCollection targetCards = new CardCollection();
private final List<Player> targetPlayers = new ArrayList<>();
private final PlayerCollection targetPlayers = new PlayerCollection();
private final List<SpellAbility> targetSpells = new ArrayList<>();
public final int getNumTargeted() {
@@ -70,8 +71,7 @@ public class TargetChoices implements Cloneable {
}
private final boolean addTarget(final Card c) {
if (!targetCards.contains(c)) {
targetCards.add(c);
if (targetCards.add(c)) {
numTargeted++;
return true;
}
@@ -79,8 +79,7 @@ public class TargetChoices implements Cloneable {
}
private final boolean addTarget(final Player p) {
if (!targetPlayers.contains(p)) {
targetPlayers.add(p);
if (targetPlayers.add(p)) {
numTargeted++;
return true;
}
@@ -213,16 +212,10 @@ public class TargetChoices implements Cloneable {
if (obj instanceof TargetChoices) {
TargetChoices compare = (TargetChoices)obj;
if (this.getNumTargeted() != compare.getNumTargeted()) {
if (getNumTargeted() != compare.getNumTargeted()) {
return false;
}
for (int i = 0; i < this.getTargets().size(); i++) {
if (!compare.getTargets().get(i).equals(this.getTargets().get(i))) {
return false;
}
}
return true;
return getTargets().equals(compare.getTargets());
} else {
return false;
}

View File

@@ -516,10 +516,17 @@ public class TriggerHandler {
else {
// get CardState does not work for transformed cards
// also its about LKI
// TODO remove this part after all spellAbility can handle LKI as host
// Currently only true for delayed Trigger
if (host.isInZone(ZoneType.Battlefield) || !host.hasAlternateState()) {
// if host changes Zone with other cards, try to use original host
if (!regtrig.getMode().equals(TriggerType.ChangesZone))
host = game.getCardState(host);
if (!regtrig.getMode().equals(TriggerType.ChangesZone)) {
Card gameHost = game.getCardState(host);
// TODO only set when the host equals the game state
if (gameHost.equalsWithTimestamp(host)) {
host = gameHost;
}
}
}
}

View File

@@ -269,7 +269,7 @@ public class TriggerSpellAbilityCast extends Trigger {
final SpellAbility castSA = (SpellAbility) runParams.get(AbilityKey.CastSA);
final SpellAbilityStackInstance si = sa.getHostCard().getGame().getStack().getInstanceFromSpellAbility(castSA);
sa.setTriggeringObject(AbilityKey.Card, castSA.getHostCard());
sa.setTriggeringObject(AbilityKey.SpellAbility, castSA);
sa.setTriggeringObject(AbilityKey.SpellAbility, castSA.copy(castSA.getHostCard(), true));
sa.setTriggeringObject(AbilityKey.StackInstance, si);
sa.setTriggeringObject(AbilityKey.SpellAbilityTargetingCards, (si != null ? si.getSpellAbility(true) : castSA).getTargets().getTargetCards());
sa.setTriggeringObjectsFrom(

View File

@@ -6,12 +6,12 @@
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@@ -65,7 +65,7 @@ import forge.util.TextUtil;
* <p>
* MagicStack class.
* </p>
*
*
* @author Forge
* @version $Id$
*/
@@ -135,14 +135,12 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// if the ability is a spell, but not a copied spell and its not already
// on the stack zone, move there
if (ability.isSpell()) {
if (!source.isCopiedSpell()) {
if (!source.isInZone(ZoneType.Stack)) {
ability.setHostCard(game.getAction().moveToStack(source, ability));
}
if (ability.equals(source.getCastSA())) {
source.setCastSA(ability.copy(source, true));
}
if (ability.isSpell() && !source.isCopiedSpell()) {
if (!source.isInZone(ZoneType.Stack)) {
ability.setHostCard(game.getAction().moveToStack(source, ability));
}
if (ability.equals(source.getCastSA())) {
source.setCastSA(ability.copy(source, true));
}
}
@@ -245,6 +243,11 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
} else if (source.isFaceDown()) {
source.turnFaceUp(null);
}
// copied always add to stack zone
if (source.isCopiedSpell()) {
game.getStackZone().add(source);
}
}
if (sp.getApi() == ApiType.Charm && sp.hasParam("ChoiceRestriction")) {
@@ -254,7 +257,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
//cancel auto-pass for all opponents of activating player
//when a new non-triggered ability is put on the stack
if (!sp.isTrigger()) {
if (!sp.isTrigger()) {
for (final Player p : activator.getOpponents()) {
p.getController().autoPassCancel();
}
@@ -271,18 +274,15 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// TODO: make working triggered ability
sp.setTotalManaSpent(totManaSpent);
AbilityUtils.resolve(sp);
// AbilityStatic should do nothing below
return;
}
else {
for (OptionalCost s : sp.getOptionalCosts()) {
source.addOptionalCostPaid(s);
}
if (sp.isCopied()) {
si = push(sp);
}
else {
// The ability is added to stack HERE
si = push(sp);
}
// The ability is added to stack HERE
si = push(sp);
}
sp.setTotalManaSpent(totManaSpent);
@@ -292,7 +292,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// Copied spells aren't cast per se so triggers shouldn't run for them.
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
if (!(sp instanceof AbilityStatic) && !sp.isCopied()) {
if (!sp.isCopied()) {
// Run SpellAbilityCast triggers
runParams.put(AbilityKey.Cost, sp.getPayCosts());
runParams.put(AbilityKey.Player, sp.getHostCard().getController());
@@ -322,17 +322,15 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
if (sp.isCycling()) {
activator.addCycled(sp);
}
if (sp.hasParam("Crew")) {
// Trigger crews!
runParams.put(AbilityKey.Vehicle, sp.getHostCard());
runParams.put(AbilityKey.Crew, sp.getPaidList("TappedCards"));
game.getTriggerHandler().runTrigger(TriggerType.Crewed, runParams, false);
}
}
// Run SpellAbilityCopy triggers
if (sp.isCopied()) {
} else {
// Run SpellAbilityCopy triggers
runParams.put(AbilityKey.Activator, sp.getActivatingPlayer());
runParams.put(AbilityKey.CopySA, si.getSpellAbility(true));
// Run SpellCopy triggers
@@ -345,7 +343,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// Run BecomesTarget triggers
// Create a new object, since the triggers aren't happening right away
List<TargetChoices> chosenTargets = sp.getAllTargetChoices();
if (!chosenTargets.isEmpty()) {
if (!chosenTargets.isEmpty()) {
runParams = Maps.newHashMap();
SpellAbility s = sp;
if (si != null) {
@@ -362,7 +360,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
if (distinctObjects.contains(tgt)) {
continue;
}
distinctObjects.add(tgt);
if (tgt instanceof Card && !((Card) tgt).hasBecomeTargetThisTurn()) {
runParams.put(AbilityKey.FirstTime, null);
@@ -443,7 +441,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// Resolving the Stack
// freeze the stack while we're in the middle of resolving
freezeStack();
freezeStack();
setResolving(true);
// The SpellAbility isn't removed from the Stack until it finishes resolving
@@ -452,21 +450,21 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
//final SpellAbility sa = pop();
// ActivePlayer gains priority first after Resolve
game.getPhaseHandler().resetPriority();
game.getPhaseHandler().resetPriority();
final Card source = sa.getHostCard();
curResolvingCard = source;
boolean thisHasFizzled = hasFizzled(sa, source, null);
if (!thisHasFizzled) {
game.copyLastState();
}
if (thisHasFizzled) { // Fizzle
if (sa.isBestow()) {
// 702.102d: if its target is illegal,
// the effect making it an Aura spell ends.
// 702.102d: if its target is illegal,
// the effect making it an Aura spell ends.
// It continues resolving as a creature spell.
source.unanimateBestow();
game.fireEvent(new GameEventCardStatsChanged(source));
@@ -482,7 +480,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
sa.resolve();
// do creatures ETB from here?
}
game.fireEvent(new GameEventSpellResolved(sa, thisHasFizzled));
finishResolving(sa, thisHasFizzled);
@@ -501,7 +499,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// remove SA and card from the stack
removeCardFromStack(sa, si, fizzle);
if (si != null) {
remove(si);
}
@@ -531,10 +529,17 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// need to update active trigger
game.getTriggerHandler().resetActiveTriggers();
if (source.isCopiedSpell() || sa.isAbility()) {
if (sa.isAbility()) {
// do nothing
return;
}
else if ((source.isInstant() || source.isSorcery() || fizzle) &&
if (source.isCopiedSpell() && source.isInZone(ZoneType.Stack)) {
source.ceaseToExist();
return;
}
if ((source.isInstant() || source.isSorcery() || fizzle) &&
source.isInZone(ZoneType.Stack)) {
// If Spell and still on the Stack then let it goto the graveyard or replace its own movement
Map<AbilityKey, Object> params = AbilityKey.newMap();
@@ -864,8 +869,8 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
game.updateStackForView();
game.fireEvent(new GameEventSpellRemovedFromStack(null));
}
@Override
@Override
public String toString() {
return TextUtil.concatNoSpace(simultaneousStackEntryList.toString(),"==", frozenStack.toString(), "==", stack.toString());
}

View File

@@ -104,8 +104,12 @@ public class Zone implements java.io.Serializable, Iterable<Card> {
if (!c.isImmutable()) {
final Zone oldZone = game.getZoneOf(c);
final ZoneType zt = oldZone == null ? ZoneType.Stack : oldZone.getZoneType();
cardsAddedThisTurn.add(zt, c);
latestStateCardsAddedThisTurn.add(zt, latestState != null ? latestState : c);
// only if the zoneType differss from this
if (zt != zoneType) {
cardsAddedThisTurn.add(zt, c);
latestStateCardsAddedThisTurn.add(zt, latestState != null ? latestState : c);
}
}
c.setTurnInZone(game.getPhaseHandler().getTurn());