Merge branch 'attackfix' into 'master'

declareAttackers fixes

See merge request core-developers/forge!6443
This commit is contained in:
Anthony Calosa
2022-03-26 00:36:21 +00:00
9 changed files with 40 additions and 30 deletions

View File

@@ -47,6 +47,7 @@ import forge.game.cost.Cost;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.Zone; import forge.game.zone.Zone;
@@ -135,13 +136,11 @@ public class AiAttackController {
if (c.isToken() && c.getCopiedPermanent() == null) { if (c.isToken() && c.getCopiedPermanent() == null) {
continue; continue;
} }
for (SpellAbility sa : c.getSpellAbilities()) { for (SpellAbility sa : Iterables.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.Animate))) {
if (sa.getApi() == ApiType.Animate) { if (ComputerUtilCost.canPayCost(sa, defender, false)
if (ComputerUtilCost.canPayCost(sa, defender, false) && sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
&& sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) { Card animatedCopy = AnimateAi.becomeAnimated(c, sa);
Card animatedCopy = AnimateAi.becomeAnimated(c, sa); defenders.add(animatedCopy);
defenders.add(animatedCopy);
}
} }
} }
} }

View File

@@ -1150,9 +1150,9 @@ public class AiBlockController {
//Check for validity of blocks in case something slipped through //Check for validity of blocks in case something slipped through
for (Card attacker : attackers) { for (Card attacker : attackers) {
if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) { if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) {
for (final Card blocker : combat.getBlockers(attacker)) { for (final Card blocker : CardLists.filterControlledBy(combat.getBlockers(attacker), ai)) {
if (blocker.getController() == ai) // don't touch other player's blockers // don't touch other player's blockers
combat.removeFromCombat(blocker); combat.removeFromCombat(blocker);
} }
} }
} }

View File

