From a54513e53d723a9797e23601015ced8c338c0fc6 Mon Sep 17 00:00:00 2001 From: TRT <> Date: Mon, 15 May 2023 17:08:45 +0200 Subject: [PATCH 01/24] Fix extrinsic Jump-start not exiling --- forge-game/src/main/java/forge/game/GameAction.java | 1 + .../i/invasion_of_fiora_marchesa_resolute_monarch.txt | 2 +- forge-gui/res/cardsfolder/l/lorehold_command.txt | 10 +++++----- forge-gui/res/cardsfolder/r/radha_heir_to_keld.txt | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 4ecb72a7584..01925daa050 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -264,6 +264,7 @@ public class GameAction { if (zoneTo.is(ZoneType.Stack)) { // try not to copy changed stats when moving to stack + copied.setChangedCardKeywords(c.getChangedCardKeywords()); // copy exiled properties when adding to stack // will be cleanup later in MagicStack diff --git a/forge-gui/res/cardsfolder/i/invasion_of_fiora_marchesa_resolute_monarch.txt b/forge-gui/res/cardsfolder/i/invasion_of_fiora_marchesa_resolute_monarch.txt index 29014d1491c..bea3c7f53cd 100644 --- a/forge-gui/res/cardsfolder/i/invasion_of_fiora_marchesa_resolute_monarch.txt +++ b/forge-gui/res/cardsfolder/i/invasion_of_fiora_marchesa_resolute_monarch.txt @@ -19,7 +19,7 @@ PT:3/6 K:Menace K:Deathtouch T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigRemoveCounter | TriggerDescription$ Whenever CARDNAME attacks, remove all counters from up to one target permanent. -SVar:TrigRemoveCounter:DB$ RemoveCounter | ValidTgts$ Permanent | CounterType$ All | CounterNum$ All +SVar:TrigRemoveCounter:DB$ RemoveCounter | ValidTgts$ Permanent | TargetMin$ 0 | TargetMax$ 1 | CounterType$ All | CounterNum$ All T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | CheckSVar$ X | SVarCompare$ EQ0 | Execute$ TrigDraw | TriggerDescription$ At the beginning of your upkeep, if you haven't been dealt combat damage since your last turn, you draw a card and you lose 1 life. SVar:TrigDraw:DB$ Draw | SubAbility$ DBLoseLife SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 1 diff --git a/forge-gui/res/cardsfolder/l/lorehold_command.txt b/forge-gui/res/cardsfolder/l/lorehold_command.txt index e4b007264a7..73574b42718 100644 --- a/forge-gui/res/cardsfolder/l/lorehold_command.txt +++ b/forge-gui/res/cardsfolder/l/lorehold_command.txt @@ -3,10 +3,10 @@ ManaCost:3 W R Types:Instant A:SP$ Charm | Cost$ 3 W R | Choices$ DBSpirit,DBIndestructible,DBHelix,DBSacrifice | CharmNum$ 2 SVar:DBSpirit:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_3_2_spirit | TokenOwner$ You | SpellDescription$ Create a 3/2 red and white Spirit token. -SVar:DBIndestructible:DB$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | KW$ Indestructible & haste | SpellDescription$ • Creatures you control get +1/+0 and gain indestructible and haste until end of turn. -SVar:DBHelix:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ • CARDNAME deals 3 damage to any target. Target player gains 3 life. -SVar:DBGainLife:DB$ GainLife | ValidTgts$ Player | TgtPrompt$ Select target player (to gain 3 life) | LifeAmount$ 3 | SpellDescription$ Target player gains 3 life. -SVar:DBSacrifice:DB$ Sacrifice | Defined$ You | SacValid$ Permanent | SubAbility$ DBDraw -SVar:DBDraw:DB$ Draw | NumCards$ 2 | SpellDescription$ Sacrifice a permanent,draw two cards. +SVar:DBIndestructible:DB$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | KW$ Indestructible & Haste | SpellDescription$ Creatures you control get +1/+0 and gain indestructible and haste until end of turn. +SVar:DBHelix:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ CARDNAME deals 3 damage to any target. Target player gains 3 life. +SVar:DBGainLife:DB$ GainLife | ValidTgts$ Player | TgtPrompt$ Select target player (to gain 3 life) | LifeAmount$ 3 +SVar:DBSacrifice:DB$ Sacrifice | Defined$ You | SacValid$ Permanent | SpellDescription$ Sacrifice a permanent, then draw two cards. | SubAbility$ DBDraw +SVar:DBDraw:DB$ Draw | NumCards$ 2 DeckHas:Ability$Token|LifeGain Oracle:Choose two —\n• Create a 3/2 red and white Spirit creature token.\n• Creatures you control get +1/+0 and gain indestructible and haste until end of turn.\n• Lorehold Command deals 3 damage to any target. Target player gains 3 life.\n• Sacrifice a permanent, then draw two cards. diff --git a/forge-gui/res/cardsfolder/r/radha_heir_to_keld.txt b/forge-gui/res/cardsfolder/r/radha_heir_to_keld.txt index f3077bfb5b1..91b85c4c69f 100644 --- a/forge-gui/res/cardsfolder/r/radha_heir_to_keld.txt +++ b/forge-gui/res/cardsfolder/r/radha_heir_to_keld.txt @@ -3,6 +3,6 @@ ManaCost:R G Types:Legendary Creature Elf Warrior PT:2/2 A:AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}. -T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigMana | TriggerDescription$ Whenever CARDNAME attacks, add R R. +T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigMana | TriggerDescription$ Whenever CARDNAME attacks, add {R}{R}. SVar:TrigMana:DB$ Mana | Produced$ R | Amount$ 2 | SpellDescription$ Add {R}{R}. Oracle:Whenever Radha, Heir to Keld attacks, you may add {R}{R}.\n{T}: Add {G}. From 87d9cf141d6f9aca4dea2012d273b0e84cc8f86e Mon Sep 17 00:00:00 2001 From: TRT <> Date: Mon, 15 May 2023 17:47:10 +0200 Subject: [PATCH 02/24] Fix text display --- forge-gui/res/cardsfolder/o/outlaws_merriment.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-gui/res/cardsfolder/o/outlaws_merriment.txt b/forge-gui/res/cardsfolder/o/outlaws_merriment.txt index 573679d31e6..07e2beeb4a0 100644 --- a/forge-gui/res/cardsfolder/o/outlaws_merriment.txt +++ b/forge-gui/res/cardsfolder/o/outlaws_merriment.txt @@ -1,8 +1,8 @@ Name:Outlaws' Merriment ManaCost:1 R W W Types:Enchantment -T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, choose one at random. Create a red and white creature token with those characteristics. -SVar:TrigCharm:DB$ Charm | Random$ True | Choices$ DBToken1,DBToken2,DBToken3 +T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, ABILITY +SVar:TrigCharm:DB$ Charm | Random$ True | Choices$ DBToken1,DBToken2,DBToken3 | AdditionalDescription$ Create a red and white creature token with those characteristics. SVar:DBToken1:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_3_1_human_warrior_trample_haste | TokenOwner$ You | SpellDescription$ 3/1 Human Warrior with trample and haste. SVar:DBToken2:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_2_1_human_cleric_lifelink_haste | TokenOwner$ You | SpellDescription$ 2/1 Human Cleric with lifelink and haste. SVar:DBToken3:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_1_2_human_rogue_haste_damage | TokenOwner$ You | SpellDescription$ 1/2 Human Rogue with haste and "When this creature enters the battlefield, it deals 1 damage to any target." From 067780f16e15f2c0a76d232579b9dcc5e437ed36 Mon Sep 17 00:00:00 2001 From: TRT <> Date: Wed, 17 May 2023 15:00:46 +0200 Subject: [PATCH 03/24] Fix Nahiri --- forge-game/src/main/java/forge/game/GameAction.java | 4 +--- forge-game/src/main/java/forge/game/card/Card.java | 1 - forge-gui/res/cardsfolder/n/nahiri_forged_in_fury.txt | 9 +++------ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 01925daa050..b3557b8c7dc 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -591,9 +591,7 @@ public class GameAction { // 400.7g try adding keyword back into card if it doesn't already have it if (zoneTo.is(ZoneType.Stack) && cause != null && cause.isSpell() && !cause.isIntrinsic() && c.equals(cause.getHostCard())) { if (cause.getKeyword() != null && !copied.getKeywords().contains(cause.getKeyword())) { - copied.addChangedCardKeywordsInternal(ImmutableList.of(cause.getKeyword()), null, false, game.getNextTimestamp(), 0, false); - // update Keyword Cache - copied.updateKeywords(); + copied.addChangedCardKeywordsInternal(ImmutableList.of(cause.getKeyword()), null, false, game.getNextTimestamp(), 0, true); } } 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 bce92e01f46..8b2f0f7b938 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -4721,7 +4721,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { final List keywords, final List removeKeywords, final boolean removeAllKeywords, final long timestamp, final long staticId, final boolean updateView) { - final KeywordsChange newCks = new KeywordsChange(keywords, removeKeywords, removeAllKeywords); changedCardKeywords.put(timestamp, staticId, newCks); diff --git a/forge-gui/res/cardsfolder/n/nahiri_forged_in_fury.txt b/forge-gui/res/cardsfolder/n/nahiri_forged_in_fury.txt index a57126348c7..6466b08c2d8 100644 --- a/forge-gui/res/cardsfolder/n/nahiri_forged_in_fury.txt +++ b/forge-gui/res/cardsfolder/n/nahiri_forged_in_fury.txt @@ -4,13 +4,10 @@ Types:Legendary Creature Kor Artificer PT:5/4 K:Affinity:Artifact.Equipment:equipment T:Mode$ Attacks | ValidCard$ Creature.equipped+YouCtrl | TriggerZones$ Battlefield | Execute$ TrigExile | TriggerDescription$ Whenever an equipped creature you control attacks, exile the top card of your library. You may play that card this turn. You may cast Equipment spells this way without paying their mana costs. -SVar:TrigExile:DB$ Dig | Defined$ You | DigNum$ 1 | ChangeNum$ All | DestinationZone$ Exile | RememberChanged$ True | SubAbility$ DBBranch -SVar:DBBranch:DB$ Branch | BranchConditionSVar$ X | TrueSubAbility$ DBEffect2 | FalseSubAbility$ DBEffect | SubAbility$ DBCleanup -SVar:DBEffect2:DB$ Effect | StaticAbilities$ STPlay2 | RememberObjects$ Remembered | ForgetOnMoved$ Exile -SVar:STPlay2:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may play that card this turn without paying its mana cost. -SVar:DBEffect:DB$ Effect | StaticAbilities$ STPlay | RememberObjects$ Remembered | ForgetOnMoved$ Exile +SVar:TrigExile:DB$ Dig | Defined$ You | DigNum$ 1 | ChangeNum$ All | DestinationZone$ Exile | RememberChanged$ True | SubAbility$ DBEffect +SVar:DBEffect:DB$ Effect | StaticAbilities$ STPlay,STPlay2 | RememberObjects$ Remembered | ForgetOnMoved$ Exile | SubAbility$ DBCleanup SVar:STPlay:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may play that card this turn. +SVar:STPlay2:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Equipment.IsRemembered | ValidAfterStack$ Spell.Equipment | AffectedZone$ Exile | Description$ You may cast Equipment spells this way without paying their mana costs. SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True -SVar:X:Remembered$Valid Equipment DeckNeeds:Type$Equipment Oracle:Affinity for Equipment\nWhenever an equipped creature you control attacks, exile the top card of your library. You may play that card this turn. You may cast Equipment spells this way without paying their mana costs. From 3f418f1824c52d8aae5e46c988314fa8a438814a Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Thu, 18 May 2023 10:48:00 +0200 Subject: [PATCH 04/24] Fix StackOverflow --- forge-game/src/main/java/forge/game/combat/CombatUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-game/src/main/java/forge/game/combat/CombatUtil.java b/forge-game/src/main/java/forge/game/combat/CombatUtil.java index a84b3b5bcb1..e64d682faf7 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -239,7 +239,7 @@ public class CombatUtil { if (!defender.equals(ge) && ge instanceof Player) { // found a player which does not goad that creature // and creature can attack this player or planeswalker - if (!attacker.isGoadedBy((Player) ge) && canAttack(attacker, ge)) { + if (!attacker.isGoadedBy((Player) ge) && !ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) { return false; } } @@ -251,7 +251,7 @@ public class CombatUtil { if (defender != null && defender.hasKeyword("Creatures your opponents control attack a player other than you if able.")) { for (GameEntity ge : getAllPossibleDefenders(attacker.getController())) { if (!defender.equals(ge) && ge instanceof Player) { - if (canAttack(attacker, ge)) { + if (!ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) { return false; } } From 15593508d62d83f203120dfaee85078610f1bc60 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sat, 20 May 2023 08:58:54 +0200 Subject: [PATCH 05/24] Fix infinite loop if nothing to discard --- .../forge/game/ability/effects/ConniveEffect.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java index fd15e1b68f5..4d69a2a07f3 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java @@ -66,7 +66,7 @@ public class ConniveEffect extends SpellAbilityEffect { for (final Player p : controllers) { CardCollection connivers = CardLists.filterControlledBy(toConnive, p); - while (connivers.size() > 0) { + while (!connivers.isEmpty()) { GameEntityCounterTable table = new GameEntityCounterTable(); final CardZoneTable triggerList = new CardZoneTable(); Map discardedMap = Maps.newHashMap(); @@ -76,18 +76,18 @@ public class ConniveEffect extends SpellAbilityEffect { Card conniver = connivers.size() > 1 ? p.getController().chooseSingleEntityForEffect(connivers, sa, Localizer.getInstance().getMessage("lblChooseConniver"), null) : connivers.get(0); + connivers.remove(conniver); p.drawCards(num, sa, moveParams); - CardCollection validDisards = - CardLists.filter(p.getCardsIn(ZoneType.Hand), CardPredicates.Presets.NON_TOKEN); - if (validDisards.isEmpty() || !p.canDiscardBy(sa, true)) { // hand being empty unlikely, just to be safe + CardCollection validDiscards = CardLists.filter(p.getCardsIn(ZoneType.Hand), CardPredicates.Presets.NON_TOKEN); + if (validDiscards.isEmpty() || !p.canDiscardBy(sa, true)) { // hand being empty unlikely, just to be safe continue; } - int amt = Math.min(validDisards.size(), num); + int amt = Math.min(validDiscards.size(), num); CardCollectionView toBeDiscarded = amt == 0 ? CardCollection.EMPTY : - p.getController().chooseCardsToDiscardFrom(p, sa, validDisards, amt, amt); + p.getController().chooseCardsToDiscardFrom(p, sa, validDiscards, amt, amt); if (toBeDiscarded.size() > 1) { toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); @@ -101,7 +101,6 @@ public class ConniveEffect extends SpellAbilityEffect { if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.equalsWithTimestamp(conniver)) { conniver.addCounter(CounterEnumType.P1P1, numCntrs, p, table); } - connivers.remove(conniver); discardedMap.put(p, CardCollection.getView(toBeDiscarded)); discard(sa, triggerList, true, discardedMap, moveParams); table.replaceCounterEffect(game, sa, true); From e5293e0aed08134d8e2536335e75984c2d52d2f1 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sat, 20 May 2023 11:54:28 +0200 Subject: [PATCH 06/24] Revert bad fix --- forge-game/src/main/java/forge/game/GameAction.java | 1 - forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index b3557b8c7dc..9b0b9fe566f 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -264,7 +264,6 @@ public class GameAction { if (zoneTo.is(ZoneType.Stack)) { // try not to copy changed stats when moving to stack - copied.setChangedCardKeywords(c.getChangedCardKeywords()); // copy exiled properties when adding to stack // will be cleanup later in MagicStack diff --git a/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt b/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt index d79a62c6984..c3e6c7b054d 100644 --- a/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt +++ b/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt @@ -4,7 +4,7 @@ Types:Legendary Creature Dragon Avatar PT:5/5 K:Flying K:Hexproof:Card.MonoColor:monocolored -S:Mode$ Continuous | Affected$ Instant.YouCtrl+numColorsEQ2,Sorcery.YouCtrl+numColorsEQ2 | AffectedZone$ Graveyard | AddKeyword$ Jump-start | Description$ Each instant and sorcery card in your graveyard that's exactly two colors has jump-start. +S:Mode$ Continuous | Affected$ Instant.YouCtrl+numColorsEQ2,Sorcery.YouCtrl+numColorsEQ2 | AffectedZone$ Graveyard,Stack | AddKeyword$ Jump-start | Description$ Each instant and sorcery card in your graveyard that's exactly two colors has jump-start. DeckHints:Type$Instant|Sorcery & Ability$Mill|Graveyard DeckHas:Ability$Graveyard|Discard Oracle:Flying, hexproof from monocolored\nEach instant and sorcery card in your graveyard that's exactly two colors has jump-start. From 987f61d43ae2539cd2f1e992245321fdbd63e4c1 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sat, 20 May 2023 12:16:58 +0200 Subject: [PATCH 07/24] Fix ChoosePlayerOrPlaneswalker vs. Battles --- .../main/java/forge/game/ability/SpellAbilityEffect.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java index 73d05760ed8..03b5da87f07 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -636,10 +636,8 @@ public abstract class SpellAbilityEffect { combat.initConstraints(); if (sa.hasParam("ChoosePlayerOrPlaneswalker")) { PlayerCollection defendingPlayers = AbilityUtils.getDefinedPlayers(sa.hasParam("ForEach") ? c : host, attacking, sa); - defs = new FCollection<>(); - for (Player p : defendingPlayers) { - defs.addAll(combat.getDefendersControlledBy(p)); - } + defs = new FCollection<>(defendingPlayers); + defs.addAll(Iterables.filter(combat.getDefendingPlaneswalkers(), CardPredicates.isControlledByAnyOf(defendingPlayers))); } else if ("True".equalsIgnoreCase(attacking)) { defs = (FCollection) combat.getDefenders(); } else { From 96b020a75977ed22ebdcacf7a4621fef4982ea61 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 20 May 2023 13:03:09 +0200 Subject: [PATCH 08/24] CardFactory: copySpellHost uses getCloneStates --- .../java/forge/game/card/CardFactory.java | 77 ++++++++----------- .../cardsfolder/d/donal_herald_of_wings.txt | 2 +- forge-gui/res/cardsfolder/f/fork.txt | 2 +- .../o/ob_nixilis_the_adversary.txt | 2 +- .../res/cardsfolder/t/tawnos_the_toymaker.txt | 2 +- 5 files changed, 38 insertions(+), 47 deletions(-) diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index f71b96b6e0a..b802b5c9faa 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -124,62 +124,49 @@ public class CardFactory { final Card source = sourceSA.getHostCard(); final Card original = targetSA.getHostCard(); final Game game = source.getGame(); - final Card c = new Card(game.nextCardId(), original.getPaperCard(), game); - copyCopiableCharacteristics(original, c, sourceSA, targetSA); + int id = game.nextCardId(); - if (sourceSA.hasParam("NonLegendary")) { - c.removeType(CardType.Supertype.Legendary); - } + // need to create a physical card first, i need the original card faces + final Card copy = CardFactory.getCard(original.getPaperCard(), controller, id, game); - if (sourceSA.hasParam("CopySetPower")) { - c.setBasePower(Integer.parseInt(sourceSA.getParam("CopySetPower"))); - } - - if (sourceSA.hasParam("CopySetToughness")) { - c.setBaseToughness(Integer.parseInt(sourceSA.getParam("CopySetToughness"))); - } - - if (sourceSA.hasParam("CopySetLoyalty")) { - c.setBaseLoyalty(AbilityUtils.calculateAmount(source, sourceSA.getParam("CopySetLoyalty"), sourceSA)); - } - - if (sourceSA.hasParam("CopyAddTypes")) { - c.addType(Arrays.asList(sourceSA.getParam("CopyAddTypes").split(" & "))); - } - - // change the color of the copy (eg: Fork) - if (sourceSA.hasParam("CopyIsColor")) { - ColorSet finalColors; - final String newColor = sourceSA.getParam("CopyIsColor"); - if (newColor.equals("ChosenColor")) { - finalColors = ColorSet.fromNames(source.getChosenColors()); - } else { - finalColors = ColorSet.fromNames(newColor.split(",")); + if (original.isTransformable()) { + // 707.8a If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced card not on the battlefield, + // the resulting token is a transforming token that has both a front face and a back face. + // The characteristics of each face are determined by the copiable values of the same face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent. + // If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield with its back face up. + // This rule does not apply to tokens that are created with their own set of characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect. + copy.setBackSide(original.isBackSide()); + if (original.isTransformed()) { + copy.incrementTransformedTimestamp(); } - - c.addColor(finalColors, !sourceSA.hasParam("OverwriteColors"), c.getTimestamp(), 0, false); } - c.clearControllers(); - c.setOwner(controller); - c.setCopiedSpell(true); - c.setCopiedPermanent(original); + 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); + } else { + copy.setState(copy.getCurrentStateName(), true, true); + } - c.setXManaCostPaidByColor(original.getXManaCostPaidByColor()); - c.setKickerMagnitude(original.getKickerMagnitude()); + copy.setCopiedSpell(true); + copy.setCopiedPermanent(original); + + copy.setXManaCostPaidByColor(original.getXManaCostPaidByColor()); + copy.setKickerMagnitude(original.getKickerMagnitude()); for (OptionalCost cost : original.getOptionalCostsPaid()) { - c.addOptionalCostPaid(cost); + copy.addOptionalCostPaid(cost); } if (targetSA.isBestow()) { - c.animateBestow(); + copy.animateBestow(); } if (sourceSA.hasParam("RememberNewCard")) { - source.addRemembered(c); + source.addRemembered(copy); } - return c; + return copy; } /** @@ -525,6 +512,7 @@ public class CardFactory { * @param from the {@link Card} to copy from. * @param to the {@link Card} to copy to. */ + @Deprecated public static void copyCopiableCharacteristics(final Card from, final Card to, SpellAbility sourceSA, SpellAbility targetSA) { final boolean toIsFaceDown = to.isFaceDown(); if (toIsFaceDown) { @@ -753,7 +741,10 @@ public class CardFactory { final CardState ret2 = new CardState(out, CardStateName.Adventure); ret2.copyFrom(in.getState(CardStateName.Adventure), false, sa); result.put(CardStateName.Adventure, ret2); - } else if (in.isTransformable() && sa instanceof SpellAbility && ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi())) { + } else if (in.isTransformable() && sa instanceof SpellAbility && ( + ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi()) || + ApiType.CopySpellAbility.equals(((SpellAbility)sa).getApi()) + )) { // CopyPermanent can copy token final CardState ret1 = new CardState(out, CardStateName.Original); ret1.copyFrom(in.getState(CardStateName.Original), false, sa); @@ -820,7 +811,7 @@ public class CardFactory { } if (state.getType().isPlaneswalker() && sa.hasParam("SetLoyalty")) { - state.setBaseLoyalty(String.valueOf(sa.getParam("SetLoyalty"))); + state.setBaseLoyalty(String.valueOf(AbilityUtils.calculateAmount(host, sa.getParam("SetLoyalty"), sa))); } // Planning a Vizier of Many Faces rework; always might come in handy diff --git a/forge-gui/res/cardsfolder/d/donal_herald_of_wings.txt b/forge-gui/res/cardsfolder/d/donal_herald_of_wings.txt index f2ce0bd82eb..2388fe1dc4b 100644 --- a/forge-gui/res/cardsfolder/d/donal_herald_of_wings.txt +++ b/forge-gui/res/cardsfolder/d/donal_herald_of_wings.txt @@ -3,7 +3,7 @@ ManaCost:2 U U Types:Legendary Creature Human Wizard PT:3/3 T:Mode$ SpellCast | TriggerZones$ Battlefield | ValidCard$ Creature.withFlying+nonLegendary | ValidActivatingPlayer$ You | ResolvedLimit$ 1 | Execute$ TrigCopy | OptionalDecider$ You | TriggerDescription$ Whenever you cast a nonlegendary creature spell with flying, you may copy it, except the copy is a 1/1 Spirit in addition to its other types. Do this only once each turn. (The copy becomes a token.) -SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | CopySetPower$ 1 | CopySetToughness$ 1 | CopyAddTypes$ Spirit +SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | SetPower$ 1 | SetToughness$ 1 | AddTypes$ Spirit DeckHas:Ability$Token DeckHints:Keyword$Flying SVar:BuffedBy:Creature.withFlying diff --git a/forge-gui/res/cardsfolder/f/fork.txt b/forge-gui/res/cardsfolder/f/fork.txt index ee58faa4933..365c0d61cc4 100644 --- a/forge-gui/res/cardsfolder/f/fork.txt +++ b/forge-gui/res/cardsfolder/f/fork.txt @@ -1,5 +1,5 @@ Name:Fork ManaCost:R R Types:Instant -A:SP$ CopySpellAbility | Cost$ R R | ValidTgts$ Instant,Sorcery | TargetType$ Spell | CopyIsColor$ Red | OverwriteColors$ True | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy. +A:SP$ CopySpellAbility | Cost$ R R | ValidTgts$ Instant,Sorcery | TargetType$ Spell | SetColor$ Red | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy. Oracle:Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy. diff --git a/forge-gui/res/cardsfolder/o/ob_nixilis_the_adversary.txt b/forge-gui/res/cardsfolder/o/ob_nixilis_the_adversary.txt index d51f0da7ee9..3020d4faec7 100644 --- a/forge-gui/res/cardsfolder/o/ob_nixilis_the_adversary.txt +++ b/forge-gui/res/cardsfolder/o/ob_nixilis_the_adversary.txt @@ -2,7 +2,7 @@ Name:Ob Nixilis, the Adversary ManaCost:1 B R Types:Legendary Planeswalker Nixilis Loyalty:3 -K:Casualty:X:NonLegendary$ True | CopySetLoyalty$ Casualty:The copy isn't legendary and has starting loyalty X. +K:Casualty:X:NonLegendary$ True | SetLoyalty$ Casualty:The copy isn't legendary and has starting loyalty X. A:AB$ RepeatEach | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | RepeatPlayers$ Opponent | RepeatSubAbility$ DBDrain | SubAbility$ DBGainLife | SpellDescription$ Each opponent loses 2 life unless they discard a card. If you control a Demon or Devil, you gain 2 life. SVar:DBDrain:DB$ LoseLife | Defined$ Player.IsRemembered | LifeAmount$ 2 | UnlessCost$ Discard<1/Card> | UnlessPayer$ Player.IsRemembered SVar:DBGainLife:DB$ GainLife | LifeAmount$ 2 | ConditionPresent$ Demon.YouCtrl,Devil.YouCtrl | StackDescription$ None diff --git a/forge-gui/res/cardsfolder/t/tawnos_the_toymaker.txt b/forge-gui/res/cardsfolder/t/tawnos_the_toymaker.txt index 061d010ce07..3612f9f6a9b 100644 --- a/forge-gui/res/cardsfolder/t/tawnos_the_toymaker.txt +++ b/forge-gui/res/cardsfolder/t/tawnos_the_toymaker.txt @@ -3,7 +3,7 @@ ManaCost:3 G U Types:Legendary Creature Human Artificer PT:3/5 T:Mode$ SpellCast | TriggerZones$ Battlefield | OptionalDecider$ You | ValidCard$ Creature.Bird,Creature.Beast | ValidActivatingPlayer$ You | NoResolvingCheck$ True | Execute$ TrigCopy | TriggerDescription$ Whenever you cast a Beast or Bird creature spell, you may copy it, except it's an artifact in addition to its other types. (The copy becomes a token.) -SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | CopyAddTypes$ Artifact +SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | AddTypes$ Artifact DeckNeeds:Type$Beast|Bird DeckHas:Ability$Token & Type$Artifact Oracle:Whenever you cast a Beast or Bird creature spell, you may copy it, except it's an artifact in addition to its other types. (The copy becomes a token.) From 76753886e74f492235ef3d30f0c6e0b69773596b Mon Sep 17 00:00:00 2001 From: Michael Kamensky Date: Sat, 20 May 2023 14:38:42 +0300 Subject: [PATCH 09/24] - Improve AI for Tempered Veteran. --- forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java | 7 +++++++ forge-gui/res/cardsfolder/t/tempered_veteran.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index 3f31a6d7431..232080c5dde 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -471,6 +471,13 @@ public class CountersPutAi extends CountersAi { if (sacSelf && c.equals(source)) { return false; } + if ("NoCounterOfType".equals(sa.getParam("AILogic"))) { + for (String ctrType : types) { + if (c.getCounters(CounterType.getType(ctrType)) > 0) { + return false; + } + } + } return sa.canTarget(c) && c.canReceiveCounters(CounterType.getType(type)); } }); diff --git a/forge-gui/res/cardsfolder/t/tempered_veteran.txt b/forge-gui/res/cardsfolder/t/tempered_veteran.txt index 052f3f53b25..61a18be49b7 100644 --- a/forge-gui/res/cardsfolder/t/tempered_veteran.txt +++ b/forge-gui/res/cardsfolder/t/tempered_veteran.txt @@ -3,6 +3,6 @@ ManaCost:1 W Types:Creature Human Knight PT:1/2 A:AB$ PutCounter | Cost$ W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature.counters_GE1_P1P1 | TgtPrompt$ Select target creature with a +1/+1 counter | SpellDescription$ Put a +1/+1 counter on target creature with a +1/+1 counter on it. -A:AB$ PutCounter | Cost$ 4 W W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Put a +1/+1 counter on target creature. +A:AB$ PutCounter | Cost$ 4 W W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | AILogic$ NoCounterOfType | SpellDescription$ Put a +1/+1 counter on target creature. DeckHas:Ability$Counters Oracle:{W}, {T}: Put a +1/+1 counter on target creature with a +1/+1 counter on it.\n{4}{W}{W}, {T}: Put a +1/+1 counter on target creature. From 028a680ef17c52c5f733b3dc03b73abd1f4c7053 Mon Sep 17 00:00:00 2001 From: Agetian Date: Sat, 20 May 2023 16:15:49 +0300 Subject: [PATCH 10/24] - Improve AI for Tempered Veteran. (#3140) --- forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java | 7 +++++++ forge-gui/res/cardsfolder/t/tempered_veteran.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index 3f31a6d7431..232080c5dde 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -471,6 +471,13 @@ public class CountersPutAi extends CountersAi { if (sacSelf && c.equals(source)) { return false; } + if ("NoCounterOfType".equals(sa.getParam("AILogic"))) { + for (String ctrType : types) { + if (c.getCounters(CounterType.getType(ctrType)) > 0) { + return false; + } + } + } return sa.canTarget(c) && c.canReceiveCounters(CounterType.getType(type)); } }); diff --git a/forge-gui/res/cardsfolder/t/tempered_veteran.txt b/forge-gui/res/cardsfolder/t/tempered_veteran.txt index 052f3f53b25..61a18be49b7 100644 --- a/forge-gui/res/cardsfolder/t/tempered_veteran.txt +++ b/forge-gui/res/cardsfolder/t/tempered_veteran.txt @@ -3,6 +3,6 @@ ManaCost:1 W Types:Creature Human Knight PT:1/2 A:AB$ PutCounter | Cost$ W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature.counters_GE1_P1P1 | TgtPrompt$ Select target creature with a +1/+1 counter | SpellDescription$ Put a +1/+1 counter on target creature with a +1/+1 counter on it. -A:AB$ PutCounter | Cost$ 4 W W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Put a +1/+1 counter on target creature. +A:AB$ PutCounter | Cost$ 4 W W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | AILogic$ NoCounterOfType | SpellDescription$ Put a +1/+1 counter on target creature. DeckHas:Ability$Counters Oracle:{W}, {T}: Put a +1/+1 counter on target creature with a +1/+1 counter on it.\n{4}{W}{W}, {T}: Put a +1/+1 counter on target creature. From 72ca26aece92e6c3fe9a13575e0c1edb61151b0c Mon Sep 17 00:00:00 2001 From: Agetian Date: Sat, 20 May 2023 23:26:23 +0300 Subject: [PATCH 11/24] Adventure Mode: some minor text correction/clarification (#3141) * - Improve AI for Tempered Veteran. * - Some text corrections/clarifications. --- .../maps/map/main_story/black_castle.tmx | 20 ++++++++-------- .../maps/map/main_story/blue_castle.tmx | 20 ++++++++-------- .../maps/map/main_story/green_castle.tmx | 20 ++++++++-------- .../maps/map/main_story/red_castle.tmx | 20 ++++++++-------- .../maps/map/main_story/templeofchandra.tmx | 24 +++++++++---------- .../maps/map/main_story/white_castle.tmx | 20 ++++++++-------- 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/black_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/black_castle.tmx index eab33989f10..5ab0337f3f2 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/black_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/black_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/blue_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/blue_castle.tmx index b6694251364..326323b2e20 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/blue_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/blue_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/green_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/green_castle.tmx index 78178402e21..56cc33c9a80 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/green_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/green_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/red_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/red_castle.tmx index c28d295a9fe..65dc919a18c 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/red_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/red_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/templeofchandra.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/templeofchandra.tmx index c11b4b257d9..f178b01e1a1 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/templeofchandra.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/templeofchandra.tmx @@ -357,12 +357,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the east", + "text":"You hear some loud sounds coming from the east", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":2}}], "action":[{"deleteMapObject":18}], @@ -379,12 +379,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the east", + "text":"You hear some loud sounds coming from the east", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":2}}], "action":[{"deleteMapObject":18}], @@ -401,11 +401,11 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the north", - "name":"flip the switch" + "text":"You hear some loud sounds coming from the north", + "name":"flip the switch" "options":[{ "action":[{"deleteMapObject":17}], "name":"ok" }] @@ -444,11 +444,11 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { "text":"You hear some rumbling and commotion from the south. The final gate appears to be open.", - "name":"flip the switch" + "name":"flip the switch" "options":[{ "action":[{"deleteMapObject":6}], "name":"ok" }] @@ -502,7 +502,7 @@ { "text":"The connected machinery whirrs to life, and the gate to your east opens.", "action":[{"deleteMapObject":-1}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "action":[{"deleteMapObject":11}], "name":"ok" }] diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/white_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/white_castle.tmx index 3473ba1bd37..48842fb4ace 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/white_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/white_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], From 581c25c610eee1bdf73e212553edc4434c3a2c2d Mon Sep 17 00:00:00 2001 From: Michael Kamensky Date: Sun, 21 May 2023 08:47:19 +0300 Subject: [PATCH 12/24] - AI: always evaluate creatures via evaluateCreature when deciding priority. --- forge-ai/src/main/java/forge/ai/AiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 04d69d7ad41..39b741578d9 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1083,7 +1083,7 @@ public class AiController { } // If both are permanent creature spells, prefer the one that evaluates higher - if (a1 == b1 && a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { + if (a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { int evalA = ComputerUtilCard.evaluateCreature(a); int evalB = ComputerUtilCard.evaluateCreature(b); if (evalA > evalB) { From fa7d5add4976a7c36390f13c503fa40aa06951b3 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 21 May 2023 07:48:56 +0200 Subject: [PATCH 13/24] FModel: load types before loading cards --- forge-gui/src/main/java/forge/model/FModel.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-gui/src/main/java/forge/model/FModel.java b/forge-gui/src/main/java/forge/model/FModel.java index adc72e64e2e..7029daf8f7c 100644 --- a/forge-gui/src/main/java/forge/model/FModel.java +++ b/forge-gui/src/main/java/forge/model/FModel.java @@ -164,6 +164,8 @@ public final class FModel { if (new AutoUpdater(true).attemptToUpdate()) { // } + // load types before loading cards + loadDynamicGamedata(); //load card database final CardStorageReader reader = new CardStorageReader(ForgeConstants.CARD_DATA_DIR, progressBarBridge, @@ -243,8 +245,6 @@ public final class FModel { Spell.setPerformanceMode(preferences.getPrefBoolean(FPref.PERFORMANCE_MODE)); - loadDynamicGamedata(); - if (progressBar != null) { FThreads.invokeInEdtLater(new Runnable() { @Override From e89c12255bf6cc0d7d147f2f792224877238d3d2 Mon Sep 17 00:00:00 2001 From: Anthony Calosa Date: Sun, 21 May 2023 15:16:49 +0800 Subject: [PATCH 14/24] fix NPE on CommanderGauntlet --- .../main/java/forge/gamemodes/gauntlet/GauntletUtil.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletUtil.java b/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletUtil.java index 58ab25885e7..d133af275e0 100644 --- a/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletUtil.java @@ -120,12 +120,14 @@ public class GauntletUtil { break; case COMMANDER_DECK: deck = DeckgenUtil.getCommanderDeck(); - eventNames.add(deck.getName()); + if (deck != null) + eventNames.add(deck.getName()); break; default: continue; } - decks.add(deck); + if (deck != null) + decks.add(deck); } gauntlet.setDecks(decks); From 26caa507a0b8f6fb7199848956391263cac99cdf Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 21 May 2023 10:33:04 +0200 Subject: [PATCH 15/24] Add TODO --- forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt b/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt index c3e6c7b054d..308d6c9f108 100644 --- a/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt +++ b/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt @@ -4,6 +4,7 @@ Types:Legendary Creature Dragon Avatar PT:5/5 K:Flying K:Hexproof:Card.MonoColor:monocolored +# TODO the AffectedZone Stack is needed for now but should be handled by the engine instead S:Mode$ Continuous | Affected$ Instant.YouCtrl+numColorsEQ2,Sorcery.YouCtrl+numColorsEQ2 | AffectedZone$ Graveyard,Stack | AddKeyword$ Jump-start | Description$ Each instant and sorcery card in your graveyard that's exactly two colors has jump-start. DeckHints:Type$Instant|Sorcery & Ability$Mill|Graveyard DeckHas:Ability$Graveyard|Discard From ea62081c00e569d2ca0d90a7fb0e26224234b157 Mon Sep 17 00:00:00 2001 From: Agetian Date: Sun, 21 May 2023 11:36:55 +0300 Subject: [PATCH 16/24] AI: Always prioritize creatures by evaluateCreature (#3142) * - Improve AI for Tempered Veteran. * - AI: always evaluate creatures via evaluateCreature when deciding priority. --- forge-ai/src/main/java/forge/ai/AiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 04d69d7ad41..39b741578d9 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1083,7 +1083,7 @@ public class AiController { } // If both are permanent creature spells, prefer the one that evaluates higher - if (a1 == b1 && a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { + if (a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { int evalA = ComputerUtilCard.evaluateCreature(a); int evalB = ComputerUtilCard.evaluateCreature(b); if (evalA > evalB) { From 0e4ac8859c1eb9ab75a69ae9e142ab5ce49bec5f Mon Sep 17 00:00:00 2001 From: Michael Kamensky Date: Sun, 21 May 2023 12:48:57 +0300 Subject: [PATCH 17/24] - AI: always evaluate creatures via evaluateCreature when deciding priority (better approach) --- forge-ai/src/main/java/forge/ai/AiController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 39b741578d9..a7120efde8a 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1087,9 +1087,9 @@ public class AiController { int evalA = ComputerUtilCard.evaluateCreature(a); int evalB = ComputerUtilCard.evaluateCreature(b); if (evalA > evalB) { - a1++; + a1 = b1 + 1; } else if (evalB > evalA) { - b1++; + b1 = a1 + 1; } } From 546eb815f48a6f052f130227eb721a973df5f665 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 21 May 2023 13:17:32 +0200 Subject: [PATCH 18/24] Update stale.yml add keep label --- .github/workflows/stale.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d72d9d61d33..9d7eb4bcad6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -26,7 +26,8 @@ jobs: close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' stale-issue-label: 'no-issue-activity' stale-pr-label: 'no-pr-activity' - exempt-pr-labels: 'awaiting-approval,work-in-progress' + exempt-issue-labels: 'keep' + exempt-pr-labels: 'awaiting-approval,work-in-progress,keep' days-before-issue-stale: 30 days-before-pr-stale: 45 days-before-issue-close: 5 From 456f1af310530fbf77f7df2ca2ca8af08896b37b Mon Sep 17 00:00:00 2001 From: Michael Kamensky Date: Sun, 21 May 2023 16:26:30 +0300 Subject: [PATCH 19/24] - Attempt to make a formula for evaluating relative priority of creature spells. --- forge-ai/src/main/java/forge/ai/AiController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index a7120efde8a..fb45f8a8ebb 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1087,9 +1087,9 @@ public class AiController { int evalA = ComputerUtilCard.evaluateCreature(a); int evalB = ComputerUtilCard.evaluateCreature(b); if (evalA > evalB) { - a1 = b1 + 1; + a1 += Math.max(1, Math.ceil(evalA / 100.0f)); } else if (evalB > evalA) { - b1 = a1 + 1; + b1 += Math.max(1, Math.ceil(evalB / 100.0f)); } } From 0824842f1d282aa290f92b476abc08fd04ce7fd7 Mon Sep 17 00:00:00 2001 From: Michael Kamensky Date: Sun, 21 May 2023 16:27:51 +0300 Subject: [PATCH 20/24] - Use Round instead of Ceil (for hopefully a bit more granularity). --- forge-ai/src/main/java/forge/ai/AiController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index fb45f8a8ebb..97066af0878 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1087,9 +1087,9 @@ public class AiController { int evalA = ComputerUtilCard.evaluateCreature(a); int evalB = ComputerUtilCard.evaluateCreature(b); if (evalA > evalB) { - a1 += Math.max(1, Math.ceil(evalA / 100.0f)); + a1 += Math.max(1, Math.round(evalA / 100.0f)); } else if (evalB > evalA) { - b1 += Math.max(1, Math.ceil(evalB / 100.0f)); + b1 += Math.max(1, Math.round(evalB / 100.0f)); } } From 5401c2622fed1456306a01f726b03219409dc0ea Mon Sep 17 00:00:00 2001 From: Agetian Date: Sun, 21 May 2023 20:56:15 +0300 Subject: [PATCH 21/24] AI hint for Nissa, Genesis Mage (#3145) * - Improve AI for Tempered Veteran. * - AI: always evaluate creatures via evaluateCreature when deciding priority. * - AI hint for Nissa, Genesis Mage. --- forge-gui/res/cardsfolder/n/nissa_genesis_mage.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui/res/cardsfolder/n/nissa_genesis_mage.txt b/forge-gui/res/cardsfolder/n/nissa_genesis_mage.txt index 0b774712eb4..12a9947f6bc 100644 --- a/forge-gui/res/cardsfolder/n/nissa_genesis_mage.txt +++ b/forge-gui/res/cardsfolder/n/nissa_genesis_mage.txt @@ -3,7 +3,7 @@ ManaCost:5 G G Types:Legendary Planeswalker Nissa Loyalty:5 A:AB$ Untap | Cost$ AddCounter<2/LOYALTY> | ValidTgts$ Creature | TargetMin$ 0 | TargetMax$ 2 | Planeswalker$ True | SubAbility$ DBUntap | TgtPrompt$ Select target creature | SpellDescription$ Untap up to two target creatures and up to two target lands. -SVar:DBUntap:DB$ Untap | ValidTgts$ Land | TargetMin$ 0 | TargetMax$ 2 | TgtPrompt$ Select target Land +SVar:DBUntap:DB$ Untap | ValidTgts$ Land | TargetMin$ 0 | TargetMax$ 2 | AILogic$ Always | TgtPrompt$ Select target Land A:AB$ Pump | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ +5 | NumDef$ +5 | SpellDescription$ Target creature gets +5/+5 until end of turn. A:AB$ Dig | Cost$ SubCounter<10/LOYALTY> | DigNum$ 10 | AnyNumber$ True | ChangeValid$ Creature,Land | DestinationZone$ Battlefield | Ultimate$ True | Planeswalker$ True | DestinationZone2$ Library | LibraryPosition$ -1 | RestRandomOrder$ True | SpellDescription$ Look at the top ten cards of your library. You may put any number of creature and/or land cards from among them onto the battlefield. Put the rest on the bottom of your library in a random order. DeckHints:Name$Nissa's Encouragement|Brambleweft Behemoth|Forest From 3a8213fcca23a8bf4fb84108e26827d44f4ef4fb Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 21 May 2023 23:59:37 +0200 Subject: [PATCH 22/24] Chain comparators correctly --- .../src/main/java/forge/ai/AiController.java | 175 +---------------- .../java/forge/ai/ComputerUtilAbility.java | 183 ++++++++++++++++++ .../main/java/forge/ai/ComputerUtilCard.java | 14 +- 3 files changed, 204 insertions(+), 168 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 97066af0878..fb05d635995 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -655,10 +655,11 @@ public class AiController { List all = ComputerUtilAbility.getSpellAbilities(cards, player); try { - Collections.sort(all, saComparator); // put best spells first + Collections.sort(all, ComputerUtilAbility.saEvaluator); // put best spells first + ComputerUtilAbility.sortCreatureSpells(all); } catch (IllegalArgumentException ex) { System.err.println(ex.getMessage()); - String assertex = ComparatorUtil.verifyTransitivity(saComparator, all); + String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all); Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex); } @@ -1016,167 +1017,6 @@ public class AiController { return false; } - // not sure "playing biggest spell" matters? - private final static Comparator saComparator = new Comparator() { - @Override - public int compare(final SpellAbility a, final SpellAbility b) { - // sort from highest cost to lowest - // we want the highest costs first - int a1 = a.getPayCosts().getTotalMana().getCMC(); - int b1 = b.getPayCosts().getTotalMana().getCMC(); - - // deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs - if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) { - return 1; - } else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) { - return -1; - } - - // deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True - if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { - return 1; - } else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { - return -1; - } - - // deprioritize pump spells with pure energy cost (can be activated last, - // since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler) - int a2 = 0, b2 = 0; - if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) { - if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { - a2 = a.getPayCosts().getCostEnergy().convertAmount(); - } - } - if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) { - if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { - b2 = b.getPayCosts().getCostEnergy().convertAmount(); - } - } - if (a2 == 0 && b2 > 0) { - return -1; - } else if (b2 == 0 && a2 > 0) { - return 1; - } - - // cast 0 mana cost spells first (might be a Mox) - if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) { - return -1; - } else if (a1 > 0 && b1 == 0 && ApiType.Mana != b.getApi()) { - return 1; - } - - if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) { - return -1; - } else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) { - return 1; - } - - if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) { - // Cheaper Spectacle costs should be preferred - // FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not? - // (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs) - if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) { - return 1; - } else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) { - return 1; - } - } - - // If both are permanent creature spells, prefer the one that evaluates higher - if (a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { - int evalA = ComputerUtilCard.evaluateCreature(a); - int evalB = ComputerUtilCard.evaluateCreature(b); - if (evalA > evalB) { - a1 += Math.max(1, Math.round(evalA / 100.0f)); - } else if (evalB > evalA) { - b1 += Math.max(1, Math.round(evalB / 100.0f)); - } - } - - a1 += getSpellAbilityPriority(a); - b1 += getSpellAbilityPriority(b); - - return b1 - a1; - } - - private int getSpellAbilityPriority(SpellAbility sa) { - int p = 0; - Card source = sa.getHostCard(); - final Player ai = source == null ? sa.getActivatingPlayer() : source.getController(); - if (ai == null) { - System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa); - return 0; - } - final boolean noCreatures = ai.getCreaturesInPlay().isEmpty(); - - if (source != null) { - // puts creatures in front of spells - if (source.isCreature()) { - p += 1; - } - if (source.hasSVar("AIPriorityModifier")) { - p += Integer.parseInt(source.getSVar("AIPriorityModifier")); - } - if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) { - p -= 10; - } - // don't play equipments before having any creatures - if (source.isEquipment() && noCreatures) { - p -= 9; - } - // don't equip stuff in main 2 if there's more stuff to cast at the moment - if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) { - p -= 1; - } - // 1. increase chance of using Surge effects - // 2. non-surged versions are usually inefficient - if (source.getOracleText().contains("surge cost") && !sa.isSurged()) { - p -= 9; - } - // move snap-casted spells to front - if (source.isInZone(ZoneType.Graveyard)) { - if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) { - p += 50; - } - } - // if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count - if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) { - p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS)); - } - } - - // use Surge and Prowl costs when able to - if (sa.isSurged() || sa.isProwl()) { - p += 9; - } - // sort planeswalker abilities with most costly first - if (sa.isPwAbility()) { - final CostPart cost = sa.getPayCosts().getCostParts().get(0); - if (cost instanceof CostRemoveCounter) { - p += cost.convertAmount() == null ? 1 : cost.convertAmount(); - } else if (cost instanceof CostPutCounter) { - p -= cost.convertAmount(); - } - if (sa.hasParam("Ultimate")) { - p += 9; - } - } - - if (ApiType.DestroyAll == sa.getApi()) { - p += 4; - } else if (ApiType.Mana == sa.getApi()) { - p -= 9; - } - - // try to cast mana ritual spells before casting spells to maximize potential mana - if ("ManaRitual".equals(sa.getParam("AILogic"))) { - p += 9; - } - - return p; - } - }; - public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa) { return getCardsToDiscard(numDiscard, uTypes, sa, CardCollection.EMPTY); } @@ -1744,10 +1584,11 @@ public class AiController { return null; try { - Collections.sort(all, saComparator); // put best spells first + Collections.sort(all, ComputerUtilAbility.saEvaluator); // put best spells first + ComputerUtilAbility.sortCreatureSpells(all); } catch (IllegalArgumentException ex) { System.err.println(ex.getMessage()); - String assertex = ComparatorUtil.verifyTransitivity(saComparator, all); + String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all); Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex); } @@ -2292,14 +2133,14 @@ public class AiController { } // TODO move to more common place - private List filterList(List input, Predicate pred) { + private static List filterList(List input, Predicate pred) { List filtered = Lists.newArrayList(Iterables.filter(input, pred)); input.removeAll(filtered); return filtered; } // TODO move to more common place - private List filterListByApi(List input, ApiType type) { + public static List filterListByApi(List input, ApiType type) { return filterList(input, SpellAbilityPredicates.isApi(type)); } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index 3eca0f0fcad..df4e8b43f5c 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -1,5 +1,7 @@ package forge.ai; +import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -15,6 +17,12 @@ import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardPredicates.Presets; +import forge.game.cost.CostPart; +import forge.game.cost.CostPayEnergy; +import forge.game.cost.CostPutCounter; +import forge.game.cost.CostRemoveCounter; +import forge.game.keyword.Keyword; +import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.OptionalCostValue; import forge.game.spellability.SpellAbility; @@ -223,4 +231,179 @@ public class ComputerUtilAbility { } return true; } + + public final static saComparator saEvaluator = new saComparator(); + + // not sure "playing biggest spell" matters? + public final static class saComparator implements Comparator { + @Override + public int compare(final SpellAbility a, final SpellAbility b) { + return compareEvaluator(a, b, false); + } + public int compareEvaluator(final SpellAbility a, final SpellAbility b, boolean safeToEvaluateCreatures) { + // sort from highest cost to lowest + // we want the highest costs first + int a1 = a.getPayCosts().getTotalMana().getCMC(); + int b1 = b.getPayCosts().getTotalMana().getCMC(); + + // deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs + if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) { + return 1; + } else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) { + return -1; + } + + // deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True + if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { + return 1; + } else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { + return -1; + } + + // deprioritize pump spells with pure energy cost (can be activated last, + // since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler) + int a2 = 0, b2 = 0; + if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) { + if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { + a2 = a.getPayCosts().getCostEnergy().convertAmount(); + } + } + if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) { + if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { + b2 = b.getPayCosts().getCostEnergy().convertAmount(); + } + } + if (a2 == 0 && b2 > 0) { + return -1; + } else if (b2 == 0 && a2 > 0) { + return 1; + } + + // cast 0 mana cost spells first (might be a Mox) + if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) { + return -1; + } else if (a1 > 0 && b1 == 0 && ApiType.Mana != b.getApi()) { + return 1; + } + + if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) { + return -1; + } else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) { + return 1; + } + + if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) { + // Cheaper Spectacle costs should be preferred + // FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not? + // (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs) + if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) { + return 1; + } else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) { + return 1; + } + } + + a1 += getSpellAbilityPriority(a); + b1 += getSpellAbilityPriority(b); + + int diff = b1 - a1; + + // If both are creature spells with roughly the same priority sort them after + if (safeToEvaluateCreatures && Math.abs(diff) < 4 && a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { + return 0; + } + + return diff; + } + + private int getSpellAbilityPriority(SpellAbility sa) { + int p = 0; + Card source = sa.getHostCard(); + final Player ai = source == null ? sa.getActivatingPlayer() : source.getController(); + if (ai == null) { + System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa); + return 0; + } + final boolean noCreatures = ai.getCreaturesInPlay().isEmpty(); + + if (source != null) { + // puts creatures in front of spells + if (source.isCreature()) { + p += 1; + } + if (source.hasSVar("AIPriorityModifier")) { + p += Integer.parseInt(source.getSVar("AIPriorityModifier")); + } + if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) { + p -= 10; + } + // don't play equipments before having any creatures + if (source.isEquipment() && noCreatures) { + p -= 9; + } + // don't equip stuff in main 2 if there's more stuff to cast at the moment + if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + p -= 1; + } + // 1. increase chance of using Surge effects + // 2. non-surged versions are usually inefficient + if (source.getOracleText().contains("surge cost") && !sa.isSurged()) { + p -= 9; + } + // move snap-casted spells to front + if (source.isInZone(ZoneType.Graveyard)) { + if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) { + p += 50; + } + } + // if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count + if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) { + p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS)); + } + } + + // use Surge and Prowl costs when able to + if (sa.isSurged() || sa.isProwl()) { + p += 9; + } + // sort planeswalker abilities with most costly first + if (sa.isPwAbility()) { + final CostPart cost = sa.getPayCosts().getCostParts().get(0); + if (cost instanceof CostRemoveCounter) { + p += cost.convertAmount() == null ? 1 : cost.convertAmount(); + } else if (cost instanceof CostPutCounter) { + p -= cost.convertAmount(); + } + if (sa.hasParam("Ultimate")) { + p += 9; + } + } + + if (ApiType.DestroyAll == sa.getApi()) { + p += 4; + } else if (ApiType.Mana == sa.getApi()) { + p -= 9; + } + + // try to cast mana ritual spells before casting spells to maximize potential mana + if ("ManaRitual".equals(sa.getParam("AILogic"))) { + p += 9; + } + + return p; + } + }; + + public static List sortCreatureSpells(List all) { + List creatures = AiController.filterListByApi(Lists.newArrayList(all), ApiType.PermanentCreature); + Collections.sort(creatures, ComputerUtilCard.EvaluateCreatureSpellComparator); + int idx = 0; + for (int i = 0; i < all.size(); i++) { + if (all.get(i).getApi() == ApiType.PermanentCreature) { + all.set(i, creatures.get(idx)); + idx++; + } + } + return all; + } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 464909d9148..ac1ecda800f 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -567,6 +567,18 @@ public class ComputerUtilCard { return evaluateCreature(b) - evaluateCreature(a); } }; + public static final Comparator EvaluateCreatureSpellComparator = new Comparator() { + @Override + public int compare(final SpellAbility a, final SpellAbility b) { + // only reorder if generic priorities can't decide + // TODO ideally we could reuse the value + int comp = ComputerUtilAbility.saEvaluator.compareEvaluator(a, b, true); + if (comp == 0) { + return evaluateCreature(b) - evaluateCreature(a); + } + return comp; + } + }; private static final CreatureEvaluator creatureEvaluator = new CreatureEvaluator(); private static final LandEvaluator landEvaluator = new LandEvaluator(); @@ -596,7 +608,7 @@ public class ComputerUtilCard { host.setState(sa.getCardStateName(), false); } - int eval = creatureEvaluator.evaluateCreature(host); + int eval = evaluateCreature(host); if (currentState != null) { host.setState(currentState, false); From 0230d29a4fc100d91af14c1460856535ccfbc29a Mon Sep 17 00:00:00 2001 From: TRT <> Date: Mon, 22 May 2023 07:53:50 +0200 Subject: [PATCH 23/24] Tweaks --- .../main/java/forge/ai/ComputerUtilAbility.java | 14 +++++++------- .../src/main/java/forge/ai/ComputerUtilCard.java | 9 ++------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index df4e8b43f5c..c8c5331068f 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -306,17 +306,16 @@ public class ComputerUtilAbility { a1 += getSpellAbilityPriority(a); b1 += getSpellAbilityPriority(b); - int diff = b1 - a1; - - // If both are creature spells with roughly the same priority sort them after - if (safeToEvaluateCreatures && Math.abs(diff) < 4 && a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { - return 0; + // If both are creature spells sort them after + if (safeToEvaluateCreatures) { + a1 += Math.round(ComputerUtilCard.evaluateCreature(a) / 100f); + b1 += Math.round(ComputerUtilCard.evaluateCreature(b) / 100f); } - return diff; + return b1 - a1; } - private int getSpellAbilityPriority(SpellAbility sa) { + private static int getSpellAbilityPriority(SpellAbility sa) { int p = 0; Card source = sa.getHostCard(); final Player ai = source == null ? sa.getActivatingPlayer() : source.getController(); @@ -395,6 +394,7 @@ public class ComputerUtilAbility { }; public static List sortCreatureSpells(List all) { + // try to smoothen power creeping by making CMC less of a factor List creatures = AiController.filterListByApi(Lists.newArrayList(all), ApiType.PermanentCreature); Collections.sort(creatures, ComputerUtilCard.EvaluateCreatureSpellComparator); int idx = 0; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index ac1ecda800f..9127805e45d 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -570,13 +570,8 @@ public class ComputerUtilCard { public static final Comparator EvaluateCreatureSpellComparator = new Comparator() { @Override public int compare(final SpellAbility a, final SpellAbility b) { - // only reorder if generic priorities can't decide - // TODO ideally we could reuse the value - int comp = ComputerUtilAbility.saEvaluator.compareEvaluator(a, b, true); - if (comp == 0) { - return evaluateCreature(b) - evaluateCreature(a); - } - return comp; + // TODO ideally we could reuse the value from the previous pass with false + return ComputerUtilAbility.saEvaluator.compareEvaluator(a, b, true); } }; From 05c34c1459460af89b2c57b93de18dcb0e57b775 Mon Sep 17 00:00:00 2001 From: TRT <> Date: Mon, 22 May 2023 10:53:21 +0200 Subject: [PATCH 24/24] Add TODO --- .../src/main/java/forge/ai/ComputerUtilAbility.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index c8c5331068f..af10923a66a 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -393,9 +393,13 @@ public class ComputerUtilAbility { } }; - public static List sortCreatureSpells(List all) { - // try to smoothen power creeping by making CMC less of a factor - List creatures = AiController.filterListByApi(Lists.newArrayList(all), ApiType.PermanentCreature); + public static List sortCreatureSpells(final List all) { + // try to smoothen power creep by making CMC less of a factor + final List creatures = AiController.filterListByApi(Lists.newArrayList(all), ApiType.PermanentCreature); + if (creatures.size() <= 1) { + return all; + } + // TODO this doesn't account for nearly identical creatures where one is a newer but more cost efficient variant Collections.sort(creatures, ComputerUtilCard.EvaluateCreatureSpellComparator); int idx = 0; for (int i = 0; i < all.size(); i++) {