implement [FIN] Vivi Ornitier ; limit mana computation for "only once per turn" abilities (#13639)

fixes #10930
This commit is contained in:
Susucre 2025-05-16 19:45:01 +02:00 committed by GitHub
parent fa20361e2e
commit a9af84f533
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 272 additions and 10 deletions

View file

@ -0,0 +1,85 @@
package mage.cards.v;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SpellCastControllerTriggeredAbility;
import mage.abilities.condition.common.MyTurnCondition;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.dynamicvalue.common.SourcePermanentPowerValue;
import mage.abilities.effects.common.DamagePlayersEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.effects.mana.AddManaInAnyCombinationEffect;
import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
* @author Susucr
*/
public final class ViviOrnitier extends CardImpl {
public ViviOrnitier(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.WIZARD);
this.power = new MageInt(0);
this.toughness = new MageInt(3);
// {0}: Add X mana in any combination of {U} and/or {R}, where X is Vivi Ornitier's power. Activate only during your turn and only once each turn.
this.addAbility(new ViviOrnitierManaAbility());
// Whenever you cast a noncreature spell, put a +1/+1 counter on Vivi Ornitier and it deals 1 damage to each opponent.
Ability ability = new SpellCastControllerTriggeredAbility(
new AddCountersSourceEffect(CounterType.P1P1.createInstance()),
StaticFilters.FILTER_SPELL_A_NON_CREATURE, false
);
ability.addEffect(new DamagePlayersEffect(1, TargetController.OPPONENT, "it").concatBy("and"));
this.addAbility(ability);
}
private ViviOrnitier(final ViviOrnitier card) {
super(card);
}
@Override
public ViviOrnitier copy() {
return new ViviOrnitier(this);
}
}
class ViviOrnitierManaAbility extends ActivatedManaAbilityImpl {
ViviOrnitierManaAbility() {
super(
Zone.BATTLEFIELD,
new AddManaInAnyCombinationEffect(
SourcePermanentPowerValue.NOT_NEGATIVE,
SourcePermanentPowerValue.NOT_NEGATIVE,
ColoredManaSymbol.U,
ColoredManaSymbol.R
),
new GenericManaCost(0)
);
this.condition = MyTurnCondition.instance;
this.maxActivationsPerTurn = 1;
}
private ViviOrnitierManaAbility(final ViviOrnitierManaAbility ability) {
super(ability);
}
public ViviOrnitierManaAbility copy() {
return new ViviOrnitierManaAbility(this);
}
@Override
public String getRule() {
return super.getRule() + " Activate only during your turn and only once each turn.";
}
}

View file