@@ -2037,7 +2037,7 @@ public class ComputerUtilCombat {
} // attacker no double strike } // attacker no double strike
return false;// should never arrive here return false;// should never arrive here
} // canDestroyBlocker }
/** /**
* <p> * <p>

View File

@@ -108,7 +108,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
if (sa.hasParam("CanTargetPlayer")) { if (sa.hasParam("CanTargetPlayer")) {
// Radiate // Radiate
// Remove targeted players because getAllCandidates include all the valid players // Remove targeted players because getAllCandidates include all the valid players
for(Player p : targetedSA.getTargets().getTargetPlayers()) for (Player p : targetedSA.getTargets().getTargetPlayers())
candidates.remove(p); candidates.remove(p);
for (GameEntity o : candidates) { for (GameEntity o : candidates) {

View File

@@ -165,7 +165,7 @@ public class AttackRequirement {
} }
} }
} }
if (!isAttackingDefender && CombatUtil.getAttackCost(attacker.getGame(), attacker, defender) == null) { if (!isAttackingDefender && CombatUtil.getAttackCost(attacker.getGame(), attacker, def) == null) {
violations++; // no one is attacking that defender or any of his PWs violations++; // no one is attacking that defender or any of his PWs
} }
} }
@@ -173,14 +173,7 @@ public class AttackRequirement {
} }
// now, count everything else // now, count everything else
final MapToAmount<GameEntity> defenderSpecificCostFree = new LinkedHashMapToAmount<>(); violations += defenderSpecific.countAll() - (isAttacking ? defenderSpecific.count(defender) : 0);
for (GameEntity e : defenderSpecific.keySet()) {
if (CombatUtil.getAttackCost(attacker.getGame(), attacker, defender) == null) {
defenderSpecificCostFree.put(e, defenderSpecific.get(e));
}
}
violations += defenderSpecificCostFree.countAll() - (isAttacking ? defenderSpecificCostFree.count(defender) : 0);
if (isAttacking) { if (isAttacking) {
final Combat combat = defender.getGame().getCombat(); final Combat combat = defender.getGame().getCombat();
final Map<Card, AttackRestriction> constraints = combat.getAttackConstraints().getRestrictions(); final Map<Card, AttackRestriction> constraints = combat.getAttackConstraints().getRestrictions();

View File

@@ -567,24 +567,34 @@ public class PhaseHandler implements java.io.Serializable {
continue; continue;
} }
final CardCollection untapFromCancel = new CardCollection();
// do a full loop first so attackers can't be used to pay for Propaganda
for (final Card attacker : combat.getAttackers()) { for (final Card attacker : combat.getAttackers()) {
final boolean shouldTapForAttack = !attacker.hasKeyword(Keyword.VIGILANCE) && !attacker.hasKeyword("Attacking doesn't cause CARDNAME to tap."); final boolean shouldTapForAttack = !attacker.hasKeyword(Keyword.VIGILANCE) && !attacker.hasKeyword("Attacking doesn't cause CARDNAME to tap.");
if (shouldTapForAttack) { if (shouldTapForAttack) {
// set tapped to true without firing triggers because it may affect propaganda costs // set tapped to true without firing triggers because it may affect propaganda costs
attacker.setTapped(true); attacker.setTapped(true);
untapFromCancel.add(attacker);
} }
}
for (final Card attacker : combat.getAttackers()) {
// TODO currently doesn't refund (can really only happen if you cancel paying for a creature with an attacking requirement that could be satisfied without a tax)
final boolean canAttack = CombatUtil.checkPropagandaEffects(game, attacker, combat); final boolean canAttack = CombatUtil.checkPropagandaEffects(game, attacker, combat);
attacker.setTapped(false);
if (canAttack) { if (!canAttack) {
if (shouldTapForAttack) {
attacker.tap(true, true);
}
} else {
combat.removeFromCombat(attacker); combat.removeFromCombat(attacker);
if (untapFromCancel.contains(attacker)) {
attacker.setTapped(false);
}
success = CombatUtil.validateAttackers(combat); success = CombatUtil.validateAttackers(combat);
if (!success) { if (!success) {
for (Card c : untapFromCancel) {
c.setTapped(false);
}
// might have been sacrificed while paying
combat.removeAbsentCombatants();
combat.initConstraints();
break; break;
} }
} }
@@ -592,6 +602,14 @@ public class PhaseHandler implements java.io.Serializable {
} while (!success); } while (!success);
for (final Card attacker : combat.getAttackers()) {
final boolean shouldTapForAttack = !attacker.hasKeyword(Keyword.VIGILANCE) && !attacker.hasKeyword("Attacking doesn't cause CARDNAME to tap.");
if (shouldTapForAttack) {
attacker.setTapped(false);
attacker.tap(true, true);
}
}
// Exert creatures here // Exert creatures here
List<Card> possibleExerters = CardLists.getKeyword(combat.getAttackers(), List<Card> possibleExerters = CardLists.getKeyword(combat.getAttackers(),
"You may exert CARDNAME as it attacks."); "You may exert CARDNAME as it attacks.");

View File

@@ -1,7 +1,7 @@
Name:Chef's Kiss Name:Chef's Kiss
ManaCost:1 R R ManaCost:1 R R
Types:Instant Types:Instant
A:SP$ ControlSpell | ValidTgts$ Card | TgtPrompt$ Select target spell that targets only a single permanent or player | TargetType$ Spell | Mode$ Gain | TargetsSingleTarget$ True | TargetValidTargeting$ Permanent,Player | SubAbility$ DBCopy | StackDescription$ SpellDescription | SpellDescription$ Gain control of target spell that targets only a single permanent or player. Copy it, then reselect the targets at random for the spell and the copy. The new targets can't be you or a permanent you control. A:SP$ ControlSpell | ValidTgts$ Card | TgtPrompt$ Select target spell that targets only a single permanent or player | TargetType$ Spell | Mode$ Gain | TargetsSingleTarget$ True | TargetValidTargeting$ Permanent.inZoneBattlefield,Player | SubAbility$ DBCopy | StackDescription$ SpellDescription | SpellDescription$ Gain control of target spell that targets only a single permanent or player. Copy it, then reselect the targets at random for the spell and the copy. The new targets can't be you or a permanent you control.
SVar:DBCopy:DB$ CopySpellAbility | Defined$ Targeted | RandomTarget$ True | RandomTargetRestriction$ Player.Other,Permanent.YouDontCtrl | SubAbility$ DBChangeTargets | StackDescription$ None SVar:DBCopy:DB$ CopySpellAbility | Defined$ Targeted | RandomTarget$ True | RandomTargetRestriction$ Player.Other,Permanent.YouDontCtrl | SubAbility$ DBChangeTargets | StackDescription$ None
SVar:DBChangeTargets:DB$ ChangeTargets | Defined$ Targeted | RandomTarget$ True | RandomTargetRestriction$ Player.Other,Permanent.YouDontCtrl SVar:DBChangeTargets:DB$ ChangeTargets | Defined$ Targeted | RandomTarget$ True | RandomTargetRestriction$ Player.Other,Permanent.YouDontCtrl
Oracle:Gain control of target spell that targets only a single permanent or player. Copy it, then reselect the targets at random for the spell and the copy. The new targets can't be you or a permanent you control. Oracle:Gain control of target spell that targets only a single permanent or player. Copy it, then reselect the targets at random for the spell and the copy. The new targets can't be you or a permanent you control.

View File

@@ -4,5 +4,5 @@ Types:Creature Human Wizard
PT:2/2 PT:2/2
K:Flash K:Flash
T:Mode$ ChangesZone | ValidCard$ Card.wasCastFromYourHandByYou+Self | Destination$ Battlefield | Execute$ TrigRadiate | TriggerDescription$ When CARDNAME enters the battlefield, if you cast it from your hand, choose target spell or ability that targets only a single permanent or player. Copy that spell or ability for each other permanent or player the spell or ability could target. Each copy targets a different one of those permanents and players. T:Mode$ ChangesZone | ValidCard$ Card.wasCastFromYourHandByYou+Self | Destination$ Battlefield | Execute$ TrigRadiate | TriggerDescription$ When CARDNAME enters the battlefield, if you cast it from your hand, choose target spell or ability that targets only a single permanent or player. Copy that spell or ability for each other permanent or player the spell or ability could target. Each copy targets a different one of those permanents and players.
SVar:TrigRadiate:DB$ CopySpellAbility | ValidTgts$ Card | TgtPrompt$ Select target spell or ability that targets a single permanent or player | TargetType$ Spell,Activated,Triggered | TargetsSingleTarget$ True | TargetValidTargeting$ Permanent,Player | Controller$ You | CopyForEachCanTarget$ True | CanTargetPlayer$ True | SpellDescription$ Choose target spell or ability spell that targets only a single permanent or player. Copy that spell for each other permanent or player the spell could target. Each copy targets a different one of those permanents and players. SVar:TrigRadiate:DB$ CopySpellAbility | ValidTgts$ Card | TgtPrompt$ Select target spell or ability that targets a single permanent or player | TargetType$ Spell,Activated,Triggered | TargetsSingleTarget$ True | TargetValidTargeting$ Permanent.inZoneBattlefield,Player | Controller$ You | CopyForEachCanTarget$ True | CanTargetPlayer$ True | SpellDescription$ Choose target spell or ability that targets only a single permanent or player. Copy that spell for each other permanent or player the spell could target. Each copy targets a different one of those permanents and players.
Oracle:Flash\nWhen Radiant Performer enters the battlefield, if you cast it from your hand, choose target spell or ability that targets only a single permanent or player. Copy that spell or ability for each other permanent or player the spell or ability could target. Each copy targets a different one of those permanents and players. Oracle:Flash\nWhen Radiant Performer enters the battlefield, if you cast it from your hand, choose target spell or ability that targets only a single permanent or player. Copy that spell or ability for each other permanent or player the spell or ability could target. Each copy targets a different one of those permanents and players.

View File

@@ -1,6 +1,6 @@
Name:Radiate Name:Radiate
ManaCost:3 R R ManaCost:3 R R
Types:Instant Types:Instant
A:SP$ CopySpellAbility | Cost$ 3 R R | ValidTgts$ Instant,Sorcery | TargetType$ Spell | TargetsSingleTarget$ True | TargetValidTargeting$ Permanent,Player | Controller$ You | CopyForEachCanTarget$ True | CanTargetPlayer$ True | SpellDescription$ Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell for each other permanent or player the spell could target. Each copy targets a different one of those permanents and players. A:SP$ CopySpellAbility | Cost$ 3 R R | ValidTgts$ Instant,Sorcery | TargetType$ Spell | TargetsSingleTarget$ True | TargetValidTargeting$ Permanent.inZoneBattlefield,Player | Controller$ You | CopyForEachCanTarget$ True | CanTargetPlayer$ True | SpellDescription$ Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell for each other permanent or player the spell could target. Each copy targets a different one of those permanents and players.
AI:RemoveDeck:Random AI:RemoveDeck:Random
Oracle:Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell for each other permanent or player the spell could target. Each copy targets a different one of those permanents and players. Oracle:Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell for each other permanent or player the spell could target. Each copy targets a different one of those permanents and players.