From 48b79e45cda5d100923d19bdb43c876a3f4ef4fe Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 16 Aug 2026 21:08:10 +0200 Subject: [PATCH 1/2] CardState: move IntrinsicSpell into LandTraitChanges --- .../src/main/java/forge/game/card/Card.java | 2 +- .../main/java/forge/game/card/CardState.java | 121 +++++++++++------- .../ai/simulation/GameSimulationTest.java | 32 ++--- 3 files changed, 92 insertions(+), 63 deletions(-) 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..326f3f2706da 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -3377,7 +3377,7 @@ public final int getMaxManaProduced() { } public final SpellAbility getFirstSpellAbility() { - return Iterables.getFirst(currentState.getNonManaAbilities(), null); + return currentState.getFirstSpellAbility(); } public final SpellPermanent getSpellPermanent() { diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index 58f519285316..cadc1e23e484 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -456,49 +456,47 @@ public final boolean removeIntrinsicKeyword(final Keyword k) { } public final FCollectionView getSpellAbilities() { - FCollection newCol = new FCollection<>(); - updateSpellAbilities(newCol); - newCol.addAll(abilities); - card.updateSpellAbilities(newCol, this); - return newCol; + FCollection result = new FCollection<>(abilities); + if (getStateName().equals(CardStateName.Original)) { + if (getCard().hasState(CardStateName.LeftSplit)) + result.addAll(getCard().getState(CardStateName.LeftSplit).abilities); + if (getCard().hasState(CardStateName.RightSplit)) + result.addAll(getCard().getState(CardStateName.RightSplit).abilities); + } + card.updateSpellAbilities(result, this); + return result; } public final FCollectionView getManaAbilities() { - FCollection newCol = new FCollection<>(); - updateSpellAbilities(newCol); - newCol.addAll(abilities); - card.updateSpellAbilities(newCol, this); - newCol.removeIf(Predicate.not(SpellAbility::isManaAbility)); - return newCol; + FCollection result = new FCollection<>(abilities); + if (getStateName().equals(CardStateName.Original)) { + if (getCard().hasState(CardStateName.LeftSplit)) + result.addAll(getCard().getState(CardStateName.LeftSplit).abilities); + if (getCard().hasState(CardStateName.RightSplit)) + result.addAll(getCard().getState(CardStateName.RightSplit).abilities); + } + card.updateSpellAbilities(result, this); + result.removeIf(Predicate.not(SpellAbility::isManaAbility)); + return result; } public final FCollectionView getNonManaAbilities() { - FCollection newCol = new FCollection<>(); - updateSpellAbilities(newCol); - newCol.addAll(abilities); - card.updateSpellAbilities(newCol, this); - newCol.removeIf(SpellAbility::isManaAbility); - return newCol; - } - - protected final void updateSpellAbilities(FCollection newCol) { - // add Split to Original + FCollection result = new FCollection<>(abilities); if (getStateName().equals(CardStateName.Original)) { - if (getCard().hasState(CardStateName.LeftSplit)) { - CardState leftState = getCard().getState(CardStateName.LeftSplit); - newCol.addAll(leftState.abilities); - leftState.updateSpellAbilities(newCol); - } - if (getCard().hasState(CardStateName.RightSplit)) { - CardState rightState = getCard().getState(CardStateName.RightSplit); - newCol.addAll(rightState.abilities); - rightState.updateSpellAbilities(newCol); - } + if (getCard().hasState(CardStateName.LeftSplit)) + result.addAll(getCard().getState(CardStateName.LeftSplit).abilities); + if (getCard().hasState(CardStateName.RightSplit)) + result.addAll(getCard().getState(CardStateName.RightSplit).abilities); } + card.updateSpellAbilities(result, this); + result.removeIf(SpellAbility::isManaAbility); + return result; + } + protected final SpellAbility getIntrinsicSpell() { // SpellPermanent only for Original State switch(getStateName()) { case Backside: if (!getCard().isModal()) { - return; + return null; } break; case Original: @@ -511,32 +509,65 @@ protected final void updateSpellAbilities(FCollection newCol) { case SpecializeW: break; default: - return; - } - // if card has left or right split, disable intrinsic Spell for original - if (getStateName().equals(CardStateName.Original) && (getCard().hasState(CardStateName.LeftSplit) || getCard().hasState(CardStateName.RightSplit))) { - return; + return null; } CardTypeView type = getTypeWithChanges(); + if (!type.isPermanent()) { + return null; + } + if (type.isLand()) { if (landAbility == null) { landAbility = new LandAbility(card, this); } - newCol.add(landAbility); + return landAbility; } else if (type.isAura()) { - newCol.add(getAuraSpell()); - } else if (type.isPermanent()) { + return getAuraSpell(); + } else { if (abilities.anyMatch(s -> ( s.isBasicSpell() && s.getSubAbility() == null && (ApiType.PermanentCreature.equals(s.getApi()) || ApiType.PermanentNoncreature.equals(s.getApi()))) )) { - return; + return null; } if (permanentAbility == null) { permanentAbility = new SpellPermanent(card, this); } - newCol.add(permanentAbility); + return permanentAbility; + } + } + + protected final void updateSpellAbilities(List newCol) { + // add Split to Original + if (getStateName().equals(CardStateName.Original) && + (getCard().hasState(CardStateName.LeftSplit) || getCard().hasState(CardStateName.RightSplit))) { + boolean skip = false; + // insert right first, so left can be added before + if (getCard().hasState(CardStateName.RightSplit)) { + skip = true; + SpellAbility spRight = getCard().getState(CardStateName.RightSplit).getIntrinsicSpell(); + if (spRight != null) { + newCol.add(0, spRight); + } + } + if (getCard().hasState(CardStateName.LeftSplit)) { + skip = true; + SpellAbility spLeft = getCard().getState(CardStateName.LeftSplit).getIntrinsicSpell(); + if (spLeft != null) { + newCol.add(0, spLeft); + } + } + + // if card has left or right split, disable intrinsic Spell for original + if (skip) { + return; + } + } + + SpellAbility sp = getIntrinsicSpell(); + if (sp != null) { + newCol.add(0, sp); } } @@ -559,9 +590,7 @@ public List applySpellAbility(List list) { list.clear(); } CardTypeView type = state.getTypeWithChanges(); - if (!type.isLand()) { - return list; - } + state.updateSpellAbilities(list); for (MagicColor.Color c : MagicColor.Color.values()) { if (c.getBasicLandType() == null) { continue; @@ -620,7 +649,7 @@ public final SpellAbility getFirstSpellAbility() { if (this.card.getCastSA() != null) { return this.card.getCastSA(); } - return Iterables.getFirst(getNonManaAbilities(), null); + return getNonManaAbilities().stream().filter(SpellAbility::isSpell).findFirst().orElse(null); } public final SpellAbility getFirstSpellAbilityWithFallback() { diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java index 8696ab41eb25..7789f39b014c 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java @@ -167,7 +167,7 @@ public void testEtbTriggers() { game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); - SpellAbility playMerchantSa = c.getSpellAbilities().get(0); + SpellAbility playMerchantSa = c.getFirstSpellAbility(); playMerchantSa.setActivatingPlayer(p); GameSimulator sim = createSimulator(p); @@ -222,7 +222,7 @@ public void testFindingOwnCard() { GameSimulator sim = createSimulator(p1); Game simGame = sim.getSimulatedGameState(); - SpellAbility fractureSa = fractureP1.getSpellAbilities().get(0); + SpellAbility fractureSa = fractureP1.getFirstSpellAbility(); AssertJUnit.assertNotNull(fractureSa); fractureSa.getTargets().add(p0); sim.simulateSpellAbility(fractureSa); @@ -318,7 +318,7 @@ public void testManifest() { game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); - SpellAbility manifestSA = soulSummons.getSpellAbilities().get(0); + SpellAbility manifestSA = soulSummons.getFirstSpellAbility(); GameSimulator sim = createSimulator(p); sim.simulateSpellAbility(manifestSA); @@ -363,7 +363,7 @@ public void testManifest2() { game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); - SpellAbility manifestSA = soulSummons.getSpellAbilities().get(0); + SpellAbility manifestSA = soulSummons.getFirstSpellAbility(); GameSimulator sim = createSimulator(p); sim.simulateSpellAbility(manifestSA); @@ -390,7 +390,7 @@ public void testManifest3() { game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); - SpellAbility manifestSA = soulSummons.getSpellAbilities().get(0); + SpellAbility manifestSA = soulSummons.getFirstSpellAbility(); GameSimulator sim = createSimulator(p); sim.simulateSpellAbility(manifestSA); @@ -626,7 +626,7 @@ public void testTokenAbilities() { game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); - SpellAbility callTheScionsSA = callTheScionsCard.getSpellAbilities().get(0); + SpellAbility callTheScionsSA = callTheScionsCard.getFirstSpellAbility(); GameSimulator sim = createSimulator(p); int score = sim.simulateSpellAbility(callTheScionsSA).value; @@ -1009,7 +1009,7 @@ public void testTransform() { AssertJUnit.assertEquals(2, lilianaInPlay.getNetPower()); AssertJUnit.assertEquals(3, lilianaInPlay.getNetToughness()); - SpellAbility playLiliana = lilianaInHand.getSpellAbilities().get(0); + SpellAbility playLiliana = lilianaInHand.getFirstSpellAbility(); GameSimulator sim = createSimulator(p); sim.simulateSpellAbility(playLiliana); Game simGame = sim.getSimulatedGameState(); @@ -1039,7 +1039,7 @@ public void testEnergy() { game.getAction().checkStateEffects(true); AssertJUnit.assertEquals(0, p.getCounters(CounterEnumType.ENERGY)); - SpellAbility playTurtle = turtleCard.getSpellAbilities().get(0); + SpellAbility playTurtle = turtleCard.getFirstSpellAbility(); GameSimulator sim = createSimulator(p); sim.simulateSpellAbility(playTurtle); Game simGame = sim.getSimulatedGameState(); @@ -1066,7 +1066,7 @@ public void testFloatingMana() { game.getAction().checkStateEffects(true); AssertJUnit.assertTrue(p1.getManaPool().isEmpty()); - SpellAbility playRitual = darkRitualCard.getSpellAbilities().get(0); + SpellAbility playRitual = darkRitualCard.getFirstSpellAbility(); GameSimulator sim = createSimulator(p1); sim.simulateSpellAbility(playRitual); Game simGame = sim.getSimulatedGameState(); @@ -1076,7 +1076,7 @@ public void testFloatingMana() { AssertJUnit.assertEquals(3, simP1.getManaPool().getAmountOfColor(MagicColor.BLACK)); Card darkConfidantCard2 = (Card) sim.getGameCopier().find(darkConfidantCard); - SpellAbility playDarkConfidant2 = darkConfidantCard2.getSpellAbilities().get(0); + SpellAbility playDarkConfidant2 = darkConfidantCard2.getFirstSpellAbility(); Card deathriteCard2 = (Card) sim.getGameCopier().find(deathriteCard); GameSimulator sim2 = createSimulator(simP1); @@ -1087,7 +1087,7 @@ public void testFloatingMana() { AssertJUnit.assertEquals(1, sim2P.getManaPool().getAmountOfColor(MagicColor.BLACK)); Card deathriteCard3 = (Card) sim2.getGameCopier().find(deathriteCard2); - SpellAbility playDeathriteCard3 = deathriteCard3.getSpellAbilities().get(0); + SpellAbility playDeathriteCard3 = deathriteCard3.getFirstSpellAbility(); GameSimulator sim3 = createSimulator(sim2P); sim3.simulateSpellAbility(playDeathriteCard3); @@ -2118,14 +2118,14 @@ public void testPathtoExileActofTreason() { GameSimulator sim = createSimulator(p0); Game simGame = sim.getSimulatedGameState(); - SpellAbility actSA = actOfTreason.getSpellAbilities().get(0); + SpellAbility actSA = actOfTreason.getFirstSpellAbility(); AssertJUnit.assertNotNull(actSA); actSA.setActivatingPlayer(p0); actSA.setTargetCard(serraAngel); sim.simulateSpellAbility(actSA); simGame.getAction().checkStateEffects(true); - SpellAbility pathSA = pathToExile.getSpellAbilities().get(0); + SpellAbility pathSA = pathToExile.getFirstSpellAbility(); AssertJUnit.assertNotNull(pathSA); pathSA.setActivatingPlayer(p0); pathSA.setTargetCard(serraAngel); @@ -2150,7 +2150,7 @@ public void testAmassTrigger() { game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); game.getAction().checkStateEffects(true); - SpellAbility playSa = c.getSpellAbilities().get(0); + SpellAbility playSa = c.getFirstSpellAbility(); playSa.setActivatingPlayer(p); GameSimulator sim = createSimulator(p); @@ -2183,7 +2183,7 @@ public void testEverAfterWithWaywardServant() { game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); game.getAction().checkStateEffects(true); - SpellAbility playSa = cardEverAfter.getSpellAbilities().get(0); + SpellAbility playSa = cardEverAfter.getFirstSpellAbility(); playSa.setActivatingPlayer(p); playSa.getTargets().add(cardWaywardServant); playSa.getTargets().add(cardRagingGoblin); @@ -2675,7 +2675,7 @@ public void testVoloJournal() { Game simGame = sim.getSimulatedGameState(); for (Card card : cards) { - SpellAbility a1 = card.getSpellAbilities().get(0); + SpellAbility a1 = card.getFirstSpellAbility(); a1.setActivatingPlayer(p); sim.simulateSpellAbility(a1); } From 48ce8c08998f80e8913e16247a69d627541cd53f Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Mon, 17 Aug 2026 06:25:12 +0200 Subject: [PATCH 2/2] reuse getSpellAbilities --- .../main/java/forge/game/card/CardState.java | 18 ++---------------- .../forge/game/keyword/KeywordCollection.java | 3 +++ 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index cadc1e23e484..873d89378cb0 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -467,26 +467,12 @@ public final FCollectionView getSpellAbilities() { return result; } public final FCollectionView getManaAbilities() { - FCollection result = new FCollection<>(abilities); - if (getStateName().equals(CardStateName.Original)) { - if (getCard().hasState(CardStateName.LeftSplit)) - result.addAll(getCard().getState(CardStateName.LeftSplit).abilities); - if (getCard().hasState(CardStateName.RightSplit)) - result.addAll(getCard().getState(CardStateName.RightSplit).abilities); - } - card.updateSpellAbilities(result, this); + FCollectionView result = getSpellAbilities(); result.removeIf(Predicate.not(SpellAbility::isManaAbility)); return result; } public final FCollectionView getNonManaAbilities() { - FCollection result = new FCollection<>(abilities); - if (getStateName().equals(CardStateName.Original)) { - if (getCard().hasState(CardStateName.LeftSplit)) - result.addAll(getCard().getState(CardStateName.LeftSplit).abilities); - if (getCard().hasState(CardStateName.RightSplit)) - result.addAll(getCard().getState(CardStateName.RightSplit).abilities); - } - card.updateSpellAbilities(result, this); + FCollectionView result = getSpellAbilities(); result.removeIf(SpellAbility::isManaAbility); return result; } diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java b/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java index 542abdb3d707..993469625568 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java @@ -24,6 +24,9 @@ public class KeywordCollection implements ICardTraitChanges, Iterable