Bunch of fixes (#2706)

* Fix Tawnos's Coffin not using ETB counters

* Cost fix

* Saga tweaks

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.59>
This commit is contained in:
tool4ever
2023-03-19 20:07:03 +01:00
committed by GitHub
parent 7b78a2052d
commit 25d2aa82b6
14 changed files with 67 additions and 39 deletions

View File

@@ -355,6 +355,31 @@ public class AiController {
return false; return false;
} }
} }
for (final Trigger tr : card.getTriggers()) {
if (!card.hasStartOfKeyword("Saga") && !card.hasStartOfKeyword("Read ahead")) {
break;
}
if (tr.getMode() != TriggerType.CounterAdded) {
continue;
}
SpellAbility exSA = tr.ensureAbility().copy(activator);
if (api != null && exSA.getApi() == api) {
rightapi = true;
}
if (exSA instanceof AbilitySub && !doTrigger(exSA, false)) {
// AI would not run this chapter if given the chance
// TODO eventually we'll want to consider playing it anyway, especially if Read ahead would still allow an immediate benefit
return false;
}
break;
}
if (api != null && !rightapi) { if (api != null && !rightapi) {
return false; return false;
} }

View File

@@ -177,7 +177,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
// TODO Determine exile from same zone for AI // TODO Determine exile from same zone for AI
return null; return null;
} else { } else {
CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability); CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost, source, c, ability);
return null == chosen ? null : PaymentDecision.card(chosen); return null == chosen ? null : PaymentDecision.card(chosen);
} }
} }

View File

@@ -66,6 +66,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostDiscard; import forge.game.cost.CostDiscard;
import forge.game.cost.CostExile;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.cost.CostPayment; import forge.game.cost.CostPayment;
import forge.game.cost.CostPutCounter; import forge.game.cost.CostPutCounter;
@@ -641,9 +642,14 @@ public class ComputerUtil {
return sacList; return sacList;
} }
public static CardCollection chooseExileFrom(final Player ai, final ZoneType zone, final String type, final Card activate, public static CardCollection chooseExileFrom(final Player ai, CostExile cost, final Card activate, final int amount, SpellAbility sa) {
final Card target, final int amount, SpellAbility sa) { CardCollection typeList;
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"), activate.getController(), activate, sa); if (cost.zoneRestriction != 1) {
typeList = new CardCollection(ai.getGame().getCardsIn(cost.from));
} else {
typeList = new CardCollection(ai.getCardsIn(cost.from));
}
typeList = CardLists.getValidCards(typeList, cost.getType().split(";"), activate.getController(), activate, sa);
// don't exile the card we're pumping // don't exile the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai); typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai);

View File

@@ -2410,7 +2410,6 @@ public class ComputerUtilCombat {
// (currently looks for the creature with maximum raw power since that's what the AI usually judges by when // (currently looks for the creature with maximum raw power since that's what the AI usually judges by when
// deciding whether the creature is worth blocking). // deciding whether the creature is worth blocking).
// If the creature doesn't change into anything, returns the original creature. // If the creature doesn't change into anything, returns the original creature.
if (attacker == null) { return null; }
Card attackerAfterTrigs = attacker; Card attackerAfterTrigs = attacker;
// Test for some special triggers that can change the creature in combat // Test for some special triggers that can change the creature in combat

View File

@@ -1412,7 +1412,7 @@ public class AttachAi extends SpellAbilityAi {
} }
// avoid randomly moving the equipment back and forth between several creatures in one turn // avoid randomly moving the equipment back and forth between several creatures in one turn
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN) && !mandatory) { if (AiCardMemory.isRememberedCard(aiPlayer, attachSource, AiCardMemory.MemorySet.ATTACHED_THIS_TURN) && !mandatory) {
return null; return null;
} }
@@ -1423,7 +1423,7 @@ public class AttachAi extends SpellAbilityAi {
} }
} }
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN); AiCardMemory.rememberCard(aiPlayer, attachSource, AiCardMemory.MemorySet.ATTACHED_THIS_TURN);
if (c == null && mandatory) { if (c == null && mandatory) {
CardLists.shuffle(list); CardLists.shuffle(list);
@@ -1674,12 +1674,6 @@ public class AttachAi extends SpellAbilityAi {
if (c == null) { if (c == null) {
return false; return false;
} }
if (sa.getHostCard() == null) {
// FIXME: Not sure what should the resolution be if a SpellAbility has no host card. This should
// not happen normally. Possibly remove this block altogether? (if it's an impossible condition).
System.out.println("AttachAi: isUsefulAttachAction unexpectedly called with SpellAbility with no host card. Assuming it's a determined useful action.");
return true;
}
// useless to equip a creature that can't attack or block. // useless to equip a creature that can't attack or block.
return !sa.getHostCard().isEquipment() || !ComputerUtilCard.isUselessCreature(ai, c); return !sa.getHostCard().isEquipment() || !ComputerUtilCard.isUselessCreature(ai, c);

View File

@@ -69,7 +69,6 @@ public class CloneAi extends SpellAbilityAi {
bFlag = true; bFlag = true;
} }
} }
} }
if (!bFlag) { // All of the defined stuff is cloned, not very useful if (!bFlag) { // All of the defined stuff is cloned, not very useful

View File

@@ -1221,4 +1221,12 @@ public class CountersPutAi extends CountersAi {
return false; return false;
} }
@Override
public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) {
if (sa.hasParam("ReadAhead")) {
return 1;
}
return max;
}
} }

