[LCI] Implement Ojer Axonil, Deepest Might (#11195)

* [LCI] Implement Ojer Axonil, Deepest Might

* add tests

* Alter text generation on ActivateIfConditionActivatedAbility to handle "and as a sorcery"
This commit is contained in:
Susucre 2023-10-08 23:52:36 +02:00 committed by GitHub
parent 287e25b28f
commit 8e1ef15b70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 521 additions and 13 deletions

View file

@ -28,7 +28,7 @@ import java.util.UUID;
public final class HeronBlessedGeist extends CardImpl {
private static final Condition condition = new PermanentsOnTheBattlefieldCondition(
new FilterEnchantmentPermanent("you control an enchantment and only as a sorcery")
new FilterEnchantmentPermanent("you control an enchantment")
);
private static final Hint hint = new ConditionHint(condition, "You control an enchantment");

View file

@ -0,0 +1,149 @@
package mage.cards.o;
import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.DiesSourceTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.abilities.keyword.TrampleAbility;
import mage.abilities.keyword.TransformAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.DamageEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.UUID;
/**
* @author Susucr
*/
public final class OjerAxonilDeepestMight extends CardImpl {
public OjerAxonilDeepestMight(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}");
this.secondSideCardClazz = mage.cards.t.TempleOfPower.class;
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.GOD);
this.power = new MageInt(4);
this.toughness = new MageInt(4);
// Trample
this.addAbility(TrampleAbility.getInstance());
// If a red source you control would deal an amount of noncombat damage less than Ojer Axonil's power to an opponent, that source deals damage equal to Ojer Axonil's power instead.
this.addAbility(new SimpleStaticAbility(new OjerAxonilDeepestMightReplacementEffect()));
// When Ojer Axonil dies, return it to the battlefield tapped and transformed under its owner's control.
this.addAbility(new TransformAbility());
this.addAbility(new DiesSourceTriggeredAbility(new OjerAxonilDeepestMightTransformEffect()));
}
private OjerAxonilDeepestMight(final OjerAxonilDeepestMight card) {
super(card);
}
@Override
public OjerAxonilDeepestMight copy() {
return new OjerAxonilDeepestMight(this);
}
}
// Inspired by Edgar, Charmed Groom
class OjerAxonilDeepestMightTransformEffect extends OneShotEffect {
OjerAxonilDeepestMightTransformEffect() {
super(Outcome.Benefit);
staticText = "return it to the battlefield tapped and transformed under its owner's control";
}
private OjerAxonilDeepestMightTransformEffect(final OjerAxonilDeepestMightTransformEffect effect) {
super(effect);
}
@Override
public OjerAxonilDeepestMightTransformEffect copy() {
return new OjerAxonilDeepestMightTransformEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
MageObject sourceObject = source.getSourceObjectIfItStillExists(game);
if (!(sourceObject instanceof Card)) {
return false;
}
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + source.getSourceId(), Boolean.TRUE);
controller.moveCards((Card) sourceObject, Zone.BATTLEFIELD, source, game, true, false, true, null);
return true;
}
}
// Inspired by Torbran, Thane of Red Fell
class OjerAxonilDeepestMightReplacementEffect extends ReplacementEffectImpl {
OjerAxonilDeepestMightReplacementEffect() {
super(Duration.WhileOnBattlefield, Outcome.Damage);
this.staticText = "If a red source you control would deal an amount of noncombat damage less "
+ "than {this}'s power to an opponent, that source deals damage equal to {this}'s power instead.";
}
private OjerAxonilDeepestMightReplacementEffect(final OjerAxonilDeepestMightReplacementEffect effect) {
super(effect);
}
@Override
public OjerAxonilDeepestMightReplacementEffect copy() {
return new OjerAxonilDeepestMightReplacementEffect(this);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Permanent ojer = source.getSourcePermanentIfItStillExists(game);
if (ojer != null && ojer.getPower().getValue() > 0) {
event.setAmount(ojer.getPower().getValue());
}
return false;
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGE_PLAYER;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
Player controller = game.getPlayer(source.getControllerId());
// Is damage to an opponent?
if (controller == null || !controller.hasOpponent(event.getTargetId(), game)) {
return false;
}
MageObject sourceObject;
Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(event.getSourceId());
if (sourcePermanent == null) {
sourceObject = game.getObject(event.getSourceId());
} else {
sourceObject = sourcePermanent;
}
Permanent ojer = source.getSourcePermanentIfItStillExists(game);
DamageEvent dmgEvent = (DamageEvent) event;
return sourceObject != null
&& ojer != null
&& dmgEvent != null
&& sourceObject.getColor(game).isRed()
&& !dmgEvent.isCombatDamage()
&& event.getAmount() > 0
&& event.getAmount() < ojer.getPower().getValue();
}
}

