BOT: "new" mechanics (#1679)

* CardFactoryUtil.addStaticAbility support "Living metal" keyword

* GameActionUtil.getAlternativeCosts() add "More Than Meets the Eye"

* Keyword.LIVING_METAL and Keyword.MORE_THAN_MEETS_THE_EYE

* TypeLists add Robot

* AlternativeCost.MTMtE

* ultra_magnus_tactician_ultra_magnus_armored_carrier.txt

* CardSplitType.Convert

* CardStateName.Converted

* GameState.addCard Converted check

* DevModeCheats for Converted

* Card.changeCardState() add Convert mode

* Card.isConvertable()

* PaperCard.hasBackFace add Convert

* Card implement convertedTimestamp

* Card.keywordsToText add Living metal to list

* DamageDealEffect.internalDamageDeal move "ExcessSVar" for more flexibility

* ComputerUtil.choosePermanentsToSacrifice improve AI for Megatron

* megatron_tyrant_megatron_destructive_force.txt

* optimus_prime_hero_optimus_prime_autobot_leader.txt

* ChangeZoneEffect.changeKnownOriginResolve support "Converted"

* Card.changeCardState() fixup
This commit is contained in:
Northmoc
2022-10-17 12:04:06 -04:00
committed by GitHub
parent 16b52a3645
commit e00387f0fa
17 changed files with 175 additions and 11 deletions

View File

@@ -840,12 +840,13 @@ public class ComputerUtil {
String logic = source.getParamOrDefault("AILogic", "");
if (logic.startsWith("SacForDamage")) {
if (c.getNetPower() <= 0) {
final int damageAmt = logic.contains("cmc") ? c.getManaCost().getCMC() : c.getNetPower();
if (damageAmt <= 0) {
return false;
} else if (c.getNetPower() >= ai.getOpponentsSmallestLifeTotal()) {
} else if (damageAmt >= ai.getOpponentsSmallestLifeTotal()) {
return true;
} else if (logic.endsWith(".GiantX2") && c.getType().hasCreatureType("Giant")
&& c.getNetPower() * 2 >= ai.getOpponentsSmallestLifeTotal()) {
&& damageAmt * 2 >= ai.getOpponentsSmallestLifeTotal()) {
return true; // TODO: generalize this for any type and actually make the AI prefer giants?
}
}

View File

@@ -313,6 +313,8 @@ public abstract class GameState {
newText.append("|Meld");
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal");
} else if (c.getCurrentStateName().equals(CardStateName.Converted)) {
newText.append("|Converted");
}
if (c.getPlayerAttachedTo() != null) {

View File

@@ -5,6 +5,7 @@ import forge.card.CardFace.FaceSelectionMethod;
public enum CardSplitType
{
None(FaceSelectionMethod.USE_PRIMARY_FACE, null),
Convert(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Converted),
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Transformed),
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),

View File

@@ -5,6 +5,7 @@ public enum CardStateName {
Original,
FaceDown,
Flipped,
Converted,
Transformed,
Meld,
LeftSplit,

View File

@@ -399,7 +399,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
@Override
public boolean hasBackFace(){
CardSplitType cst = this.rules.getSplitType();
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld || cst == CardSplitType.Modal;
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld
|| cst == CardSplitType.Modal || cst == CardSplitType.Convert;
}
// Return true if card is one of the five basic lands that can be added for free

View File

@@ -275,6 +275,28 @@ public final class GameActionUtil {
foretold.setPayCosts(new Cost(k[1], false));
alternatives.add(foretold);
} else if (keyword.startsWith("More Than Meets the Eye")) {
final String[] k = keyword.split(":");
final Cost convertCost = new Cost(k[1], true);
final SpellAbility newSA = new SpellPermanent(source);
newSA.setCardState(source.getAlternateState());
newSA.setPayCosts(convertCost);
newSA.setActivatingPlayer(activator);
newSA.putParam("PrecostDesc", k[0] + " ");
newSA.putParam("CostDesc", convertCost.toString());
// makes new SpellDescription
final StringBuilder desc = new StringBuilder();
desc.append(newSA.getCostDescription());
desc.append("(").append(inst.getReminderText()).append(")");
newSA.setDescription(desc.toString());
newSA.putParam("AfterDescription", "(Converted)");
newSA.setAlternativeCost(AlternativeCost.MTMtE);
alternatives.add(newSA);
}
}

View File

@@ -588,6 +588,14 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
continue;
}
}
if (sa.hasParam("Converted")) {
if (gameCard.isConvertable()) {
gameCard.changeCardState("Convert", null, sa);
} else {
// If it can't convert, don't change zones.
continue;
}
}
if (sa.hasParam("WithCountersType")) {
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
int cAmount = AbilityUtils.calculateAmount(hostCard, sa.getParamOrDefault("WithCountersAmount", "1"), sa);

View File

@@ -298,9 +298,9 @@ public class DamageDealEffect extends DamageBaseEffect {
}
} else {
damageMap.put(sourceLKI, c, dmg);
if (sa.hasParam("ExcessSVar")) {
sa.setSVar(sa.getParam("ExcessSVar"), Integer.toString(excess));
}
}
if (sa.hasParam("ExcessSVar")) {
sa.setSVar(sa.getParam("ExcessSVar"), Integer.toString(excess));
}
}
}

