From a27f25b37d8856357759d97bc18333a98d46414c Mon Sep 17 00:00:00 2001 From: BigCrunch22 <22bigcrunch@gmail.com> Date: Sun, 23 Aug 2026 19:03:58 +0800 Subject: [PATCH] Implement Poison Tolerance keyword - Rosewater's Nemesis (Poison Tolerance) --- .../src/main/java/forge/ai/AiAttackController.java | 5 +++-- .../src/main/java/forge/ai/ComputerUtilCombat.java | 4 ++-- forge-ai/src/main/java/forge/ai/SpecialAiLogic.java | 6 +++--- .../forge/ai/ability/CountersProliferateAi.java | 2 +- forge-ai/src/main/java/forge/ai/ability/DrawAi.java | 6 +++--- forge-game/src/main/java/forge/game/card/Card.java | 2 +- .../src/main/java/forge/game/keyword/Keyword.java | 1 + .../java/forge/game/keyword/PoisonTolerance.java | 9 +++++++++ .../src/main/java/forge/game/player/Player.java | 13 +++++++++++-- forge-gui/res/cardsfolder/r/rosewaters_nemesis.txt | 8 ++++++++ 10 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/keyword/PoisonTolerance.java create mode 100644 forge-gui/res/cardsfolder/r/rosewaters_nemesis.txt diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index 0df07d76b2ff..498e0b376f07 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -748,7 +748,8 @@ private boolean doAssault() { } int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, defendingOpponent); - if (totalPoisonDamage >= 10 - defendingOpponent.getPoisonCounters()) { + final int poisonThreshold = defendingOpponent.getPoisonCounterThreshold(); + if (totalPoisonDamage >= poisonThreshold - defendingOpponent.getPoisonCounters()) { return true; } for (Card trampler : trampleDmg.keySet()) { @@ -757,7 +758,7 @@ private boolean doAssault() { totalPoisonDamage += dmg; } totalPoisonDamage += ComputerUtilCombat.predictExtraPoisonWithDamage(trampler, defendingOpponent, dmg); - if (totalPoisonDamage >= 10 - defendingOpponent.getPoisonCounters()) { + if (totalPoisonDamage >= poisonThreshold - defendingOpponent.getPoisonCounters()) { return true; } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index c6af8aa21f16..8aa32ade176d 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -489,7 +489,7 @@ public static boolean lifeInSeriousDanger(final Player ai, final Combat combat, } } - if (resultingPoison(ai, combat) >= ai.getGame().getRules().getPoisonCountersToLose()) { + if (resultingPoison(ai, combat) >= ai.getPoisonCounterThreshold()) { return true; } @@ -2437,7 +2437,7 @@ public static Card mostDangerousAttacker(CardCollection list, Player ai, Combat } int life = ai.getLife(); - int poisonLife = 10 - ai.getPoisonCounters(); + int poisonLife = ai.getPoisonCounterThreshold() - ai.getPoisonCounters(); double percentLife = life * 1.0 / damageScore; double percentPoison = poisonLife * 1.0 / poisonScore; diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java index 9b514a68343e..633087e76620 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java +++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java @@ -169,7 +169,7 @@ public static boolean doAristocratLogic(final Player ai, final SpellAbility sa) final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp - int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); + int lethalDmg = isInfect ? defPlayer.getPoisonCounterThreshold() - defPlayer.getPoisonCounters() : defPlayer.getLife(); if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) { lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent @@ -275,7 +275,7 @@ public static AiAbilityDecision doAristocratWithCountersLogic(final Player ai, f final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; final boolean isInfect = source.hasKeyword(Keyword.INFECT); - int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); + int lethalDmg = isInfect ? defPlayer.getPoisonCounterThreshold() - defPlayer.getPoisonCounters() : defPlayer.getLife(); if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) { lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent @@ -422,4 +422,4 @@ public static boolean preferHasteForRiot(SpellAbility sa, Player player) { // haste might not be good enough? return false; } -} \ No newline at end of file +} diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java index b93ae2fd4047..39eac72e9e90 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java @@ -62,7 +62,7 @@ protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { for (final Player o : ai.getOpponents()) { // Lethal poison - proliferating would win the game - if (o.getPoisonCounters() >= 9 && o.canReceiveCounters(CounterEnumType.POISON) + if (o.getPoisonCounters() >= o.getPoisonCounterThreshold() - 1 && o.canReceiveCounters(CounterEnumType.POISON) && !o.cantLoseCheck(GameLossReason.Poisoned)) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java index 2f4b76073e1f..e981762c1c6c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -399,7 +399,7 @@ private boolean targetAI(final Player ai, final SpellAbility sa, final boolean m // try to make opponent lose to poison // currently only Caress of Phyrexia if (getPoison != null && oppA.canReceiveCounters(CounterEnumType.POISON)) { - if (oppA.getPoisonCounters() + numCards > 9) { + if (oppA.getPoisonCounters() + numCards >= oppA.getPoisonCounterThreshold()) { sa.getTargets().add(oppA); return true; } @@ -443,7 +443,7 @@ private boolean targetAI(final Player ai, final SpellAbility sa, final boolean m } if (getPoison != null && ai.canReceiveCounters(CounterEnumType.POISON)) { - if (numCards + ai.getPoisonCounters() >= 8) { + if (numCards + ai.getPoisonCounters() >= ai.getPoisonCounterThreshold() - 2) { aiTarget = false; } } @@ -500,7 +500,7 @@ private boolean targetAI(final Player ai, final SpellAbility sa, final boolean m } // ally would lose because of poison - if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards > 9) { + if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards >= ally.getPoisonCounterThreshold()) { continue; } diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 5249b18cd2aa..5b6e5b6a0a63 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -2583,7 +2583,7 @@ private String keywordsToText(final Collection keywords) { || keyword.startsWith("Afterlife") || keyword.startsWith("Hideaway") || keyword.startsWith("Toxic") || keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage") || keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Ripple") - || keyword.startsWith("Ward")) { + || keyword.startsWith("Ward") || keyword.startsWith("Poison Tolerance")) { sbLong.append(inst.getTitle()).append(" (").append(inst.getReminderText()).append(")"); } else if (keyword.startsWith("Partner with:")) { final String[] k = keyword.split(":"); diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 646f3131b54c..c106649c4703 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -144,6 +144,7 @@ public enum Keyword { PERSIST("Persist", SimpleKeyword.class, false, "When this creature dies, if it had no -1/-1 counters on it, return it to the battlefield under its owner's control with a -1/-1 counter on it."), PHASING("Phasing", SimpleKeyword.class, true, "This phases in or out before you untap during each of your untap steps. While it's phased out, it's treated as though it doesn't exist."), PLOT("Plot", KeywordWithCost.class, false, "You may pay %s and exile this card from your hand. Cast it as a sorcery on a later turn without paying its mana cost. Plot only as a sorcery."), + POISON_TOLERANCE("Poison Tolerance", PoisonTolerance.class, false, "It takes %d additional poison counters for you to lose the game to poison."), POISONOUS("Poisonous", KeywordWithAmount.class, false, "Whenever this creature deals combat damage to a player, that player gets {%d:poison counter}."), PROTECTION("Protection", Protection.class, true, "This creature can't be blocked, targeted, dealt damage, or equipped/enchanted by %s."), PROTOTYPE("Prototype", KeywordWithCost.class, false, "You may cast this spell with different mana cost, color, and size. It keeps its abilities and types."), diff --git a/forge-game/src/main/java/forge/game/keyword/PoisonTolerance.java b/forge-game/src/main/java/forge/game/keyword/PoisonTolerance.java new file mode 100644 index 000000000000..23709932da71 --- /dev/null +++ b/forge-game/src/main/java/forge/game/keyword/PoisonTolerance.java @@ -0,0 +1,9 @@ +package forge.game.keyword; + +public class PoisonTolerance extends KeywordWithAmount { + + @Override + public String getTitle() { + return getKeyword() + " +" + getAmountString(); + } +} diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index ebb2b60f8280..cb8ee7ca8196 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -974,6 +974,15 @@ public final void addPoisonCounters(final int num, final Player source, GameEnti public final void removePoisonCounters(final int num, final Player source) { subtractCounter(CounterEnumType.POISON, num, source); } + // Rule 704.5c base threshold, plus this player's own Poison Tolerance + // bonus from permanents they control (e.g. Rosewater's Nemesis). + public final int getPoisonCounterThreshold() { + int threshold = game.getRules().getPoisonCountersToLose(); + for (final Card c : getCardsIn(ZoneType.Battlefield)) { + threshold += c.getKeywordMagnitude(Keyword.POISON_TOLERANCE); + } + return threshold; + } // ================ POISON Merged ================================= public final void addChangedKeywords(final List addKeywords, final List removeKeywords, final Long timestamp, final long staticId) { List kws = Lists.newArrayList(); @@ -2045,9 +2054,9 @@ public final boolean checkLoseCondition() { return true; } - // Rule 704.5c - If a player has ten or more poison counters, he or she loses the game. + // Rule 704.5c - If a player has ten or more poison counters, he or she loses the game. Handles Poison Tolerance increasing the threshold as well. // 704.6b In a Two-Headed Giant game, if a team has fifteen or more poison counters, that team loses the game. See rule 810, “Two-Headed Giant Variant.” - if (getCounters(CounterEnumType.POISON) >= 10 && loseConditionMet(GameLossReason.Poisoned, null)) { + if (getCounters(CounterEnumType.POISON) >= getPoisonCounterThreshold() && loseConditionMet(GameLossReason.Poisoned, null)) { return true; } diff --git a/forge-gui/res/cardsfolder/r/rosewaters_nemesis.txt b/forge-gui/res/cardsfolder/r/rosewaters_nemesis.txt new file mode 100644 index 000000000000..e69c0a1b4fc7 --- /dev/null +++ b/forge-gui/res/cardsfolder/r/rosewaters_nemesis.txt @@ -0,0 +1,8 @@ +Name:Rosewater's Nemesis +ManaCost:3 W W +Types:Creature Monkey Cleric +PT:4/6 +K:Vigilance +K:Protection:Phyrexian +K:Poison Tolerance:3 +Oracle:Vigilance, protection from Phyrexians\nPoison Tolerance +3 (It takes three additional poison counters for you to lose the game to poison.)