Additional and alternative costs improved:

* Now player must choose additional costs before ability's modes;
* Fixed broken kicker ability from ZNR (see comments from d4ca287f0f);
* Improved compatibility of additional cost with cost modification effects (fixed that optional multi-costs doesn't affected by cost modification);
* Improved compatibility of additional cost with alternative cost (some cards ignores additional cost on alternative usage, e.g. on play free);
This commit is contained in:
Oleg Agafonov 2020-09-13 09:56:55 +04:00
parent 586538a66c
commit 6e0c7e868c
17 changed files with 504 additions and 156 deletions

View file

@ -8,7 +8,7 @@ import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.FightTargetsEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.abilities.keyword.KickerAbility;
import mage.abilities.keyword.KickerWithAnyNumberModesAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
@ -33,12 +33,11 @@ public final class InscriptionOfAbundance extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{G}");
// Kicker {2}{G}
this.addAbility(new KickerAbility(new ManaCostsImpl<>("{2}{G}")));
this.addAbility(new KickerWithAnyNumberModesAbility(new ManaCostsImpl<>("{2}{G}")));
// Choose one. If this spell was kicked, choose any number instead.
this.getSpellAbility().getModes().setAllWhenKicked(true);
// Put two +1/+1 counters on target creature.
this.getSpellAbility().getModes().setChooseText("choose one. If this spell was kicked, choose any number instead.");
this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance(2)));
this.getSpellAbility().addTarget(new TargetCreaturePermanent());

View file

@ -7,7 +7,7 @@ import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.ReturnToHandTargetEffect;
import mage.abilities.effects.keyword.ScryEffect;
import mage.abilities.keyword.KickerAbility;
import mage.abilities.keyword.KickerWithAnyNumberModesAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
@ -29,12 +29,11 @@ public final class InscriptionOfInsight extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{U}");
// Kicker {2}{U}{U}
this.addAbility(new KickerAbility(new ManaCostsImpl<>("{2}{U}{U}")));
this.addAbility(new KickerWithAnyNumberModesAbility(new ManaCostsImpl<>("{2}{U}{U}")));
// Choose one. If this spell was kicked, choose any number instead.
this.getSpellAbility().getModes().setAllWhenKicked(true);
// Return up to two target creatures to their owners' hands.
this.getSpellAbility().getModes().setChooseText("choose one. If this spell was kicked, choose any number instead.");
this.getSpellAbility().addEffect(new ReturnToHandTargetEffect());
this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, 2));

View file

@ -5,7 +5,7 @@ import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.DestroyTargetEffect;
import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect;
import mage.abilities.effects.common.discard.DiscardTargetEffect;
import mage.abilities.keyword.KickerAbility;
import mage.abilities.keyword.KickerWithAnyNumberModesAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
@ -40,12 +40,11 @@ public final class InscriptionOfRuin extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}");
// Kicker {2}{B}{B}
this.addAbility(new KickerAbility(new ManaCostsImpl<>("{2}{B}{B}")));
this.addAbility(new KickerWithAnyNumberModesAbility(new ManaCostsImpl<>("{2}{B}{B}")));
// Choose one. If this spell was kicked, choose any number instead.
this.getSpellAbility().getModes().setAllWhenKicked(true);
// Target opponent discards two cards.
this.getSpellAbility().getModes().setChooseText("choose one. If this spell was kicked, choose any number instead.");
this.getSpellAbility().addEffect(new DiscardTargetEffect(2));
this.getSpellAbility().addTarget(new TargetOpponent());

View file

