package forge; import com.esotericsoftware.minlog.Log; import forge.card.abilityFactory.AbilityFactory; import forge.card.cardFactory.CardFactoryUtil; import forge.card.mana.ManaCost; import forge.card.spellability.*; import forge.gui.GuiUtils; import forge.gui.input.Input; import forge.gui.input.Input_PayManaCost_Ability; import java.util.ArrayList; import java.util.HashMap; import java.util.Stack; /** * <p>MagicStack class.</p> * * @author Forge * @version $Id: $ */ public class MagicStack extends MyObservable { private ArrayList<SpellAbility> simultaneousStackEntryList = new ArrayList<SpellAbility>(); private Stack<SpellAbility_StackInstance> stack = new Stack<SpellAbility_StackInstance>(); private Stack<SpellAbility_StackInstance> frozenStack = new Stack<SpellAbility_StackInstance>(); private boolean frozen = false; private boolean bResolving = false; private int splitSecondOnStack = 0; /** * <p>isFrozen.</p> * * @return a boolean. */ public boolean isFrozen() { return frozen; } /** * <p>Setter for the field <code>frozen</code>.</p> * * @param frozen a boolean. */ public void setFrozen(boolean frozen) { this.frozen = frozen; } /** * <p>reset.</p> */ public void reset() { stack.clear(); frozen = false; splitSecondOnStack = 0; frozenStack.clear(); this.updateObservers(); } /** * <p>isSplitSecondOnStack.</p> * * @return a boolean. */ public boolean isSplitSecondOnStack() { return splitSecondOnStack > 0; } /** * <p>incrementSplitSecond.</p> * * @param sp a {@link forge.card.spellability.SpellAbility} object. */ public void incrementSplitSecond(SpellAbility sp) { if (sp.getSourceCard().hasKeyword("Split Second")) splitSecondOnStack++; } /** * <p>decrementSplitSecond.</p> * * @param sp a {@link forge.card.spellability.SpellAbility} object. */ public void decrementSplitSecond(SpellAbility sp) { if (sp.getSourceCard().hasKeyword("Split Second")) splitSecondOnStack--; if (splitSecondOnStack < 0) splitSecondOnStack = 0; } /** * <p>freezeStack.</p> */ public void freezeStack() { frozen = true; } /** * <p>addAndUnfreeze.</p> * * @param ability a {@link forge.card.spellability.SpellAbility} object. */ public void addAndUnfreeze(SpellAbility ability) { ability.getRestrictions().abilityActivated(); if (ability.getRestrictions().getActivationNumberSacrifice() != -1 && ability.getRestrictions().getNumberTurnActivations() >= ability.getRestrictions().getActivationNumberSacrifice()) { ability.getSourceCard().addExtrinsicKeyword("At the beginning of the end step, sacrifice CARDNAME."); } // triggered abilities should go on the frozen stack if (!ability.isTrigger()) frozen = false; this.add(ability); // if the ability is a spell, but not a copied spell and its not already on the stack zone, move there if (ability.isSpell()) { Card source = ability.getSourceCard(); if (!source.isCopiedSpell() && !AllZone.getZone(source).is(Constant.Zone.Stack)) AllZone.getGameAction().moveToStack(source); } if (ability.isTrigger()) unfreezeStack(); } /** * <p>unfreezeStack.</p> */ public void unfreezeStack() { frozen = false; boolean checkState = !frozenStack.isEmpty(); while (!frozenStack.isEmpty()) { SpellAbility sa = frozenStack.pop().getSpellAbility(); this.add(sa); } if (checkState) AllZone.getGameAction().checkStateEffects(); } /** * <p>clearFrozen.</p> */ public void clearFrozen() { // TODO: frozen triggered abilities and undoable costs have nasty consequences frozen = false; frozenStack.clear(); } /** * <p>setResolving.</p> * * @param b a boolean. */ public void setResolving(boolean b) { bResolving = b; if (!bResolving) chooseOrderOfSimultaneousStackEntryAll(); } /** * <p>getResolving.</p> * * @return a boolean. */ public boolean getResolving() { return bResolving; } /** * <p>add.</p> * * @param sp a {@link forge.card.spellability.SpellAbility} object. * @param useX a boolean. */ public void add(SpellAbility sp, boolean useX) { if (!useX) this.add(sp); else { // TODO make working triggered abilities! if (sp instanceof Ability_Mana || sp instanceof Ability_Triggered) sp.resolve(); else { push(sp); /*if (sp.getTargetCard() != null) CardFactoryUtil.checkTargetingEffects(sp, sp.getTargetCard());*/ } } } /** * <p>getMultiKickerSpellCostChange.</p> * * @param sa a {@link forge.card.spellability.SpellAbility} object. * @return a {@link forge.card.mana.ManaCost} object. */ public ManaCost getMultiKickerSpellCostChange(SpellAbility sa) { int Max = 25; String[] Numbers = new String[Max]; for (int no = 0; no < Max; no++) Numbers[no] = String.valueOf(no); ManaCost manaCost = new ManaCost(sa.getManaCost()); String Mana = manaCost.toString(); int MultiKickerPaid = AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid; String Number_ManaCost = " "; if (Mana.toString().length() == 1) Number_ManaCost = Mana.toString().substring(0, 1); else if (Mana.toString().length() == 0) Number_ManaCost = "0"; // Should Never Occur else Number_ManaCost = Mana.toString().substring(0, 2); Number_ManaCost = Number_ManaCost.trim(); for (int check = 0; check < Max; check++) { if (Number_ManaCost.equals(Numbers[check])) { if (check - MultiKickerPaid < 0) { MultiKickerPaid = MultiKickerPaid - check; AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid = MultiKickerPaid; Mana = Mana.replaceFirst(String.valueOf(check), "0"); } else { Mana = Mana.replaceFirst(String.valueOf(check), String.valueOf(check - MultiKickerPaid)); MultiKickerPaid = 0; AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid = MultiKickerPaid; } } Mana = Mana.trim(); if (Mana.equals("")) Mana = "0"; manaCost = new ManaCost(Mana); } String Color_cut = AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid_Colored; for (int Colored_Cut = 0; Colored_Cut < Color_cut.length(); Colored_Cut++) { if ("WUGRB".contains(Color_cut.substring(Colored_Cut, Colored_Cut + 1))) { if (!Mana.equals(Mana.replaceFirst((Color_cut.substring(Colored_Cut, Colored_Cut + 1)), ""))) { Mana = Mana.replaceFirst(Color_cut.substring(Colored_Cut, Colored_Cut + 1), ""); AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid_Colored = AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid_Colored .replaceFirst(Color_cut.substring(Colored_Cut, Colored_Cut + 1), ""); Mana = Mana.trim(); if (Mana.equals("")) Mana = "0"; manaCost = new ManaCost(Mana); } } } return manaCost; } //TODO - this may be able to use a straight copy of MultiKicker cost change /** * <p>getReplicateSpellCostChange.</p> * * @param sa a {@link forge.card.spellability.SpellAbility} object. * @return a {@link forge.card.mana.ManaCost} object. */ public ManaCost getReplicateSpellCostChange(SpellAbility sa) { ManaCost manaCost = new ManaCost(sa.getManaCost()); //String Mana = manaCost.toString(); return manaCost; } /** * <p>add.</p> * * @param sp a {@link forge.card.spellability.SpellAbility} object. */ public void add(final SpellAbility sp) { ArrayList<Target_Choices> chosenTargets = sp.getAllTargetChoices(); if (sp instanceof Ability_Mana) { // Mana Abilities go straight through sp.resolve(); sp.resetOnceResolved(); return; } if (frozen) { SpellAbility_StackInstance si = new SpellAbility_StackInstance(sp); frozenStack.push(si); return; } // if activating player slips through the cracks, assign activating // Player to the controller here if (null == sp.getActivatingPlayer()) { sp.setActivatingPlayer(sp.getSourceCard().getController()); System.out.println(sp.getSourceCard().getName() + " - activatingPlayer not set before adding to stack."); } if (AllZone.getPhase().is(Constant.Phase.Cleanup)) { // If something triggers during Cleanup, need to repeat AllZone.getPhase().repeatPhase(); } // TODO: triggered abilities need to be fixed if (!(sp instanceof Ability_Triggered || sp instanceof Ability_Static)) AllZone.getPhase().setPriority(sp.getActivatingPlayer()); // when something is added we need to setPriority if (sp instanceof Ability_Triggered || sp instanceof Ability_Static) // TODO make working triggered ability sp.resolve(); else { if (sp.isKickerAbility()) { sp.getSourceCard().setKicked(true); SpellAbility[] sa = sp.getSourceCard().getSpellAbility(); int AbilityNumber = 0; for (int i = 0; i < sa.length; i++) if (sa[i] == sp) AbilityNumber = i; sp.getSourceCard().setAbilityUsed(AbilityNumber); } if (sp.getSourceCard().isCopiedSpell()) push(sp); else if (!sp.isMultiKicker() && !sp.isReplicate() && !sp.isXCost()) { push(sp); } else if (sp.getPayCosts() != null && !sp.isMultiKicker() && !sp.isReplicate()) { push(sp); } else if (sp.isXCost()) { // TODO: convert any X costs to use abCost so it happens earlier final SpellAbility sa = sp; final Ability ability = new Ability(sp.getSourceCard(), sa.getXManaCost()) { public void resolve() { Card crd = this.getSourceCard(); crd.addXManaCostPaid(1); } }; final Command unpaidCommand = new Command() { private static final long serialVersionUID = -3342222770086269767L; public void execute() { push(sa); } }; final Command paidCommand = new Command() { private static final long serialVersionUID = -2224875229611007788L; public void execute() { ability.resolve(); Card crd = sa.getSourceCard(); AllZone.getInputControl().setInput(new Input_PayManaCost_Ability("Pay X cost for " + crd.getName() + " (X=" + crd.getXManaCostPaid() + ")\r\n", ability.getManaCost(), this, unpaidCommand, true)); } }; Card crd = sa.getSourceCard(); if (sp.getSourceCard().getController().isHuman()) { AllZone.getInputControl().setInput(new Input_PayManaCost_Ability("Pay X cost for " + sp.getSourceCard().getName() + " (X=" + crd.getXManaCostPaid() + ")\r\n", ability.getManaCost(), paidCommand, unpaidCommand, true)); } else // computer { int neededDamage = CardFactoryUtil.getNeededXDamage(sa); while (ComputerUtil.canPayCost(ability) && neededDamage != sa.getSourceCard().getXManaCostPaid()) { ComputerUtil.playNoStack(ability); } push(sa); } } else if (sp.isMultiKicker()) { // TODO: convert multikicker support in abCost so this doesn't happen here // both X and multi is not supported yet final SpellAbility sa = sp; final Ability ability = new Ability(sp.getSourceCard(), sp.getMultiKickerManaCost()) { public void resolve() { this.getSourceCard().addMultiKickerMagnitude(1); } }; final Command unpaidCommand = new Command() { private static final long serialVersionUID = -3342222770086269767L; public void execute() { push(sa); } }; final Command paidCommand = new Command() { private static final long serialVersionUID = -6037161763374971106L; public void execute() { ability.resolve(); ManaCost manaCost = getMultiKickerSpellCostChange(ability); if (manaCost.isPaid()) { this.execute(); } else { if (AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid == 0 && AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid_Colored.equals("")) { AllZone.getInputControl().setInput(new Input_PayManaCost_Ability( "Multikicker for " + sa.getSourceCard() + "\r\n" + "Times Kicked: " + sa.getSourceCard().getMultiKickerMagnitude() + "\r\n", manaCost.toString(), this, unpaidCommand)); } else { AllZone.getInputControl().setInput(new Input_PayManaCost_Ability("Multikicker for " + sa.getSourceCard() + "\r\n" + "Mana in Reserve: " + ((AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid != 0) ? AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid : "") + AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid_Colored + "\r\n" + "Times Kicked: " + sa.getSourceCard().getMultiKickerMagnitude() + "\r\n", manaCost.toString(), this, unpaidCommand)); } } } }; if (sp.getActivatingPlayer().isHuman()) { ManaCost manaCost = getMultiKickerSpellCostChange(ability); if (manaCost.isPaid()) { paidCommand.execute(); } else { if (AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid == 0 && AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid_Colored.equals("")) { AllZone.getInputControl().setInput(new Input_PayManaCost_Ability("Multikicker for " + sa.getSourceCard() + "\r\n" + "Times Kicked: " + sa.getSourceCard().getMultiKickerMagnitude() + "\r\n", manaCost.toString(), paidCommand, unpaidCommand)); } else { AllZone.getInputControl().setInput(new Input_PayManaCost_Ability("Multikicker for " + sa.getSourceCard() + "\r\n" + "Mana in Reserve: " + ((AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid != 0) ? AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid : "") + AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid_Colored + "\r\n" + "Times Kicked: " + sa.getSourceCard().getMultiKickerMagnitude() + "\r\n", manaCost.toString(), paidCommand, unpaidCommand)); } } } else // computer { while (ComputerUtil.canPayCost(ability)) ComputerUtil.playNoStack(ability); push(sa); } } else if (sp.isReplicate()) { // TODO: convert multikicker/replicate support in abCost so this doesn't happen here // X and multi and replicate are not supported yet final SpellAbility sa = sp; final Ability ability = new Ability(sp.getSourceCard(), sp.getReplicateManaCost()) { public void resolve() { this.getSourceCard().addReplicateMagnitude(1); } }; final Command unpaidCommand = new Command() { private static final long serialVersionUID = -3180458633098297855L; public void execute() { push(sa); for (int i = 0; i < sp.getSourceCard().getReplicateMagnitude(); i++) { AllZone.getCardFactory().copySpellontoStack(sp.getSourceCard(), sp.getSourceCard(), false); } } }; final Command paidCommand = new Command() { private static final long serialVersionUID = 132624005072267304L; public void execute() { ability.resolve(); ManaCost manaCost = getReplicateSpellCostChange(ability); if (manaCost.isPaid()) { this.execute(); } else { /* if (AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid == 0 && AllZone.getGameAction().CostCutting_GetMultiMickerManaCostPaid_Colored.equals("")) { AllZone.getInputControl().setInput(new Input_PayManaCost_Ability( "Replicate for "+ sa.getSourceCard() + "\r\n" + "Times Kicked: " + sa.getSourceCard().getMultiKickerMagnitude() + "\r\n", manaCost.toString(), this, unpaidCommand)); } else {*/ AllZone.getInputControl().setInput(new Input_PayManaCost_Ability("Replicate for " + sa.getSourceCard() + "\r\n" + "Times Replicated: " + sa.getSourceCard().getReplicateMagnitude() + "\r\n", manaCost.toString(), this, unpaidCommand)); //} } } }; if (sp.getSourceCard().getController().equals( AllZone.getHumanPlayer())) { ManaCost manaCost = getMultiKickerSpellCostChange(ability); if (manaCost.isPaid()) { paidCommand.execute(); } else { AllZone.getInputControl().setInput(new Input_PayManaCost_Ability("Replicate for " + sa.getSourceCard() + "\r\n" + "Times Replicated: " + sa.getSourceCard().getReplicateMagnitude() + "\r\n", manaCost.toString(), paidCommand, unpaidCommand)); } } else // computer { while (ComputerUtil.canPayCost(ability)) ComputerUtil.playNoStack(ability); push(sa); } } } if (!sp.getSourceCard().isCopiedSpell()) //Copied spells aren't cast per se so triggers shouldn't run for them. { //Run SpellAbilityCast triggers HashMap<String, Object> runParams = new HashMap<String, Object>(); runParams.put("Cost", sp.getPayCosts()); runParams.put("Player", sp.getSourceCard().getController()); runParams.put("Activator", sp.getActivatingPlayer()); runParams.put("CastSA", sp); AllZone.getTriggerHandler().runTrigger("SpellAbilityCast", runParams); //Run SpellCast triggers if (sp.isSpell()) { AllZone.getTriggerHandler().runTrigger("SpellCast", runParams); } //Run AbilityCast triggers if (sp.isAbility()) { AllZone.getTriggerHandler().runTrigger("AbilityCast", runParams); } //Run Cycled triggers if (sp.isCycling()) { runParams.clear(); runParams.put("Card", sp.getSourceCard()); AllZone.getTriggerHandler().runTrigger("Cycled", runParams); } //Run BecomesTarget triggers runParams.clear(); runParams.put("SourceSA", sp); if (chosenTargets.size() > 0) { for (Target_Choices tc : chosenTargets) { if (tc != null) { if (tc.getTargetCards() != null) { for (Object tgt : tc.getTargets()) { runParams.put("Target", tgt); AllZone.getTriggerHandler().runTrigger("BecomesTarget", runParams); } } } } } //Not sure these clauses are necessary. Consider it a precaution for backwards compatibility for hardcoded cards. if (sp.getTargetCard() != null) { runParams.put("Target", sp.getTargetCard()); AllZone.getTriggerHandler().runTrigger("BecomesTarget", runParams); } if (sp.getTargetList() != null) { if (sp.getTargetList().size() > 0) { for (Card ctgt : sp.getTargetList()) { runParams.put("Target", ctgt); AllZone.getTriggerHandler().runTrigger("BecomesTarget", runParams); } } } if (sp.getTargetPlayer() != null) { runParams.put("Target", sp.getTargetPlayer()); AllZone.getTriggerHandler().runTrigger("BecomesTarget", runParams); } } if (sp instanceof Spell_Permanent && sp.getSourceCard().getName().equals("Mana Vortex")) { final SpellAbility counter = new Ability(sp.getSourceCard(), "0") { @Override public void resolve() { Input in = new Input() { private static final long serialVersionUID = -2042489457719935420L; @Override public void showMessage() { AllZone.getDisplay().showMessage("Mana Vortex - select a land to sacrifice"); ButtonUtil.enableOnlyCancel(); } @Override public void selectButtonCancel() { AllZone.getStack().pop(); AllZone.getGameAction().moveToGraveyard(sp.getSourceCard()); stop(); } @Override public void selectCard(Card c, PlayerZone zone) { if (zone.is(Constant.Zone.Battlefield) && c.getController().isHuman() && c.isLand()) { AllZone.getGameAction().sacrifice(c); stop(); } } }; SpellAbility_StackInstance prev = peekInstance(); if (prev.isSpell() && prev.getSourceCard().getName().equals("Mana Vortex")) { if (sp.getSourceCard().getController().isHuman()) { AllZone.getInputControl().setInput(in); } else {//Computer CardList lands = AllZoneUtil.getPlayerLandsInPlay(AllZone.getComputerPlayer()); if (!lands.isEmpty()) { AllZone.getComputerPlayer().sacrificePermanent("prompt", lands); } else { AllZone.getStack().pop(); AllZone.getGameAction().moveToGraveyard(sp.getSourceCard()); } } } }//resolve() };//SpellAbility counter.setStackDescription(sp.getSourceCard().getName() + " - counter Mana Vortex unless you sacrifice a land."); add(counter); } /* * Whenever a player casts a spell, counter it if a card with the same name * is in a graveyard or a nontoken permanent with the same name is on the battlefield. */ if (sp.isSpell() && AllZoneUtil.isCardInPlay("Bazaar of Wonders")) { boolean found = false; CardList all = AllZoneUtil.getCardsInPlay(); all = all.filter(AllZoneUtil.nonToken); CardList graves = AllZoneUtil.getCardsInGraveyard(); all.addAll(graves); for (Card c : all) { if (sp.getSourceCard().getName().equals(c.getName())) found = true; } if (found) { CardList bazaars = AllZoneUtil.getCardsInPlay("Bazaar of Wonders"); //should only be 1... for (final Card bazaar : bazaars) { final SpellAbility counter = new Ability(bazaar, "0") { @Override public void resolve() { if (AllZone.getStack().size() > 0) AllZone.getStack().pop(); }//resolve() };//SpellAbility counter.setStackDescription(bazaar.getName() + " - counter " + sp.getSourceCard().getName() + "."); add(counter); } } } //Lurking Predators if (sp.isSpell()) { Player player = sp.getSourceCard().getController(); CardList lurkingPredators = AllZoneUtil.getPlayerCardsInPlay(player, "Lurking Predators"); for (int i = 0; i < lurkingPredators.size(); i++) { StringBuilder revealMsg = new StringBuilder(""); if (lurkingPredators.get(i).getController().isHuman()) { revealMsg.append("You reveal: "); if (AllZone.getHumanLibrary().size() == 0) { revealMsg.append("Nothing!"); GameActionUtil.showInfoDialg(revealMsg.toString()); continue; } Card revealed = AllZone.getHumanLibrary().get(0); revealMsg.append(revealed.getName()); if (!revealed.isCreature()) { revealMsg.append("\n\rPut it on the bottom of your library?"); if (GameActionUtil.showYesNoDialog(lurkingPredators.get(i), revealMsg.toString())) { AllZone.getGameAction().moveToBottomOfLibrary(revealed); } else { AllZone.getGameAction().moveToLibrary(revealed); } } else { GameActionUtil.showInfoDialg(revealMsg.toString()); AllZone.getGameAction().moveToPlay(revealed); } } else { revealMsg.append("Computer reveals: "); if (AllZone.getComputerLibrary().size() == 0) { revealMsg.append("Nothing!"); GameActionUtil.showInfoDialg(revealMsg.toString()); continue; } Card revealed = AllZone.getComputerLibrary().get(0); revealMsg.append(revealed.getName()); if (!revealed.isCreature()) { GameActionUtil.showInfoDialg(revealMsg.toString()); if (lurkingPredators.size() > i) { AllZone.getGameAction().moveToBottomOfLibrary(revealed); } else { AllZone.getGameAction().moveToLibrary(revealed); } } else { GameActionUtil.showInfoDialg(revealMsg.toString()); AllZone.getGameAction().moveToPlay(revealed); } } } } /*if (sp.getTargetCard() != null) CardFactoryUtil.checkTargetingEffects(sp, sp.getTargetCard());*/ if (simultaneousStackEntryList.size() > 0) AllZone.getPhase().passPriority(); } /** * <p>size.</p> * * @return a int. */ public int size() { return stack.size(); } // Push should only be used by add. /** * <p>push.</p> * * @param sp a {@link forge.card.spellability.SpellAbility} object. */ private void push(SpellAbility sp) { if (null == sp.getActivatingPlayer()) { sp.setActivatingPlayer(sp.getSourceCard().getController()); System.out.println(sp.getSourceCard().getName() + " - activatingPlayer not set before adding to stack."); } incrementSplitSecond(sp); SpellAbility_StackInstance si = new SpellAbility_StackInstance(sp); stack.push(si); this.updateObservers(); if (sp.isSpell() && !sp.getSourceCard().isCopiedSpell()) { Phase.increaseSpellCount(sp); GameActionUtil.executePlayCardEffects(sp); } } /** * <p>resolveStack.</p> */ public void resolveStack() { // Resolving the Stack GuiDisplayUtil.updateGUI(); this.freezeStack(); // freeze the stack while we're in the middle of resolving setResolving(true); SpellAbility sa = AllZone.getStack().pop(); AllZone.getPhase().resetPriority(); // ActivePlayer gains priority first after Resolve Card source = sa.getSourceCard(); if (hasFizzled(sa, source)) {//Fizzle // TODO: Spell fizzles, what's the best way to alert player? Log.debug(source.getName() + " ability fizzles."); finishResolving(sa, true); } else if (sa.getAbilityFactory() != null) { AbilityFactory.handleRemembering(sa.getAbilityFactory()); AbilityFactory.resolve(sa, true); } else { sa.resolve(); finishResolving(sa, false); } } /** * <p>removeCardFromStack.</p> * * @param sa a {@link forge.card.spellability.SpellAbility} object. * @param fizzle a boolean. * @since 1.0.15 */ public void removeCardFromStack(SpellAbility sa, boolean fizzle) { Card source = sa.getSourceCard(); //do nothing if (sa.getSourceCard().isCopiedSpell() || sa.isAbility()) { } // Handle cards that need to be moved differently else if (sa.isBuyBackAbility() && !fizzle) { AllZone.getGameAction().moveToHand(source); } else if (sa.isFlashBackAbility()) { AllZone.getGameAction().exile(source); sa.setFlashBackAbility(false); } // If Spell and still on the Stack then let it goto the graveyard or replace its own movement else if (!source.isCopiedSpell() && (source.isInstant() || source.isSorcery() || fizzle) && AllZone.getZone(source).is(Constant.Zone.Stack)) { if (source.getReplaceMoveToGraveyard().size() == 0) AllZone.getGameAction().moveToGraveyard(source); else { source.replaceMoveToGraveyard(); } } } /** * <p>finishResolving.</p> * * @param sa a {@link forge.card.spellability.SpellAbility} object. * @param fizzle a boolean. * @since 1.0.15 */ public void finishResolving(SpellAbility sa, boolean fizzle) { //remove card from the stack removeCardFromStack(sa, fizzle); // After SA resolves we have to do a handful of things setResolving(false); this.unfreezeStack(); sa.resetOnceResolved(); AllZone.getGameAction().checkStateEffects(); AllZone.getPhase().setNeedToNextPhase(false); if (AllZone.getPhase().inCombat()) CombatUtil.showCombat(); GuiDisplayUtil.updateGUI(); //TODO - this is a huge hack. Why is this necessary? //hostCard in AF is not the same object that's on the battlefield //verified by System.identityHashCode(card); Card tmp = sa.getSourceCard(); if (tmp.getClones().size() > 0) { for (Card c : AllZoneUtil.getCardsInPlay()) { if (c.equals(tmp)) { c.setClones(tmp.getClones()); } } } } /** * <p>hasFizzled.</p> * * @param sa a {@link forge.card.spellability.SpellAbility} object. * @param source a {@link forge.Card} object. * @return a boolean. */ public boolean hasFizzled(SpellAbility sa, Card source) { // By default this has not fizzled boolean fizzle = false; boolean firstTarget = true; SpellAbility fizzSA = sa; while (true) { Target tgt = fizzSA.getTarget(); if (tgt != null && tgt.getMinTargets(source, fizzSA) == 0 && tgt.getNumTargeted() == 0) { // Don't assume fizzled for minTargets == 0 and nothing is targeted } else if (firstTarget && (tgt != null || fizzSA.getTargetCard() != null || fizzSA.getTargetPlayer() != null)) { // If there is at least 1 target, fizzle switches because ALL targets need to be invalid fizzle = true; firstTarget = false; } if (tgt != null) { // With multi-targets, as long as one target is still legal, we'll try to go through as much as possible ArrayList<Object> tgts = tgt.getTargets(); for (Object o : tgts) { if (o instanceof Player) { Player p = (Player) o; fizzle &= !(p.canTarget(fizzSA.getTargetCard())); } if (o instanceof Card) { Card card = (Card) o; fizzle &= !(CardFactoryUtil.isTargetStillValid(fizzSA, card)); } if (o instanceof SpellAbility) { SpellAbility tgtSA = (SpellAbility) o; fizzle &= !(Target_Selection.matchSpellAbility(fizzSA, tgtSA, tgt)); } } } else if (fizzSA.getTargetCard() != null) { // Fizzling will only work for Abilities that use the Target class, // since the info isn't available otherwise fizzle &= !CardFactoryUtil.isTargetStillValid(fizzSA, fizzSA.getTargetCard()); } else if (fizzSA.getTargetPlayer() != null) { fizzle &= !fizzSA.getTargetPlayer().canTarget(source); } if (fizzSA.getSubAbility() != null) fizzSA = fizzSA.getSubAbility(); else break; } return fizzle; } /** * <p>pop.</p> * * @return a {@link forge.card.spellability.SpellAbility} object. */ public SpellAbility pop() { SpellAbility sp = stack.pop().getSpellAbility(); decrementSplitSecond(sp); this.updateObservers(); return sp; } // CAREFUL! Peeking while an SAs Targets are being choosen may cause issues // index = 0 is the top, index = 1 is the next to top, etc... /** * <p>peekInstance.</p> * * @param index a int. * @return a {@link forge.card.spellability.SpellAbility_StackInstance} object. */ public SpellAbility_StackInstance peekInstance(int index) { return stack.get(index); } /** * <p>peekAbility.</p> * * @param index a int. * @return a {@link forge.card.spellability.SpellAbility} object. */ public SpellAbility peekAbility(int index) { return stack.get(index).getSpellAbility(); } /** * <p>peekInstance.</p> * * @return a {@link forge.card.spellability.SpellAbility_StackInstance} object. */ public SpellAbility_StackInstance peekInstance() { return stack.peek(); } /** * <p>peekAbility.</p> * * @return a {@link forge.card.spellability.SpellAbility} object. */ public SpellAbility peekAbility() { return stack.peek().getSpellAbility(); } /** * <p>remove.</p> * * @param sa a {@link forge.card.spellability.SpellAbility} object. */ public void remove(SpellAbility sa) { SpellAbility_StackInstance si = getInstanceFromSpellAbility(sa); if (si == null) return; remove(si); } /** * <p>remove.</p> * * @param si a {@link forge.card.spellability.SpellAbility_StackInstance} object. */ public void remove(SpellAbility_StackInstance si) { if (stack.remove(si)) { decrementSplitSecond(si.getSpellAbility()); } frozenStack.remove(si); this.updateObservers(); } /** * <p>getInstanceFromSpellAbility.</p> * * @param sa a {@link forge.card.spellability.SpellAbility} object. * @return a {@link forge.card.spellability.SpellAbility_StackInstance} object. */ public SpellAbility_StackInstance getInstanceFromSpellAbility(SpellAbility sa) { // TODO: Confirm this works! for (SpellAbility_StackInstance si : stack) { if (si.getSpellAbility().equals(sa)) return si; } return null; } /** * <p>hasSimultaneousStackEntries.</p> * * @return a boolean. */ public boolean hasSimultaneousStackEntries() { return simultaneousStackEntryList.size() > 0; } /** * <p>addSimultaneousStackEntry.</p> * * @param sa a {@link forge.card.spellability.SpellAbility} object. */ public void addSimultaneousStackEntry(SpellAbility sa) { simultaneousStackEntryList.add(sa); } /** * <p>chooseOrderOfSimultaneousStackEntryAll.</p> */ public void chooseOrderOfSimultaneousStackEntryAll() { chooseOrderOfSimultaneousStackEntry(AllZone.getPhase().getPlayerTurn()); chooseOrderOfSimultaneousStackEntry(AllZone.getPhase().getPlayerTurn().getOpponent()); } /** * <p>chooseOrderOfSimultaneousStackEntry.</p> * * @param activePlayer a {@link forge.Player} object. */ public void chooseOrderOfSimultaneousStackEntry(Player activePlayer) { if (simultaneousStackEntryList.size() == 0) return; ArrayList<SpellAbility> activePlayerSAs = new ArrayList<SpellAbility>(); for (int i = 0; i < simultaneousStackEntryList.size(); i++) { if (simultaneousStackEntryList.get(i).getActivatingPlayer() == null) { if (simultaneousStackEntryList.get(i).getSourceCard().getController().equals(activePlayer)) { activePlayerSAs.add(simultaneousStackEntryList.get(i)); simultaneousStackEntryList.remove(i); i--; } } else { if (simultaneousStackEntryList.get(i).getActivatingPlayer().equals(activePlayer)) { activePlayerSAs.add(simultaneousStackEntryList.get(i)); simultaneousStackEntryList.remove(i); i--; } } } if (activePlayerSAs.size() == 0) return; if (activePlayer.isComputer()) { for (SpellAbility sa : activePlayerSAs) { sa.doTrigger(sa.isMandatory()); ComputerUtil.playStack(sa); } } else { while (activePlayerSAs.size() > 1) { SpellAbility next = (SpellAbility) GuiUtils.getChoice("Choose which spell or ability to put on the stack next.", activePlayerSAs.toArray()); activePlayerSAs.remove(next); if (next.isTrigger()) { System.out.println("Stack order: AllZone.getGameAction().playSpellAbility(next)"); AllZone.getGameAction().playSpellAbility(next); } else { System.out.println("Stack order: AllZone.getStack().add(next)"); add(next); } } if (activePlayerSAs.get(0).isTrigger()) AllZone.getGameAction().playSpellAbility(activePlayerSAs.get(0)); else add(activePlayerSAs.get(0)); //AllZone.getGameAction().playSpellAbility(activePlayerSAs.get(0)); } } public boolean hasStateTrigger(int triggerID) { for(SpellAbility_StackInstance SI : stack) { if(SI.isStateTrigger(triggerID)) { return true; } } return false; } }