CardStateName: combine Modal with Transformed (#8305)

* CardStateName: combine Modal with Transformed

* Make new TMDFC transformable

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
This commit is contained in:
Hans Mackowiak
2025-08-07 11:28:24 +02:00
committed by GitHub
parent 0c87ed7381
commit a4671b62d4
18 changed files with 75 additions and 57 deletions

View File

@@ -481,7 +481,7 @@ public class AiController {
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
// don't play MDFC land if other side is spell and enough lands are available
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
return false;
}

View File

@@ -321,9 +321,7 @@ public abstract class GameState {
newText.append(":Cloaked");
}
}
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
newText.append("|Transformed");
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
@@ -332,8 +330,12 @@ public abstract class GameState {
newText.append(":");
newText.append(c.getMeldedWith().getName()).append(suffix);
}
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
if (c.isModal()) {
newText.append("|Modal");
} else {
newText.append("|Transformed");
}
}
if (c.getPlayerAttachedTo() != null) {
@@ -1314,8 +1316,8 @@ public abstract class GameState {
if (info.endsWith("Cloaked")) {
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
}
} else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true);
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
c.setState(CardStateName.Backside, true);
c.setBackSide(true);
} else if (info.startsWith("Flipped")) {
c.setState(CardStateName.Flipped, true);
@@ -1333,9 +1335,6 @@ public abstract class GameState {
}
c.setState(CardStateName.Meld, true);
c.setBackSide(true);
} else if (info.startsWith("Modal")) {
c.setState(CardStateName.Modal, true);
c.setBackSide(true);
}
else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";

View File

@@ -153,8 +153,8 @@ public class PlayAi extends SpellAbilityAi {
final boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
final CardStateName state;
if (sa.hasParam("CastTransformed")) {
state = CardStateName.Transformed;
options.forEach(c -> c.changeToState(CardStateName.Transformed));
state = CardStateName.Backside;
options.forEach(c -> c.changeToState(CardStateName.Backside));
} else {
state = CardStateName.Original;
}

View File

@@ -166,6 +166,24 @@ public final class CardRules implements ICardCharacteristics {
return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values());
}
public boolean isTransformable() {
if (CardSplitType.Transform == getSplitType()) {
return true;
}
if (CardSplitType.Modal != getSplitType()) {
return false;
}
for (ICardFace face : getAllFaces()) {
for (String spell : face.getAbilities()) {
if (spell.contains("AB$ SetState") && spell.contains("Mode$ Transform")) {
return true;
}
}
// TODO check keywords if needed
}
return false;
}
public ICardFace getWSpecialize() {
return specializedParts.get(CardStateName.SpecializeW);
}

View File

