From a502ee94d5cc5c65854f0e925b7e10a5063dbd48 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:40:45 -0500 Subject: [PATCH] [TLA] Implement Firebender Ascension Also implemented an OptionalOneShotEffect for ease of stapling together a mandatory effect and an optional effect on the same ability. --- .../src/mage/cards/f/FirebenderAscension.java | 133 ++++++++++++++++++ .../src/mage/sets/AvatarTheLastAirbender.java | 2 + .../decorator/OptionalOneShotEffect.java | 85 +++++++++++ 3 files changed, 220 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FirebenderAscension.java create mode 100644 Mage/src/main/java/mage/abilities/decorator/OptionalOneShotEffect.java diff --git a/Mage.Sets/src/mage/cards/f/FirebenderAscension.java b/Mage.Sets/src/mage/cards/f/FirebenderAscension.java new file mode 100644 index 00000000000..cb6ef523762 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FirebenderAscension.java @@ -0,0 +1,133 @@ +package mage.cards.f; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SourceHasCounterCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.decorator.OptionalOneShotEffect; +import mage.abilities.effects.common.CopyStackObjectEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.DefenderAttackedEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.SoldierFirebendingToken; +import mage.game.stack.StackObject; +import mage.target.targetpointer.FixedTarget; + +/** + * + * @author Grath + */ +public final class FirebenderAscension extends CardImpl { + + public FirebenderAscension(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}"); + + // When this enchantment enters, create a 2/2 red Soldier creature token with firebending 1. + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new SoldierFirebendingToken()))); + + // Whenever a creature you control attacking causes a triggered ability of that creature to trigger, put a quest + // counter on this enchantment. Then if it has four or more quest counters on it, you may copy that ability. You + // may choose new targets for the copy. + this.addAbility(new FirebenderAscensionTriggeredAbility()); + } + + private FirebenderAscension(final FirebenderAscension card) { + super(card); + } + + @Override + public FirebenderAscension copy() { + return new FirebenderAscension(this); + } +} + +class FirebenderAscensionTriggeredAbility extends TriggeredAbilityImpl { + + private static final Condition condition = new SourceHasCounterCondition(CounterType.QUEST, 4); + + FirebenderAscensionTriggeredAbility() { + super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.QUEST.createInstance(), true), false); + addEffect(new ConditionalOneShotEffect(new OptionalOneShotEffect(new CopyStackObjectEffect(), + "You may copy that ability. You may choose new targets for the copy."), condition, + "Then if it has four or more quest counters on it, you may copy that ability. You may choose new targets for the copy.")); + setTriggerPhrase("Whenever a creature you control attacking causes a triggered ability of that creature to trigger, "); + } + + private FirebenderAscensionTriggeredAbility(final FirebenderAscensionTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.TRIGGERED_ABILITY; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + StackObject stackObject = game.getStack().getStackObject(event.getTargetId()); + Permanent permanent = game.getPermanent(event.getSourceId()); + if (stackObject == null || permanent == null || !permanent.isCreature(game)) { + return false; // only creatures + } + if (this.getControllerId() != permanent.getControllerId()) { + return false; // only your creatures + } + Ability stackAbility = stackObject.getStackAbility(); + if (!stackAbility.isTriggeredAbility()) { + return false; + } + GameEvent triggerEvent = ((TriggeredAbility) stackAbility).getTriggerEvent(); + GameEvent.EventType eventType = triggerEvent.getType(); + if (triggerEvent == null || (eventType != GameEvent.EventType.ATTACKER_DECLARED && + eventType != GameEvent.EventType.DECLARED_ATTACKERS && + eventType != GameEvent.EventType.DEFENDER_ATTACKED)) { + return false; // only attacking triggers + } + switch (triggerEvent.getType()) { + case ATTACKER_DECLARED: + if (triggerEvent.getSourceId() != permanent.getId()) { + return false; + } // only triggered abilities of that creature + break; + case DECLARED_ATTACKERS: + if (game + .getCombat() + .getAttackers() + .stream() + .noneMatch(permanent.getId()::equals)) { + return false; + } // only if the creature was one of the attackers that were declared + // This might give some false positives? + break; + case DEFENDER_ATTACKED: + if (((DefenderAttackedEvent) triggerEvent) + .getAttackers(game) + .stream() + .noneMatch(permanent::equals)) { + return false; + } // only if the creature was one of the attackers that were declared + // This might give some false positives? + } + getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game)); + return true; + } + + @Override + public FirebenderAscensionTriggeredAbility copy() { + return new FirebenderAscensionTriggeredAbility(this); + } + +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index a4a643b9d89..749609032a9 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -110,6 +110,8 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Fire Nation Raider", 135, Rarity.COMMON, mage.cards.f.FireNationRaider.class)); cards.add(new SetCardInfo("Fire Nation Warship", 256, Rarity.UNCOMMON, mage.cards.f.FireNationWarship.class)); cards.add(new SetCardInfo("Fire Sages", 136, Rarity.UNCOMMON, mage.cards.f.FireSages.class)); + cards.add(new SetCardInfo("Firebender Ascension", 137, Rarity.RARE, mage.cards.f.FirebenderAscension.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Firebender Ascension", 312, Rarity.RARE, mage.cards.f.FirebenderAscension.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Firebending Student", 139, Rarity.RARE, mage.cards.f.FirebendingStudent.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Firebending Student", 342, Rarity.RARE, mage.cards.f.FirebendingStudent.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Firebending Student", 393, Rarity.RARE, mage.cards.f.FirebendingStudent.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage/src/main/java/mage/abilities/decorator/OptionalOneShotEffect.java b/Mage/src/main/java/mage/abilities/decorator/OptionalOneShotEffect.java new file mode 100644 index 00000000000..c4d59047de3 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/decorator/OptionalOneShotEffect.java @@ -0,0 +1,85 @@ +package mage.abilities.decorator; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.effects.Effects; +import mage.abilities.effects.OneShotEffect; +import mage.game.Game; +import mage.players.Player; +import mage.target.targetpointer.TargetPointer; +import mage.util.CardUtil; + +/** + * Adds condition to {@link OneShotEffect}. Acts as decorator. + * + * @author Grath + */ +public class OptionalOneShotEffect extends OneShotEffect { + + private final Effects effects = new Effects(); + + public OptionalOneShotEffect(OneShotEffect effect, String text) { + super(effect.getOutcome()); + if (effect != null) { + this.effects.add(effect); + } + this.staticText = text; + } + + protected OptionalOneShotEffect(final OptionalOneShotEffect effect) { + super(effect); + this.effects.addAll(effect.effects.copy()); + } + + @Override + public boolean apply(Game game, Ability source) { + // nothing to do - no problem + if (effects.isEmpty()) { + return true; + } + Player player = game.getPlayer(source.getControllerId()); + if (player != null && player.chooseUse(outcome, staticText, source, game)) { + effects.setTargetPointer(this.getTargetPointer().copy()); + effects.forEach(effect -> effect.apply(game, source)); + return true; + } + return false; + } + + public OptionalOneShotEffect addEffect(OneShotEffect effect) { + this.effects.add(effect); + return this; + } + + @Override + public void setValue(String key, Object value) { + super.setValue(key, value); + this.effects.setValue(key, value); + } + + @Override + public OptionalOneShotEffect copy() { + return new OptionalOneShotEffect(this); + } + + @Override + public String getText(Mode mode) { + if (staticText != null && !staticText.isEmpty()) { + return staticText; + } + return "you may " + CardUtil.getTextWithFirstCharLowerCase(effects.getText(mode)); + } + + @Override + public OptionalOneShotEffect setTargetPointer(TargetPointer targetPointer) { + effects.setTargetPointer(targetPointer); + super.setTargetPointer(targetPointer); + return this; + } + + @Override + public OptionalOneShotEffect withTargetDescription(String target) { + effects.forEach(effect -> effect.withTargetDescription(target)); + return this; + } +}