View File

@@ -2,8 +2,6 @@ package forge.game.ability.effects;
import java.util.Map.Entry; import java.util.Map.Entry;
import forge.game.Game;
import forge.game.GameEntityCounterTable;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CounterType; import forge.game.card.CounterType;
@@ -21,19 +19,16 @@ public class CountersNoteEffect extends SpellAbilityEffect {
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
final Game game = source.getGame();
Player p = sa.getActivatingPlayer(); Player p = sa.getActivatingPlayer();
String mode = sa.getParamOrDefault("Mode", "Load"); String mode = sa.getParamOrDefault("Mode", "Load");
GameEntityCounterTable table = new GameEntityCounterTable();
for (Card c : getDefinedCardsOrTargeted(sa)) { for (Card c : getDefinedCardsOrTargeted(sa)) {
if (mode.equals(MODE_STORE)) { if (mode.equals(MODE_STORE)) {
noteCounters(c, source); noteCounters(c, source);
} else if (mode.equals(MODE_LOAD)) { } else if (mode.equals(MODE_LOAD)) {
loadCounters(c, source, p, sa, table); loadCounters(c, source, p, sa);
} }
} }
table.replaceCounterEffect(game, sa, false);
} }
public static void noteCounters(Card notee, Card source) { public static void noteCounters(Card notee, Card source) {
@@ -44,13 +39,13 @@ public class CountersNoteEffect extends SpellAbilityEffect {
} }
} }
private void loadCounters(Card notee, Card source, final Player p, final SpellAbility sa, GameEntityCounterTable table) { private void loadCounters(Card notee, Card source, final Player p, final SpellAbility sa) {
for (Entry<String, String> svar : source.getSVars().entrySet()) { for (Entry<String, String> svar : source.getSVars().entrySet()) {
String key = svar.getKey(); String key = svar.getKey();
if (key.startsWith(NOTE_COUNTERS)) { if (key.startsWith(NOTE_COUNTERS)) {
notee.addCounter( notee.addEtbCounter(
CounterType.getType(key.substring(NOTE_COUNTERS.length())), CounterType.getType(key.substring(NOTE_COUNTERS.length())),
Integer.parseInt(svar.getValue()), p, table); Integer.parseInt(svar.getValue()), p);
} }
// TODO Probably should "remove" the svars that were temporarily used // TODO Probably should "remove" the svars that were temporarily used
} }

View File

@@ -6374,7 +6374,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
continue; continue;
} }
if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) { if (!ZoneType.Battlefield.toString().equals(params.get("Destination"))) {
continue; continue;
} }

