TLA: Earthbend (#8386)

This commit is contained in:
Hans Mackowiak
2025-08-15 09:24:45 +02:00
committed by GitHub
parent c4d58e3dba
commit f55bf4691d
21 changed files with 198 additions and 41 deletions

View File

@@ -3,7 +3,6 @@ package forge.game.ability;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.spellability.AbilityActivated;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.TargetRestrictions;
import java.util.Map;
@@ -18,14 +17,7 @@ public class AbilityApiBased extends AbilityActivated {
api = api0;
effect = api.getSpellEffect();
if (api.equals(ApiType.Mana) || api.equals(ApiType.ManaReflected)) {
this.setManaPart(new AbilityManaPart(this, mapParams));
this.setUndoable(true); // will try at least
}
if (api.equals(ApiType.ChangeZone) || api.equals(ApiType.ChangeZoneAll)) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
effect.buildSpellAbility(this);
}
@Override

View File

@@ -202,15 +202,6 @@ public final class AbilityFactory {
final Card hostCard = state.getCard();
TargetRestrictions abTgt = mapParams.containsKey("ValidTgts") ? readTarget(mapParams) : null;
if (api == ApiType.CopySpellAbility || api == ApiType.Counter || api == ApiType.ChangeTargets || api == ApiType.ControlSpell) {
// Since all "CopySpell" ABs copy things on the Stack no need for it to be everywhere
// Since all "Counter" or "ChangeTargets" abilities only target the Stack Zone
// No need to have each of those scripts have that info
if (abTgt != null) {
abTgt.setZone(ZoneType.Stack);
}
}
if (abCost == null) {
abCost = parseAbilityCost(state, mapParams, type);
}

View File

@@ -81,6 +81,7 @@ public enum ApiType {
Draft (DraftEffect.class),
Draw (DrawEffect.class),
EachDamage (DamageEachEffect.class),
Earthbend (EarthbendEffect.class),
Effect (EffectEffect.class),
Encode (EncodeEffect.class),
EndCombatPhase (EndCombatPhaseEffect.class),

View File

@@ -49,6 +49,8 @@ public abstract class SpellAbilityEffect {
return sa.getDescription();
}
public void buildSpellAbility(final SpellAbility sa) {}
/**
* Returns this effect description with needed prelude and epilogue.
* @param params

View File

@@ -4,7 +4,6 @@ import java.util.Map;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.Spell;
import forge.game.spellability.TargetRestrictions;
@@ -24,13 +23,7 @@ public class SpellApiBased extends Spell {
// A spell is always intrinsic
this.setIntrinsic(true);
if (api.equals(ApiType.Mana) || api.equals(ApiType.ManaReflected)) {
this.setManaPart(new AbilityManaPart(this, mapParams));
}
if (api.equals(ApiType.ChangeZone) || api.equals(ApiType.ChangeZoneAll)) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
effect.buildSpellAbility(this);
}
@Override

View File

@@ -2,8 +2,6 @@ package forge.game.ability;
import java.util.Map;
import forge.game.ability.effects.ChangeZoneAllEffect;
import forge.game.ability.effects.ChangeZoneEffect;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.spellability.AbilityStatic;
@@ -20,9 +18,7 @@ public class StaticAbilityApiBased extends AbilityStatic {
api = api0;
effect = api.getSpellEffect();
if (effect instanceof ChangeZoneEffect || effect instanceof ChangeZoneAllEffect) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
effect.buildSpellAbility(this);
}
@Override

View File

@@ -18,6 +18,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetChoices;
import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Localizer;
@@ -27,6 +28,13 @@ import forge.util.Localizer;
*/
public class ChangeTargetsEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityEffect#resolve(forge.card.spellability.SpellAbility)
*/

View File

@@ -7,6 +7,7 @@ import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
@@ -21,6 +22,12 @@ import forge.util.Localizer;
import forge.util.TextUtil;
public class ChangeZoneAllEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
AbilityFactory.adjustChangeZoneTarget(sa.getMapParams(), sa);
}
@Override
protected String getStackDescription(SpellAbility sa) {
// TODO build Stack Description will need expansion as more cards are added

View File

@@ -6,6 +6,7 @@ import com.google.common.collect.Maps;
import forge.card.CardStateName;
import forge.card.CardType;
import forge.game.*;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
@@ -34,6 +35,11 @@ import java.util.Map;
public class ChangeZoneEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
AbilityFactory.adjustChangeZoneTarget(sa.getMapParams(), sa);
}
@Override
protected String getStackDescription(SpellAbility sa) {
if (sa.isHidden()) {

View File

@@ -10,8 +10,17 @@ import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
public class ControlSpellEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility)
*/

View File

@@ -23,6 +23,12 @@ import java.util.Map;
public class CopySpellAbilityEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
@Override
protected String getStackDescription(SpellAbility sa) {

View File

@@ -21,6 +21,13 @@ import java.util.List;
import java.util.Map;
public class CounterEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();

View File

@@ -0,0 +1,88 @@
package forge.game.ability.effects;
import java.util.Arrays;
import java.util.EnumSet;
import forge.card.RemoveType;
import forge.game.Game;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCopyService;
import forge.game.card.CounterEnumType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
import forge.util.Lang;
public class EarthbendEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder("Earthbend ");
final Card card = sa.getHostCard();
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa);
sb.append(amount).append(". (Target land you control becomes a 0/0 creature with haste thats still a land. Put ");
sb.append(Lang.nounWithNumeral(amount, "+1/+1 counter"));
sb.append(" on it. When it dies or is exiled, return it to the battlefield tapped.)");
return sb.toString();
}
@Override
public void buildSpellAbility(final SpellAbility sa) {
TargetRestrictions abTgt = new TargetRestrictions("Select target land you control", "Land.YouCtrl".split(","), "1", "1");
sa.setTargetRestrictions(abTgt);
}
@Override
public void resolve(SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
final Player pl = sa.getActivatingPlayer();
int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("Num", "1"), sa);
long ts = game.getNextTimestamp();
String desc = "When it dies or is exiled, return it to the battlefield tapped.";
String sbTrigA = "Mode$ ChangesZone | ValidCard$ Card.IsTriggerRemembered | Origin$ Battlefield | Destination$ Graveyard | TriggerDescription$ " + desc;
String sbTrigB = "Mode$ Exiled | Origin$ Battlefield | ValidCard$ Card.IsTriggerRemembered | TriggerZones$ Battlefield | TriggerDescription$ " + desc;
// Earthbend should only target one land
for (Card c : getTargetCards(sa)) {
c.addNewPT(0, 0, ts, 0);
c.addChangedCardTypes(Arrays.asList("Creature"), null, false, EnumSet.noneOf(RemoveType.class), ts, 0, true, false);
c.addChangedCardKeywords(Arrays.asList("Haste"), null, false, ts, null);
GameEntityCounterTable table = new GameEntityCounterTable();
c.addCounter(CounterEnumType.P1P1, num, pl, table);
table.replaceCounterEffect(game, sa, true);
buildTrigger(sa, c, sbTrigA, "Graveyard");
buildTrigger(sa, c, sbTrigB, "Exile");
}
game.getTriggerHandler().runTrigger(TriggerType.Earthbend, AbilityKey.mapFromPlayer(pl), false);
}
protected void buildTrigger(SpellAbility sa, Card c, String sbTrig, String zone) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
String trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRemembered | Origin$ " + zone + " | Destination$ Battlefield | Tapped$ True";
final Trigger trig = TriggerHandler.parseTrigger(sbTrig, CardCopyService.getLKICopy(source), sa.isIntrinsic());
final SpellAbility newSa = AbilityFactory.getAbility(trigSA, sa.getHostCard());
newSa.setIntrinsic(sa.isIntrinsic());
trig.addRemembered(c);
trig.setOverridingAbility(newSa);
trig.setSpawningAbility(sa.copy(sa.getHostCard(), true));
trig.setKeyword(trig.getSpawningAbility().getKeyword());
game.getTriggerHandler().registerDelayedTrigger(trig);
}
}