@ -1,4 +1,3 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
@ -7,12 +6,149 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author LevelX2
* @author LevelX2, JayDi85
*/
public class EntwineTest extends CardTestPlayerBase {
@Test
public void test_CastWithoutEntwine() {
// Choose one
// Barbed Lightning deals 3 damage to target creature.
// Barbed Lightning deals 3 damage to target player or planeswalker.
// Entwine {2} (Choose both if you pay the entwine cost.)
addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
//
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning");
setChoice(playerA, "No"); // not use Entwine
setModeChoice(playerA, "1"); // target creature
addTarget(playerA, "Balduvian Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertLife(playerA, 20);
assertPermanentCount(playerA, "Balduvian Bears", 0);
assertTappedCount("Mountain", true, 3);
}
@Test
public void test_CastEntwine_Normal() {
// Choose one
// Barbed Lightning deals 3 damage to target creature.
// Barbed Lightning deals 3 damage to target player or planeswalker.
// Entwine {2} (Choose both if you pay the entwine cost.)
addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2);
//
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning");
setChoice(playerA, "Yes"); // use Entwine
addTarget(playerA, "Balduvian Bears");
addTarget(playerA, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertLife(playerA, 20 - 3);
assertPermanentCount(playerA, "Balduvian Bears", 0);
assertTappedCount("Mountain", true, 3 + 2);
}
@Test
public void test_CastEntwine_CostReduction() {
addCustomEffect_SpellCostModification(playerA, -4);
// Choose one
// Barbed Lightning deals 3 damage to target creature.
// Barbed Lightning deals 3 damage to target player or planeswalker.
// Entwine {2} (Choose both if you pay the entwine cost.)
addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // -4 as cost reduction
//
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning");
setChoice(playerA, "Yes"); // use Entwine
addTarget(playerA, "Balduvian Bears");
addTarget(playerA, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertLife(playerA, 20 - 3);
assertPermanentCount(playerA, "Balduvian Bears", 0);
assertTappedCount("Mountain", true, 1);
}
@Test
public void test_CastEntwine_CostIncreasing() {
addCustomEffect_SpellCostModification(playerA, 5);
// Choose one
// Barbed Lightning deals 3 damage to target creature.
// Barbed Lightning deals 3 damage to target player or planeswalker.
// Entwine {2} (Choose both if you pay the entwine cost.)
addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2 + 5);
//
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning");
setChoice(playerA, "Yes"); // use Entwine
addTarget(playerA, "Balduvian Bears");
addTarget(playerA, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertLife(playerA, 20 - 3);
assertPermanentCount(playerA, "Balduvian Bears", 0);
assertTappedCount("Mountain", true, 3 + 2 + 5);
}
@Test
public void test_CastEntwine_FreeFromHand() {
// You may cast nonland cards from your hand without paying their mana costs.
addCard(Zone.BATTLEFIELD, playerA, "Omniscience");
// Choose one
// Barbed Lightning deals 3 damage to target creature.
// Barbed Lightning deals 3 damage to target player or planeswalker.
// Entwine {2} (Choose both if you pay the entwine cost.)
addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R}
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); // only Entwine pay need
//
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning");
setChoice(playerA, "Yes"); // cast for free
setChoice(playerA, "Yes"); // use Entwine
addTarget(playerA, "Balduvian Bears");
addTarget(playerA, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertLife(playerA, 20 - 3);
assertPermanentCount(playerA, "Balduvian Bears", 0);
assertTappedCount("Plains", true, 2);
}
@Test
public void test_ToothAndNail() {
setStrictChooseMode(true);
@ -27,17 +163,13 @@ public class EntwineTest extends CardTestPlayerBase {
// Entwine {2}
addCard(Zone.HAND, playerA, "Tooth and Nail"); // Sorcery {5}{G}{G}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tooth and Nail");
setChoice(playerA, "Yes"); // Message: Pay Entwine {2} ?
addTarget(playerA, "Silvercoat Lion^Pillarfield Ox");
setChoice(playerA, "Silvercoat Lion^Pillarfield Ox");
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Silvercoat Lion", 1);

View file

@ -400,6 +400,7 @@ public class KickerTest extends CardTestPlayerBase {
assertTappedCount("Swamp", true, 5);
assertGraveyardCount(playerA, "Marsh Casualties", 1);
assertPowerToughness(playerB, "Centaur Courser", 1, 1);
}
}

View file

@ -0,0 +1,158 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author JayDi85
*/
public class KickerWithAnyNumberModesAbilityTest extends CardTestPlayerBase {
@Test
public void test_WithoutKicker() {
// Kicker {2}{G}
// Choose one. If this spell was kicked, choose any number instead.
// Put two +1/+1 counters on target creature.
// Target player gains X life, where X is the greatest power among creatures they control.
// Target creature you control fights target creature you don't control.
addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance");
setChoice(playerA, "No"); // no kicker
setModeChoice(playerA, "1");
addTarget(playerA, "Balduvian Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2);
assertLife(playerA, 20);
assertTappedCount("Forest", true, 2);
}
@Test
public void test_Kicker_Normal() {
// Kicker {2}{G}
// Choose one. If this spell was kicked, choose any number instead.
// Put two +1/+1 counters on target creature.
// Target player gains X life, where X is the greatest power among creatures they control.
// Target creature you control fights target creature you don't control.
addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 3);
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance");
setChoice(playerA, "Yes"); // use kicker
setModeChoice(playerA, "2");
setModeChoice(playerA, "1");
addTarget(playerA, playerA); // gain x life
addTarget(playerA, "Balduvian Bears"); // get counters
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2);
assertLife(playerA, 20 + 4);
assertTappedCount("Forest", true, 2 + 3);
}
@Test
public void test_Kicker_CostReduction() {
addCustomEffect_SpellCostModification(playerA, -4);
// Kicker {2}{G}
// Choose one. If this spell was kicked, choose any number instead.
// Put two +1/+1 counters on target creature.
// Target player gains X life, where X is the greatest power among creatures they control.
// Target creature you control fights target creature you don't control.
addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 3 - 3); // -3 by cost reduction
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance");
setChoice(playerA, "Yes"); // use kicker
setModeChoice(playerA, "2");
setModeChoice(playerA, "1");
addTarget(playerA, playerA); // gain x life
addTarget(playerA, "Balduvian Bears"); // get counters
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2);
assertLife(playerA, 20 + 4);
assertTappedCount("Forest", true, 2 + 3 - 3);
}
@Test
public void test_Kicker_CostIncreasing() {
addCustomEffect_SpellCostModification(playerA, 5);
// Kicker {2}{G}
// Choose one. If this spell was kicked, choose any number instead.
// Put two +1/+1 counters on target creature.
// Target player gains X life, where X is the greatest power among creatures they control.
// Target creature you control fights target creature you don't control.
addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 3 + 5); // +5 by cost increase
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance");
setChoice(playerA, "Yes"); // use kicker
setModeChoice(playerA, "2");
setModeChoice(playerA, "1");
addTarget(playerA, playerA); // gain x life
addTarget(playerA, "Balduvian Bears"); // get counters
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2);
assertLife(playerA, 20 + 4);
assertTappedCount("Forest", true, 2 + 3 + 5);
}
@Test
public void test_Kicker_FreeFromHand() {
// You may cast nonland cards from your hand without paying their mana costs.
addCard(Zone.BATTLEFIELD, playerA, "Omniscience");
// Kicker {2}{G}
// Choose one. If this spell was kicked, choose any number instead.
// Put two +1/+1 counters on target creature.
// Target player gains X life, where X is the greatest power among creatures they control.
// Target creature you control fights target creature you don't control.
addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); // -2 by free cast
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance");
setChoice(playerA, "Yes"); // use free cast
setChoice(playerA, "Yes"); // use kicker
setModeChoice(playerA, "2");
setModeChoice(playerA, "1");
addTarget(playerA, playerA); // gain x life
addTarget(playerA, "Balduvian Bears"); // get counters
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2);
assertLife(playerA, 20 + 4);
assertTappedCount("Forest", true, 3);
}
}