View file

@ -2,10 +2,9 @@ package mage.cards.s;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.ActivateIfConditionActivatedAbility;
import mage.abilities.condition.Condition;
import mage.abilities.condition.common.MainPhaseStackEmptyCondition;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.decorator.ConditionalActivatedAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.keyword.LifelinkAbility;
import mage.abilities.keyword.VigilanceAbility;
@ -13,6 +12,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.TimingRule;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.token.AngelToken;
@ -39,10 +39,11 @@ public final class SpeakerOfTheHeavens extends CardImpl {
// Lifelink
this.addAbility(LifelinkAbility.getInstance());
// {T}: Create a 4/4 white Angel creature token with flying. Activate this ability only if you have at least 7 more life than your starting life total and only any time you could cast a sorcery.
this.addAbility(new ConditionalActivatedAbility(
// {T}: Create a 4/4 white Angel creature token with flying. Activate only if you have at least 7 more life than your starting life total and only as a sorcery.
this.addAbility(new ActivateIfConditionActivatedAbility(
Zone.BATTLEFIELD, new CreateTokenEffect(new AngelToken()),
new TapSourceCost(), SpeakerOfTheHeavensCondition.instance
new TapSourceCost(), SpeakerOfTheHeavensCondition.instance,
TimingRule.SORCERY
));
}
@ -61,10 +62,6 @@ enum SpeakerOfTheHeavensCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
if (!MainPhaseStackEmptyCondition.instance.apply(game, source)
|| !game.isActivePlayer(source.getControllerId())) {
return false;
}
Player player = game.getPlayer(source.getControllerId());
if (player == null || player.getLife() < game.getStartingLife() + 7) {
return false;
@ -74,6 +71,6 @@ enum SpeakerOfTheHeavensCondition implements Condition {
@Override
public String toString() {
return "you have at least 7 life more than your starting life total and only as a sorcery";
return "you have at least 7 life more than your starting life total";
}
}

View file

@ -0,0 +1,157 @@
package mage.cards.t;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.ActivateIfConditionActivatedAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.TransformSourceEffect;
import mage.abilities.hint.Hint;
import mage.abilities.mana.RedManaAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.TimingRule;
import mage.constants.WatcherScope;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.stack.StackObject;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author Susucr
*/
public final class TempleOfPower extends CardImpl {
public TempleOfPower(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.LAND}, "");
this.nightCard = true;
// <i>(Transforms from Ojer Axonil, Deepest Might.)</i>
Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new InfoEffect("<i>(Transforms from Ojer Axonil, Deepest Might.)</i>"));
ability.setRuleAtTheTop(true);
this.addAbility(ability);
// {T}: Add {R}.
this.addAbility(new RedManaAbility());
// {2}{R}, {T}: Transform Temple of Power. Activate only if red sources you controlled dealt 4 or more noncombat damage this turn and only as a sorcery.
ability = new ActivateIfConditionActivatedAbility(
Zone.BATTLEFIELD,
new TransformSourceEffect(),
new ManaCostsImpl("{2}{R}"),
TempleOfPowerCondition.instance,
TimingRule.SORCERY
);
ability.addWatcher(new TempleOfPowerWatcher());
ability.addCost(new TapSourceCost());
ability.addHint(TempleOfPowerHint.instance);
this.addAbility(ability);
}
private TempleOfPower(final TempleOfPower card) {
super(card);
}
@Override
public TempleOfPower copy() {
return new TempleOfPower(this);
}
}
enum TempleOfPowerCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
TempleOfPowerWatcher watcher = game.getState().getWatcher(TempleOfPowerWatcher.class);
return watcher != null
&& 4 <= watcher.damageForPlayer(source.getControllerId());
}
@Override
public String toString() {
return "if red sources you controlled dealt 4 or more noncombat damage this turn";
}
}
enum TempleOfPowerHint implements Hint {
instance;
@Override
public String getText(Game game, Ability ability) {
TempleOfPowerWatcher watcher = game.getState().getWatcher(TempleOfPowerWatcher.class);
if (watcher == null) {
return "";
}
return "Non-combat damage from red source: "
+ watcher.damageForPlayer(ability.getControllerId());
}
@Override
public TempleOfPowerHint copy() {
return instance;
}
}
class TempleOfPowerWatcher extends Watcher {
// player -> total non combat damage from red source controlled by that player dealt this turn.
private final Map<UUID, Integer> damageMap = new HashMap<>();
public TempleOfPowerWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.DAMAGED_PLAYER
|| event.getType() == GameEvent.EventType.DAMAGED_PERMANENT) {
DamagedEvent dmgEvent = (DamagedEvent) event;
// watch only non combat damage events.
if (dmgEvent == null || dmgEvent.isCombatDamage()) {
return;
}
MageObject sourceObject;
UUID sourceControllerId;
Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(event.getSourceId());
if (sourcePermanent == null) {
sourceObject = game.getSpellOrLKIStack(event.getSourceId());
sourceControllerId = ((StackObject) sourceObject).getControllerId();
} else {
sourceObject = sourcePermanent;
sourceControllerId = sourcePermanent.getControllerId();
}
// watch only red sources dealing damage
if (sourceObject == null || !sourceObject.getColor().isRed()) {
return;
}
damageMap.compute(sourceControllerId, (k, i) -> (i == null ? 0 : i) + event.getAmount());
}
}
@Override
public void reset() {
damageMap.clear();
super.reset();
}
int damageForPlayer(UUID playerId) {
return damageMap.getOrDefault(playerId, 0);
}
}

