From 0700036b96bd99375ba693b5f6cec8af2882a6aa Mon Sep 17 00:00:00 2001 From: Maxmtg Date: Mon, 1 Apr 2013 14:47:38 +0000 Subject: [PATCH] InputSelectTargets moved to separate class, SpellAbilityRequirements - all options are passed as parameters to fillRequirements --- .gitattributes | 1 + .../SpellAbilityRequirements.java | 45 ++-- .../card/spellability/TargetChooser.java | 246 +----------------- .../control/input/InputSelectTargets.java | 212 +++++++++++++++ src/main/java/forge/game/GameActionPlay.java | 46 ++-- 5 files changed, 258 insertions(+), 292 deletions(-) create mode 100644 src/main/java/forge/control/input/InputSelectTargets.java diff --git a/.gitattributes b/.gitattributes index a350675a54e..94cea2207e7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13847,6 +13847,7 @@ src/main/java/forge/control/input/InputSelectCards.java -text src/main/java/forge/control/input/InputSelectCardsFromList.java -text src/main/java/forge/control/input/InputSelectMany.java -text src/main/java/forge/control/input/InputSelectManyBase.java -text +src/main/java/forge/control/input/InputSelectTargets.java -text src/main/java/forge/control/input/InputSynchronized.java -text src/main/java/forge/control/input/InputSyncronizedBase.java -text src/main/java/forge/control/input/package-info.java svneol=native#text/plain diff --git a/src/main/java/forge/card/spellability/SpellAbilityRequirements.java b/src/main/java/forge/card/spellability/SpellAbilityRequirements.java index 98e88306ea3..0d18237f63a 100644 --- a/src/main/java/forge/card/spellability/SpellAbilityRequirements.java +++ b/src/main/java/forge/card/spellability/SpellAbilityRequirements.java @@ -39,24 +39,15 @@ import forge.game.zone.Zone; */ public class SpellAbilityRequirements { private final SpellAbility ability; - private final TargetChooser select; private final CostPayment payment; - private boolean isFree; - private boolean skipStack = false; - private boolean isAlreadyTargeted = false; - - public void setAlreadyTargeted() { isAlreadyTargeted = true; } - public final void setSkipStack() { this.skipStack = true; } - public void setFree() { this.isFree = true; } public SpellAbilityRequirements(final SpellAbility sa, final CostPayment cp) { this.ability = sa; - this.select = new TargetChooser(sa); this.payment = cp; } - public final void fillRequirements() { + public final void fillRequirements(boolean isAlreadyTargeted, boolean isFree, boolean skipStack) { final GameState game = Singletons.getModel().getGame(); // used to rollback @@ -75,26 +66,26 @@ public class SpellAbilityRequirements { // Announce things like how many times you want to Multikick or the value of X if (!this.announceRequirements()) { - this.select.setCancel(true); - rollbackAbility(fromZone, zonePosition); + rollbackAbility(fromZone, zonePosition, null); return; } + final TargetChooser select = new TargetChooser(ability); // Skip to paying if parent ability doesn't target and has no // subAbilities. // (or trigger case where its already targeted) - boolean acceptsTargets = this.select.doesTarget() || this.ability.getSubAbility() != null; + boolean acceptsTargets = select.doesTarget() || this.ability.getSubAbility() != null; if (!isAlreadyTargeted && acceptsTargets) { - this.select.clearTargets(); - this.select.chooseTargets(); - if (this.select.isCanceled()) { - rollbackAbility(fromZone, zonePosition); + select.clearTargets(); + select.chooseTargets(); + if (select.isCanceled()) { + rollbackAbility(fromZone, zonePosition, select); return; } } // Payment - boolean paymentMade = this.isFree; + boolean paymentMade = isFree; if (!paymentMade) { this.payment.changeCost(); @@ -102,12 +93,12 @@ public class SpellAbilityRequirements { } if (!paymentMade) { - rollbackAbility(fromZone, zonePosition); + rollbackAbility(fromZone, zonePosition, select); return; } - else if (this.isFree || this.payment.isFullyPaid()) { - if (this.skipStack) { + else if (isFree || this.payment.isFullyPaid()) { + if (skipStack) { AbilityUtils.resolve(this.ability, false); } else { @@ -116,13 +107,13 @@ public class SpellAbilityRequirements { game.getStack().addAndUnfreeze(this.ability); } - // Warning about this - resolution may come in another thread, and it would still need its targets - this.select.clearTargets(); + // no worries here. The same thread must resolve, and by this moment ability will have been resolved already + select.clearTargets(); game.getAction().checkStateEffects(); } } - private void rollbackAbility(Zone fromZone, int zonePosition) { + private void rollbackAbility(Zone fromZone, int zonePosition, TargetChooser select) { // cancel ability during target choosing final Card c = this.ability.getSourceCard(); @@ -136,8 +127,8 @@ public class SpellAbilityRequirements { Singletons.getModel().getGame().getAction().moveTo(fromZone, c, zonePosition >= 0 ? Integer.valueOf(zonePosition) : null); } - if (this.select != null) { - this.select.clearTargets(); + if (select != null) { + select.clearTargets(); } this.ability.resetOnceResolved(); @@ -147,7 +138,7 @@ public class SpellAbilityRequirements { } - public boolean announceRequirements() { + private boolean announceRequirements() { // Announcing Requirements like Choosing X or Multikicker // SA Params as comma delimited list String announce = ability.getParam("Announce"); diff --git a/src/main/java/forge/card/spellability/TargetChooser.java b/src/main/java/forge/card/spellability/TargetChooser.java index 343ba088d56..64ecd9b766f 100644 --- a/src/main/java/forge/card/spellability/TargetChooser.java +++ b/src/main/java/forge/card/spellability/TargetChooser.java @@ -20,24 +20,19 @@ package forge.card.spellability; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import com.google.common.base.Predicate; import forge.Card; import forge.CardLists; import forge.FThreads; -import forge.GameEntity; import forge.card.ability.AbilityUtils; -import forge.card.ability.ApiType; -import forge.control.input.InputSyncronizedBase; +import forge.control.input.InputSelectTargets; import forge.game.GameState; import forge.game.player.Player; import forge.game.zone.Zone; import forge.game.zone.ZoneType; import forge.gui.GuiChoose; -import forge.view.ButtonUtil; /** *

