Reworking goad effects (ready for review) (#8034)

* changing goad to designation, refactored goad effects to be continuous

* [AFC] Implemented Vengeful Ancestor

* reworked effects which goad an attached creature

* updated goading implementation

* updated combat with new goad logic

* some more changes, added a test

* another fix

* update to test, still fails

* added more failing tests

* more failing tests

* added additional goad check

* small fix to two tests (still failing

* added a regular combat test (passes and fails randomly)

* fixed bug in computer player random selection

* some changes to how TargetDefender is handled

* removed unnecessary class

* more combat fixes, tests pass now

* removed tests which no longer work due to combat changes

* small merge fix

* [NEC] Implemented Komainu Battle Armor

* [NEC] Implemented Kaima, the Fractured Calm

* [NEC] added all variants
This commit is contained in:
Evan Kranzler 2022-02-15 09:18:21 -05:00 committed by GitHub
parent 5725873aeb
commit 4591ac07cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 812 additions and 438 deletions

View file

@ -1,6 +1,5 @@
package mage.abilities.common;
import java.util.UUID;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
@ -11,9 +10,11 @@ import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import java.util.UUID;
/**
*
* @author LevelX2
*/
public class AttacksAllTriggeredAbility extends TriggeredAbilityImpl {
@ -74,18 +75,15 @@ public class AttacksAllTriggeredAbility extends TriggeredAbilityImpl {
return false;
}
}
getEffects().setValue("attacker", permanent);
switch (setTargetPointer) {
case PERMANENT:
for (Effect effect : getEffects()) {
effect.setTargetPointer(new FixedTarget(permanent, game));
}
getEffects().setTargetPointer(new FixedTarget(permanent, game));
break;
case PLAYER:
UUID playerId = controller ? permanent.getControllerId() : game.getCombat().getDefendingPlayerId(permanent.getId(), game);
if (playerId != null) {
for (Effect effect : getEffects()) {
effect.setTargetPointer(new FixedTarget(playerId));
}
getEffects().setTargetPointer(new FixedTarget(playerId));
}
break;
}
@ -101,10 +99,8 @@ public class AttacksAllTriggeredAbility extends TriggeredAbilityImpl {
@Override
public String getTriggerPhrase() {
return "Whenever " + (filter.getMessage().startsWith("an") ? "" : "a ")
+ filter.getMessage() + " attacks"
+ (attacksYouOrYourPlaneswalker ? " you or a planeswalker you control" : "")
+ ", " ;
return "Whenever " + CardUtil.addArticle(filter.getMessage()) + " attacks"
+ (attacksYouOrYourPlaneswalker ? " you or a planeswalker you control" : "") + ", ";
}
}

View file

@ -1,78 +0,0 @@
package mage.abilities.common;
import mage.abilities.Ability;
import mage.abilities.StaticAbility;
import mage.abilities.effects.Effect;
import mage.abilities.effects.RestrictionEffect;
import mage.abilities.effects.common.combat.AttacksIfAbleAttachedEffect;
import mage.constants.AttachmentType;
import mage.constants.Duration;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public class GoadAttachedAbility extends StaticAbility {
public GoadAttachedAbility(Effect... effects) {
super(Zone.BATTLEFIELD, null);
for (Effect effect : effects) {
this.addEffect(effect);
}
this.addEffect(new AttacksIfAbleAttachedEffect(
Duration.WhileOnBattlefield, AttachmentType.AURA
).setText((getEffects().size() > 1 ? ", " : " ") + "and is goaded. <i>(It attacks each combat if able"));
this.addEffect(new GoadAttackEffect());
}
private GoadAttachedAbility(final GoadAttachedAbility ability) {
super(ability);
}
@Override
public GoadAttachedAbility copy() {
return new GoadAttachedAbility(this);
}
}
class GoadAttackEffect extends RestrictionEffect {
GoadAttackEffect() {
super(Duration.WhileOnBattlefield);
staticText = "and attacks a player other than you if able.)</i>";
}
private GoadAttackEffect(final GoadAttackEffect effect) {
super(effect);
}
@Override
public GoadAttackEffect copy() {
return new GoadAttackEffect(this);
}
@Override
public boolean applies(Permanent permanent, Ability source, Game game) {
Permanent attachment = game.getPermanent(source.getSourceId());
return attachment != null && attachment.getAttachedTo() != null
&& permanent.getId().equals(attachment.getAttachedTo());
}
@Override
public boolean canAttack(Permanent attacker, UUID defenderId, Ability source, Game game, boolean canUseChooseDialogs) {
if (defenderId == null
|| game.getState().getPlayersInRange(attacker.getControllerId(), game).size() == 2) { // just 2 players left, so it may attack you
return true;
}
// A planeswalker controlled by the controller is the defender
if (game.getPermanent(defenderId) != null) {
return !game.getPermanent(defenderId).getControllerId().equals(source.getControllerId());
}
// The controller is the defender
return !defenderId.equals(source.getControllerId());
}
}

View file

@ -1,46 +0,0 @@
package mage.abilities.effects.common.combat;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.effects.RestrictionEffect;
import mage.constants.Duration;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
*/
public class CantAttackControllerDueToGoadEffect extends RestrictionEffect {
public CantAttackControllerDueToGoadEffect(Duration duration) {
super(duration);
}
public CantAttackControllerDueToGoadEffect(final CantAttackControllerDueToGoadEffect effect) {
super(effect);
}
@Override
public CantAttackControllerDueToGoadEffect copy() {
return new CantAttackControllerDueToGoadEffect(this);
}
@Override
public boolean applies(Permanent permanent, Ability source, Game game) {
return this.getTargetPointer().getTargets(game, source).contains(permanent.getId());
}
@Override
public boolean canAttack(Permanent attacker, UUID defenderId, Ability source, Game game, boolean canUseChooseDialogs) {
if (defenderId == null
|| game.getState().getPlayersInRange(attacker.getControllerId(), game).size() == 2) { // just 2 players left, so it may attack you
return true;
}
// A planeswalker controlled by the controller is the defender
if (game.getPermanent(defenderId) != null) {
return !game.getPermanent(defenderId).getControllerId().equals(source.getControllerId());
}
// The controller is the defender
return !defenderId.equals(source.getControllerId());
}
}

View file

@ -0,0 +1,44 @@
package mage.abilities.effects.common.combat;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
*/
public class GoadAttachedEffect extends ContinuousEffectImpl {
public GoadAttachedEffect() {
super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Detriment);
staticText = "and is goaded";
}
private GoadAttachedEffect(final GoadAttachedEffect effect) {
super(effect);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
return false;
}
Permanent attached = game.getPermanent(permanent.getAttachedTo());
if (attached == null) {
return false;
}
attached.addGoadingPlayer(source.getControllerId());
return true;
}
@Override
public GoadAttachedEffect copy() {
return new GoadAttachedEffect(this);
}
}