View file

@ -26,7 +26,9 @@ public final class LostCavernsOfIxalan extends ExpansionSet {
cards.add(new SetCardInfo("Ghalta, Stampede Tyrant", 185, Rarity.MYTHIC, mage.cards.g.GhaltaStampedeTyrant.class));
cards.add(new SetCardInfo("Island", 288, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Mountain", 290, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Ojer Axonil, Deepest Might", 317, Rarity.MYTHIC, mage.cards.o.OjerAxonilDeepestMight.class));
cards.add(new SetCardInfo("Plains", 287, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Swamp", 289, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Temple of Power", 317, Rarity.MYTHIC, mage.cards.t.TempleOfPower.class));
}
}

View file

@ -0,0 +1,194 @@
package org.mage.test.cards.single.lci;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author Susucr
*/
public class OjerAxonilDeepestMightTest extends CardTestPlayerBase {
/**
* Ojer Axonil, Deepest Might
* {2}{R}{R}
* Legendary Creature God
*
* Trample
* If a red source you control would deal an amount of noncombat damage less than Ojer Axonils power to an opponent, that source deals damage equal to Ojer Axonils power instead.
* When Ojer Axonil dies, return it to the battlefield tapped and transformed under its owners control.
* 4/4
*
* Temple of Power
* Land
*
* (Transforms from Ojer Axonil, Deepest Might.)
* {T}: Add {R}.
* {2}{R}, {T}: Transform Temple of Power. Activate only if red sources you controlled dealt 4 or more noncombat damage this turn and only as a sorcery.
*/
private static final String ojer = "Ojer Axonil, Deepest Might";
private static final String temple = "Temple of Power";
private static final String templeTransformAbility = "{2}{R}, {T}: Transform {this}. Activate only if red sources you controlled dealt 4 or more noncombat damage this turn and only as a sorcery.";
/**
* Lightning Bolt
* {R}
* Instant
*
* Lightning Bolt deals 3 damage to any target.
*/
private static final String bolt = "Lightning Bolt";
/**
* Lava Axe
* {4}{R}
* Sorcery
*
* Lava Axe deals 5 damage to target player or planeswalker.
*/
private static final String axe = "Lava Axe";
@Test
public void testReplacement_BoltFace() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, ojer, 1);
addCard(Zone.HAND, playerA, bolt, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerB, 20 - 4);
}
@Test
public void testReplacement_BoltOwnFace() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, ojer, 1);
addCard(Zone.HAND, playerA, bolt, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerA);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 20 - 3); // only work on opponnent
}
@Test
public void testReplacement_BoltOjer() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, ojer, 1);
addCard(Zone.HAND, playerA, bolt, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, ojer);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertDamageReceived(playerA, ojer, 3); // does not work on creatures
}
@Test
public void testReplacement_CombatDamageNotReplaced() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, ojer, 1);
addCard(Zone.BATTLEFIELD, playerA, "Raging Goblin", 1);
attack(1, playerA, "Raging Goblin", playerB);
setStopAt(1, PhaseStep.END_COMBAT);
execute();
assertLife(playerB, 20 - 1); // does not alter combat damage
}
@Test
public void testReplacement_Hellrider() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, ojer, 1);
// Whenever a creature you control attacks, Hellrider deals 1 damage to the player or planeswalker its attacking.
addCard(Zone.BATTLEFIELD, playerA, "Hellrider", 1);
addCard(Zone.BATTLEFIELD, playerA, "Raging Goblin", 1);
attack(1, playerA, "Raging Goblin", playerB);
setStopAt(1, PhaseStep.END_COMBAT);
execute();
assertLife(playerB, 20 - 1 - 4); // Hellrider's trigger is altered.
}
@Test
public void testReplacement_LavaAxeFace() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, ojer, 1);
addCard(Zone.HAND, playerA, axe, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, axe, playerB);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerB, 20 - 5); // no replacement
}
@Test
public void testReplacement_GiantGrowth_LavaAxeFace() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, ojer, 1);
addCard(Zone.HAND, playerA, axe, 1);
addCard(Zone.HAND, playerA, "Giant Growth", 1); // +3/+3 until end of turn
addCard(Zone.BATTLEFIELD, playerA, "Taiga", 6);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, axe, playerB);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Giant Growth", ojer);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerB, 20 - (4 + 3));
}
@Test
public void testTransform() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, ojer, 1);
addCard(Zone.HAND, playerA, axe, 1);
addCard(Zone.HAND, playerA, "Bathe in Dragonfire", 1); // 4 damage to target creature
addCard(Zone.BATTLEFIELD, playerA, "Battlefield Forge", 5 + 3); // Using Forge to distinguish the mana ability from the Temple one.
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bathe in Dragonfire", ojer, true);
checkPermanentTapped("temple in play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, temple, true, 1);
checkPlayableAbility("condition false", 3, PhaseStep.PRECOMBAT_MAIN, playerA, templeTransformAbility, false);
activateManaAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}. {this} deals 1 damage to you.", 5);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, axe, playerB);
waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN);
activateManaAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Add {R}. {this} deals 1 damage to you.", 3);
checkPlayableAbility("condition true", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, templeTransformAbility, true);
activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, templeTransformAbility);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 5);
assertPermanentCount(playerA, ojer, 1);
assertTapped(ojer, true);
}
}