View File

@@ -29,6 +29,14 @@ import io.sentry.Sentry;
public class ManaEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
sa.setManaPart(new AbilityManaPart(sa, sa.getMapParams()));
if (sa.getParent() == null) {
sa.setUndoable(true); // will try at least
}
}
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();

View File

@@ -17,6 +17,14 @@ import forge.util.Localizer;
public class ManaReflectedEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
sa.setManaPart(new AbilityManaPart(sa, sa.getMapParams()));
if (sa.getParent() == null) {
sa.setUndoable(true); // will try at least
}
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellEffect#resolve(java.util.Map, forge.card.spellability.SpellAbility)
*/

View File

@@ -22,7 +22,6 @@ import java.util.List;
import com.google.common.collect.Lists;
import forge.game.IHasSVars;
import forge.game.ability.AbilityFactory;
import forge.game.ability.ApiType;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -87,13 +86,7 @@ public final class AbilitySub extends SpellAbility implements java.io.Serializab
effect = api.getSpellEffect();
if (api.equals(ApiType.Mana) || api.equals(ApiType.ManaReflected)) {
this.setManaPart(new AbilityManaPart(this, mapParams));
}
if (api.equals(ApiType.ChangeZone) || api.equals(ApiType.ChangeZoneAll)) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
effect.buildSpellAbility(this);
}
@Override

