From ce4f12179cf2c3f1fdfded224e22014fe879fb45 Mon Sep 17 00:00:00 2001 From: Lyu Zong-Hong Date: Fri, 26 Mar 2021 13:05:04 +0900 Subject: [PATCH] Add Camouflage --- .../src/main/java/forge/ai/SpellApiToAi.java | 1 + .../main/java/forge/game/ability/ApiType.java | 1 + .../ability/effects/CamouflageEffect.java | 108 ++++++++++++++++++ .../java/forge/game/phase/PhaseHandler.java | 9 +- .../replacement/ReplaceDeclareBlocker.java | 29 +++++ .../game/replacement/ReplacementType.java | 1 + forge-gui/res/cardsfolder/c/camouflage.txt | 7 ++ forge-gui/res/languages/de-DE.properties | 3 + forge-gui/res/languages/en-US.properties | 3 + forge-gui/res/languages/es-ES.properties | 3 + forge-gui/res/languages/it-IT.properties | 3 + forge-gui/res/languages/ja-JP.properties | 3 + forge-gui/res/languages/zh-CN.properties | 3 + 13 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 forge-game/src/main/java/forge/game/ability/effects/CamouflageEffect.java create mode 100644 forge-game/src/main/java/forge/game/replacement/ReplaceDeclareBlocker.java create mode 100644 forge-gui/res/cardsfolder/c/camouflage.txt diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index 6aad3f581c1..7f6c3b81469 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -34,6 +34,7 @@ public enum SpellApiToAi { .put(ApiType.BidLife, BidLifeAi.class) .put(ApiType.Bond, BondAi.class) .put(ApiType.Branch, AlwaysPlayAi.class) + .put(ApiType.Camouflage, ChooseCardAi.class) .put(ApiType.ChangeCombatants, ChangeCombatantsAi.class) .put(ApiType.ChangeTargets, ChangeTargetsAi.class) .put(ApiType.ChangeX, AlwaysPlayAi.class) diff --git a/forge-game/src/main/java/forge/game/ability/ApiType.java b/forge-game/src/main/java/forge/game/ability/ApiType.java index 024a2b35155..56aef8eab84 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -30,6 +30,7 @@ public enum ApiType { Block (BlockEffect.class), Bond (BondEffect.class), Branch (BranchEffect.class), + Camouflage (CamouflageEffect.class), ChangeCombatants (ChangeCombatantsEffect.class), ChangeTargets (ChangeTargetsEffect.class), ChangeText (ChangeTextEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/effects/CamouflageEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CamouflageEffect.java new file mode 100644 index 00000000000..481941f2f32 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/CamouflageEffect.java @@ -0,0 +1,108 @@ +package forge.game.ability.effects; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import forge.game.ability.AbilityUtils; +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.card.CardLists; +import forge.game.combat.Combat; +import forge.game.combat.CombatUtil; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +public class CamouflageEffect extends SpellAbilityEffect { + + private void randomizeBlockers(SpellAbility sa, Combat combat, Player declarer, Player defender, List attackers, List blockerPiles) { + CardLists.shuffle(attackers); + for (int i = 0; i < attackers.size(); i++) { + final Card attacker = attackers.get(i); + CardCollection blockers = blockerPiles.get(i); + + // Remove all illegal blockers first + for (int j = blockers.size() - 1; j >= 0; j--) { + final Card blocker = blockers.get(j); + if (!CombatUtil.canBlock(attacker, blocker, combat)) { + blockers.remove(j); + } + } + + if (attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.") && + blockers.size() < defender.getCreaturesInPlay().size() || + blockers.size() < CombatUtil.needsBlockers(attacker)) { + // If not enough remaining creatures to block, don't add them as blocker + continue; + } + + if (attacker.hasKeyword("CantBeBlockedByAmount GT1") && blockers.size() > 1) { + // If no more than one creature can block, order the player to choose one to block + Card chosen = declarer.getController().chooseCardsForEffect(blockers, sa, + Localizer.getInstance().getMessage("lblChooseBlockerForAttacker", attacker.toString()), 1, 1, false, null).get(0); + combat.addBlocker(attacker, chosen); + continue; + } + + // Add all remaning blockers normally + for (final Card blocker : blockers) { + combat.addBlocker(attacker, blocker); + } + } + } + + @Override + public void resolve(SpellAbility sa) { + Card hostCard = sa.getHostCard(); + Player declarer = getDefinedPlayersOrTargeted(sa).get(0); + Player defender = AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("Defender"), sa).get(0); + Combat combat = hostCard.getGame().getCombat(); + List attackers = combat.getAttackers(); + List blockerPiles = new ArrayList<>(); + + if (declarer.isAI()) { + // For AI player, just let it declare blockers normally, then randomize it later. + declarer.getController().declareBlockers(defender, combat); + // Remove all blockers first + for (final Card attacker : attackers) { + CardCollection blockers = combat.getBlockers(attacker); + blockerPiles.add(blockers); + for (final Card blocker : blockers) { + combat.removeFromCombat(blocker); + } + } + } else { // Human player + CardCollection pool = new CardCollection(defender.getCreaturesInPlay()); + // remove all blockers that can't block + for (final Card blocker : pool) { + if (!CombatUtil.canBlock(blocker)) { + pool.remove(blocker); + } + } + List blockedSoFar = new ArrayList<>(Collections.nCopies(pool.size(), 0)); + + for (int i = 0; i < attackers.size(); i++) { + int size = pool.size(); + CardCollection blockers = new CardCollection(declarer.getController().chooseCardsForEffect( + pool, sa, Localizer.getInstance().getMessage("lblChooseBlockersForPile", String.valueOf(i + 1)), 0, size, false, null)); + blockerPiles.add(blockers); + // Remove chosen creatures, unless it can block additional attackers + for (final Card blocker : blockers) { + int index = pool.indexOf(blocker); + Integer blockedCount = blockedSoFar.get(index) + 1; + if (!blocker.canBlockAny() && blocker.canBlockAdditional() < blockedCount) { + pool.remove(index); + blockedSoFar.remove(index); + } else { + blockedSoFar.set(index, blockedCount); + } + } + } + } + + randomizeBlockers(sa, combat, declarer, defender, attackers, blockerPiles); + } + +} diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 51b04c6986b..61355f6b54e 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -653,7 +653,14 @@ public class PhaseHandler implements java.io.Serializable { } if (combat.isPlayerAttacked(p)) { if (CombatUtil.canBlock(p, combat)) { - whoDeclaresBlockers.getController().declareBlockers(p, combat); + // Replacement effects (for Camouflage) + final Map repRunParams = AbilityKey.mapFromAffected(p); + repRunParams.put(AbilityKey.Player, whoDeclaresBlockers); + ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.DeclareBlocker, repRunParams); + if (repres == ReplacementResult.NotReplaced) { + // If not replaced, run normal declare blockers + whoDeclaresBlockers.getController().declareBlockers(p, combat); + } } } else { continue; } diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceDeclareBlocker.java b/forge-game/src/main/java/forge/game/replacement/ReplaceDeclareBlocker.java new file mode 100644 index 00000000000..dc0024c7797 --- /dev/null +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceDeclareBlocker.java @@ -0,0 +1,29 @@ +package forge.game.replacement; + +import java.util.Map; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.spellability.SpellAbility; + +public class ReplaceDeclareBlocker extends ReplacementEffect { + + public ReplaceDeclareBlocker(final Map mapParams, final Card host, final boolean intrinsic) { + super(mapParams, host, intrinsic); + } + + @Override + public boolean canReplace(Map runParams) { + if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) { + return false; + } + return true; + } + + @Override + public void setReplacingObjects(Map runParams, SpellAbility sa) { + sa.setReplacingObject(AbilityKey.DefendingPlayer, runParams.get(AbilityKey.Affected)); + // Here the Player is the one who would declare blockers (may be changed by some Card's effect) + sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Player)); + } +} diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementType.java b/forge-game/src/main/java/forge/game/replacement/ReplacementType.java index da55335f69c..abc4c0605ec 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementType.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementType.java @@ -21,6 +21,7 @@ public enum ReplacementType { CreateToken(ReplaceToken.class), DamageDone(ReplaceDamage.class), DealtDamage(ReplaceDealtDamage.class), + DeclareBlocker(ReplaceDeclareBlocker.class), Destroy(ReplaceDestroy.class), Discard(ReplaceDiscard.class), Draw(ReplaceDraw.class), diff --git a/forge-gui/res/cardsfolder/c/camouflage.txt b/forge-gui/res/cardsfolder/c/camouflage.txt new file mode 100644 index 00000000000..d46d860dfd7 --- /dev/null +++ b/forge-gui/res/cardsfolder/c/camouflage.txt @@ -0,0 +1,7 @@ +Name:Camouflage +ManaCost:G +Types:Instant +A:SP$ Effect | Cost$ G | ReplacementEffects$ RDeclareBlocker | ActivationPhases$ Declare Attackers | PlayerTurn$ True | AILogic$ Evasion | SpellDescription$ Cast this spell only during your declare attackers step. This turn, instead of declaring blockers, each defending player chooses any number of creatures they control and divides them into a number of piles equal to the number of attacking creatures for whom that player is the defending player. Creatures those players control that can block additional creatures may likewise be put into additional piles. Assign each pile to a different one of those attacking creatures at random. Each creature in a pile that can block the creature that pile is assigned to does so. (Piles can be empty.) +SVar:RDeclareBlocker:Event$ DeclareBlocker | ValidPlayer$ Opponent | ReplaceWith$ DBCamouflage | Description$ This turn, instead of declaring blockers, each defending player chooses any number of creatures they control and divides them into a number of piles equal to the number of attacking creatures for whom that player is the defending player. Creatures those players control that can block additional creatures may likewise be put into additional piles. Assign each pile to a different one of those attacking creatures at random. Each creature in a pile that can block the creature that pile is assigned to does so. (Piles can be empty.) +SVar:DBCamouflage:DB$ Camouflage | Defined$ ReplacedPlayer | Defender$ ReplacedDefendingPlayer | AILogic$ BestBlocker +Oracle:Cast this spell only during your declare attackers step.\nThis turn, instead of declaring blockers, each defending player chooses any number of creatures they control and divides them into a number of piles equal to the number of attacking creatures for whom that player is the defending player. Creatures those players control that can block additional creatures may likewise be put into additional piles. Assign each pile to a different one of those attacking creatures at random. Each creature in a pile that can block the creature that pile is assigned to does so. (Piles can be empty.) diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index 360f21920e0..a78d4144714 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=Möchtest du überbieten? Aktuelles Gebot: lblTopBidWithValueLife=hat mit {0} Leben überboten #BondEffect.java lblSelectACardPair=Wähle Karte zum Verbinden +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard=Welchen Verteidiger mit {0} angreifen? #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index a49a1f47510..c8d2811d194 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=Do you want to top bid? Current Bid \= lblTopBidWithValueLife=topped bid with {0} life #BondEffect.java lblSelectACardPair=Select a card to pair with +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard=Choose which defender to attack with {0} #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/es-ES.properties b/forge-gui/res/languages/es-ES.properties index 226f28075fd..425cd9f822b 100644 --- a/forge-gui/res/languages/es-ES.properties +++ b/forge-gui/res/languages/es-ES.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=¿Quieres hacer una puja máxima? Puja actual \= lblTopBidWithValueLife=puja más alta con {0} de vida #BondEffect.java lblSelectACardPair=Selecciona una carta para emparejarla con +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard=Elige con qué defensor atacar con {0} #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/it-IT.properties b/forge-gui/res/languages/it-IT.properties index c40b513a2ed..57f59cbe438 100644 --- a/forge-gui/res/languages/it-IT.properties +++ b/forge-gui/res/languages/it-IT.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=Do you want to top bid? Current Bid \= lblTopBidWithValueLife=topped bid with {0} life #BondEffect.java lblSelectACardPair=Select a card to pair with +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard=Choose which defender to attack with {0} #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/ja-JP.properties b/forge-gui/res/languages/ja-JP.properties index 219998afa3b..85845de8b18 100644 --- a/forge-gui/res/languages/ja-JP.properties +++ b/forge-gui/res/languages/ja-JP.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=競りの点数を上げますか? 現在の点数 \= lblTopBidWithValueLife={0}点のライフで競りの点数をつけた #BondEffect.java lblSelectACardPair=組にしたいカードを選ぶ +#CamouflageEffect.java +lblChooseBlockerForAttacker={0}をブロックするクリーチャーを選ぶ +lblChooseBlockersForPile={0}番の束に入れるクリーチャーを選ぶ(空にできる) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard={0}が攻撃する対象を選ぶ #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/zh-CN.properties b/forge-gui/res/languages/zh-CN.properties index 1bb03642e6b..d24b2583448 100644 --- a/forge-gui/res/languages/zh-CN.properties +++ b/forge-gui/res/languages/zh-CN.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=你想要喊更高的价? 现在价钱 \= lblTopBidWithValueLife=最高喊价为{0}生命 #BondEffect.java lblSelectACardPair=选择要组成搭档的牌 +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWith=选择守军进行进攻 #ChangeTargetsEffect.java