View file

@ -192,7 +192,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase {
/**
* Omniscience is not allowing me to cast spells for free. I'm playing a
* Commander game against the Computer, if that helps.
*
* <p>
* Edit: It's not letting me cast fused spells for free. Others seems to be
* working.
*/
@ -276,16 +276,23 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase {
// Choose one - Barbed Lightning deals 3 damage to target creature; or Barbed Lightning deals 3 damage to target player.
// Entwine {2} (Choose both if you pay the entwine cost.)
addCard(Zone.HAND, playerA, "Barbed Lightning", 1);
addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R}
// Creature - 3/3 Swampwalk
addCard(Zone.BATTLEFIELD, playerB, "Bog Wraith", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning", "Bog Wraith");
addTarget(playerA, playerB);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning");
setChoice(playerA, "Yes"); // cast without cost
setChoice(playerA, "Yes"); // pay Entwine
addTarget(playerA, "Bog Wraith"); // target form mode 1
addTarget(playerA, playerB); // target for mode 2
setStopAt(1, PhaseStep.BEGIN_COMBAT);
showBattlefield("after", 1, PhaseStep.POSTCOMBAT_MAIN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerA, "Barbed Lightning", 1);
assertGraveyardCount(playerB, "Bog Wraith", 1);
@ -293,7 +300,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase {
assertLife(playerA, 20);
assertLife(playerB, 17);
assertTapped("Plains", true); // plains have to be tapped because {2} from Entwine have to be paid
assertTappedCount("Plains", true, 2); // plains have to be tapped because {2} from Entwine have to be paid
}
/**

View file

@ -1,4 +1,3 @@
package org.mage.test.cards.modal;
import mage.abilities.keyword.SwampwalkAbility;
@ -8,7 +7,6 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author LevelX2
*/
public class ChooseOneTest extends CardTestPlayerBase {
@ -48,8 +46,10 @@ public class ChooseOneTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Funeral Charm", "Silvercoat Lion");
setModeChoice(playerA, "2");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerA, "Funeral Charm", 1);
assertPowerToughness(playerB, "Silvercoat Lion", 4, 1);

View file

@ -1,9 +1,5 @@
package mage.abilities;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import mage.MageIdentifier;
import mage.MageObject;
import mage.abilities.costs.*;
@ -36,6 +32,11 @@ import mage.util.ThreadLocalStringBuilder;
import mage.watchers.Watcher;
import org.apache.log4j.Logger;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
/**
* @author BetaSteward_at_googlemail.com
*/
@ -227,22 +228,10 @@ public abstract class AbilityImpl implements Ability {
}
game.applyEffects();
/* 20130201 - 601.2b
* If the spell is modal the player announces the mode choice (see rule 700.2).
*/
if (!getModes().choose(game, this)) {
return false;
}
MageObject sourceObject = getSourceObject(game);
if (getSourceObjectZoneChangeCounter() == 0) {
setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(getSourceId()));
}
if (controller.isTestMode()) {
if (!controller.addTargets(this, game)) {
return false;
}
}
/* 20130201 - 601.2b
* If the player wishes to splice any cards onto the spell (see rule 702.45), he
@ -268,9 +257,6 @@ public abstract class AbilityImpl implements Ability {
}
}
if (getModes().getAdditionalCost() != null) {
getModes().getAdditionalCost().addOptionalAdditionalModeCosts(this, game);
}
// 20130201 - 601.2b
// If the spell has alternative or additional costs that will be paid as it's being cast such
// as buyback, kicker, or convoke costs (see rules 117.8 and 117.9), the player announces his
@ -311,8 +297,29 @@ public abstract class AbilityImpl implements Ability {
handlePhyrexianManaCosts(game, sourceId, controller);
/* 20130201 - 601.2b
* If the spell is modal the player announces the mode choice (see rule 700.2).
*/
// rules:
// You kick a spell as you cast it. You declare whether you're going to pay a kicker cost at the same
// time you'd choose a spell's mode, and then you actually pay it at the same time you pay the spell's mana cost.
// Kicking a spell is always optional.
if (!getModes().choose(game, this)) {
return false;
}
// unit tests only: it allows to add targets/choices by two ways:
// 1. From cast/activate command params (process it here)
// 2. From single addTarget/setChoice, it's a preffered method for tests (process it in normal choose dialogs like human player)
if (controller.isTestMode()) {
if (!controller.addTargets(this, game)) {
return false;
}
}
for (UUID modeId : this.getModes().getSelectedModes()) {
this.getModes().setActiveMode(modeId);
//20121001 - 601.2c
// 601.2c The player announces their choice of an appropriate player, object, or zone for
// each target the spell requires. A spell may require some targets only if an alternative or
@ -333,8 +340,10 @@ public abstract class AbilityImpl implements Ability {
if (sourceObject != null && this.getAbilityType() != AbilityType.TRIGGERED) { // triggered abilities check this already in playerImpl.triggerAbility
sourceObject.adjustTargets(this, game);
}
if (!getTargets().isEmpty()) {
Outcome outcome = getEffects().getOutcome(this);
// only activated abilities can be canceled by human user (not triggered)
boolean canCancel = this instanceof ActivatedAbility && controller.isHuman();
if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, canCancel)) {
@ -434,7 +443,11 @@ public abstract class AbilityImpl implements Ability {
boolean alternativeCostUsed = false;
if (sourceObject != null && !(sourceObject instanceof Permanent)) {
// it's important to apply alternative cost first
// example: Omniscience gives free mana as alternative, but Entwine ability adds {2} as additional
Abilities<Ability> abilities = CardUtil.getAbilities(sourceObject, game);
// 1. ALTERNATIVE COSTS
for (Ability ability : abilities) {
// if cast for noMana no Alternative costs are allowed
if (canUseAlternativeCost && !noMana && ability instanceof AlternativeSourceCosts) {
@ -447,11 +460,7 @@ public abstract class AbilityImpl implements Ability {
}
}
}
if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) {
((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game);
}
}
// controller specific alternate spell costs
if (canUseAlternativeCost && !noMana && !alternativeCostUsed) {
if (this.getAbilityType() == AbilityType.SPELL
@ -469,6 +478,13 @@ public abstract class AbilityImpl implements Ability {
}
}
}
// 2. ADDITIONAL COST
for (Ability ability : abilities) {
if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) {
((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game);
}
}
}
return alternativeCostUsed;
@ -806,7 +822,18 @@ public abstract class AbilityImpl implements Ability {
@Override
public void addCost(Cost cost) {
if (cost != null) {
if (cost == null) {
return;
}
if (cost instanceof Costs) {
// as list of costs
Costs<Cost> list = (Costs<Cost>) cost;
for (Cost single : list) {
addCost(single);
}
} else {
// as single cost
if (cost instanceof ManaCost) {
this.addManaCost((ManaCost) cost);
} else {

View file

@ -1,6 +1,5 @@
package mage.abilities;
import mage.abilities.condition.common.KickedCondition;
import mage.abilities.costs.OptionalAdditionalModeSourceCosts;
import mage.cards.Card;
import mage.constants.Outcome;
@ -33,11 +32,9 @@ public class Modes extends LinkedHashMap<UUID, Mode> {
private TargetController modeChooser;
private boolean eachModeMoreThanOnce; // each mode can be selected multiple times during one choice
private boolean eachModeOnlyOnce; // state if each mode can be chosen only once as long as the source object exists
private OptionalAdditionalModeSourceCosts optionalAdditionalModeSourceCosts = null; // only set if costs have to be paid
private Filter maxModesFilter = null; // calculates the max number of available modes
private boolean isRandom = false;
private String chooseText = null;
private boolean allWhenKicked = false;
private boolean resetEachTurn = false;
private int turnNum = 0;
@ -68,12 +65,10 @@ public class Modes extends LinkedHashMap<UUID, Mode> {
this.modeChooser = modes.modeChooser;
this.eachModeOnlyOnce = modes.eachModeOnlyOnce;
this.eachModeMoreThanOnce = modes.eachModeMoreThanOnce;
this.optionalAdditionalModeSourceCosts = modes.optionalAdditionalModeSourceCosts;
this.maxModesFilter = modes.maxModesFilter; // can't change so no copy needed
this.isRandom = modes.isRandom;
this.chooseText = modes.chooseText;
this.allWhenKicked = modes.allWhenKicked;
this.resetEachTurn = modes.resetEachTurn;
this.turnNum = modes.turnNum;
if (modes.getSelectedModes().isEmpty()) {
@ -228,15 +223,6 @@ public class Modes extends LinkedHashMap<UUID, Mode> {
this.turnNum = game.getTurnNum();
}
}
if (this.allWhenKicked) {
if (KickedCondition.instance.apply(game, source)) {
this.setMinModes(0);
this.setMaxModes(3);
} else {
this.setMinModes(1);
this.setMaxModes(1);
}
}
if (this.size() > 1) {
this.clearSelectedModes();
if (this.isRandom) {
@ -244,15 +230,18 @@ public class Modes extends LinkedHashMap<UUID, Mode> {
this.addSelectedMode(modes.get(RandomUtil.nextInt(modes.size())).getId());
return true;
}
// check if mode modifying abilities exist
Card card = game.getCard(source.getSourceId());
if (card != null) {
for (Ability modeModifyingAbility : card.getAbilities()) {
for (Ability modeModifyingAbility : card.getAbilities(game)) {
if (modeModifyingAbility instanceof OptionalAdditionalModeSourceCosts) {
// cost must check activation conditional in changeModes
((OptionalAdditionalModeSourceCosts) modeModifyingAbility).changeModes(source, game);
}
}
}
// check if all modes can be activated automatically
if (this.size() == this.getMinModes() && !isEachModeMoreThanOnce()) {
Set<UUID> onceSelectedModes = null;
@ -434,8 +423,6 @@ public class Modes extends LinkedHashMap<UUID, Mode> {
StringBuilder sb = new StringBuilder();
if (this.chooseText != null) {
sb.append(chooseText);
} else if (this.allWhenKicked) {
sb.append("choose one. If this spell was kicked, choose any number instead.");
} else if (this.getMaxModesFilter() != null) {
sb.append("choose one or more. Each mode must target ").append(getMaxModesFilter().getMessage());
} else if (this.getMinModes() == 0 && this.getMaxModes() == 1) {
@ -499,22 +486,10 @@ public class Modes extends LinkedHashMap<UUID, Mode> {
this.eachModeMoreThanOnce = eachModeMoreThanOnce;
}
public OptionalAdditionalModeSourceCosts getAdditionalCost() {
return optionalAdditionalModeSourceCosts;
}
public void setAdditionalCost(OptionalAdditionalModeSourceCosts optionalAdditionalModeSourceCosts) {
this.optionalAdditionalModeSourceCosts = optionalAdditionalModeSourceCosts;
}
public void setRandom(boolean isRandom) {
this.isRandom = isRandom;
}
public void setAllWhenKicked(boolean allWhenKicked) {
this.allWhenKicked = allWhenKicked;
}
public boolean isResetEachTurn() {
return resetEachTurn;
}

View file

@ -1,4 +1,3 @@
package mage.abilities.costs;
import java.util.ArrayList;
@ -161,5 +160,4 @@ public class CostsImpl<T extends Cost> extends ArrayList<T> implements Costs<T>
public Costs<T> copy() {
return new CostsImpl(this);
}
}

View file

@ -1,10 +1,11 @@
package mage.abilities.costs;
import mage.util.Copyable;
/**
* @author LevelX2
*/
public interface OptionalAdditionalCost extends Cost {
public interface OptionalAdditionalCost extends Cost, Copyable<OptionalAdditionalCost> {
String getName();
@ -36,13 +37,11 @@ public interface OptionalAdditionalCost extends Cost {
/**
* If the player intends to pay the cost, the cost will be activated
*
*/
void activate();
/**
* Reset the activate and count information
*
*/
void reset();

View file

@ -1,18 +1,15 @@
package mage.abilities.costs;
import mage.abilities.Ability;
import mage.game.Game;
/**
* Cost that can change ability's modes (example: Kicker or Entwine can make all modes selectabled).
*
* @author LevelX2
*/
public interface OptionalAdditionalModeSourceCosts {
void addOptionalAdditionalModeCosts(Ability ability, Game game);
public interface OptionalAdditionalModeSourceCosts extends OptionalAdditionalSourceCosts {
void changeModes(Ability ability, Game game);
String getCastMessageSuffix();
}

View file

@ -3,14 +3,19 @@ package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.costs.*;
import mage.abilities.costs.Cost;
import mage.abilities.costs.OptionalAdditionalCost;
import mage.abilities.costs.OptionalAdditionalCostImpl;
import mage.abilities.costs.OptionalAdditionalModeSourceCosts;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.constants.AbilityType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.players.Player;
import java.util.Iterator;
import java.util.HashSet;
import java.util.Set;
/**
* 702.40. Entwine
@ -24,20 +29,23 @@ import java.util.Iterator;
* 702.40b If the entwine cost was paid, follow the text of each of the modes in
* the order written on the card when the spell resolves.
*
* @author LevelX2
* @author JayDi85
*/
public class EntwineAbility extends StaticAbility implements OptionalAdditionalModeSourceCosts {
private static final String keywordText = "Entwine";
protected static final String reminderText = "You may {cost} in addition to any other costs to use all modes.";
protected OptionalAdditionalCost additionalCost;
protected Set<String> activations = new HashSet<>(); // same logic as KickerAbility: activations per zoneChangeCounter
public EntwineAbility(String manaString) {
super(Zone.STACK, null);
this.additionalCost = new OptionalAdditionalCostImpl(keywordText, "Choose both if you pay the entwine cost.", new ManaCostsImpl(manaString));
this.additionalCost = new OptionalAdditionalCostImpl(keywordText, reminderText, new ManaCostsImpl(manaString));
}
public EntwineAbility(Cost cost) {
this(cost, "Choose both if you pay the entwine cost.");
this(cost, reminderText);
}
public EntwineAbility(Cost cost, String reminderText) {
@ -48,7 +56,10 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
public EntwineAbility(final EntwineAbility ability) {
super(ability);
additionalCost = ability.additionalCost;
if (ability.additionalCost != null) {
this.additionalCost = ability.additionalCost.copy();
}
this.activations.addAll(ability.activations);
}
@Override
@ -57,61 +68,25 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
}
@Override
public void addCost(Cost cost) {
if (additionalCost != null) {
((Costs) additionalCost).add(cost);
}
}
@Override
public boolean isActivated() {
if (additionalCost != null) {
return additionalCost.isActivated();
}
return false;
}
public void resetCosts() {
if (additionalCost != null) {
additionalCost.reset();
}
}
@Override
public void changeModes(Ability ability, Game game) {
public void addOptionalAdditionalCosts(Ability ability, Game game) {
if (!(ability instanceof SpellAbility)) {
return;
}
Player player = game.getPlayer(ability.getControllerId());
if (player == null) {
return;
}
this.resetCosts();
this.resetCosts(game, ability);
if (additionalCost == null) {
return;
}
if (additionalCost.canPay(ability, ability.getSourceId(), ability.getControllerId(), game)
&& player.chooseUse(Outcome.Benefit, "Pay " + additionalCost.getText(false) + " ?", ability, game)) {
additionalCost.activate();
int modeCount = ability.getModes().size();
ability.getModes().setAdditionalCost(this);
ability.getModes().setMinModes(modeCount);
ability.getModes().setMaxModes(modeCount);
}
}
@Override
public void addOptionalAdditionalModeCosts(Ability ability, Game game) {
if (additionalCost.isActivated()) {
for (Iterator it = ((Costs) additionalCost).iterator(); it.hasNext();) {
Cost cost = (Cost) it.next();
if (cost instanceof ManaCostsImpl) {
ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy());
} else {
ability.getCosts().add(cost.copy());
}
}
addCostsToAbility(additionalCost, ability);
activateCost(game, ability);
}
}
@ -134,11 +109,52 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
}
}
public String getReminderText() {
if (additionalCost != null) {
return additionalCost.getReminderText();
} else {
return "";
public void changeModes(Ability ability, Game game) {
if (!costWasActivated(ability, game)) {
return;
}
// activate max modes all the time
int maxModes = ability.getModes().size();
ability.getModes().setMinModes(maxModes);
ability.getModes().setMaxModes(maxModes);
}
private void addCostsToAbility(Cost cost, Ability ability) {
ability.addCost(cost.copy());
}
private void resetCosts(Game game, Ability source) {
if (additionalCost != null) {
additionalCost.reset();
}
String key = getActivationKey(source, game);
this.activations.remove(key);
}
private void activateCost(Game game, Ability source) {
String key = getActivationKey(source, game);
this.activations.add(key);
}
public boolean costWasActivated(Ability ability, Game game) {
String key = getActivationKey(ability, game);
return this.activations.contains(key);
}
private String getActivationKey(Ability source, Game game) {
// same logic as KickerAbility, uses for source ability only
int zcc = 0;
if (source.getAbilityType() == AbilityType.TRIGGERED) {
zcc = source.getSourceObjectZoneChangeCounter();
}
if (zcc == 0) {
zcc = game.getState().getZoneChangeCounter(source.getSourceId());
}
if (zcc > 0 && (source.getAbilityType() == AbilityType.TRIGGERED)) {
--zcc;
}
return String.valueOf(zcc);
}
}

View file

@ -82,7 +82,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
public KickerAbility(final KickerAbility ability) {
super(ability);
for (OptionalAdditionalCost cost : ability.kickerCosts) {
this.kickerCosts.add((OptionalAdditionalCost) cost.copy());
this.kickerCosts.add(cost.copy());
}
this.keywordText = ability.keywordText;
this.reminderText = ability.reminderText;
@ -113,7 +113,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
cost.reset();
}
String key = getActivationKey(source, "", game);
for (Iterator<String> iterator = activations.keySet().iterator(); iterator.hasNext();) {
for (Iterator<String> iterator = activations.keySet().iterator(); iterator.hasNext(); ) {
String activationKey = iterator.next();
if (activationKey.startsWith(key)
&& activations.get(activationKey) > 0) {
@ -195,12 +195,12 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
"Pay " + times + kickerCost.getText(false) + " ?", ability, game)) {
this.activateKicker(kickerCost, ability, game);
if (kickerCost instanceof Costs) {
for (Iterator itKickerCost = ((Costs) kickerCost).iterator(); itKickerCost.hasNext();) {
for (Iterator itKickerCost = ((Costs) kickerCost).iterator(); itKickerCost.hasNext(); ) {
Object kickerCostObject = itKickerCost.next();
if ((kickerCostObject instanceof Costs)
|| (kickerCostObject instanceof CostsImpl)) {
for (@SuppressWarnings("unchecked") Iterator<Cost> itDetails
= ((Costs) kickerCostObject).iterator(); itDetails.hasNext();) {
= ((Costs) kickerCostObject).iterator(); itDetails.hasNext(); ) {
addKickerCostsToAbility(itDetails.next(), ability, game);
}
} else {

View file

@ -0,0 +1,40 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.OptionalAdditionalModeSourceCosts;
import mage.game.Game;
/**
* Same as KickerAbility, but can enable any number modes in spell ability
*
* @author JayDi85
*/
public class KickerWithAnyNumberModesAbility extends KickerAbility implements OptionalAdditionalModeSourceCosts {
public KickerWithAnyNumberModesAbility(Cost cost) {
super(cost);
}
public KickerWithAnyNumberModesAbility(final KickerWithAnyNumberModesAbility ability) {
super(ability);
}
@Override
public void changeModes(Ability ability, Game game) {
if (!isKicked(game, ability, "")) {
return;
}
// activate any number modes
int maxModes = ability.getModes().size();
ability.getModes().setMinModes(0);
ability.getModes().setMaxModes(maxModes);
}
@Override
public KickerWithAnyNumberModesAbility copy() {
return new KickerWithAnyNumberModesAbility(this);
}
}

View file

@ -2,6 +2,7 @@
package mage.abilities.keyword;
import mage.abilities.condition.common.KickedCondition;
import mage.abilities.costs.Cost;
import mage.abilities.costs.OptionalAdditionalCost;