@ -299,6 +299,9 @@ public final class FinalFantasy extends ExpansionSet {
cards.add(new SetCardInfo("Vincent Valentine", 383, Rarity.RARE, mage.cards.v.VincentValentine.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Vincent Valentine", 454, Rarity.RARE, mage.cards.v.VincentValentine.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Vincent Valentine", 528, Rarity.RARE, mage.cards.v.VincentValentine.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Vivi Ornitier", 248, Rarity.MYTHIC, mage.cards.v.ViviOrnitier.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Vivi Ornitier", 321, Rarity.MYTHIC, mage.cards.v.ViviOrnitier.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Vivi Ornitier", 514, Rarity.MYTHIC, mage.cards.v.ViviOrnitier.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Warrior's Sword", 169, Rarity.COMMON, mage.cards.w.WarriorsSword.class));
cards.add(new SetCardInfo("Wastes", 309, Rarity.COMMON, mage.cards.w.Wastes.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("White Auracite", 41, Rarity.COMMON, mage.cards.w.WhiteAuracite.class, NON_FULL_USE_VARIOUS));

View file

@ -0,0 +1,72 @@
package org.mage.test.cards.mana;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class LimitPerTurnTest extends CardTestPlayerBase {
/**
* {@link mage.cards.s.ShireScarecrow Shire Scarecrow} {2}
* Artifact Creature Scarecrow
* Defender
* {1}: Add one mana of any color. Activate only once each turn.
* 0/3
*/
private static final String scarecrow = "Shire Scarecrow";
@Test
public void test_LimitOncePerTurn() {
String vanguard = "Elite Vanguard"; // {W} 2/1
String goblin = "Raging Goblin"; // {R} 1/1
addCard(Zone.BATTLEFIELD, playerA, scarecrow, 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2);
addCard(Zone.HAND, playerA, vanguard, 1);
addCard(Zone.HAND, playerA, goblin, 1);
checkPlayableAbility("1: Vanguard can be cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + vanguard, true);
checkPlayableAbility("1: goblin can be cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + goblin, true);
setChoice(playerA, "White"); // choice for Scarecrow mana
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, vanguard);
checkPlayableAbility("2: goblin can not be cast", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + goblin, false);
checkPlayableAbility("3: goblin can be cast on turn 3", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + goblin, true);
setChoice(playerA, "Red"); // choice for Scarecrow mana
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, goblin);
setStopAt(3, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, 5);
}
@Test
public void test_MultipleScarecrows() {
String deus = "Deus of Calamity"; // {R/G}{R/G}{R/G}{R/G}{R/G}
String boggart = "Boggart Ram-Gang"; // {R/G}{R/G}{R/G}
addCard(Zone.BATTLEFIELD, playerA, scarecrow, 3);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5);
addCard(Zone.HAND, playerA, deus, 1);
addCard(Zone.HAND, playerA, boggart, 1);
checkPlayableAbility("1: boggart can be cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + boggart, true);
checkPlayableAbility("1: deus can not be cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + deus, false);
setChoice(playerA, "Red"); // choice for Scarecrow mana
setChoice(playerA, "Red"); // choice for Scarecrow mana
setChoice(playerA, "Green"); // choice for Scarecrow mana
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, boggart);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, boggart, 1);
}
}

View file

@ -0,0 +1,65 @@
package org.mage.test.cards.single.fin;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class ViviOrnitierTest extends CardTestPlayerBase {
/**
* {@link mage.cards.v.ViviOrnitier Vivi Ornitier} {1}{U}{R}
* Legendary Creature Wizard
* {0}: Add X mana in any combination of {U} and/or {R}, where X is Vivi Ornitiers power. Activate only during your turn and only once each turn.
* Whenever you cast a noncreature spell, put a +1/+1 counter on Vivi Ornitier and it deals 1 damage to each opponent.
* 0/3
*/
private static final String vivi = "Vivi Ornitier";
/**
* Creatures you control get +2/+2.
*/
private static final String dictate = "Dictate of Heliod";
private static final String bolt = "Lightning Bolt";
private static final String incinerate = "Incinerate";
@Test
public void test_NoPower() {
addCard(Zone.BATTLEFIELD, playerA, vivi, 1);
addCard(Zone.HAND, playerA, bolt);
checkPlayableAbility("bolt can not be cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + bolt, false);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
setStrictChooseMode(true);
execute();
}
@Test
public void test_2Power() {
addCard(Zone.BATTLEFIELD, playerA, vivi, 1);
addCard(Zone.BATTLEFIELD, playerA, dictate, 1);
addCard(Zone.HAND, playerA, bolt);
addCard(Zone.HAND, playerA, incinerate);
checkPlayableAbility("1: bolt can be cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + bolt, true);
checkPlayableAbility("1: incinerate can be cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + incinerate, true);
setChoice(playerA, "X=0"); // choose {U} color distribution for vivi on 2 power
setChoice(playerA, "X=2"); // choose {R} color distribution for vivi on 2 power
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, incinerate, playerB);
checkPlayableAbility("2: bolt can not be cast", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + bolt, false);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerB, 20 - 3 - 1);
assertPowerToughness(playerA, vivi, 3, 6);
}
}

View file

@ -99,6 +99,11 @@ public interface ActivatedAbility extends Ability {
int getMaxActivationsPerTurn(Game game);
/**
* how many more time can this be activated this turn?
*/
int getMaxMoreActivationsThisTurn(Game game);
ActivatedAbility setTiming(TimingRule timing);
ActivatedAbility setCondition(Condition condition);

View file

@ -215,6 +215,23 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
|| activationInfo.activationCounter < getMaxActivationsPerTurn(game);
}
public int getMaxMoreActivationsThisTurn(Game game) {
if (getMaxActivationsPerTurn(game) == Integer.MAX_VALUE && maxActivationsPerGame == Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
ActivationInfo activationInfo = getActivationInfo(game);
if (activationInfo == null) {
return Math.min(maxActivationsPerGame, getMaxActivationsPerTurn(game));
}
if (activationInfo.totalActivations >= maxActivationsPerGame) {
return 0;
}
if (activationInfo.turnNum != game.getTurnNum()) {
return getMaxActivationsPerTurn(game);
}
return Math.max(0, getMaxActivationsPerTurn(game) - activationInfo.activationCounter);
}
@Override
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (!hasMoreActivationsThisTurn(game) || !super.activate(game, allowedIdentifiers, noMana)) {

View file

@ -1,13 +1,13 @@
package mage.abilities.mana;
import java.util.List;
import java.util.Set;
import mage.Mana;
import mage.constants.ManaType;
import mage.game.Game;
import java.util.List;
import java.util.Set;
/**
*
* @author LevelX2
*/
public interface ManaAbility {
@ -20,7 +20,7 @@ public interface ManaAbility {
* @return
*/
List<Mana> getNetMana(Game game);
/**
* Used to check the possible mana production to determine which spells
* and/or abilities can be used. (player.getPlayable()).
@ -28,7 +28,7 @@ public interface ManaAbility {
*
* @param game
* @param possibleManaInPool The possible mana already produced by other sources for this calculation option
* @return
* @return
*/
List<Mana> getNetMana(Game game, Mana possibleManaInPool);
@ -60,7 +60,12 @@ public interface ManaAbility {
* @return
*/
boolean isPoolDependant();
/**
* How many more times can this ability be activated this turn
*/
int getMaxMoreActivationsThisTurn(Game game);
ManaAbility setPoolDependant(boolean pooleDependant);
}

View file

@ -393,6 +393,7 @@ public class ManaOptions extends LinkedHashSet<Mana> {
&& onlyManaCosts
&& manaToAdd.countColored() > 0;
boolean canHaveBetterValues;
int maxRepeat = manaAbility.getMaxMoreActivationsThisTurn(game);
Mana possibleMana = new Mana();
Mana improvedMana = new Mana();
@ -401,9 +402,11 @@ public class ManaOptions extends LinkedHashSet<Mana> {
// example: {G}: Add one mana of any color
for (Mana possiblePay : ManaOptions.getPossiblePayCombinations(cost, startingMana)) {
improvedMana.setToMana(startingMana);
int currentAttempt = 0;
do {
// loop until all mana replaced by better values
canHaveBetterValues = false;
currentAttempt++;
// it's impossible to analyse all payment order (pay {R} for {1}, {Any} for {G}, etc)
// so use simple cost simulation by subtract
@ -441,7 +444,7 @@ public class ManaOptions extends LinkedHashSet<Mana> {
}
improvedMana.setToMana(possibleMana);
}
} while (repeatable && canHaveBetterValues && improvedMana.includesMana(possiblePay));
} while (repeatable && (currentAttempt < maxRepeat) && canHaveBetterValues && improvedMana.includesMana(possiblePay));
}
return oldManaWasReplaced;
}
@ -670,12 +673,14 @@ final class Comparators {
for (T first : elements) {
for (T second : elements) {
int firstGreaterThanSecond = comparator.compare(first, second);
if (firstGreaterThanSecond <= 0)
if (firstGreaterThanSecond <= 0) {
continue;
}
for (T third : elements) {
int secondGreaterThanThird = comparator.compare(second, third);
if (secondGreaterThanThird <= 0)
if (secondGreaterThanThird <= 0) {
continue;
}
int firstGreaterThanThird = comparator.compare(first, third);
if (firstGreaterThanThird <= 0) {
// Uncomment the following line to step through the failed case

View file

@ -104,6 +104,11 @@ public abstract class TriggeredManaAbility extends TriggeredAbilityImpl implemen
return poolDependant;
}
@Override
public int getMaxMoreActivationsThisTurn(Game game) {
return getRemainingTriggersLimitEachTurn(game);
}
@Override
public TriggeredManaAbility setPoolDependant(boolean poolDependant) {
this.poolDependant = poolDependant;