diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index 54650e8da1f..0f9a7f16857 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -1267,6 +1267,8 @@ public abstract class GameState { c.setRenowned(true); } else if (info.startsWith("Solved")) { c.setSolved(true); + } else if (info.startsWith("Saddled")) { + c.setSaddled(true); } else if (info.startsWith("Suspected")) { c.setSuspected(true); } else if (info.startsWith("Monstrous")) { diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index c2f1dc3422d..a2cb5b79d79 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -22,7 +22,7 @@ public enum SpellApiToAi { .put(ApiType.AddOrRemoveCounter, CountersPutOrRemoveAi.class) .put(ApiType.AddPhase, AddPhaseAi.class) .put(ApiType.AddTurn, AddTurnAi.class) - .put(ApiType.AlterAttribute, AlwaysPlayAi.class) + .put(ApiType.AlterAttribute, AlterAttributeAi.class) .put(ApiType.Amass, AmassAi.class) .put(ApiType.Animate, AnimateAi.class) .put(ApiType.AnimateAll, AnimateAllAi.class) diff --git a/forge-ai/src/main/java/forge/ai/ability/AlterAttributeAi.java b/forge-ai/src/main/java/forge/ai/ability/AlterAttributeAi.java new file mode 100644 index 00000000000..ac602043165 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/AlterAttributeAi.java @@ -0,0 +1,107 @@ +package forge.ai.ability; + +import java.util.List; +import java.util.Map; + +import forge.ai.SpellAbilityAi; +import forge.game.ability.AbilityUtils; +import forge.game.card.Card; +import forge.game.combat.CombatUtil; +import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.player.PlayerActionConfirmMode; +import forge.game.spellability.SpellAbility; + +public class AlterAttributeAi extends SpellAbilityAi { + + @Override + protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { + final Card source = sa.getHostCard(); + boolean activate = Boolean.valueOf(sa.getParamOrDefault("Activate", "true")); + String[] attributes = sa.getParam("Attributes").split(","); + + if (sa.usesTargeting()) { + // TODO add targeting logic + // needed for Suspected + return false; + } + + final List defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); + + for (Card c : defined) { + for (String attr : attributes) { + switch (attr.trim()) { + case "Solve": + case "Solved": + // there is currently no effect that would un-solve something + if (!c.isSolved() && activate) { + return true; + } + break; + case "Suspect": + case "Suspected": + // is Suspected good or bad? + // currently Suspected is better + if (!activate) { + return false; + } + return true; + + case "Saddle": + case "Saddled": + // AI should not try to Saddle again? + if (c.isSaddled()) { + return false; + } + return true; + } + } + } + return false; + } + + @Override + protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { + final Card source = sa.getHostCard(); + String[] attributes = sa.getParam("Attributes").split(","); + + // currently Phase is only checked for Saddled + + for (String attr : attributes) { + switch (attr.trim()) { + case "Saddle": + case "Saddled": + if (!ph.isPlayerTurn(ai)) { + return false; + } + // it is too late for combat, Saddle is Sorcery Speed + if (!ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) { + return false; + } + // would card attack? + if (!CombatUtil.canAttack(source)) { + return false; + } + } + } + + return true; + } + + @Override + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { + boolean activate = Boolean.valueOf(sa.getParamOrDefault("Activate", "true")); + String[] attributes = sa.getParam("Attributes").split(","); + + for (String attr : attributes) { + switch (attr.trim()) { + case "Suspect": + case "Suspected": + return activate; + } + } + + return true; + } +} diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java index 79fdd62b4d0..ba17c1df223 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java @@ -399,6 +399,9 @@ public class GameCopier { if (c.isSolved()) { newCard.setSolved(true); } + if (c.isSaddled()) { + newCard.setSaddled(true); + } if (c.isSuspected()) { newCard.setSuspected(true); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/AlterAttributeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AlterAttributeEffect.java index 2a6b3b885f1..a19645db98c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AlterAttributeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AlterAttributeEffect.java @@ -48,6 +48,11 @@ public class AlterAttributeEffect extends SpellAbilityEffect { case "Suspected": altered = c.setSuspected(activate); break; + case "Saddle": + case "Saddled": + // currently clean up in Card manually + altered = c.setSaddled(activate); + break; // Other attributes: renown, monstrous, suspected, etc 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 41ba8809b9e..9b021f6fd8f 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -217,6 +217,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private boolean renowned; private boolean solved = false; + private boolean saddled = false; private Long suspectedTimestamp = null; private StaticAbility suspectedStatic = null; @@ -2364,6 +2365,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { || keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing:") || keyword.startsWith("Afterlife") || keyword.startsWith("Hideaway") || keyword.startsWith("Toxic") || keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage") + || keyword.startsWith("Saddle") || keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")) { final String[] k = keyword.split(":"); sbLong.append(k[0]).append(" ").append(k[1]).append(" (").append(inst.getReminderText()).append(")"); @@ -2671,6 +2673,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars { if (solved) { sb.append("Solved\r\n"); } + if (saddled) { + sb.append("Saddled\r\n"); + } if (isSuspected()) { sb.append("Suspected\r\n"); } @@ -6342,6 +6347,14 @@ public class Card extends GameEntity implements Comparable, IHasSVars { return true; } + public final boolean isSaddled() { + return saddled; + } + public final boolean setSaddled(final boolean saddled) { + this.saddled = saddled; + return true; + } + public Long getSuspectedTimestamp() { return this.suspectedTimestamp; } @@ -6933,6 +6946,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { if (!StaticAbilityNoCleanupDamage.damageNotRemoved(this)) { setDamage(0); } + setSaddled(false); setHasBeenDealtDeathtouchDamage(false); setHasBeenDealtExcessDamageThisTurn(false); setRegeneratedThisTurn(0); diff --git a/forge-game/src/main/java/forge/game/card/CardCopyService.java b/forge-game/src/main/java/forge/game/card/CardCopyService.java index f7a265bb299..ad2c7859773 100644 --- a/forge-game/src/main/java/forge/game/card/CardCopyService.java +++ b/forge-game/src/main/java/forge/game/card/CardCopyService.java @@ -297,6 +297,7 @@ public class CardCopyService { newCopy.setMonstrous(copyFrom.isMonstrous()); newCopy.setRenowned(copyFrom.isRenowned()); newCopy.setSolved(copyFrom.isSolved()); + newCopy.setSaddled(copyFrom.isSaddled()); newCopy.setSuspectedTimestamp(copyFrom.getSuspectedTimestamp()); newCopy.setColor(copyFrom.getColor().getColor()); diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index d56595a4690..f43b50418f2 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -3386,6 +3386,18 @@ public class CardFactoryUtil { sa.setSVar("X", "Count$xPaid"); } inst.addSpellAbility(sa); + } else if (keyword.startsWith("Saddle")) { + final String[] k = keyword.split(":"); + final String power = k[1]; + + // tapXType has a special check for withTotalPower, and NEEDS it to be "+withTotalPowerGE" + String effect = "AB$ AlterAttribute | Cost$ tapXType" + + "| CostDesc$ Saddle " + power + " | Attributes$ Saddle | Secondary$ True | Defined$ Self | SorcerySpeed$ True " + + "| SpellDescription$ (" + inst.getReminderText() + ")"; + + final SpellAbility sa = AbilityFactory.getAbility(effect, card); + sa.setIntrinsic(intrinsic); + inst.addSpellAbility(sa); } else if (keyword.startsWith("Scavenge")) { final String[] k = keyword.split(":"); final String manacost = k[1]; diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index 2d63b49521f..093242a8a86 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -1905,6 +1905,10 @@ public class CardProperty { if (card.isSolved()) { return false; } + } else if (property.equals("IsSaddled")) { + if (!card.isSaddled()) { + return false; + } } else if (property.equals("IsSuspected")) { if (!card.isSuspected()) { return false; diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index c00a0e3cf35..7e15a160cd1 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -159,6 +159,7 @@ public enum Keyword { RETRACE("Retrace", SimpleKeyword.class, false, "You may cast this card from your graveyard by discarding a land card in addition to paying its other costs."), RIOT("Riot", SimpleKeyword.class, false, "This creature enters the battlefield with your choice of a +1/+1 counter or haste."), RIPPLE("Ripple", KeywordWithAmount.class, false, "When you cast this spell, you may reveal the top {%d:card} of your library. You may cast any of those cards with the same name as this spell without paying their mana costs. Put the rest on the bottom of your library in any order."), + SADDLE("Saddle", KeywordWithAmount.class, false, "Tap any number of other creatures you control with total power %1$d or more: This Mount becomes saddled until end of turn. Saddle only as a sorcery."), SHADOW("Shadow", SimpleKeyword.class, true, "This creature can block or be blocked by only creatures with shadow."), SHROUD("Shroud", SimpleKeyword.class, true, "This can't be the target of spells or abilities."), SKULK("Skulk", SimpleKeyword.class, true, "This creature can't be blocked by creatures with greater power."), diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java index 9dcf1c7523f..9a2e75566a3 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java @@ -491,9 +491,11 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { } } - // 702.36e + // 702.37e // If the permanent wouldn't have a morph cost if it were face up, it can't be turned face up this way. - if (sa.isMorphUp() && c.isInPlay()) { + // 702.168b + // If the permanent wouldn't have a disguise cost if it were face up, it can't be turned face up this way. + if ((sa.isMorphUp() || sa.isDisguiseUp()) && c.isInPlay()) { Card cp = c; if (!c.isLKI()) { cp = CardCopyService.getLKICopy(c); diff --git a/forge-gui/res/cardsfolder/upcoming/drover_grizzly.txt b/forge-gui/res/cardsfolder/upcoming/drover_grizzly.txt new file mode 100644 index 00000000000..6b0ef29abf7 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/drover_grizzly.txt @@ -0,0 +1,8 @@ +Name:Drover Grizzly +ManaCost:2 G +Types:Creature Bear Mount +PT:4/2 +T:Mode$ Attacks | ValidCard$ Card.Self+IsSaddled | TriggerZones$ Battlefield | Execute$ TrigPump | TriggerDescription$ Whenever CARDNAME attacks while saddled, creatures you control gain trample until end of turn. +SVar:TrigPump:DB$ PumpAll | ValidCards$ Creature.YouCtrl | KW$ Trample +K:Saddle:1 +Oracle:Whenever Drover Grizzly attacks while saddled, creatures you control gain trample until end of turn.\nSaddle 1 (Tap any number of other creatures you control with total power 1 or more: This Mount becomes saddled until end of turn. Saddle only as a sorcery.) diff --git a/forge-gui/res/lists/TypeLists.txt b/forge-gui/res/lists/TypeLists.txt index 8c5d4fe1689..53ede9cc6d0 100644 --- a/forge-gui/res/lists/TypeLists.txt +++ b/forge-gui/res/lists/TypeLists.txt @@ -181,6 +181,7 @@ Mongoose:Mongooses Monk:Monks Monkey:Monkeys Moonfolk:Moonfolks +Mount:Mounts Mouse:Mice Mutant:Mutants Myr:Myrs