View File

@@ -2,7 +2,7 @@ Name:Creeping Chill
ManaCost:3 B ManaCost:3 B
Types:Sorcery Types:Sorcery
A:SP$ DamageAll | StackDescription$ CARDNAME deals 3 damage to each opponent and | Cost$ 3 B | ValidPlayers$ Player.Opponent | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ CARDNAME deals 3 damage to each opponent and you gain 3 life. A:SP$ DamageAll | StackDescription$ CARDNAME deals 3 damage to each opponent and | Cost$ 3 B | ValidPlayers$ Player.Opponent | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ CARDNAME deals 3 damage to each opponent and you gain 3 life.
T:Mode$ ChangesZone | Origin$ Library | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigExile | OptionalDecider$ You | TriggerDescription$ When CARDNAME is put into your graveyard from your library, you may exile it. If you do, then a opponent and you gain 3 life. T:Mode$ ChangesZone | Origin$ Library | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigExile | OptionalDecider$ You | TriggerDescription$ When CARDNAME is put into your graveyard from your library, you may exile it. If you do, CARDNAME deals 3 damage to each opponent and you gain 3 life.
SVar:TrigExile:DB$ ChangeZone | Defined$ TriggeredCardLKICopy | Origin$ Graveyard | Destination$ Exile | SubAbility$ DBDamage SVar:TrigExile:DB$ ChangeZone | Defined$ TriggeredCardLKICopy | Origin$ Graveyard | Destination$ Exile | SubAbility$ DBDamage
SVar:DBDamage:DB$ DealDamage | Defined$ Player.Opponent | NumDmg$ 3 | SubAbility$ DBGainLife SVar:DBDamage:DB$ DealDamage | Defined$ Player.Opponent | NumDmg$ 3 | SubAbility$ DBGainLife
SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 3 SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 3

View File

@@ -4,6 +4,6 @@ Types:Legendary Creature Phyrexian Vampire
PT:5/4 PT:5/4
K:Flying K:Flying
T:Mode$ ChangesZone | ValidCard$ Creature.nonToken+Other+YouCtrl | Origin$ Battlefield | Destination$ Graveyard | TriggerZones$ Battlefield | Execute$ TrigExile | OptionalDecider$ You | TriggerDescription$ Whenever another nontoken creature you control dies, you may pay 2 life and exile it. If you do, create a token that's a copy of that creature, except it's 1/1 and has toxic 1. (Players dealt combat damage by it also get a poison counter.) T:Mode$ ChangesZone | ValidCard$ Creature.nonToken+Other+YouCtrl | Origin$ Battlefield | Destination$ Graveyard | TriggerZones$ Battlefield | Execute$ TrigExile | OptionalDecider$ You | TriggerDescription$ Whenever another nontoken creature you control dies, you may pay 2 life and exile it. If you do, create a token that's a copy of that creature, except it's 1/1 and has toxic 1. (Players dealt combat damage by it also get a poison counter.)
SVar:TrigExile:AB$ CopyPermanent | Cost$ PayLife<2> ExileFromGrave<1/Card.TriggeredCard/Exile nontoken creature that just died> | AddKeywords$ Toxic:1 | Defined$ TriggeredCardLKICopy | SetPower$ 1 | SetToughness$ 1 SVar:TrigExile:AB$ CopyPermanent | Cost$ PayLife<2> ExileAnyGrave<1/Card.TriggeredCard/Exile nontoken creature that just died> | AddKeywords$ Toxic:1 | Defined$ TriggeredCardLKICopy | SetPower$ 1 | SetToughness$ 1
DeckHas:Ability$Token DeckHas:Ability$Token
Oracle:Flying\nWhenever another nontoken creature you control dies, you may pay 2 life and exile it. If you do, create a token that's a copy of that creature, except it's 1/1 and has toxic 1. (Players dealt combat damage by it also get a poison counter.) Oracle:Flying\nWhenever another nontoken creature you control dies, you may pay 2 life and exile it. If you do, create a token that's a copy of that creature, except it's 1/1 and has toxic 1. (Players dealt combat damage by it also get a poison counter.)

