Fix handling of damage to permanents (WIP) (#7592)

* initial refactor of damage events

* cleaned up some instances of classes that need to be removed

* removed old damage event classes

* removed outdated imports

* temporarily refactor Everlasting Torment (this will need to be changed more)

* updated damage handling to use new changes

* some reworking of lethal/excess damage plus a test

* updated damage marking to handle planeswalkers

* updated implementation of Phyrexian Unlife

* updated implementation of Everlasting Torment

* added some more excess damage tests

* small change to wither check
This commit is contained in:
Evan Kranzler 2021-02-22 17:11:24 -05:00 committed by GitHub
parent 39f6b69391
commit 5390963d38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
201 changed files with 1132 additions and 1187 deletions

View file

@ -50,15 +50,17 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
private static final Logger logger = Logger.getLogger(PermanentImpl.class);
static class MarkedDamageInfo implements Serializable {
private static class MarkedDamageInfo implements Serializable {
public MarkedDamageInfo(Counter counter, MageObject sourceObject) {
private final Counter counter;
private final MageObject sourceObject;
private final boolean addCounters;
private MarkedDamageInfo(Counter counter, MageObject sourceObject, boolean addCounters) {
this.counter = counter;
this.sourceObject = sourceObject;
this.addCounters = addCounters;
}
Counter counter;
MageObject sourceObject;
}
private static final ThreadLocalStringBuilder threadLocalBuilder = new ThreadLocalStringBuilder(300);
@ -146,7 +148,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
if (permanent.markedDamage != null) {
markedDamage = new ArrayList<>();
for (MarkedDamageInfo mdi : permanent.markedDamage) {
markedDamage.add(new MarkedDamageInfo(mdi.counter.copy(), mdi.sourceObject));
markedDamage.add(new MarkedDamageInfo(mdi.counter.copy(), mdi.sourceObject, mdi.addCounters));
}
}
if (permanent.info != null) {
@ -864,71 +866,127 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
*/
private int doDamage(int damageAmount, UUID attackerId, Ability source, Game game, boolean preventable, boolean combat, boolean markDamage, List<UUID> appliedEffects) {
int damageDone = 0;
if (damageAmount > 0 && canDamage(game.getObject(attackerId), game)) {
if (this.isPlaneswalker()) {
damageDone = damagePlaneswalker(damageAmount, attackerId, source, game, preventable, combat, markDamage, appliedEffects);
if (damageAmount < 1 || !canDamage(game.getObject(attackerId), game)) {
return 0;
}
DamageEvent event = new DamagePermanentEvent(objectId, attackerId, controllerId, damageAmount, preventable, combat);
event.setAppliedEffects(appliedEffects);
if (game.replaceEvent(event)) {
return 0;
}
int actualDamage = checkProtectionAbilities(event, attackerId, source, game);
if (actualDamage < 1) {
return 0;
}
int lethal = getLethalDamage(attackerId, game);
MageObject attacker = game.getObject(attackerId);
if (this.isCreature()) {
if (checkWither(event, attacker, game)) {
if (markDamage) {
// mark damage only
markDamage(CounterType.M1M1.createInstance(actualDamage), attacker, true);
} else {
Ability damageSourceAbility = null;
if (attacker instanceof Permanent) {
damageSourceAbility = ((Permanent) attacker).getSpellAbility();
}
// deal damage immediately
addCounters(CounterType.M1M1.createInstance(actualDamage), game.getControllerId(attackerId), damageSourceAbility, game);
}
} else {
damageDone = damageCreature(damageAmount, attackerId, source, game, preventable, combat, markDamage, appliedEffects);
}
if (damageDone > 0) {
UUID sourceControllerId = null;
Abilities sourceAbilities = null;
MageObject attacker = game.getPermanentOrLKIBattlefield(attackerId);
if (attacker == null) {
StackObject stackObject = game.getStack().getStackObject(attackerId);
if (stackObject != null) {
attacker = stackObject.getStackAbility().getSourceObject(game);
} else {
attacker = game.getObject(attackerId);
}
if (attacker instanceof Spell) {
sourceAbilities = ((Spell) attacker).getAbilities(game);
sourceControllerId = ((Spell) attacker).getControllerId();
} else if (attacker instanceof Card) {
sourceAbilities = ((Card) attacker).getAbilities(game);
sourceControllerId = ((Card) attacker).getOwnerId();
} else if (attacker instanceof CommandObject) {
sourceControllerId = ((CommandObject) attacker).getControllerId();
sourceAbilities = attacker.getAbilities();
} else {
attacker = null;
}
} else {
sourceAbilities = ((Permanent) attacker).getAbilities(game);
sourceControllerId = ((Permanent) attacker).getControllerId();
}
if (attacker != null && sourceAbilities != null) {
if (sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) {
if (markDamage) {
game.getPermanent(attackerId).markLifelink(damageDone);
} else {
Player player = game.getPlayer(sourceControllerId);
player.gainLife(damageDone, game, source);
}
}
if (sourceAbilities.containsKey(DeathtouchAbility.getInstance().getId())) {
deathtouched = true;
}
if (dealtDamageByThisTurn == null) {
dealtDamageByThisTurn = new HashSet<>();
}
// Unstable ability - Earl of Squirrel
if (sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) {
Player player = game.getPlayer(sourceControllerId);
new SquirrelToken().putOntoBattlefield(damageDone, game, source, player.getId());
}
dealtDamageByThisTurn.add(new MageObjectReference(attacker, game));
}
if (attacker == null) {
game.informPlayers(getLogName() + " gets " + damageDone + " damage");
} else {
game.informPlayers(attacker.getLogName() + " deals " + damageDone + " damage to " + getLogName());
}
this.damage = CardUtil.overflowInc(this.damage, actualDamage);
}
}
if (this.isPlaneswalker()) {
int loyalty = getCounters(game).getCount(CounterType.LOYALTY);
int countersToRemove = Math.min(actualDamage, loyalty);
if (attacker != null && markDamage) {
markDamage(CounterType.LOYALTY.createInstance(countersToRemove), attacker, false);
} else {
removeCounters(CounterType.LOYALTY.getName(), countersToRemove, source, game);
}
}
DamagedEvent damagedEvent = new DamagedPermanentEvent(this.getId(), attackerId, this.getControllerId(), actualDamage, combat);
damagedEvent.setExcess(actualDamage - lethal);
game.fireEvent(damagedEvent);
game.getState().addSimultaneousDamage(damagedEvent, game);
damageDone = actualDamage;
if (damageDone < 1) {
return 0;
}
UUID sourceControllerId = null;
Abilities sourceAbilities = null;
attacker = game.getPermanentOrLKIBattlefield(attackerId);
if (attacker == null) {
StackObject stackObject = game.getStack().getStackObject(attackerId);
if (stackObject != null) {
attacker = stackObject.getStackAbility().getSourceObject(game);
} else {
attacker = game.getObject(attackerId);
}
if (attacker instanceof Spell) {
sourceAbilities = ((Spell) attacker).getAbilities(game);
sourceControllerId = ((Spell) attacker).getControllerId();
} else if (attacker instanceof Card) {
sourceAbilities = ((Card) attacker).getAbilities(game);
sourceControllerId = ((Card) attacker).getOwnerId();
} else if (attacker instanceof CommandObject) {
sourceControllerId = ((CommandObject) attacker).getControllerId();
sourceAbilities = attacker.getAbilities();
} else {
attacker = null;
}
} else {
sourceAbilities = ((Permanent) attacker).getAbilities(game);
sourceControllerId = ((Permanent) attacker).getControllerId();
}
if (attacker != null && sourceAbilities != null) {
if (sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) {
if (markDamage) {
game.getPermanent(attackerId).markLifelink(damageDone);
} else {
Player player = game.getPlayer(sourceControllerId);
player.gainLife(damageDone, game, source);
}
}
if (sourceAbilities.containsKey(DeathtouchAbility.getInstance().getId())) {
deathtouched = true;
}
if (dealtDamageByThisTurn == null) {
dealtDamageByThisTurn = new HashSet<>();
}
// Unstable ability - Earl of Squirrel
if (sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) {
Player player = game.getPlayer(sourceControllerId);
new SquirrelToken().putOntoBattlefield(damageDone, game, source, player.getId());
}
dealtDamageByThisTurn.add(new MageObjectReference(attacker, game));
}
if (attacker == null) {
game.informPlayers(getLogName() + " gets " + damageDone + " damage");
} else {
game.informPlayers(attacker.getLogName() + " deals " + damageDone + " damage to " + getLogName());
}
return damageDone;
}
private static boolean checkWither(DamageEvent event, MageObject attacker, Game game) {
if (event.isAsThoughWither() || event.isAsThoughInfect()) {
return true;
}
if (attacker == null) {
return false;
}
Abilities abilities;
if (attacker instanceof Card) {
abilities = ((Card) attacker).getAbilities(game);
} else {
abilities = attacker.getAbilities();
}
return abilities.containsKey(InfectAbility.getInstance().getId())
|| abilities.containsKey(WitherAbility.getInstance().getId());
}
@Override
public void markLifelink(int damage) {
markedLifelink += damage;
@ -960,81 +1018,43 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
} else if (mdi.sourceObject instanceof Permanent) {
source = ((Permanent) mdi.sourceObject).getSpellAbility();
}
addCounters(mdi.counter, game.getControllerId(mdi.sourceObject.getId()), source, game);
if (mdi.addCounters) {
addCounters(mdi.counter, game.getControllerId(mdi.sourceObject.getId()), source, game);
} else {
removeCounters(mdi.counter, source, game);
}
}
markedDamage.clear();
return 0;
}
@Override
public int getLethalDamage(UUID attackerId, Game game) {
int lethal = Integer.MAX_VALUE;
if (this.isCreature()) {
if (game.getState().getActivePowerInsteadOfToughnessForDamageLethalityFilters().stream().anyMatch(f -> f.match(this, game))) {
lethal = Math.min(lethal, power.getValue());
} else {
lethal = Math.min(lethal, toughness.getValue());
}
lethal = Math.max(lethal - this.damage, 0);
Card attacker = game.getCard(attackerId);
if (attacker != null && attacker.getAbilities(game).containsKey(DeathtouchAbility.getInstance().getId())) {
lethal = Math.min(1, lethal);
}
}
if (this.isPlaneswalker()) {
lethal = Math.min(lethal, this.getCounters(game).getCount(CounterType.LOYALTY));
}
return lethal;
}
@Override
public void removeAllDamage(Game game) {
damage = 0;
deathtouched = false;
}
protected int damagePlaneswalker(int damage, UUID attackerId, Ability source, Game game, boolean preventable, boolean combat, boolean markDamage, List<UUID> appliedEffects) {
GameEvent event = new DamagePlaneswalkerEvent(objectId, attackerId, controllerId, damage, preventable, combat);
event.setAppliedEffects(appliedEffects);
if (game.replaceEvent(event)) {
return 0;
}
int actualDamage = checkProtectionAbilities(event, attackerId, source, game);
if (actualDamage <= 0) {
return 0;
}
int countersToRemove = Math.min(actualDamage, getCounters(game).getCount(CounterType.LOYALTY));
removeCounters(CounterType.LOYALTY.getName(), countersToRemove, source, game);
DamagedEvent damagedEvent = new DamagedPlaneswalkerEvent(objectId, attackerId, controllerId, actualDamage, combat);
damagedEvent.setExcess(actualDamage - countersToRemove);
game.fireEvent(damagedEvent);
game.getState().addSimultaneousDamage(damagedEvent, game);
return actualDamage;
}
protected int damageCreature(int damage, UUID attackerId, Ability source, Game game, boolean preventable, boolean combat, boolean markDamage, List<UUID> appliedEffects) {
GameEvent event = new DamageCreatureEvent(this.getId(), attackerId, this.getControllerId(), damage, preventable, combat);
event.setAppliedEffects(appliedEffects);
if (game.replaceEvent(event)) {
return 0;
}
int actualDamage = checkProtectionAbilities(event, attackerId, source, game);
if (actualDamage <= 0) {
return 0;
}
MageObject attacker = game.getObject(attackerId);
int lethal = 0;
if (game.getState().getActivePowerInsteadOfToughnessForDamageLethalityFilters().stream().anyMatch(f -> f.match(this, game))) {
lethal = power.getValue();
} else {
lethal = toughness.getValue();
}
lethal = Math.max(lethal - this.damage, 0);
if (attacker.getAbilities().containsKey(DeathtouchAbility.getInstance().getId())) {
lethal = Math.min(1, lethal);
}
if (attacker != null && (attacker.getAbilities().containsKey(InfectAbility.getInstance().getId())
|| attacker.getAbilities().containsKey(WitherAbility.getInstance().getId()))) {
if (markDamage) {
// mark damage only
markDamage(CounterType.M1M1.createInstance(actualDamage), attacker);
} else {
Ability damageSourceAbility = null;
if (attacker instanceof Permanent) {
damageSourceAbility = ((Permanent) attacker).getSpellAbility();
}
// deal damage immediately
addCounters(CounterType.M1M1.createInstance(actualDamage), game.getControllerId(attackerId), damageSourceAbility, game);
}
} else {
this.damage = CardUtil.overflowInc(this.damage, actualDamage);
}
DamagedEvent damagedEvent = new DamagedCreatureEvent(this.getId(), attackerId, this.getControllerId(), actualDamage, combat);
damagedEvent.setExcess(actualDamage - lethal);
game.fireEvent(damagedEvent);
game.getState().addSimultaneousDamage(damagedEvent, game);
return actualDamage;
}
private int checkProtectionAbilities(GameEvent event, UUID attackerId, Ability source, Game game) {
MageObject attacker = game.getObject(attackerId);
if (attacker != null && hasProtectionFrom(attacker, game)) {
@ -1049,11 +1069,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return event.getAmount();
}
private void markDamage(Counter counter, MageObject source) {
private void markDamage(Counter counter, MageObject source, boolean addCounters) {
if (markedDamage == null) {
markedDamage = new ArrayList<>();
}
markedDamage.add(new MarkedDamageInfo(counter, source));
markedDamage.add(new MarkedDamageInfo(counter, source, addCounters));
}
@Override