View File

@@ -220,6 +220,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private long bestowTimestamp = -1;
private long transformedTimestamp = 0;
private long convertedTimestamp = 0;
private long mutatedTimestamp = -1;
private int timesMutated = 0;
private boolean tributed = false;
@@ -392,6 +393,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public long getTransformedTimestamp() { return transformedTimestamp; }
public void incrementTransformedTimestamp() { this.transformedTimestamp++; }
public long getConvertedTimestamp() { return convertedTimestamp; }
public void incrementConvertedTimestamp() { this.convertedTimestamp++; }
public CardState getCurrentState() {
return currentState;
}
@@ -625,6 +629,29 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return retResult;
} else if (mode.equals("Convert") && (isConvertable() || hasMergedCard())) {
// Need to remove mutated states, otherwise the changeToState() will fail
if (hasMergedCard()) {
removeMutatedStates();
}
CardCollectionView cards = hasMergedCard() ? getMergedCards() : new CardCollection(this);
boolean retResult = false;
for (final Card c : cards) {
if (!c.isConvertable()) {
continue;
}
c.backside = !c.backside;
boolean result = c.changeToState(c.backside ? CardStateName.Converted : CardStateName.Original);
retResult = retResult || result;
}
if (hasMergedCard()) {
rebuildMutatedStates(cause);
game.getTriggerHandler().clearActiveTriggers(this, null);
game.getTriggerHandler().registerActiveTrigger(this, false);
}
return retResult;
} else if (mode.equals("Flip")) {
// 709.4. Flipping a permanent is a one-way process.
if (isFlipped()) {
@@ -930,12 +957,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return getRules() != null && getRules().getSplitType() == CardSplitType.Meld;
}
public final boolean isConvertable() {
return getRules() != null && getRules().getSplitType() == CardSplitType.Convert;
}
public final boolean isModal() {
return getRules() != null && getRules().getSplitType() == CardSplitType.Modal;
}
public final boolean hasBackSide() {
return isDoubleFaced() || isMeldable() || isModal();
return isDoubleFaced() || isMeldable() || isModal() || isConvertable();
}
public final boolean isFlipCard() {
@@ -2035,7 +2066,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|| keyword.startsWith("Escape") || keyword.startsWith("Foretell:")
|| keyword.startsWith("Disturb") || keyword.startsWith("Madness:")
|| keyword.startsWith("Reconfigure") || keyword.startsWith("Squad")
|| keyword.startsWith("Miracle")) {
|| keyword.startsWith("Miracle") || keyword.startsWith("More Than Meets the Eye")) {
String[] k = keyword.split(":");
sbLong.append(k[0]);
if (k.length > 1) {
@@ -2148,7 +2179,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|| keyword.equals("Living Weapon") || keyword.equals("Myriad") || keyword.equals("Exploit")
|| keyword.equals("Changeling") || keyword.equals("Delve") || keyword.equals("Decayed")
|| keyword.equals("Split second") || keyword.equals("Sunburst")
|| keyword.equals("Double team")
|| keyword.equals("Double team") || keyword.equals("Living metal")
|| keyword.equals("Suspend") // for the ones without amount
|| keyword.equals("Foretell") // for the ones without cost
|| keyword.equals("Ascend") || keyword.equals("Totem armor")

View File

@@ -3692,6 +3692,9 @@ public class CardFactoryUtil {
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+notSharesColorWith | Secondary$ True " +
" | Description$ Intimidate ( " + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Living metal")) {
String effect = "Mode$ Continuous | Affected$ Card.Self | AddType$ Creature | Condition$ PlayerTurn | Secondary$ True";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Nightbound")) {
String effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Nightbound | Secondary$ True | Description$ This permanent can't be transformed except by its nightbound ability.";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));

View File

@@ -109,6 +109,7 @@ public enum Keyword {
LANDWALK("Landwalk", KeywordWithType.class, false, "This creature is unblockable as long as defending player controls a %s."),
LEVEL_UP("Level up", KeywordWithCost.class, false, "%s: Put a level counter on this. Level up only as a sorcery."),
LIFELINK("Lifelink", SimpleKeyword.class, true, "Damage dealt by this creature also causes its controller to gain that much life."),
LIVING_METAL("Living metal", SimpleKeyword.class, true, "As long as it's your turn, this Vehicle is also a creature."),
LIVING_WEAPON("Living Weapon", SimpleKeyword.class, true, "When this Equipment enters the battlefield, create a 0/0 black Phyrexian Germ creature token, then attach this to it."),
MADNESS("Madness", KeywordWithCost.class, false, "If you discard this card, discard it into exile. When you do, cast it for its madness cost or put it into your graveyard."),
MELEE("Melee", SimpleKeyword.class, false, "Whenever this creature attacks, it gets +1/+1 until end of turn for each opponent you attacked this combat."),
@@ -119,6 +120,7 @@ public enum Keyword {
// technically not a keyword but easier this way
MONSTROSITY("Monstrosity", KeywordWithCostAndAmount.class, false, "If this creature isn't monstrous, put {%2$d:+1/+1 counter} on it and it becomes monstrous."),
MODULAR("Modular", Modular.class, false, "This creature enters the battlefield with {%d:+1/+1 counter} on it. When it dies, you may put its +1/+1 counters on target artifact creature."),
MORE_THAN_MEETS_THE_EYE("More Than Meets the Eye", KeywordWithCost.class, false, "You may cast this card converted for %s."),
MORPH("Morph", KeywordWithCost.class, false, "You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its morph cost."),
MULTIKICKER("Multikicker", KeywordWithCost.class, false, "You may pay an additional %s any number of times as you cast this spell."),
MUTATE("Mutate", KeywordWithCost.class, true, "If you cast this spell for its mutate cost, put it over or under target non-Human creature you own. They mutate into the creature on top plus all abilities from under it."),

View File

@@ -13,6 +13,7 @@ public enum AlternativeCost {
Flashback,
Foretold,
Madness,
MTMtE, // More Than Meets the Eye (Transformers Universes Beyond)
Mutate,
Offering,
Outlast, // ActivatedAbility

View File

@@ -0,0 +1,30 @@
Name:Megatron, Tyrant
ManaCost:3 R W B
Types:Legendary Artifact Creature Robot
PT:7/5
K:More Than Meets the Eye:1 R W B
S:Mode$ CantBeCast | ValidCard$ Card | Caster$ Opponent | Phases$ BeginCombat->EndCombat | Description$ Your opponents can't cast spells during combat.
T:Mode$ Phase | Phase$ Main2 | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigConvert | OptionalDecider$ You | TriggerDescription$ At the beginning of your postcombat main phase, you may convert NICKNAME. If you do, add {C} for each 1 life your opponents have lost this turn.
SVar:TrigConvert:DB$ SetState | Mode$ Convert | RememberChanged$ True | SubAbility$ DBMana
SVar:DBMana:DB$ Mana | ConditionDefined$ Remembered | ConditionPresent$ Card | Produced$ C | Amount$ X | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:X:Count$LifeOppsLostThisTurn
AlternateMode:Convert
Oracle:More Than Meets the Eye {1}{R}{W}{B} (You may cast this card converted for {1}{R}{W}{B}.)\nYour opponents can't cast spells during combat.\nAt the beginning of your postcombat main phase, you may convert Megatron. If you do, add {C} for each 1 life your opponents have lost this turn.
ALTERNATE
Name:Megatron, Destructive Force
ManaCost:no cost
Colors:white,black,red
Types:Legendary Artifact Vehicle
PT:4/5
K:Living metal
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigImmediate | TriggerDescription$ Whenever NICKNAME attacks, you may sacrifice another artifact. When you do, NICKNAME deals damage equal to the sacrificed artifact's mana value to target creature. If excess damage would be dealt to that creature this way, instead that damage is dealt to that creature's controller and you convert NICKNAME.
SVar:TrigImmediate:AB$ ImmediateTrigger | Cost$ Sac<1/Artifact.Other/another artifact> | RememberObjects$ Sacrificed | Execute$ TrigDamage | AILogic$ SacForDamage.cmc | TriggerDescription$ When you do, NICKNAME deals damage equal to the sacrificed artifact's mana value to target creature. If excess damage would be dealt to that creature this way, instead that damage is dealt to that creature's controller and you convert NICKNAME.
SVar:TrigDamage:DB$ DealDamage | ValidTgts$ Creature | NumDmg$ X | ExcessSVar$ Excess | ExcessDamage$ TargetedController | SubAbility$ DBConvert
SVar:DBConvert:DB$ SetState | ConditionCheckSVar$ Excess | Mode$ Convert
SVar:X:TriggerRemembered$CardManaCost
SVar:HasAttackEffect:TRUE
DeckHas:Ability$Sacrifice
Oracle:Living metal (As long as it's your turn, this Vehicle is also a creature.)\nWhenever Megatron attacks, you may sacrifice another artifact. When you do, Megatron deals damage equal to the sacrificed artifact's mana value to target creature. If excess damage would be dealt to that creature this way, instead that damage is dealt to that creature's controller and you convert Megatron.

View File

@@ -0,0 +1,29 @@
Name:Optimus Prime, Hero
ManaCost:3 U R W
Types:Legendary Artifact Creature Robot
PT:4/8
K:More Than Meets the Eye:2 U R W
T:Mode$ Phase | Phase$ End of Turn | TriggerZones$ Battlefield | Execute$ TrigBolster | TriggerDescription$ At the beginning of each end step, bolster 1. (Choose a creature with the least toughness among creatures you control and put a +1/+1 counter on it.)
SVar:TrigBolster:DB$ PutCounter | Bolster$ True | CounterType$ P1P1
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigReturn | TriggerDescription$ When NICKNAME dies, return it to the battlefield converted under its owner's control.
SVar:TrigReturn:DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Battlefield | Converted$ True
AlternateMode:Convert
DeckHas:Ability$Counters
Oracle:More Than Meets the Eye {2}{U}{R}{W} (You may cast this card converted for {2}{U}{R}{W}.)\nAt the beginning of each end step, bolster 1. (Choose a creature with the least toughness among creatures you control and put a +1/+1 counter on it.)\nWhen Optimus Prime dies, return it to the battlefield converted under its owner's control.
ALTERNATE
Name:Optimus Prime, Autobot Leader
ManaCost:no cost
Colors:white,blue,red
Types:Legendary Artifact Vehicle
PT:6/8
K:Living metal
K:Trample
T:Mode$ AttackersDeclared | AttackingPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigBolster | TriggerDescription$ Whenever you attack, bolster 2. The chosen creature gains trample until end of turn. When that creature deals combat damage to a player this turn, convert NICKNAME.
SVar:TrigBolster:DB$ PutCounter | Bolster$ True | CounterType$ P1P1 | CounterNum$ 2 | RememberPut$ True | SubAbility$ DBPump
SVar:DBPump:DB$ Pump | Defined$ Remembered | KW$ Trample | SubAbility$ DBDelayedTrigger
SVar:DBDelayedTrigger:DB$ DelayedTrigger | Mode$ DamageDone | RememberObjects$ Remembered | ValidSource$ Card.IsTriggerRemembered | ValidTarget$ Player | CombatDamage$ True | ThisTurn$ True | Execute$ TrigConvert | TriggerDescription$ When that creature deals combat damage to a player this turn, convert NICKNAME.
SVar:TrigConvert:DB$ SetState | Mode$ Convert | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
Oracle:Living metal (As long as it's your turn, this Vehicle is also a creature.)\nTrample\nWhenever you attack, bolster 2. The chosen creature gains trample until end of turn. When that creature deals combat damage to a player this turn, convert Optimus Prime.

View File

@@ -0,0 +1,30 @@
Name:Ultra Magnus, Tactician
ManaCost:4 R G W
Types:Legendary Artifact Creature Robot
PT:7/7
K:More Than Meets the Eye:2 R G W
K:Ward:2
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigChange | TriggerZones$ Battlefield | OptionalDecider$ You | TriggerDescription$ Whenever NICKNAME attacks, you may put an artifact creature card from your hand onto the battlefield tapped and attacking. If you do, convert NICKNAME at end of combat.
SVar:TrigChange:DB$ ChangeZone | Origin$ Hand | Destination$ Battlefield | ChangeType$ Artifact.Creature | Tapped$ True | Attacking$ True | RememberChanged$ True | SubAbility$ DBDelayedTrigger
SVar:DBDelayedTrigger:DB$ DelayedTrigger | ConditionDefined$ Remembered | ConditionPresent$ Card | Mode$ Phase | Phase$ EndCombat | ValidPlayer$ Player | Execute$ TrigConvert | SubAbility$ DBCleanup | TriggerDescription$ If you do, convert NICKNAME at end of combat.
SVar:TrigConvert:DB$ SetState | Mode$ Convert
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:HasAttackEffect:TRUE
AlternateMode:Convert
Oracle:More Than Meets the Eye {2}{R}{G}{W} (You may cast this card converted for {2}{R}{G}{W}.)\nWard {2}\nWhenever Ultra Magnus attacks, you may put an artifact creature card from your hand onto the battlefield tapped and attacking. If you do, convert Ultra Magnus at end of combat.
ALTERNATE
Name:Ultra Magnus, Armored Carrier
ManaCost:no cost
Colors:red,green,white
Types:Legendary Artifact Vehicle
PT:4/7
K:Living metal
K:Haste
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigPump | TriggerZones$ Battlefield | TriggerDescription$ Formidable — Whenever NICKNAME attacks, attacking creatures you control gain indestructible until end of turn. If those creatures have total power 8 or greater, convert NICKNAME.
SVar:TrigPump:DB$ PumpAll | ValidCards$ Creature.attacking+YouCtrl | KW$ Indestructible | SubAbility$ DBConvert
SVar:DBConvert:DB$ SetState | Mode$ Convert | ConditionCheckSVar$ FormidableTest | ConditionSVarCompare$ GE8
SVar:FormidableTest:Count$SumPower_Creature.attacking+YouCtrl
SVar:HasAttackEffect:TRUE
Oracle:Living metal (As long as it's your turn, this Vehicle is also a creature.)\nHaste\nFormidable — Whenever Ultra Magnus attacks, attacking creatures you control gain indestructible until end of turn. If those creatures have total power 8 or greater, convert Ultra Magnus.

View File

@@ -217,6 +217,7 @@ Rebel:Rebels
Reflection:Reflections
Rhino:Rhinos
Rigger:Riggers
Robot:Robots
Rogue:Rogues
Sable:Sables
Salamander:Salamanders

View File

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