View file

@ -2,19 +2,19 @@ package mage.abilities.effects.common.combat;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
/**
* @author TheElk801
*/
public class GoadTargetEffect extends OneShotEffect {
public class GoadTargetEffect extends ContinuousEffectImpl {
/**
* 701.36. Goad
@ -24,10 +24,10 @@ public class GoadTargetEffect extends OneShotEffect {
* each combat if able and attacks a player other than that player if able.
*/
public GoadTargetEffect() {
super(Outcome.Detriment);
super(Duration.UntilYourNextTurn, Layer.RulesEffects, SubLayer.NA, Outcome.Detriment);
}
public GoadTargetEffect(final GoadTargetEffect effect) {
private GoadTargetEffect(final GoadTargetEffect effect) {
super(effect);
}
@ -37,26 +37,22 @@ public class GoadTargetEffect extends OneShotEffect {
}
@Override
public boolean apply(Game game, Ability source) {
public void init(Ability source, Game game) {
super.init(source, game);
Permanent targetCreature = game.getPermanent(getTargetPointer().getFirst(game, source));
Player controller = game.getPlayer(source.getControllerId());
if (targetCreature != null && controller != null) {
// TODO: Allow goad to target controller, current AttacksIfAbleTargetEffect does not support it
// https://github.com/magefree/mage/issues/5283
/*
If the creature doesnt meet any of the above exceptions and can attack, it must attack a player other than
the controller of the spell or ability that goaded it if able. If the creature cant attack any of those
players but could otherwise attack, it must attack an opposing planeswalker (controlled by any opponent)
or the player that goaded it. (2016-08-23)
*/
ContinuousEffect effect = new AttacksIfAbleTargetEffect(Duration.UntilYourNextTurn);
effect.setTargetPointer(new FixedTarget(getTargetPointer().getFirst(game, source), game));
game.addEffect(effect, source);
effect = new CantAttackControllerDueToGoadEffect(Duration.UntilYourNextTurn); // remember current controller
effect.setTargetPointer(new FixedTarget(getTargetPointer().getFirst(game, source), game));
game.addEffect(effect, source);
game.informPlayers(controller.getLogName() + " is goading " + targetCreature.getLogName());
}
}
@Override
public boolean apply(Game game, Ability source) {
Permanent targetCreature = game.getPermanent(getTargetPointer().getFirst(game, source));
if (targetCreature == null) {
return false;
}
targetCreature.addGoadingPlayer(source.getControllerId());
return true;
}

View file

@ -440,65 +440,76 @@ public class Combat implements Serializable, Copyable<Combat> {
for (Permanent creature : player.getAvailableAttackers(game)) {
boolean mustAttack = false;
Set<UUID> defendersForcedToAttack = new HashSet<>();
// check if a creature has to attack
for (Map.Entry<RequirementEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRequirementEffects(creature, false, game).entrySet()) {
RequirementEffect effect = entry.getKey();
if (effect.mustAttack(game)
&& checkAttackRestrictions(player, game)) { // needed for Goad Effect
if (creature.getGoadingPlayers().isEmpty()) {
// check if a creature has to attack
for (Map.Entry<RequirementEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRequirementEffects(creature, false, game).entrySet()) {
RequirementEffect effect = entry.getKey();
if (!effect.mustAttack(game)) {
continue;
}
mustAttack = true;
for (Ability ability : entry.getValue()) {
UUID defenderId = effect.mustAttackDefender(ability, game);
if (defenderId != null) {
if (defenders.contains(defenderId)) {
defendersForcedToAttack.add(defenderId);
}
if (defenderId != null && defenders.contains(defenderId)) {
defendersForcedToAttack.add(defenderId);
}
break;
}
}
} else {
// if creature is goaded then we start with assumption that it needs to attack any player
mustAttack = true;
defendersForcedToAttack.addAll(defenders);
}
if (mustAttack) {
// check which defenders the forced to attack creature can attack without paying a cost
Set<UUID> defendersCostlessAttackable = new HashSet<>(defenders);
for (UUID defenderId : defenders) {
if (game.getContinuousEffects().checkIfThereArePayCostToAttackBlockEffects(
new DeclareAttackerEvent(defenderId, creature.getId(), creature.getControllerId()), game)) {
defendersCostlessAttackable.remove(defenderId);
defendersForcedToAttack.remove(defenderId);
}
}
// force attack only if a defender can be attacked without paying a cost
if (!defendersCostlessAttackable.isEmpty()) {
creaturesForcedToAttack.put(creature.getId(), defendersForcedToAttack);
// No need to attack a special defender
if (defendersForcedToAttack.isEmpty()) {
if (defendersCostlessAttackable.size() == 1) {
player.declareAttacker(creature.getId(), defendersCostlessAttackable.iterator().next(), game, false);
} else {
TargetDefender target = new TargetDefender(defendersCostlessAttackable, creature.getId());
target.setRequired(true);
target.setTargetName("planeswalker or player for " + creature.getLogName() + " to attack (must attack effect)");
if (player.chooseTarget(Outcome.Damage, target, null, game)) {
player.declareAttacker(creature.getId(), target.getFirstTarget(), game, false);
}
}
} else {
if (defendersForcedToAttack.size() == 1) {
player.declareAttacker(creature.getId(), defendersForcedToAttack.iterator().next(), game, false);
} else {
TargetDefender target = new TargetDefender(defendersForcedToAttack, creature.getId());
target.setRequired(true);
target.setTargetName("planeswalker or player for " + creature.getLogName() + " to attack (must attack effect)");
if (player.chooseTarget(Outcome.Damage, target, null, game)) {
player.declareAttacker(creature.getId(), target.getFirstTarget(), game, false);
}
}
}
}
if (!mustAttack) {
continue;
}
// check which defenders the forced to attack creature can attack without paying a cost
Set<UUID> defendersCostlessAttackable = new HashSet<>(defenders);
for (UUID defenderId : defenders) {
if (game.getContinuousEffects().checkIfThereArePayCostToAttackBlockEffects(
new DeclareAttackerEvent(defenderId, creature.getId(), creature.getControllerId()), game
)) {
defendersCostlessAttackable.remove(defenderId);
defendersForcedToAttack.remove(defenderId);
continue;
}
for (Map.Entry<RestrictionEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRestrictionEffects(creature, game).entrySet()) {
if (entry
.getValue()
.stream()
.anyMatch(ability -> entry.getKey().canAttack(
creature, defenderId, ability, game, false
))) {
continue;
}
defendersCostlessAttackable.remove(defenderId);
defendersForcedToAttack.remove(defenderId);
break;
}
}
// if creature can attack someone other than a player that goaded them
// then they attack one of those players, otherwise they attack any player
if (!defendersForcedToAttack.stream().allMatch(creature.getGoadingPlayers()::contains)) {
defendersForcedToAttack.removeAll(creature.getGoadingPlayers());
}
// force attack only if a defender can be attacked without paying a cost
if (defendersCostlessAttackable.isEmpty()) {
continue;
}
creaturesForcedToAttack.put(creature.getId(), defendersForcedToAttack);
// No need to attack a special defender
Set<UUID> defendersToChooseFrom = defendersForcedToAttack.isEmpty() ? defendersCostlessAttackable : defendersForcedToAttack;
if (defendersToChooseFrom.size() == 1) {
player.declareAttacker(creature.getId(), defendersToChooseFrom.iterator().next(), game, false);
continue;
}
TargetDefender target = new TargetDefender(defendersToChooseFrom, creature.getId());
target.setRequired(true);
target.setTargetName("planeswalker or player for " + creature.getLogName() + " to attack (must attack effect)");
if (player.chooseTarget(Outcome.Damage, target, null, game)) {
player.declareAttacker(creature.getId(), target.getFirstTarget(), game, false);
}
}
}
@ -529,28 +540,30 @@ public class Combat implements Serializable, Copyable<Combat> {
for (Map.Entry<RestrictionEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRestrictionEffects(attackingCreature, game).entrySet()) {
RestrictionEffect effect = entry.getKey();
for (Ability ability : entry.getValue()) {
if (!effect.canAttackCheckAfter(numberAttackers, ability, game, true)) {
MageObject sourceObject = ability.getSourceObject(game);
if (attackingPlayer.isHuman()) {
attackingPlayer.resetPlayerPassedActions();
game.informPlayer(attackingPlayer, attackingCreature.getIdName() + " can't attack this way (" + (sourceObject == null ? "null" : sourceObject.getIdName()) + ')');
return false;
} else {
// remove attacking creatures for AI that are not allowed to attack
// can create possible not allowed attack scenarios, but not sure how to solve this
for (CombatGroup combatGroup : this.getGroups()) {
if (combatGroup.getAttackers().contains(attackingCreatureId)) {
attackerToRemove = attackingCreatureId;
}
}
check = true; // do the check again
if (numberOfChecks > 50) {
logger.error("Seems to be an AI declare attacker lock (reached 50 check iterations) " + (sourceObject == null ? "null" : sourceObject.getIdName()));
return true; // break the check
}
continue Check;
}
if (effect.canAttackCheckAfter(numberAttackers, ability, game, true)) {
continue;
}
MageObject sourceObject = ability.getSourceObject(game);
if (attackingPlayer.isHuman()) {
attackingPlayer.resetPlayerPassedActions();
game.informPlayer(attackingPlayer, attackingCreature.getIdName() + " can't attack this way (" + (sourceObject == null ? "null" : sourceObject.getIdName()) + ')');
return false;
}
// remove attacking creatures for AI that are not allowed to attack
// can create possible not allowed attack scenarios, but not sure how to solve this
if (this.getGroups()
.stream()
.map(CombatGroup::getAttackers)
.flatMap(Collection::stream)
.anyMatch(attackingCreatureId::equals)) {
attackerToRemove = attackingCreatureId;
}
check = true; // do the check again
if (numberOfChecks > 50) {
logger.error("Seems to be an AI declare attacker lock (reached 50 check iterations) " + (sourceObject == null ? "null" : sourceObject.getIdName()));
return true; // break the check
}
continue Check;
}
}
}
@ -1300,21 +1313,20 @@ public class Combat implements Serializable, Copyable<Combat> {
@SuppressWarnings("deprecation")
public boolean declareAttacker(UUID creatureId, UUID defenderId, UUID playerId, Game game) {
Permanent attacker = game.getPermanent(creatureId);
if (attacker != null) {
if (!game.replaceEvent(new DeclareAttackerEvent(defenderId, creatureId, playerId))) {
if (addAttackerToCombat(creatureId, defenderId, game)) {
if (!attacker.hasAbility(VigilanceAbility.getInstance(), game)
&& !attacker.hasAbility(JohanVigilanceAbility.getInstance(), game)) {
if (!attacker.isTapped()) {
attacker.setTapped(true);
attackersTappedByAttack.add(attacker.getId());
}
}
return true;
}
}
if (attacker == null
|| game.replaceEvent(new DeclareAttackerEvent(defenderId, creatureId, playerId))
|| !addAttackerToCombat(creatureId, defenderId, game)) {
return false;
}
return false;
if (attacker.hasAbility(VigilanceAbility.getInstance(), game)
|| attacker.hasAbility(JohanVigilanceAbility.getInstance(), game)) {
return true;
}
if (!attacker.isTapped()) {
attacker.setTapped(true);
attackersTappedByAttack.add(attacker.getId());
}
return true;
}
public boolean addAttackerToCombat(UUID attackerId, UUID defenderId, Game game) {

View file

@ -87,6 +87,10 @@ public interface Permanent extends Card, Controllable {
*/
boolean setClassLevel(int classLevel);
void addGoadingPlayer(UUID playerId);
Set<UUID> getGoadingPlayers();
void setCardNumber(String cid);
void setExpansionSetCode(String expansionSetCode);

View file

@ -72,6 +72,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected boolean manifested = false;
protected boolean morphed = false;
protected int classLevel = 1;
protected final Set<UUID> goadingPlayers = new HashSet<>();
protected UUID originalControllerId;
protected UUID controllerId;
protected UUID beforeResetControllerId;
@ -165,6 +166,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.monstrous = permanent.monstrous;
this.renowned = permanent.renowned;
this.classLevel = permanent.classLevel;
this.goadingPlayers.addAll(permanent.goadingPlayers);
this.pairedPermanent = permanent.pairedPermanent;
this.bandedCards.addAll(permanent.bandedCards);
this.timesLoyaltyUsed = permanent.timesLoyaltyUsed;
@ -209,6 +211,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.minBlockedBy = 1;
this.maxBlockedBy = 0;
this.copy = false;
this.goadingPlayers.clear();
}
@Override
@ -1345,14 +1348,10 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
}
//20101001 - 508.1c
if (defenderId == null) {
boolean oneCanBeAttacked = false;
for (UUID defenderToCheckId : game.getCombat().getDefenders()) {
if (canAttackCheckRestrictionEffects(defenderToCheckId, game)) {
oneCanBeAttacked = true;
break;
}
}
if (!oneCanBeAttacked) {
if (game.getCombat()
.getDefenders()
.stream()
.noneMatch(defenderToCheckId -> canAttackCheckRestrictionEffects(defenderToCheckId, game))) {
return false;
}
} else if (!canAttackCheckRestrictionEffects(defenderId, game)) {
@ -1582,6 +1581,16 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return false;
}
@Override
public void addGoadingPlayer(UUID playerId) {
this.goadingPlayers.add(playerId);
}
@Override
public Set<UUID> getGoadingPlayers() {
return goadingPlayers;
}
@Override
public void setPairedCard(MageObjectReference pairedCard) {
this.pairedPermanent = pairedCard;

View file

@ -2625,10 +2625,9 @@ public abstract class PlayerImpl implements Player, Serializable {
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null
&& attacker.canAttack(defenderId, game)
&& attacker.isControlledBy(playerId)) {
if (!game.getCombat().declareAttacker(attackerId, defenderId, playerId, game)) {
game.undo(playerId);
}
&& attacker.isControlledBy(playerId)
&& !game.getCombat().declareAttacker(attackerId, defenderId, playerId, game)) {
game.undo(playerId);
}
}

View file

@ -38,6 +38,7 @@ public class TargetDefender extends TargetImpl {
this.filter = new FilterPlaneswalkerOrPlayer(defenders);
this.targetName = filter.getMessage();
this.attackerId = attackerId;
this.notTarget = true;
}
public TargetDefender(final TargetDefender target) {

View file

@ -3,6 +3,8 @@ package mage.util;
import java.awt.*;
import java.util.Collection;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
/**
* Created by IGOUDT on 5-9-2016.