@@ -7,13 +7,13 @@ import java.util.EnumSet;
public enum CardSplitType
{
None(FaceSelectionMethod.USE_PRIMARY_FACE, null),
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Transformed),
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped),
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
Omen(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal),
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);
public static final EnumSet<CardSplitType> DUAL_FACED_CARDS = EnumSet.of(

View File

@@ -5,12 +5,11 @@ public enum CardStateName {
Original,
FaceDown,
Flipped,
Transformed,
Backside,
Meld,
LeftSplit,
RightSplit,
Secondary,
Modal,
EmptyRoom,
SpecializeW,
SpecializeU,
@@ -42,7 +41,7 @@ public enum CardStateName {
return CardStateName.Flipped;
}
if ("DoubleFaced".equalsIgnoreCase(value)) {
return CardStateName.Transformed;
return CardStateName.Backside;
}
throw new IllegalArgumentException("No element named " + value + " in enum CardCharactersticName");

View File

@@ -2989,14 +2989,14 @@ public class AbilityUtils {
if (tgtCard.isFaceDown()) {
collectSpellsForPlayEffect(list, original, controller, withAltCost);
} else {
if (state == CardStateName.Transformed && tgtCard.isPermanent() && !tgtCard.isAura()) {
if (state == CardStateName.Backside && !tgtCard.isModal() && tgtCard.isPermanent() && !tgtCard.isAura()) {
// casting defeated battle
Spell sp = new SpellPermanent(tgtCard, original);
sp.setCardState(original);
list.add(sp);
}
if (tgtCard.isModal() && tgtCard.hasState(CardStateName.Modal)) {
collectSpellsForPlayEffect(list, tgtCard.getState(CardStateName.Modal), controller, withAltCost);
if (tgtCard.isModal() && tgtCard.hasState(CardStateName.Backside)) {
collectSpellsForPlayEffect(list, tgtCard.getState(CardStateName.Backside), controller, withAltCost);
}
}

View File

@@ -302,7 +302,7 @@ public class CopyPermanentEffect extends TokenEffectBase {
copy.setStates(CardFactory.getCloneStates(original, copy, sa));
// force update the now set State
if (original.isTransformable()) {
copy.setState(original.isTransformed() ? CardStateName.Transformed : CardStateName.Original, true, true);
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
} else {
copy.setState(copy.getCurrentStateName(), true, true);
}

View File

@@ -276,13 +276,13 @@ public class PlayEffect extends SpellAbilityEffect {
CardStateName state = CardStateName.Original;
if (sa.hasParam("CastTransformed")) {
if (!tgtCard.changeToState(CardStateName.Transformed)) {
if (!tgtCard.changeToState(CardStateName.Backside)) {
// Failed to transform. In the future, we might need to just remove this option and continue
amount--;
System.err.println("CastTransformed failed for '" + tgtCard + "'.");
continue;
}
state = CardStateName.Transformed;
state = CardStateName.Backside;
}
List<SpellAbility> sas = AbilityUtils.getSpellsFromPlayEffect(tgtCard, controller, state, !altCost);

View File

@@ -554,8 +554,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public boolean setState(final CardStateName state, boolean updateView, boolean forceUpdate) {
boolean rollback = state == CardStateName.Original
&& (currentStateName == CardStateName.Flipped || currentStateName == CardStateName.Transformed);
boolean transform = state == CardStateName.Flipped || state == CardStateName.Transformed || state == CardStateName.Meld;
&& (currentStateName == CardStateName.Flipped || currentStateName == CardStateName.Backside);
boolean transform = state == CardStateName.Flipped || state == CardStateName.Backside || state == CardStateName.Meld;
boolean needsTransformAnimation = transform || rollback;
// faceDown has higher priority over clone states
// while text change states doesn't apply while the card is faceDown
@@ -675,7 +675,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
c.backside = !c.backside;
// 613.7g A transforming double-faced permanent receives a new timestamp each time it transforms.
c.setLayerTimestamp(ts);
boolean result = c.changeToState(c.backside ? CardStateName.Transformed : CardStateName.Original);
boolean result = c.changeToState(c.backside ? CardStateName.Backside : CardStateName.Original);
retResult = retResult || result;
}
if (hasMergedCard()) {
@@ -939,7 +939,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return true;
}
CardStateName destState = transformCard.backside ? CardStateName.Original : CardStateName.Transformed;
CardStateName destState = transformCard.backside ? CardStateName.Original : CardStateName.Backside;
// use Original State for the transform check
if (!transformCard.getOriginalState(destState).getType().isPermanent()) {
@@ -1056,7 +1056,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public final boolean isTransformable() {
return getRules() != null && getRules().getSplitType() == CardSplitType.Transform;
return getRules() != null && getRules().isTransformable();
}
public final boolean isMeldable() {
@@ -2930,7 +2930,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
sb.append("(Gain the next level as a sorcery to add its ability.)").append(linebreak);
}
if (state.getStateName().equals(CardStateName.Transformed) &&
if (state.getStateName().equals(CardStateName.Backside) && state.getCard().isTransformable() &&
state.getView().getOracleText().startsWith("(Transforms")) {
sb.append("(").append(Localizer.getInstance().getMessage("lblTransformsFrom",
CardTranslation.getTranslatedName(state.getCard().getState(CardStateName.Original).getName())));
@@ -5753,7 +5753,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return isInstant() || isSorcery() || (isAura() && !isInZone(ZoneType.Battlefield));
}
public final boolean hasPlayableLandFace() { return isLand() || (isModal() && getState(CardStateName.Modal).getType().isLand()); }
public final boolean hasPlayableLandFace() { return isLand() || (isModal() && getState(CardStateName.Backside).getType().isLand()); }
public final boolean isLand() { return getType().isLand(); }
public final boolean isBasicLand() { return getType().isBasicLand(); }
@@ -7550,7 +7550,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
System.out.println(TextUtil.concatWithSpace("Illegal Split Card CMC mode", mode.toString(),"passed to getCMC!"));
break;
}
} else if (currentStateName == CardStateName.Transformed) {
} else if (currentStateName == CardStateName.Backside && !isModal()) {
// Except in the cases were we clone the back-side of a DFC.
if (getCopiedPermanent() != null) {
return 0;
@@ -7699,8 +7699,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
}
// Add Modal Spells
if (isModal() && hasState(CardStateName.Modal)) {
for (SpellAbility sa : getState(CardStateName.Modal).getSpellAbilities()) {
if (isModal() && hasState(CardStateName.Backside)) {
for (SpellAbility sa : getState(CardStateName.Backside).getSpellAbilities()) {
//add alternative costs as additional spell abilities
// only add Spells there
if (sa.isSpell() || sa.isLandAbility()) {

View File

@@ -150,7 +150,7 @@ public class CardCopyService {
}
final boolean fromIsFlipCard = copyFrom.isFlipCard();
final boolean fromIsTransformedCard = copyFrom.getCurrentStateName() == CardStateName.Transformed || copyFrom.getCurrentStateName() == CardStateName.Meld;
final boolean fromIsTransformedCard = copyFrom.getCurrentStateName() == CardStateName.Backside || copyFrom.getCurrentStateName() == CardStateName.Meld;
if (fromIsFlipCard) {
if (to.getCurrentStateName().equals(CardStateName.Flipped)) {
@@ -163,7 +163,7 @@ public class CardCopyService {
&& sourceSA != null && ApiType.CopySpellAbility.equals(sourceSA.getApi())
&& targetSA != null && targetSA.isSpell() && targetSA.getHostCard().isPermanent()) {
copyState(copyFrom, CardStateName.Original, to, CardStateName.Original);
copyState(copyFrom, CardStateName.Transformed, to, CardStateName.Transformed);
copyState(copyFrom, CardStateName.Backside, to, CardStateName.Backside);
// 707.10g If an effect creates a copy of a transforming permanent spell, the copy is also a transforming permanent spell that has both a front face and a back face.
// The characteristics of its front and back face are determined by the copiable values of the same face of the spell it is a copy of, as modified by any other copy effects.
// If the spell it is a copy of has its back face up, the copy is created with its back face up. The token thats put onto the battlefield as that spell resolves is a transforming token.
@@ -257,8 +257,8 @@ public class CardCopyService {
newCopy.getState(CardStateName.Flipped).copyFrom(copyFrom.getState(CardStateName.Flipped), true);
} else if (copyFrom.isTransformable()) {
newCopy.getState(CardStateName.Original).copyFrom(copyFrom.getState(CardStateName.Original), true);
newCopy.addAlternateState(CardStateName.Transformed, false);
newCopy.getState(CardStateName.Transformed).copyFrom(copyFrom.getState(CardStateName.Transformed), true);
newCopy.addAlternateState(CardStateName.Backside, false);
newCopy.getState(CardStateName.Backside).copyFrom(copyFrom.getState(CardStateName.Backside), true);
} else if (copyFrom.hasState(CardStateName.Secondary)) {
newCopy.getState(CardStateName.Original).copyFrom(copyFrom.getState(CardStateName.Original), true);
newCopy.addAlternateState(CardStateName.Secondary, false);

View File

@@ -102,7 +102,7 @@ public class CardFactory {
copy.setStates(getCloneStates(original, copy, sourceSA));
// force update the now set State
if (original.isTransformable()) {
copy.setState(original.isTransformed() ? CardStateName.Transformed : CardStateName.Original, true, true);
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
} else {
copy.setState(copy.getCurrentStateName(), true, true);
}
@@ -549,9 +549,9 @@ public class CardFactory {
ret1.copyFrom(in.getState(CardStateName.Original), false, sa);
result.put(CardStateName.Original, ret1);
final CardState ret2 = new CardState(out, CardStateName.Transformed);
ret2.copyFrom(in.getState(CardStateName.Transformed), false, sa);
result.put(CardStateName.Transformed, ret2);
final CardState ret2 = new CardState(out, CardStateName.Backside);
ret2.copyFrom(in.getState(CardStateName.Backside), false, sa);
result.put(CardStateName.Backside, ret2);
} else if (in.isSplitCard()) {
// for split cards, copy all three states
final CardState ret1 = new CardState(out, CardStateName.Original);

View File

@@ -416,10 +416,14 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
// SpellPermanent only for Original State
switch(getStateName()) {
case Backside:
if (!getCard().isModal()) {
return;
}
break;
case Original:
case LeftSplit:
case RightSplit:
case Modal:
case SpecializeB:
case SpecializeG:
case SpecializeR:

View File

@@ -1964,9 +1964,9 @@ public class Player extends GameEntity implements Comparable<Player> {
speedEffect.setOwner(this);
speedEffect.setGamePieceType(GamePieceType.EFFECT);
speedEffect.addAlternateState(CardStateName.Transformed, false);
speedEffect.addAlternateState(CardStateName.Backside, false);
CardState speedFront = speedEffect.getState(CardStateName.Original);
CardState speedBack = speedEffect.getState(CardStateName.Transformed);
CardState speedBack = speedEffect.getState(CardStateName.Backside);
speedFront.setImageKey("t:speed");
speedFront.setName("Start Your Engines!");
@@ -1990,7 +1990,7 @@ public class Player extends GameEntity implements Comparable<Player> {
speedEffect.updateStateForView();
if(this.maxSpeed())
speedEffect.setState(CardStateName.Transformed, true);
speedEffect.setState(CardStateName.Backside, true);
final PlayerZone com = getZone(ZoneType.Command);
com.add(speedEffect);
@@ -2006,8 +2006,8 @@ public class Player extends GameEntity implements Comparable<Player> {
String label = this.maxSpeed() ? localizer.getMessage("lblMaxSpeed") : localizer.getMessage("lblSpeed", this.speed);
speedEffect.setOverlayText(label);
if(maxSpeed() && speedEffect.getCurrentStateName() == CardStateName.Original)
speedEffect.setState(CardStateName.Transformed, true);
else if(!maxSpeed() && speedEffect.getCurrentStateName() == CardStateName.Transformed)
speedEffect.setState(CardStateName.Backside, true);
else if(!maxSpeed() && speedEffect.getCurrentStateName() == CardStateName.Backside)
speedEffect.setState(CardStateName.Original, true);
}

View File

@@ -1755,7 +1755,7 @@ public class GameSimulationTest extends SimulationTest {
AssertJUnit.assertFalse(outlaw.isCloned());
AssertJUnit.assertTrue(outlaw.isTransformable());
AssertJUnit.assertTrue(outlaw.hasState(CardStateName.Transformed));
AssertJUnit.assertTrue(outlaw.hasState(CardStateName.Backside));
AssertJUnit.assertTrue(outlaw.canTransform(null));
AssertJUnit.assertFalse(outlaw.isBackSide());
@@ -1788,7 +1788,7 @@ public class GameSimulationTest extends SimulationTest {
AssertJUnit.assertTrue(clonedOutLaw.isCloned());
AssertJUnit.assertTrue(clonedOutLaw.isTransformable());
AssertJUnit.assertTrue(clonedOutLaw.hasState(CardStateName.Transformed));
AssertJUnit.assertTrue(clonedOutLaw.hasState(CardStateName.Backside));
AssertJUnit.assertTrue(clonedOutLaw.canTransform(null));
AssertJUnit.assertFalse(clonedOutLaw.isBackSide());
@@ -1807,7 +1807,7 @@ public class GameSimulationTest extends SimulationTest {
AssertJUnit.assertTrue(transformOutLaw.isCloned());
AssertJUnit.assertTrue(transformOutLaw.isTransformable());
AssertJUnit.assertTrue(transformOutLaw.hasState(CardStateName.Transformed));
AssertJUnit.assertTrue(transformOutLaw.hasState(CardStateName.Backside));
AssertJUnit.assertTrue(transformOutLaw.canTransform(null));
AssertJUnit.assertTrue(transformOutLaw.isBackSide());
@@ -1822,7 +1822,7 @@ public class GameSimulationTest extends SimulationTest {
AssertJUnit.assertFalse(transformOutLaw.isCloned());
AssertJUnit.assertTrue(transformOutLaw.isTransformable());
AssertJUnit.assertTrue(transformOutLaw.hasState(CardStateName.Transformed));
AssertJUnit.assertTrue(transformOutLaw.hasState(CardStateName.Backside));
AssertJUnit.assertTrue(transformOutLaw.canTransform(null));
AssertJUnit.assertTrue(transformOutLaw.isBackSide());

View File

@@ -241,7 +241,7 @@ public class FCardPanel extends FDisplayObject {
CardRenderer.drawCardWithOverlays(g, card, x2, y, w2, h, getStackPosition());
} else {
//transform
if (card.getCurrentState().getState() == CardStateName.Transformed || card.getCurrentState().getState() == CardStateName.Flipped) {
if (card.getCurrentState().getState() == CardStateName.Backside || card.getCurrentState().getState() == CardStateName.Flipped) {
DURATION = 0.16f;
CardRenderer.drawCardWithOverlays(g, card, x2, y, w2, h, getStackPosition());
} else if (card.getCurrentState().getState() == CardStateName.Meld) {

View File

@@ -249,9 +249,8 @@ public abstract class AbstractGuiGame implements IGuiGame, IMayViewCards {
}
return true; //original can always be shown if not a face down that can't be shown
case Flipped:
case Transformed:
case Meld:
case Modal:
case Backside:
return true;
case Secondary:
if (cv.isFaceDown()) {

View File

@@ -2964,8 +2964,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
}
} else {
forgeCard.changeToState(forgeCard.getRules().getSplitType().getChangedStateName());
if (forgeCard.getCurrentStateName().equals(CardStateName.Transformed) ||
forgeCard.getCurrentStateName().equals(CardStateName.Modal)) {
if (forgeCard.getCurrentStateName().equals(CardStateName.Backside)) {
forgeCard.setBackSide(true);
}
}