@@ -48,249 +43,18 @@ import forge.view.ButtonUtil; * @version $Id$ */ public class TargetChooser { - /** - * TODO: Write javadoc for this type. - * - */ - public static final class InputSelectTargets extends InputSyncronizedBase { - private final List choices; - // some cards can be targeted several times (eg: distribute damage as you choose) - private final Map targetDepth = new HashMap(); - private final Target tgt; - private final SpellAbility sa; - private boolean bCancel = false; - private boolean bOk = false; - private final boolean mandatory; - private static final long serialVersionUID = -1091595663541356356L; - - public final boolean hasCancelled() { return bCancel; } - public final boolean hasPressedOk() { return bOk; } - /** - * TODO: Write javadoc for Constructor. - * @param select - * @param choices - * @param req - * @param alreadyTargeted - * @param targeted - * @param tgt - * @param sa - * @param mandatory - */ - public InputSelectTargets(List choices, SpellAbility sa, boolean mandatory) { - super(sa.getActivatingPlayer()); - this.choices = choices; - this.tgt = sa.getTarget(); - this.sa = sa; - this.mandatory = mandatory; - } - - @Override - public void showMessage() { - final StringBuilder sb = new StringBuilder(); - sb.append("Targeted:\n"); - for (final Entry o : targetDepth.entrySet()) { - sb.append(o.getKey()); - if( o.getValue() > 1 ) - sb.append(" (").append(o.getValue()).append(" times)"); - sb.append("\n"); - } - //sb.append(tgt.getTargetedString()).append("\n"); - sb.append(tgt.getVTSelection()); - - showMessage(sb.toString()); - - // If reached Minimum targets, enable OK button - if (!tgt.isMinTargetsChosen(sa.getSourceCard(), sa) || tgt.isDividedAsYouChoose()) { - if (mandatory && tgt.hasCandidates(sa, true)) { - // Player has to click on a target - ButtonUtil.disableAll(); - } else { - ButtonUtil.enableOnlyCancel(); - } - } else { - if (mandatory && tgt.hasCandidates(sa, true)) { - // Player has to click on a target or ok - ButtonUtil.enableOnlyOk(); - } else { - ButtonUtil.enableAllFocusOk(); - } - } - } - - @Override - public void selectButtonCancel() { - bCancel = true; - this.done(); - } - - @Override - public void selectButtonOK() { - bOk = true; - this.done(); - } - - @Override - public void selectCard(final Card card) { - if (!tgt.isUniqueTargets() && targetDepth.containsKey(card)) { - return; - } - - // leave this in temporarily, there some seriously wrong things going on here - if (!card.canBeTargetedBy(sa)) { - showMessage("Cannot target this card (Shroud? Protection? Restrictions?)."); - return; - } - if (!choices.contains(card)) { - showMessage("This card is not a valid choice for some other reason besides (Shroud? Protection? Restrictions?)."); - return; - } - - if (tgt.isDividedAsYouChoose()) { - final int stillToDivide = tgt.getStillToDivide(); - int allocatedPortion = 0; - // allow allocation only if the max targets isn't reached and there are more candidates - if ((tgt.getNumTargeted() + 1 < tgt.getMaxTargets(sa.getSourceCard(), sa)) - && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) { - final Integer[] choices = new Integer[stillToDivide]; - for (int i = 1; i <= stillToDivide; i++) { - choices[i - 1] = i; - } - String apiBasedMessage = "Distribute how much to "; - if (sa.getApi() == ApiType.DealDamage) { - apiBasedMessage = "Select how much damage to deal to "; - } else if (sa.getApi() == ApiType.PreventDamage) { - apiBasedMessage = "Select how much damage to prevent to "; - } else if (sa.getApi() == ApiType.PutCounter) { - apiBasedMessage = "Select how many counters to distribute to "; - } - final StringBuilder sb = new StringBuilder(); - sb.append(apiBasedMessage); - sb.append(card.toString()); - Integer chosen = GuiChoose.oneOrNone(sb.toString(), choices); - if (null == chosen) { - return; - } - allocatedPortion = chosen; - } else { // otherwise assign the rest of the damage/protection - allocatedPortion = stillToDivide; - } - tgt.setStillToDivide(stillToDivide - allocatedPortion); - tgt.addDividedAllocation(card, allocatedPortion); - } - addTarget(card); - } // selectCard() - - @Override - public void selectPlayer(final Player player) { - if (!tgt.isUniqueTargets() && targetDepth.containsKey(player)) { - return; - } - - if (!sa.canTarget(player)) { - showMessage("Cannot target this player (Hexproof? Protection? Restrictions?)."); - return; - } - - if (tgt.isDividedAsYouChoose()) { - final int stillToDivide = tgt.getStillToDivide(); - int allocatedPortion = 0; - // allow allocation only if the max targets isn't reached and there are more candidates - if ((tgt.getNumTargeted() + 1 < tgt.getMaxTargets(sa.getSourceCard(), sa)) && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) { - final Integer[] choices = new Integer[stillToDivide]; - for (int i = 1; i <= stillToDivide; i++) { - choices[i - 1] = i; - } - String apiBasedMessage = "Distribute how much to "; - if (sa.getApi() == ApiType.DealDamage) { - apiBasedMessage = "Select how much damage to deal to "; - } else if (sa.getApi() == ApiType.PreventDamage) { - apiBasedMessage = "Select how much damage to prevent to "; - } - final StringBuilder sb = new StringBuilder(); - sb.append(apiBasedMessage); - sb.append(player.getName()); - Integer chosen = GuiChoose.oneOrNone(sb.toString(), choices); - if (null == chosen) { - return; - } - allocatedPortion = chosen; - } else { // otherwise assign the rest of the damage/protection - allocatedPortion = stillToDivide; - } - tgt.setStillToDivide(stillToDivide - allocatedPortion); - tgt.addDividedAllocation(player, allocatedPortion); - } - addTarget(player); - } - - void addTarget(GameEntity ge) { - tgt.addTarget(ge); - Integer val = targetDepth.get(ge); - targetDepth.put(ge, val == null ? Integer.valueOf(1) : Integer.valueOf(val.intValue() + 1) ); - - if(hasAllTargets()) { - bOk = true; - this.done(); - } - else - this.showMessage(); - } - - void done() { - this.stop(); - } - - boolean hasAllTargets() { - return tgt.isMaxTargetsChosen(sa.getSourceCard(), sa); - } - } - private final SpellAbility ability; - /** - *

- * Constructor for Target_Selection. - *

- * - * @param tgt - * a {@link forge.card.spellability.Target} object. - * @param sa - * a {@link forge.card.spellability.SpellAbility} object. - */ + public TargetChooser(final SpellAbility sa) { this.ability = sa; } - /** - *

- * getTgt. - *

- * - * @return a {@link forge.card.spellability.Target} object. - */ - public final Target getTgt() { + private final Target getTgt() { return this.ability.getTarget(); } - /** - *

- * Getter for the field ability. - *

- * - * @return a {@link forge.card.spellability.SpellAbility} object. - */ - public final SpellAbility getAbility() { - return this.ability; - } - - /** - *

- * Getter for the field card. - *

- * - * @return a {@link forge.Card} object. - */ - public final Card getCard() { + private final Card getCard() { return this.ability.getSourceCard(); } @@ -342,7 +106,7 @@ public class TargetChooser { public final boolean chooseTargets() { Target tgt = getTgt(); - final boolean canTarget = tgt == null ? false : tgt.doesTarget(); + final boolean canTarget = doesTarget(); final int minTargets = canTarget ? tgt.getMinTargets(getCard(), ability) : 0; final int maxTargets = canTarget ? tgt.getMaxTargets(getCard(), ability) : 0; final int numTargeted = canTarget ? tgt.getNumTargeted() : 0; diff --git a/src/main/java/forge/control/input/InputSelectTargets.java b/src/main/java/forge/control/input/InputSelectTargets.java new file mode 100644 index 00000000000..3d5462a7a0b --- /dev/null +++ b/src/main/java/forge/control/input/InputSelectTargets.java @@ -0,0 +1,212 @@ +package forge.control.input; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import forge.Card; +import forge.GameEntity; +import forge.card.ability.ApiType; +import forge.card.spellability.SpellAbility; +import forge.card.spellability.Target; +import forge.game.player.Player; +import forge.gui.GuiChoose; +import forge.view.ButtonUtil; + +/** + * TODO: Write javadoc for this type. + * + */ +public final class InputSelectTargets extends InputSyncronizedBase { + private final List choices; + // some cards can be targeted several times (eg: distribute damage as you choose) + private final Map targetDepth = new HashMap(); + private final Target tgt; + private final SpellAbility sa; + private boolean bCancel = false; + private boolean bOk = false; + private final boolean mandatory; + private static final long serialVersionUID = -1091595663541356356L; + + public final boolean hasCancelled() { return bCancel; } + public final boolean hasPressedOk() { return bOk; } + /** + * TODO: Write javadoc for Constructor. + * @param select + * @param choices + * @param req + * @param alreadyTargeted + * @param targeted + * @param tgt + * @param sa + * @param mandatory + */ + public InputSelectTargets(List choices, SpellAbility sa, boolean mandatory) { + super(sa.getActivatingPlayer()); + this.choices = choices; + this.tgt = sa.getTarget(); + this.sa = sa; + this.mandatory = mandatory; + } + + @Override + public void showMessage() { + final StringBuilder sb = new StringBuilder(); + sb.append("Targeted:\n"); + for (final Entry o : targetDepth.entrySet()) { + sb.append(o.getKey()); + if( o.getValue() > 1 ) + sb.append(" (").append(o.getValue()).append(" times)"); + sb.append("\n"); + } + //sb.append(tgt.getTargetedString()).append("\n"); + sb.append(tgt.getVTSelection()); + + showMessage(sb.toString()); + + // If reached Minimum targets, enable OK button + if (!tgt.isMinTargetsChosen(sa.getSourceCard(), sa) || tgt.isDividedAsYouChoose()) { + if (mandatory && tgt.hasCandidates(sa, true)) { + // Player has to click on a target + ButtonUtil.disableAll(); + } else { + ButtonUtil.enableOnlyCancel(); + } + } else { + if (mandatory && tgt.hasCandidates(sa, true)) { + // Player has to click on a target or ok + ButtonUtil.enableOnlyOk(); + } else { + ButtonUtil.enableAllFocusOk(); + } + } + } + + @Override + public void selectButtonCancel() { + bCancel = true; + this.done(); + } + + @Override + public void selectButtonOK() { + bOk = true; + this.done(); + } + + @Override + public void selectCard(final Card card) { + if (!tgt.isUniqueTargets() && targetDepth.containsKey(card)) { + return; + } + + // leave this in temporarily, there some seriously wrong things going on here + if (!card.canBeTargetedBy(sa)) { + showMessage("Cannot target this card (Shroud? Protection? Restrictions?)."); + return; + } + if (!choices.contains(card)) { + showMessage("This card is not a valid choice for some other reason besides (Shroud? Protection? Restrictions?)."); + return; + } + + if (tgt.isDividedAsYouChoose()) { + final int stillToDivide = tgt.getStillToDivide(); + int allocatedPortion = 0; + // allow allocation only if the max targets isn't reached and there are more candidates + if ((tgt.getNumTargeted() + 1 < tgt.getMaxTargets(sa.getSourceCard(), sa)) + && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) { + final Integer[] choices = new Integer[stillToDivide]; + for (int i = 1; i <= stillToDivide; i++) { + choices[i - 1] = i; + } + String apiBasedMessage = "Distribute how much to "; + if (sa.getApi() == ApiType.DealDamage) { + apiBasedMessage = "Select how much damage to deal to "; + } else if (sa.getApi() == ApiType.PreventDamage) { + apiBasedMessage = "Select how much damage to prevent to "; + } else if (sa.getApi() == ApiType.PutCounter) { + apiBasedMessage = "Select how many counters to distribute to "; + } + final StringBuilder sb = new StringBuilder(); + sb.append(apiBasedMessage); + sb.append(card.toString()); + Integer chosen = GuiChoose.oneOrNone(sb.toString(), choices); + if (null == chosen) { + return; + } + allocatedPortion = chosen; + } else { // otherwise assign the rest of the damage/protection + allocatedPortion = stillToDivide; + } + tgt.setStillToDivide(stillToDivide - allocatedPortion); + tgt.addDividedAllocation(card, allocatedPortion); + } + addTarget(card); + } // selectCard() + + @Override + public void selectPlayer(final Player player) { + if (!tgt.isUniqueTargets() && targetDepth.containsKey(player)) { + return; + } + + if (!sa.canTarget(player)) { + showMessage("Cannot target this player (Hexproof? Protection? Restrictions?)."); + return; + } + + if (tgt.isDividedAsYouChoose()) { + final int stillToDivide = tgt.getStillToDivide(); + int allocatedPortion = 0; + // allow allocation only if the max targets isn't reached and there are more candidates + if ((tgt.getNumTargeted() + 1 < tgt.getMaxTargets(sa.getSourceCard(), sa)) && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) { + final Integer[] choices = new Integer[stillToDivide]; + for (int i = 1; i <= stillToDivide; i++) { + choices[i - 1] = i; + } + String apiBasedMessage = "Distribute how much to "; + if (sa.getApi() == ApiType.DealDamage) { + apiBasedMessage = "Select how much damage to deal to "; + } else if (sa.getApi() == ApiType.PreventDamage) { + apiBasedMessage = "Select how much damage to prevent to "; + } + final StringBuilder sb = new StringBuilder(); + sb.append(apiBasedMessage); + sb.append(player.getName()); + Integer chosen = GuiChoose.oneOrNone(sb.toString(), choices); + if (null == chosen) { + return; + } + allocatedPortion = chosen; + } else { // otherwise assign the rest of the damage/protection + allocatedPortion = stillToDivide; + } + tgt.setStillToDivide(stillToDivide - allocatedPortion); + tgt.addDividedAllocation(player, allocatedPortion); + } + addTarget(player); + } + + void addTarget(GameEntity ge) { + tgt.addTarget(ge); + Integer val = targetDepth.get(ge); + targetDepth.put(ge, val == null ? Integer.valueOf(1) : Integer.valueOf(val.intValue() + 1) ); + + if(hasAllTargets()) { + bOk = true; + this.done(); + } + else + this.showMessage(); + } + + void done() { + this.stop(); + } + + boolean hasAllTargets() { + return tgt.isMaxTargetsChosen(sa.getSourceCard(), sa); + } +} \ No newline at end of file diff --git a/src/main/java/forge/game/GameActionPlay.java b/src/main/java/forge/game/GameActionPlay.java index d822d19a906..b5d0950368c 100644 --- a/src/main/java/forge/game/GameActionPlay.java +++ b/src/main/java/forge/game/GameActionPlay.java @@ -66,7 +66,9 @@ public class GameActionPlay { public final void playSpellAbilityWithoutPayingManaCost(final SpellAbility sa) { FThreads.checkEDT("GameActionPlay.playSpellAbilityWithoutPayingManaCost", false); final Card source = sa.getSourceCard(); - setSplitCardState(source, sa); // Split card support + + if( source.isSplitCard()) + setSplitCardState(source, sa); if (sa.getPayCosts() != null) { if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { @@ -75,8 +77,7 @@ public class GameActionPlay { final CostPayment payment = new CostPayment(sa.getPayCosts(), sa); final SpellAbilityRequirements req = new SpellAbilityRequirements(sa, payment); - req.setFree(); - req.fillRequirements(); + req.fillRequirements(false, true, false); } else { if (sa.isSpell()) { final Card c = sa.getSourceCard(); @@ -356,8 +357,8 @@ public class GameActionPlay { final Card source = sa.getSourceCard(); - // Split card support - setSplitCardState(source, sa); + if(source.isSplitCard()) + setSplitCardState(source, sa); if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { CharmEffect.makeChoices(sa); @@ -389,7 +390,7 @@ public class GameActionPlay { } final SpellAbilityRequirements req = new SpellAbilityRequirements(sa, payment); - req.fillRequirements(); + req.fillRequirements(false, false, false); } else { ManaCostBeingPaid manaCost = new ManaCostBeingPaid(sa.getManaCost()); if (sa.getSourceCard().isCopiedSpell() && sa.isSpell()) { @@ -437,10 +438,8 @@ public class GameActionPlay { } final SpellAbilityRequirements req = new SpellAbilityRequirements(sa, payment); - if( useOldTargets ) - req.setAlreadyTargeted(); - req.setSkipStack(); - req.fillRequirements(); + + req.fillRequirements(useOldTargets, false, true); } else { ManaCostBeingPaid manaCost = new ManaCostBeingPaid(sa.getManaCost()); if (sa.getSourceCard().isCopiedSpell() && sa.isSpell()) { @@ -492,21 +491,20 @@ public class GameActionPlay { private void setSplitCardState(final Card source, SpellAbility sa) { // Split card support - if (source.isSplitCard()) { - List leftSplitAbilities = source.getState(CardCharacteristicName.LeftSplit).getSpellAbility(); - List rightSplitAbilities = source.getState(CardCharacteristicName.RightSplit).getSpellAbility(); - for (SpellAbility a : leftSplitAbilities) { - if (sa == a || sa.getDescription().equals(String.format("%s (without paying its mana cost)", a.getDescription()))) { - source.setState(CardCharacteristicName.LeftSplit); - break; - } - } - for (SpellAbility a : rightSplitAbilities) { - if (sa == a || sa.getDescription().equals(String.format("%s (without paying its mana cost)", a.getDescription()))) { - source.setState(CardCharacteristicName.RightSplit); - break; - } + List leftSplitAbilities = source.getState(CardCharacteristicName.LeftSplit).getSpellAbility(); + List rightSplitAbilities = source.getState(CardCharacteristicName.RightSplit).getSpellAbility(); + for (SpellAbility a : leftSplitAbilities) { + if (sa == a || sa.getDescription().equals(String.format("%s (without paying its mana cost)", a.getDescription()))) { + source.setState(CardCharacteristicName.LeftSplit); + return; } } + for (SpellAbility a : rightSplitAbilities) { + if (sa == a || sa.getDescription().equals(String.format("%s (without paying its mana cost)", a.getDescription()))) { + source.setState(CardCharacteristicName.RightSplit); + return; + } + } + throw new RuntimeException("Not found which part to choose for ability " + sa + " from card " + source); } }