Skip to content

Commit

Permalink
King of the Oathbreakers should trigger for Changeling (Discord post) (
Browse files Browse the repository at this point in the history
…#6453)

* Fix NPE with Street Spasm
  • Loading branch information
tool4ever authored Oct 29, 2024
1 parent 5db52c3 commit de4b35c
Show file tree
Hide file tree
Showing 14 changed files with 60 additions and 58 deletions.
14 changes: 6 additions & 8 deletions forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
final String damage = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(source, damage, sa);

if (damage.equals("X") || source.getSVar("X").equals("Count$xPaid") || sourceName.equals("Crater's Claws")) {
if (sa.getSVar("X").equals("Count$xPaid") || sa.getSVar(damage).equals("Count$xPaid") || sourceName.equals("Crater's Claws")) {
if (damage.equals("X") || source.getSVar("X").equals("Count$xPaid")) {
if (sa.getSVar("X").equals("Count$xPaid") || sa.getSVar(damage).equals("Count$xPaid")) {
dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());

// Try not to waste spells like Blaze or Fireball on early targets, try to do more damage with them if possible
Expand All @@ -106,7 +106,7 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
if (MyRandom.percentTrue(holdChance)) {
int threshold = aic.getIntProperty(AiProps.HOLD_X_DAMAGE_SPELLS_THRESHOLD);
boolean inDanger = ComputerUtil.aiLifeInDanger(ai, false, 0);
boolean isLethal = sa.getTargetRestrictions().canTgtPlayer() && dmg >= ai.getWeakestOpponent().getLife() && !ai.getWeakestOpponent().cantLoseForZeroOrLessLife();
boolean isLethal = sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer() && dmg >= ai.getWeakestOpponent().getLife() && !ai.getWeakestOpponent().cantLoseForZeroOrLessLife();
if (dmg < threshold && ai.getGame().getPhaseHandler().getTurn() / 2 < threshold && !inDanger && !isLethal) {
return false;
}
Expand All @@ -116,7 +116,7 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
// Set PayX here to maximum value. It will be adjusted later depending on the target.
sa.setXManaCostPaid(dmg);
} else if (sa.getSVar(damage).contains("InYourHand") && source.isInZone(ZoneType.Hand)) {
dmg = AbilityUtils.calculateAmount(source, damage, sa) - 1; // the card will be spent casting the spell, so actual damage is 1 less
dmg -= - 1; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
if (sa.getTargetRestrictions().canTgtPlayer()) {
Expand Down Expand Up @@ -260,11 +260,9 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
aic.reserveManaSourcesForNextSpell(chainDmg.getKey(), sa);
}
} else {
} else if (!damageTargetAI(ai, sa, dmg, false)) {
// simple targeting when there is no spell chaining plan
if (!damageTargetAI(ai, sa, dmg, false)) {
return false;
}
return false;
}

if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) ||
Expand Down
6 changes: 5 additions & 1 deletion forge-core/src/main/java/forge/card/DeckHints.java
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,11 @@ private Iterable<PaperCard> getCardsForFilter(Iterable<PaperCard> cardList, Type
Iterables.addAll(cards, getMatchingItems(cardList, CardRulesPredicates.name(StringOp.EQUALS, p), PaperCard::getRules));
break;
case TYPE:
Iterables.addAll(cards, getMatchingItems(cardList, CardRulesPredicates.joinedType(StringOp.CONTAINS_IC, p), PaperCard::getRules));
Predicate<CardRules> typePred = CardRulesPredicates.joinedType(StringOp.CONTAINS_IC, p);
if (CardType.isACreatureType(p)) {
typePred = Predicates.or(CardRulesPredicates.hasKeyword("Changeling"), typePred);
}
Iterables.addAll(cards, getMatchingItems(cardList, typePred, PaperCard::getRules));
break;
case NONE:
case ABILITY: // already done above
Expand Down
56 changes: 26 additions & 30 deletions forge-game/src/main/java/forge/game/GameAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,6 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer

copied = new CardCopyService(c).copyCard(false);

// CR 707.12 casting of a card copy
if (zoneTo.is(ZoneType.Stack) && c.isRealToken()) {
copied.setCopiedPermanent(c.getCopiedPermanent());
//TODO: Feels like this should fit here and seems to work but it'll take a fair bit more testing to be sure.
//copied.setGamePieceType(GamePieceType.COPIED_SPELL);
}

copied.setGameTimestamp(c.getGameTimestamp());

if (zoneTo.is(ZoneType.Stack)) {
Expand All @@ -267,6 +260,13 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
copied.setExiledBy(c.getExiledBy());
copied.setDrawnThisTurn(c.getDrawnThisTurn());

// CR 707.12 casting of a card copy
if (c.isRealToken()) {
copied.setCopiedPermanent(c.getCopiedPermanent());
//TODO: Feels like this should fit here and seems to work but it'll take a fair bit more testing to be sure.
//copied.setGamePieceType(GamePieceType.COPIED_SPELL);
}

if (c.isTransformed()) {
copied.incrementTransformedTimestamp();
}
Expand All @@ -277,6 +277,7 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer

// CR 112.2 A spell’s controller is, by default, the player who put it on the stack.
copied.setController(cause.getActivatingPlayer(), 0);

KeywordInterface kw = cause.getKeyword();
if (kw != null) {
copied.addKeywordForStaticAbility(kw);
Expand All @@ -290,25 +291,24 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
copied.setBackSide(false);
}

copied.setUnearthed(c.isUnearthed());

// need to copy counters when card enters another zone than hand or library
if (lastKnownInfo.hasKeyword("Counters remain on CARDNAME as it moves to any zone other than a player's hand or library.") &&
!(zoneTo.is(ZoneType.Hand) || zoneTo.is(ZoneType.Library))) {
copied.setCounters(Maps.newHashMap(lastKnownInfo.getCounters()));
}
}

// perpetual stuff
if (c.hasIntensity()) {
copied.setIntensity(c.getIntensity(false));
}
if (c.isSpecialized()) {
copied.setState(c.getCurrentStateName(), false);
}
if (c.hasPerpetual()) {
copied.setPerpetual(c);
// perpetual stuff
if (c.hasIntensity()) {
copied.setIntensity(c.getIntensity(false));
}
if (c.isSpecialized()) {
copied.setState(c.getCurrentStateName(), false);
}
if (c.hasPerpetual()) {
copied.setPerpetual(c);
}
}

// ensure that any leftover keyword/type changes are cleared in the state view
copied.updateStateForView();

Expand Down Expand Up @@ -780,8 +780,6 @@ private Card moveTo(final Zone zoneTo, Card c, Integer position, SpellAbility ca
final Zone zoneFrom = game.getZoneOf(c);
// String prevName = prev != null ? prev.getZoneName() : "";

// Card lastKnownInfo = c;

// Handle the case that one component of a merged permanent got take to the subgame
if (zoneTo.is(ZoneType.Subgame) && (c.hasMergedCard() || c.isMerged())) {
c.moveMergedToSubgame(cause);
Expand Down Expand Up @@ -913,7 +911,7 @@ public final Card moveToVariantDeck(Card c, ZoneType zone, int deckPosition, Spe
}
return changeZone(game.getZoneOf(c), deck, c, deckPosition, cause, params);
}

public final Card moveToJunkyard(Card c, SpellAbility cause, Map<AbilityKey, Object> params) {
final PlayerZone junkyard = c.getOwner().getZone(ZoneType.Junkyard);
return moveTo(junkyard, c, cause, params);
Expand All @@ -931,7 +929,6 @@ public final Card exile(final Card c, SpellAbility cause, Map<AbilityKey, Object
final PlayerZone removed = c.getOwner().getZone(ZoneType.Exile);
final Card copied = moveTo(removed, c, cause, params);

// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c);
runParams.put(AbilityKey.Cause, cause);
if (origin != null) { // is generally null when adding via dev mode
Expand Down Expand Up @@ -1264,18 +1261,19 @@ public boolean checkStateEffects(final boolean runEvents, final Set<Card> affect
AbilityKey.addCardZoneTableParams(mapParams, table);

for (final Player p : game.getPlayers()) {
for (final ZoneType zt : ZoneType.values()) {
if (zt == ZoneType.Command)
p.checkKeywordCard();
p.checkKeywordCard();

for (final ZoneType zt : ZoneType.values()) {
if (zt == ZoneType.Battlefield) {
continue;
}
for (final Card c : p.getCardsIn(zt).threadSafeIterable()) {
checkAgain |= stateBasedAction704_5d(c);
// Dungeon Card won't affect other cards, so don't need to set checkAgain
stateBasedAction_Dungeon(c);
stateBasedAction_Scheme(c);
if (zt == ZoneType.Command) {
stateBasedAction_Scheme(c);
}
}
}
}
Expand Down Expand Up @@ -1557,7 +1555,7 @@ private void stateBasedAction_Scheme(Card c) {
return;
}
if (!game.getStack().hasSourceOnStack(c, null)) {
moveTo(ZoneType.SchemeDeck, c, null, AbilityKey.newMap());
moveTo(ZoneType.SchemeDeck, c, -1, null, AbilityKey.newMap());
}
}

Expand Down Expand Up @@ -1676,7 +1674,6 @@ public void checkGameOverCondition() {
FCollectionView<Player> allPlayers = game.getPlayers();
for (Player p : allPlayers) {
if (p.checkLoseCondition()) { // this will set appropriate outcomes
// Run triggers
if (losers == null) {
losers = Lists.newArrayListWithCapacity(3);
}
Expand Down Expand Up @@ -1898,7 +1895,6 @@ public final CardCollection sacrifice(final Iterable<Card> list, final SpellAbil
}
}
for (Map.Entry<Player, Collection<Card>> e : lki.asMap().entrySet()) {
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(e.getKey());
runParams.put(AbilityKey.Cards, new CardCollection(e.getValue()));
runParams.put(AbilityKey.Cause, source);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,6 @@ else if (!sa.hasParam("NoLooking")) {
}
}


for (Card c : movedCards) {
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
AbilityKey.addCardZoneTableParams(moveParams, zoneMovements);
Expand Down
10 changes: 7 additions & 3 deletions forge-game/src/main/java/forge/game/phase/Untap.java
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,10 @@ private static boolean optionalUntap(final Card c) {
}

public static void doPhasing(final Player turn) {
Game game = turn.getGame();

// Needs to include phased out cards
final List<Card> list = CardLists.filter(turn.getGame().getCardsIncludePhasingIn(ZoneType.Battlefield),
final List<Card> list = CardLists.filter(game.getCardsIncludePhasingIn(ZoneType.Battlefield),
c -> (c.isPhasedOut(turn) && c.isDirectlyPhasedOut())
|| (c.hasKeyword(Keyword.PHASING) && c.getController().equals(turn))
);
Expand Down Expand Up @@ -299,11 +301,13 @@ public static void doPhasing(final Player turn) {
if (!phasedOut.isEmpty()) {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Cards, phasedOut);
turn.getGame().getTriggerHandler().runTrigger(TriggerType.PhaseOutAll, runParams, false);
game.getTriggerHandler().runTrigger(TriggerType.PhaseOutAll, runParams, false);
}
if (!toPhase.isEmpty()) {
// refresh statics for phased in permanents (e.g. so King of the Oathbreakers sees Changeling)
game.getAction().checkStaticAbilities();
// collect now before some zone change during Untap resets triggers
turn.getGame().getTriggerHandler().collectTriggerForWaiting();
game.getTriggerHandler().collectTriggerForWaiting();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -694,8 +694,7 @@ public void runReplaceDamage(final boolean isCombat, final CardDamageMap damageM

// Determine if need to divide shield among affected entity and
// determine if the prevent next N damage shield is large enough to replace all damage
Map<String, String> mapParams = chosenRE.getMapParams();
if ((mapParams.containsKey("PreventionEffect") && mapParams.get("PreventionEffect").equals("NextN"))
if ((chosenRE.hasParam("PreventionEffect") && chosenRE.getParam("PreventionEffect").equals("NextN"))
|| apiType == ApiType.ReplaceSplitDamage) {
if (apiType == ApiType.ReplaceDamage) {
shieldAmount = AbilityUtils.calculateAmount(effectSA.getHostCard(), effectSA.getParamOrDefault("Amount", "1"), effectSA);
Expand Down
4 changes: 3 additions & 1 deletion forge-game/src/main/java/forge/game/zone/MagicStack.java
Original file line number Diff line number Diff line change
Expand Up @@ -625,9 +625,11 @@ public final void resolveStack() {
}

game.fireEvent(new GameEventSpellResolved(sa, thisHasFizzled));
finishResolving(sa, thisHasFizzled);

game.getAction().checkStaticAbilities();

finishResolving(sa, thisHasFizzled);

game.copyLastState();
if (isEmpty() && !hasSimultaneousStackEntries()) {
// assuming that if the stack is empty, no reason to hold on to old LKI data (everything is a new object)
Expand Down
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/a/arcums_weathervane.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ Name:Arcum's Weathervane
ManaCost:2
Types:Artifact
A:AB$ Animate | Cost$ 2 T | ValidTgts$ Land.Snow | TgtPrompt$ Select target snow land | RemoveTypes$ Snow | Duration$ Permanent | SpellDescription$ Target snow land is no longer snow.
A:AB$ Animate | Cost$ 2 T | ValidTgts$ Land.nonSnow | TgtPrompt$ Select target nonsnow land | Types$ Snow | Duration$ Permanent | SpellDescription$ Target nonsnow basic land becomes snow.
A:AB$ Animate | Cost$ 2 T | ValidTgts$ Land.nonSnow+Basic | TgtPrompt$ Select target nonsnow basic land | Types$ Snow | Duration$ Permanent | SpellDescription$ Target nonsnow basic land becomes snow.
AI:RemoveDeck:Random
Oracle:{2}, {T}: Target snow land is no longer snow.\n{2}, {T}: Target nonsnow basic land becomes snow.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/g/graveyard_shovel.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Name:Graveyard Shovel
ManaCost:2
Types:Artifact
A:AB$ ChangeZone | Cost$ 2 T | ValidTgts$ Player | DefinedPlayer$ Targeted | TgtPrompt$ Select target player | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | Hidden$ True | Chooser$ Targeted | Mandatory$ True | SubAbility$ DBGainLife | ForgetOtherTargets$ True | RememberChanged$ True | IsCurse$ True | StackDescription$ Target player exiles a card from their graveyard. If it's a creature card, you gain 2 life. | SpellDescription$ Target player exiles a card from their graveyard. If it's a creature card, you gain 2 life.
A:AB$ ChangeZone | Cost$ 2 T | ValidTgts$ Player | DefinedPlayer$ Targeted | TgtPrompt$ Select target player | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | Hidden$ True | Chooser$ Targeted | Mandatory$ True | SubAbility$ DBGainLife | RememberChanged$ True | IsCurse$ True | StackDescription$ SpellDescription | SpellDescription$ Target player exiles a card from their graveyard. If it's a creature card, you gain 2 life.
SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 2 | ConditionDefined$ Remembered | ConditionPresent$ Creature | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
Oracle:{2}, {T}: Target player exiles a card from their graveyard. If it's a creature card, you gain 2 life.
6 changes: 3 additions & 3 deletions forge-gui/res/cardsfolder/j/jinnie_fay_jetmirs_second.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ Name:Jinnie Fay, Jetmir's Second
ManaCost:RG G GW
Types:Legendary Creature Elf Druid
PT:3/3
R:Event$ CreateToken | ActiveZones$ Battlefield | ValidToken$ Card.YouCtrl | ReplaceWith$ GenericChoice | Optional$ True | Description$ If you would create one or more tokens, you may instead create that many 2/2 green Cat creature tokens with haste or that many 3/1 green Dog creature tokens with vigilance.
R:Event$ CreateToken | ActiveZones$ Battlefield | ValidPlayer$ You | ReplaceWith$ GenericChoice | Optional$ True | Description$ If you would create one or more tokens, you may instead create that many 2/2 green Cat creature tokens with haste or that many 3/1 green Dog creature tokens with vigilance.
SVar:GenericChoice:DB$ GenericChoice | Choices$ Cat,Dog
SVar:Cat:DB$ ReplaceToken | Type$ ReplaceToken | ValidCard$ Card.YouCtrl | TokenScript$ g_2_2_cat_haste | SpellDescription$ Create that many 2/2 green Cat creature tokens with haste.
SVar:Dog:DB$ ReplaceToken | Type$ ReplaceToken | ValidCard$ Card.YouCtrl | TokenScript$ g_3_1_dog_vigilance | SpellDescription$ Create that many 3/1 green Dog creature tokens with vigilance.
SVar:Cat:DB$ ReplaceToken | Type$ ReplaceToken | TokenScript$ g_2_2_cat_haste | SpellDescription$ Create that many 2/2 green Cat creature tokens with haste.
SVar:Dog:DB$ ReplaceToken | Type$ ReplaceToken | TokenScript$ g_3_1_dog_vigilance | SpellDescription$ Create that many 3/1 green Dog creature tokens with vigilance.
AI:RemoveDeck:Random
DeckHas:Type$Cat|Dog
DeckNeeds:Ability$Token
Expand Down
6 changes: 3 additions & 3 deletions forge-gui/res/cardsfolder/r/ratonhnhake_ton.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ PT:3/3
S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Hexproof | IsPresent$ Card.Self+dealtDamagetoAny | PresentCompare$ EQ0 | Description$ As long as CARDNAME hasn't dealt damage yet, it has hexproof and can't be blocked.
S:Mode$ CantBlockBy | ValidAttacker$ Card.Self | Secondary$ True | IsPresent$ Card.Self+dealtDamagetoAny | PresentCompare$ EQ0 | Description$ As long as CARDNAME hasn't dealt damage yet, it has hexproof and can't be blocked.
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigToken | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, create a 1/1 black Assassin creature token with menace. When you do, return target Equipment card from your graveyard to the battlefield, then attach it to that token.
SVar:TrigToken:DB$ Token | TokenScript$ b_1_1_assassin_menace | RememberTokens$ True | SubAbility$ DBTrigger
SVar:DBTrigger:DB$ ImmediateTrigger | Execute$ TrigChangeZone | ConditionDefined$ Remembered | ConditionPresent$ Card | TriggerDescription$ When you do, return target creature card from your graveyard to your hand.
SVar:TrigToken:DB$ Token | TokenScript$ b_1_1_assassin_menace | RememberOriginalTokens$ True | SubAbility$ DBTrigger
SVar:DBTrigger:DB$ ImmediateTrigger | Execute$ TrigChangeZone | TriggerAmount$ Remembered$Amount | RememberObjects$ Remembered | SubAbility$ DBCleanup | TriggerDescription$ When you do, return target creature card from your graveyard to your hand.
SVar:TrigChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Equipment.YouCtrl | SubAbility$ DBAttach
SVar:DBAttach:DB$ Attach | Object$ Targeted | Defined$ Remembered | SubAbility$ DBCleanup
SVar:DBAttach:DB$ Attach | Object$ Targeted | Defined$ DelayTriggerRememberedLKI
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
DeckHas:Ability$Token
Oracle:As long as Ratonhnhaké:ton hasn't dealt damage yet, it has hexproof and can't be blocked.\nWhenever Ratonhnhaké:ton deals combat damage to a player, create a 1/1 black Assassin creature token with menace. When you do, return target Equipment card from your graveyard to the battlefield, then attach it to that token.
Loading

0 comments on commit de4b35c

Please sign in to comment.