View File

@@ -2,16 +2,12 @@ Name:Tawnos's Coffin
ManaCost:4 ManaCost:4
Types:Artifact Types:Artifact
K:You may choose not to untap CARDNAME during your untap step. K:You may choose not to untap CARDNAME during your untap step.
A:AB$ Pump | Cost$ 3 T | ValidTgts$ Creature | ImprintCards$ Targeted | SubAbility$ DBRememberAura | StackDescription$ SpellDescription | SpellDescription$ Exile target creature and all Auras attached to it. Note the number and kind of counters that were on that creature. When CARDNAME leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. A:AB$ Effect | Cost$ 3 T | ValidTgts$ Creature | Triggers$ LeavesPlay,Untap | ImprintCards$ Targeted | RememberObjects$ Valid Aura.AttachedTo Targeted | NoteCounterDefined$ Targeted | Duration$ Permanent | SubAbility$ DBExile | StackDescription$ SpellDescription | SpellDescription$ Exile target creature and all Auras attached to it. Note the number and kind of counters that were on that creature. When CARDNAME leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent.
SVar:DBRememberAura:DB$ PumpAll | ValidCards$ Aura.AttachedTo Creature.IsImprinted | RememberAllPumped$ True | StackDescription$ None | SubAbility$ DBEffect SVar:DBExile:DB$ ChangeZoneAll | Origin$ Battlefield | Destination$ Exile | ChangeType$ Targeted.Self,Aura.AttachedTo Card.Self
SVar:DBEffect:DB$ Effect | Triggers$ LeavesPlay,Untap | ImprintCards$ ParentTarget | RememberObjects$ Remembered | NoteCounterDefined$ Imprinted | Duration$ Permanent | SubAbility$ DBExile SVar:LeavesPlay:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.EffectSource | Execute$ TrigCounters | TriggerController$ TriggeredCardController | OneOff$ True | TriggerDescription$ When EFFECTSOURCE leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent.
SVar:DBExile:DB$ ChangeZoneAll | Origin$ Battlefield | Destination$ Exile | ChangeType$ Card.IsRemembered,Card.IsImprinted | SubAbility$ DBCleanup SVar:Untap:Mode$ Untaps | ValidCard$ Card.EffectSource | Execute$ TrigCounters | TriggerController$ TriggeredCardController | OneOff$ True | Secondary$ True | TriggerDescription$ When EFFECTSOURCE leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent.
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | ClearImprinted$ True SVar:TrigCounters:DB$ NoteCounters | Mode$ Load | Defined$ Imprinted | SubAbility$ TrigReturn
SVar:LeavesPlay:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.EffectSource | Execute$ RestoreCounters | TriggerController$ TriggeredCardController | TriggerDescription$ When EFFECTSOURCE leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. SVar:TrigReturn:DB$ ChangeZone | Defined$ Imprinted | Origin$ Exile | Destination$ Battlefield | Tapped$ True | SubAbility$ TrigAuraReturn
SVar:Untap:Mode$ Untaps | ValidCard$ Card.EffectSource | Execute$ TrigReturn | TriggerController$ TriggeredCardController | Secondary$ True | TriggerDescription$ When EFFECTSOURCE leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. SVar:TrigAuraReturn:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Battlefield | AttachedTo$ Imprinted
SVar:TrigReturn:DB$ ChangeZone | Defined$ Imprinted | Origin$ Exile | Destination$ Battlefield | Tapped$ True | SubAbility$ RestoreCounters
SVar:RestoreCounters:DB$ NoteCounters | Mode$ Load | Defined$ Imprinted | SubAbility$ TrigAuraReturn
SVar:TrigAuraReturn:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Battlefield | AttachedTo$ Valid Creature.IsImprinted | SubAbility$ ExileSelf
SVar:ExileSelf:DB$ ChangeZone | Origin$ Command | Destination$ Exile | Defined$ Self
AI:RemoveDeck:All AI:RemoveDeck:All
Oracle:You may choose not to untap Tawnos's Coffin during your untap step.\n{3}, {T}: Exile target creature and all Auras attached to it. Note the number and kind of counters that were on that creature. When Tawnos's Coffin leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. Oracle:You may choose not to untap Tawnos's Coffin during your untap step.\n{3}, {T}: Exile target creature and all Auras attached to it. Note the number and kind of counters that were on that creature. When Tawnos's Coffin leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent.

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Vampire Noble
PT:4/4 PT:4/4
K:Ward:Discard<1/Card> K:Ward:Discard<1/Card>
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Vampire.Other+nonToken+YouCtrl | TriggerZones$ Battlefield | Execute$ TrigToken | TriggerDescription$ Whenever another nontoken Vampire you control dies, you may pay {1} and exile it. If you do, create a 1/1 black Bat creature token with flying. It gains "When this creature deals combat damage to a player, sacrifice it and return the exiled card to the battlefield tapped." T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Vampire.Other+nonToken+YouCtrl | TriggerZones$ Battlefield | Execute$ TrigToken | TriggerDescription$ Whenever another nontoken Vampire you control dies, you may pay {1} and exile it. If you do, create a 1/1 black Bat creature token with flying. It gains "When this creature deals combat damage to a player, sacrifice it and return the exiled card to the battlefield tapped."
SVar:TrigToken:AB$ Token | Cost$ 1 ExileFromGrave<1/Card.TriggeredNewCard/the Vampire card> | TokenRemembered$ ExiledCards | TokenScript$ b_1_1_bat_flying | ImprintTokens$ True | SubAbility$ DBAnimate SVar:TrigToken:AB$ Token | Cost$ 1 ExileAnyGrave<1/Card.TriggeredNewCard/the Vampire card> | TokenRemembered$ ExiledCards | TokenScript$ b_1_1_bat_flying | ImprintTokens$ True | SubAbility$ DBAnimate
SVar:DBAnimate:DB$ Animate | Defined$ Imprinted | Duration$ Permanent | Triggers$ CDTrigger SVar:DBAnimate:DB$ Animate | Defined$ Imprinted | Duration$ Permanent | Triggers$ CDTrigger
SVar:CDTrigger:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigSac | TriggerZones$ Battlefield | TriggerDescription$ When this creature deals combat damage to a player, sacrifice it and return the exiled card to the battlefield tapped. SVar:CDTrigger:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigSac | TriggerZones$ Battlefield | TriggerDescription$ When this creature deals combat damage to a player, sacrifice it and return the exiled card to the battlefield tapped.
SVar:TrigSac:DB$ Sacrifice | SubAbility$ DBReturn SVar:TrigSac:DB$ Sacrifice | SubAbility$ DBReturn

View File

@@ -320,7 +320,13 @@ public class HumanPlay {
costExile.payAsDecided(p, PaymentDecision.card(p.getCardsIn(ZoneType.Graveyard)), sourceAbility, hcd.isEffect()); costExile.payAsDecided(p, PaymentDecision.card(p.getCardsIn(ZoneType.Graveyard)), sourceAbility, hcd.isEffect());
} else { } else {
from = costExile.getFrom(); from = costExile.getFrom();
CardCollection list = CardLists.getValidCards(p.getCardsIn(from), part.getType().split(";"), p, source, sourceAbility); CardCollection list;
if (costExile.zoneRestriction != 1) {
list = new CardCollection(p.getGame().getCardsIn(from));
} else {
list = new CardCollection(p.getCardsIn(from));
}
list = CardLists.getValidCards(list, part.getType().split(";"), p, source, sourceAbility);
final int nNeeded = getAmountFromPart(part, source, sourceAbility); final int nNeeded = getAmountFromPart(part, source, sourceAbility);
if (list.size() < nNeeded) { if (list.size() < nNeeded) {
return false; return false;