Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AnimateSubAbility rework #6819

Merged
merged 21 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 47 additions & 15 deletions forge-game/src/main/java/forge/game/GameAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.*;
import forge.game.event.*;
import forge.game.extrahands.BackupPlanService;
Expand Down Expand Up @@ -149,7 +150,6 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
if (!found) {
c.clearControllers();
if (cause != null) {
unanimateOnAbortedChange(cause, c);
}
return c;
}
Expand Down Expand Up @@ -361,7 +361,6 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
if (repres == ReplacementResult.Prevented) {
c.clearControllers();
if (cause != null) {
unanimateOnAbortedChange(cause, c);
if (cause.hasParam("Transformed") || cause.hasParam("FaceDown")) {
c.setBackSide(false);
c.changeToState(CardStateName.Original);
Expand Down Expand Up @@ -588,6 +587,8 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
}
}

handleStaticEffect(copied, cause);

if (!table.isEmpty()) {
// we don't want always trigger before counters are placed
game.getTriggerHandler().suppressMode(TriggerType.Always);
Expand Down Expand Up @@ -725,6 +726,50 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
return copied;
}

private void handleStaticEffect(Card copied, SpellAbility cause) {
if (cause != null && cause.hasParam("StaticEffect") && copied.isPermanent()) {
final Card source = cause.getHostCard();
if (cause.hasParam("StaticEffectCheckSVar")) {
String cmp = cause.getParamOrDefault("StaticEffectSVarCompare$", "GE1");
tool4ever marked this conversation as resolved.
Show resolved Hide resolved
int lhs = AbilityUtils.calculateAmount(source, cause.getParam("StaticEffectCheckSVar"), cause);
int rhs = AbilityUtils.calculateAmount(source, cmp.substring(2), cause);
if (!Expressions.compare(lhs, cmp, rhs)) {
return;
}
}

Long timestamp;
// check if player ordered it manually
if (cause.hasSVar("StaticEffectTimestamp")) {
timestamp = Long.parseLong(cause.getSVar("StaticEffectTimestamp"));
} else {
// else create default value (or realign)
timestamp = game.getNextTimestamp();
cause.setSVar("StaticEffectTimestamp", String.valueOf(timestamp));
}
String name = "Static Effect #" + source.getGameTimestamp();
// check if this isn't the first card being moved
Optional<Card> opt = IterableUtil.tryFind(cause.getActivatingPlayer().getZone(ZoneType.Command).getCards(), CardPredicates.nameEquals(name));

Card eff;
if (opt.isPresent()) {
eff = opt.get();
// update in case player manually ordered
eff.setLayerTimestamp(timestamp);
} else {
// otherwise create effect first
eff = SpellAbilityEffect.createEffect(cause, cause.getActivatingPlayer(), name, source.getImageKey(), timestamp);
eff.setRenderForUI(false);
StaticAbility stAb = eff.addStaticAbility(AbilityUtils.getSVar(cause, cause.getParam("StaticEffect")));
stAb.putParam("EffectZone", "Command");
SpellAbilityEffect.addForgetOnMovedTrigger(copied, "Battlefield");
game.getAction().moveToCommand(eff, cause);
}

eff.addRemembered(copied);
}
}

private void storeChangesZoneAll(Card c, Zone zoneFrom, Zone zoneTo, Map<AbilityKey, Object> params) {
if (params != null && params.containsKey(AbilityKey.InternalTriggerTable)) {
((CardZoneTable) params.get(AbilityKey.InternalTriggerTable)).put(zoneFrom != null ? zoneFrom.getZoneType() : null, zoneTo.getZoneType(), c);
Expand Down Expand Up @@ -2669,19 +2714,6 @@ public static boolean attachAuraOnIndirectEnterBattlefield(final Card source, Ma
return false;
}

private static void unanimateOnAbortedChange(final SpellAbility cause, final Card c) {
if (cause.hasParam("AnimateSubAbility")) {
long unanimateTimestamp = Long.parseLong(cause.getAdditionalAbility("AnimateSubAbility").getSVar("unanimateTimestamp"));
c.removeChangedCardKeywords(unanimateTimestamp, 0);
c.removeChangedCardTypes(unanimateTimestamp, 0);
c.removeChangedName(unanimateTimestamp, 0);
c.removeNewPT(unanimateTimestamp, 0);
if (c.removeChangedCardTraits(unanimateTimestamp, 0)) {
c.updateStateForView();
}
}
}

public CardCollectionView getLastState(final AbilityKey key, final SpellAbility cause, final Map<AbilityKey, Object> params, final boolean refreshIfEmpty) {
CardCollectionView lastState = null;
if (params != null) {
Expand Down
22 changes: 21 additions & 1 deletion forge-game/src/main/java/forge/game/GameActionUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellAbilityEffect;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.*;
import forge.game.card.CardPlayOption.PayManaCost;
import forge.game.cost.Cost;
Expand All @@ -34,6 +35,7 @@
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerController;
import forge.game.player.PlayerController.FullControlFlag;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementLayer;
Expand Down Expand Up @@ -835,9 +837,11 @@ public static String generatedMana(final SpellAbility sa) {
}

public static CardCollectionView orderCardsByTheirOwners(Game game, CardCollectionView list, ZoneType dest, SpellAbility sa) {
if (list.size() <= 1) {
if (list.size() <= 1 &&
(sa == null || !sa.getActivatingPlayer().getController().isFullControl(FullControlFlag.LayerTimestampOrder))) {
return list;
}
Card eff = null;
CardCollection completeList = new CardCollection();
// CR 613.7m use APNAP
PlayerCollection players = game.getPlayersInTurnOrder(game.getPhaseHandler().getPlayerTurn());
Expand All @@ -853,12 +857,28 @@ public static CardCollectionView orderCardsByTheirOwners(Game game, CardCollecti
subList.add(c);
}
}
if (sa != null && sa.getActivatingPlayer() == p && sa.hasParam("StaticEffect")) {
// create helper card for ordering
eff = new DetachedCardEffect(sa.getHostCard(), "Static Effect of " + sa.getHostCard());
subList.add(eff);
tool4ever marked this conversation as resolved.
Show resolved Hide resolved
}
CardCollectionView subListView = subList;
if (subList.size() > 1) {
subListView = p.getController().orderMoveToZoneList(subList, dest, sa);
}
completeList.addAll(subListView);
}
if (eff != null) {
int idx = completeList.indexOf(eff);
if (idx > 0) {
// effects with this param have the responsibility to realign it when later cards are reached
sa.setSVar("StaticEffectUntilCardID", String.valueOf(completeList.get(idx - 1).getId()));
// add generous offset to timestamp, to ensure it applies last compared to cards that were ordered to ETB before it
idx += completeList.size() * 2;
}
sa.setSVar("StaticEffectTimestamp", String.valueOf(game.getNextTimestamp() + idx));
completeList.remove(eff);
}
return completeList;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -585,14 +585,16 @@ protected static void addLeaveBattlefieldReplacement(final Card eff, final Strin
eff.addReplacementEffect(re);
}

// create a basic template for Effect to be used somewhere else
public static Card createEffect(final SpellAbility sa, final Player controller, final String name,
final String image) {
// create a basic template for Effect to be used somewhere els
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image) {
return createEffect(sa, controller, name, image, controller.getGame().getNextTimestamp());
}
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image, final long timestamp) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
final Card eff = new Card(game.nextCardId(), game);

eff.setGameTimestamp(game.getNextTimestamp());
eff.setGameTimestamp(timestamp);
eff.setName(name);
Hanmac marked this conversation as resolved.
Show resolved Hide resolved
eff.setColor(hostCard.getColor().getColor());
// if name includes emblem then it should be one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,6 @@ public void resolve(SpellAbility sa) {

if (destination == ZoneType.Battlefield) {
moveParams.put(AbilityKey.SimultaneousETB, cards);
if (sa.hasAdditionalAbility("AnimateSubAbility")) {
// need LKI before Animate does apply
moveParams.put(AbilityKey.CardLKI, CardCopyService.getLKICopy(c));

final SpellAbility animate = sa.getAdditionalAbility("AnimateSubAbility");
source.addRemembered(c);
AbilityUtils.resolve(animate);
source.removeRemembered(c);
animate.setSVar("unanimateTimestamp", String.valueOf(game.getTimestamp()));
}
if (sa.hasParam("Tapped")) {
c.setTapped(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import forge.game.event.GameEventCombatChanged;
import forge.game.keyword.Keyword;
import forge.game.player.*;
import forge.game.player.PlayerController.FullControlFlag;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
Expand Down Expand Up @@ -516,20 +517,23 @@ private void changeKnownOriginResolve(final SpellAbility sa) {
}

// CR 401.4
if (destination.isDeck() && !shuffle && tgtCards.size() > 1) {
if (((destination.isDeck() && tgtCards.size() > 1) || chooser.getController().isFullControl(FullControlFlag.LayerTimestampOrder)) && !shuffle) {
if (sa.hasParam("RandomOrder")) {
final CardCollection random = new CardCollection(tgtCards);
CardLists.shuffle(random);
tgtCards = random;
} else if (sa.hasParam("Chooser")) {
tgtCards = chooser.getController().orderMoveToZoneList(tgtCards, destination, sa);
} else {
if (sa.hasParam("Chooser"))
tgtCards = chooser.getController().orderMoveToZoneList(tgtCards, destination, sa);
else
tgtCards = GameActionUtil.orderCardsByTheirOwners(game, tgtCards, destination, sa);
tgtCards = GameActionUtil.orderCardsByTheirOwners(game, tgtCards, destination, sa);
}
}

for (final Card tgtC : tgtCards) {
if (sa.hasSVar("StaticEffectUntilCardID") && sa.getSVarInt("StaticEffectUntilCardID") == tgtC.getId()) {
sa.removeSVar("StaticEffectTimestamp");
}

final Card gameCard = game.getCardState(tgtC, null);
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
Expand Down Expand Up @@ -595,12 +599,13 @@ private void changeKnownOriginResolve(final SpellAbility sa) {
}
}
if (sa.hasParam("WithCountersType")) {
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
int cAmount = AbilityUtils.calculateAmount(hostCard, sa.getParamOrDefault("WithCountersAmount", "1"), sa);

GameEntityCounterTable table = new GameEntityCounterTable();
table.put(activator, gameCard, cType, cAmount);
moveParams.put(AbilityKey.CounterTable, table);
for (String type : sa.getParam("WithCountersType").split(",")) {
CounterType cType = CounterType.getType(type);
table.put(activator, gameCard, cType, cAmount);
}
} else if (sa.hasParam("WithNotedCounters")) {
CountersNoteEffect.loadCounters(gameCard, hostCard, chooser, sa, moveParams);
}
Expand Down Expand Up @@ -652,19 +657,6 @@ private void changeKnownOriginResolve(final SpellAbility sa) {
}
}

if (sa.hasAdditionalAbility("AnimateSubAbility")) {
// need LKI before Animate does apply
if (!moveParams.containsKey(AbilityKey.CardLKI)) {
moveParams.put(AbilityKey.CardLKI, CardCopyService.getLKICopy(gameCard));
}

final SpellAbility animate = sa.getAdditionalAbility("AnimateSubAbility");
hostCard.addRemembered(gameCard);
AbilityUtils.resolve(animate);
hostCard.removeRemembered(gameCard);
animate.setSVar("unanimateTimestamp", String.valueOf(game.getTimestamp()));
}

// need to be facedown before it hits the battlefield in case of Replacement Effects or Trigger
if (sa.hasParam("FaceDown")) {
gameCard.turnFaceDown(true);
Expand Down Expand Up @@ -1277,16 +1269,6 @@ else if (destination.equals(ZoneType.Battlefield)) {
if (sa.hasParam("Tapped")) {
c.setTapped(true);
}
if (sa.hasAdditionalAbility("AnimateSubAbility")) {
// need LKI before Animate does apply
moveParams.put(AbilityKey.CardLKI, CardCopyService.getLKICopy(c));

final SpellAbility animate = sa.getAdditionalAbility("AnimateSubAbility");
source.addRemembered(c);
AbilityUtils.resolve(animate);
source.removeRemembered(c);
animate.setSVar("unanimateTimestamp", String.valueOf(game.getTimestamp()));
}
if (sa.hasParam("GainControl")) {
final String g = sa.getParam("GainControl");
Player newController = g.equals("True") ? sa.getActivatingPlayer() :
Expand Down
10 changes: 0 additions & 10 deletions forge-game/src/main/java/forge/game/ability/effects/DigEffect.java
Original file line number Diff line number Diff line change
Expand Up @@ -399,16 +399,6 @@ else if (!sa.hasParam("NoLooking")) {
moveParams.put(AbilityKey.CounterTable, table);
}
}
if (sa.hasAdditionalAbility("AnimateSubAbility")) {
// need LKI before Animate does apply
moveParams.put(AbilityKey.CardLKI, CardCopyService.getLKICopy(c));

final SpellAbility animate = sa.getAdditionalAbility("AnimateSubAbility");
host.addRemembered(c);
AbilityUtils.resolve(animate);
host.removeRemembered(c);
animate.setSVar("unanimateTimestamp", String.valueOf(game.getTimestamp()));
}
c = game.getAction().moveTo(c.getController().getZone(destZone1), c, sa, moveParams);
if (destZone1.equals(ZoneType.Battlefield)) {
if (addToCombat(c, sa, "Attacking", "Blocking")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public enum FullControlFlag {
NoPaymentFromManaAbility,
NoFreeCombatCostHandling,
AllowPaymentStartWithMissingResources,
//AdditionalLayerTimestampOrder // tokens etc.
LayerTimestampOrder // for StaticEffect$, tokens later etc.
}

private Set<FullControlFlag> fullControls = EnumSet.noneOf(FullControlFlag.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1516,6 +1516,7 @@ public void showFullControl(PlayerView pv, MouseEvent e) {
addFullControlEntry(menu, "lblNoPaymentFromManaAbility", FullControlFlag.NoPaymentFromManaAbility, controlFlags);
addFullControlEntry(menu, "lblNoFreeCombatCostHandling", FullControlFlag.NoFreeCombatCostHandling, controlFlags);
addFullControlEntry(menu, "lblAllowPaymentStartWithMissingResources", FullControlFlag.AllowPaymentStartWithMissingResources, controlFlags);
addFullControlEntry(menu, "lblLayerTimestampOrder", FullControlFlag.LayerTimestampOrder, controlFlags);

menu.show(view.getControl().getFieldViewFor(pv).getAvatarArea(), e.getX(), e.getY());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ protected void buildMenu() {
addItem(getFullControlMenuEntry("lblNoPaymentFromManaAbility", FullControlFlag.NoPaymentFromManaAbility, controlFlags));
addItem(getFullControlMenuEntry("lblNoFreeCombatCostHandling", FullControlFlag.NoFreeCombatCostHandling, controlFlags));
addItem(getFullControlMenuEntry("lblAllowPaymentStartWithMissingResources", FullControlFlag.AllowPaymentStartWithMissingResources, controlFlags));
addItem(getFullControlMenuEntry("lblLayerTimestampOrder", FullControlFlag.LayerTimestampOrder, controlFlags));
}
};

Expand Down
4 changes: 2 additions & 2 deletions forge-gui/res/cardsfolder/d/dance_of_the_manse.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Name:Dance of the Manse
ManaCost:X W U
Types:Sorcery
A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Artifact.cmcLEX+YouOwn,Enchantment.cmcLEX+YouOwn+nonAura | TgtPrompt$ Select target artifact or non-Aura enchantment in your graveyard | TargetMin$ 0 | TargetMax$ X | AnimateSubAbility$ DBAnimate | SpellDescription$ Return up to X target artifact and/or non-Aura enchantment cards with mana value X or less from your graveyard to the battlefield. If X is 6 or more, those permanents are 4/4 creatures in addition to their other types.
SVar:DBAnimate:DB$ Animate | Defined$ Remembered | Types$ Creature | Power$ 4 | Toughness$ 4 | Duration$ Permanent | ConditionCheckSVar$ X | ConditionSVarCompare$ GE6
A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Artifact.cmcLEX+YouOwn,Enchantment.cmcLEX+YouOwn+nonAura | TgtPrompt$ Select target artifact or non-Aura enchantment in your graveyard | TargetMin$ 0 | TargetMax$ X | StaticEffect$ Animate | StaticEffectCheckSVar$ X | StaticEffectSVarCompare$ GE6 | SpellDescription$ Return up to X target artifact and/or non-Aura enchantment cards with mana value X or less from your graveyard to the battlefield. If X is 6 or more, those permanents are 4/4 creatures in addition to their other types.
SVar:Animate:Mode$ Continuous | Affected$ Card.IsRemembered | AddType$ Creature | SetPower$ 4 | SetToughness$ 4
SVar:X:Count$xPaid
AI:RemoveDeck:All
Oracle:Return up to X target artifact and/or non-Aura enchantment cards each with mana value X or less from your graveyard to the battlefield. If X is 6 or more, those permanents are 4/4 creatures in addition to their other types.
Loading
Loading