View file

@ -6,6 +6,7 @@ import mage.abilities.condition.Condition;
import mage.abilities.condition.InvertCondition;
import mage.abilities.costs.Cost;
import mage.abilities.effects.Effect;
import mage.constants.TimingRule;
import mage.constants.Zone;
import mage.game.Game;
@ -13,10 +14,14 @@ import mage.game.Game;
* @author LevelX2
*/
public class ActivateIfConditionActivatedAbility extends ActivatedAbilityImpl {
public ActivateIfConditionActivatedAbility(Zone zone, Effect effect, Cost cost, Condition condition) {
this(zone, effect, cost, condition, TimingRule.INSTANT);
}
public ActivateIfConditionActivatedAbility(Zone zone, Effect effect, Cost cost, Condition condition, TimingRule timing) {
super(zone, effect, cost);
this.condition = condition;
this.timing = timing;
}
protected ActivateIfConditionActivatedAbility(final ActivateIfConditionActivatedAbility ability) {
@ -41,7 +46,11 @@ public class ActivateIfConditionActivatedAbility extends ActivatedAbilityImpl {
&& !condition.toString().startsWith("if")) {
sb.append("if ");
}
sb.append(condition.toString()).append('.');
sb.append(condition.toString());
if (timing == TimingRule.SORCERY) {
sb.append(" and only as a sorcery");
}
sb.append('.');
return sb.toString();
}