Merge branch 'add_camouflage' into 'master'

Add Camouflage

See merge request core-developers/forge!4268
This commit is contained in:
Michael Kamensky
2021-03-26 10:57:03 +00:00
13 changed files with 173 additions and 1 deletions

View File

@@ -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)

View File

@@ -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),

View File

@@ -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<Card> attackers, List<CardCollection> 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<Card> attackers = combat.getAttackers();
List<CardCollection> 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<Integer> 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);
}
}

View File

@@ -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<AbilityKey, Object> 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; }

View File

@@ -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<String, String> mapParams, final Card host, final boolean intrinsic) {
super(mapParams, host, intrinsic);
}
@Override
public boolean canReplace(Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) {
return false;
}
return true;
}
@Override
public void setReplacingObjects(Map<AbilityKey, Object> 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));
}
}

View File

@@ -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),

View File

@@ -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.)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=競りの点数を上げますか? 現在の点数 \=
lblTopBidWithValueLife={0}点のライフで競りの点数をつけた
#BondEffect.java
lblSelectACardPair=組にしたいカードを選ぶ
#CamouflageEffect.java
lblChooseBlockerForAttacker={0}をブロックするクリーチャーを選ぶ
lblChooseBlockersForPile={0}番の束に入れるクリーチャーを選ぶ(空にできる)
#ChangeCombatantsEffect.java
lblChooseDefenderToAttackWithCard={0}が攻撃する対象を選ぶ
#ChangeTargetsEffect.java

View File

@@ -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