View File

@@ -415,7 +415,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return false;
}
protected final void setManaPart(AbilityManaPart manaPart0) {
public final void setManaPart(AbilityManaPart manaPart0) {
manaPart = manaPart0;
}

View File

@@ -289,7 +289,7 @@ public class TargetRestrictions {
*/
public final boolean isMinTargetsChosen(final Card c, final SpellAbility sa) {
int min = getMinTargets(c, sa);
if (min == 0 || (sa.isDividedAsYouChoose() && ObjectUtils.defaultIfNull(sa.getDividedValue(), 0) == 0)) {
if (min == 0 || (sa.isDividedAsYouChoose() && ObjectUtils.getIfNull(sa.getDividedValue(), 0) == 0)) {
return true;
}
return min <= sa.getTargets().size();

View File

@@ -0,0 +1,36 @@
package forge.game.trigger;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import forge.util.Localizer;
public class TriggerElementalbend extends Trigger {
public TriggerElementalbend(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
@Override
public boolean performTest(Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) {
return false;
}
return true;
}
@Override
public void setTriggeringObjects(SpellAbility sa, Map<AbilityKey, Object> runParams) {
sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player);
}
@Override
public String getImportantStackObjects(SpellAbility sa) {
StringBuilder sb = new StringBuilder();
sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player));
return sb.toString();
}
}

View File

@@ -74,6 +74,7 @@ public enum TriggerType {
Discover(TriggerDiscover.class),
Drawn(TriggerDrawn.class),
DungeonCompleted(TriggerCompletedDungeon.class),
Earthbend(TriggerElementalbend.class),
Evolved(TriggerEvolved.class),
ExcessDamage(TriggerExcessDamage.class),
ExcessDamageAll(TriggerExcessDamageAll.class),

View File

@@ -0,0 +1,5 @@
Name:Earthbending Lesson
ManaCost:3 G
Types:Sorcery Lesson
A:SP$ Earthbend | Num$ 4 | SpellDescription$ Earthbend 4. (Target land you control becomes a 0/0 creature with haste thats still a land. Put four +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)
Oracle:Earthbend 4. (Target land you control becomes a 0/0 creature with haste thats still a land. Put four +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)