rework PhantomReplacementEffect used by 7 Phantom cards (#12189)

This commit is contained in:
Susucre 2024-04-27 17:34:59 +02:00 committed by GitHub
parent 6193c9aee6
commit d645facdc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 366 additions and 62 deletions

View file

@ -1,22 +1,28 @@
package mage.abilities.effects;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.constants.Duration;
import mage.constants.PhaseStep;
import mage.constants.WatcherScope;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.turn.Step;
import mage.watchers.Watcher;
import java.util.HashSet;
import java.util.Set;
/**
* Created by IGOUDT on 22-3-2017.
* This requires to add the PhantomPreventionWatcher
*
* @author Susucr
*/
public class PhantomPreventionEffect extends PreventionEffectImpl {
// remember turn and phase step to check if counter in this step was already removed
private int turn = 0;
private Step combatPhaseStep = null;
public static PhantomPreventionWatcher createWatcher() {
return new PhantomPreventionWatcher();
}
public PhantomPreventionEffect() {
super(Duration.WhileOnBattlefield);
@ -25,8 +31,6 @@ public class PhantomPreventionEffect extends PreventionEffectImpl {
protected PhantomPreventionEffect(final PhantomPreventionEffect effect) {
super(effect);
this.turn = effect.turn;
this.combatPhaseStep = effect.combatPhaseStep;
}
@Override
@ -37,27 +41,19 @@ public class PhantomPreventionEffect extends PreventionEffectImpl {
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
preventDamageAction(event, source, game);
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) {
boolean removeCounter = true;
// check if in the same combat damage step already a counter was removed
if (game.getTurn().getPhase().getStep().getType() == PhaseStep.COMBAT_DAMAGE) {
if (game.getTurnNum() == turn
&& game.getTurn().getStep().equals(combatPhaseStep)) {
removeCounter = false;
} else {
turn = game.getTurnNum();
combatPhaseStep = game.getTurn().getStep();
PhantomPreventionWatcher watcher = game.getState().getWatcher(PhantomPreventionWatcher.class);
if (permanent != null && watcher != null) {
MageObjectReference mor = new MageObjectReference(source.getId(), source.getSourceObjectZoneChangeCounter(), game);
if (!watcher.hadMORCounterRemovedThisBatch(mor)) {
watcher.addMOR(mor);
if (permanent.getCounters(game).containsKey(CounterType.P1P1)) {
StringBuilder sb = new StringBuilder(permanent.getName()).append(": ");
permanent.removeCounters(CounterType.P1P1.createInstance(), source, game);
sb.append("Removed a +1/+1 counter ");
game.informPlayers(sb.toString());
}
}
if (removeCounter && permanent.getCounters(game).containsKey(CounterType.P1P1)) {
StringBuilder sb = new StringBuilder(permanent.getName()).append(": ");
permanent.removeCounters(CounterType.P1P1.createInstance(), source, game);
sb.append("Removed a +1/+1 counter ");
game.informPlayers(sb.toString());
}
}
return false;
@ -72,5 +68,38 @@ public class PhantomPreventionEffect extends PreventionEffectImpl {
}
return false;
}
}
class PhantomPreventionWatcher extends Watcher {
// We keep a very short-lived set of which PhantomPreventionEffect caused
// +1/+1 to get removed during the current damage batch.
private final Set<MageObjectReference> morRemovedCounterThisDamageBatch = new HashSet<>();
PhantomPreventionWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
// This watcher resets every time a Damage Batch could have fired (even if all damage was prevented)
if (event.getType() != GameEvent.EventType.DAMAGED_BATCH_COULD_HAVE_FIRED) {
return;
}
morRemovedCounterThisDamageBatch.clear();
}
@Override
public void reset() {
super.reset();
morRemovedCounterThisDamageBatch.clear();
}
boolean hadMORCounterRemovedThisBatch(MageObjectReference mor) {
return morRemovedCounterThisDamageBatch.contains(mor);
}
void addMOR(MageObjectReference mor) {
morRemovedCounterThisDamageBatch.add(mor);
}
}

View file

@ -818,6 +818,16 @@ public class GameState implements Serializable, Copyable<GameState> {
return !simultaneousEvents.isEmpty();
}
// There might be no damage dealt, but we want to fire that damage (in a batch) could have been dealt.
public void addBatchDamageCouldHaveBeenFired(boolean combat, Game game) {
for (GameEvent event : simultaneousEvents) {
if (event instanceof DamagedBatchCouldHaveFiredEvent && event.getFlag() == combat) {
return;
}
}
addSimultaneousEvent(new DamagedBatchCouldHaveFiredEvent(combat), game);
}
public void addSimultaneousDamage(DamagedEvent damagedEvent, Game game) {
// Combine multiple damage events in the single event (batch)
// Note: one event can be stored in multiple batches

View file

@ -0,0 +1,15 @@
package mage.game.events;
/**
* Does not contain any info on damage events, and can fire even when all damage is prevented.
* Fire any time a DAMAGED_BATCH_FOR_ALL could have fired (combat & noncombat).
* It is not a batch event (doesn't contain sub events), the name is a little ambiguous.
*
* @author Susucr
*/
public class DamagedBatchCouldHaveFiredEvent extends GameEvent {
public DamagedBatchCouldHaveFiredEvent(boolean combat) {
super(EventType.DAMAGED_BATCH_COULD_HAVE_FIRED, null, null, null, 0, combat);
}
}

View file

@ -129,6 +129,12 @@ public class GameEvent implements Serializable {
includes all damage events, both permanent damage and player damage, in single batch event
*/
DAMAGED_BATCH_FOR_ALL,
/* DAMAGED_BATCH_FIRED
* Does not contain any info on damage events, and can fire even when all damage is prevented.
* Fire any time a DAMAGED_BATCH_FOR_ALL could have fired (combat & noncombat).
* It is not a batch event (doesn't contain sub events), the name is a little ambiguous.
*/
DAMAGED_BATCH_COULD_HAVE_FIRED,
/* DAMAGE_CAUSES_LIFE_LOSS,
targetId the id of the damaged player

View file

@ -1024,6 +1024,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
}
DamageEvent event = new DamagePermanentEvent(objectId, attackerId, controllerId, damageAmount, preventable, combat);
event.setAppliedEffects(appliedEffects);
// Even if no damage was dealt, some watchers would need a reset next time actions are processed.
// For instance PhantomPreventionWatcher used by the [[Phantom Wurm]] type of replacement effect.
game.getState().addBatchDamageCouldHaveBeenFired(combat, game);
if (game.replaceEvent(event)) {
return 0;
}

View file

@ -1,13 +1,13 @@
package mage.game.turn;
import java.util.UUID;
import mage.constants.PhaseStep;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import java.util.UUID;
/**
* @author BetaSteward_at_googlemail.com
*/
@ -65,6 +65,9 @@ public class CombatDamageStep extends Step {
for (CombatGroup group : game.getCombat().getBlockingGroups()) {
group.applyDamage(game);
}
// Even if no damage was dealt, some watchers need a reset. For instance PhantomPreventionWatcher.
game.getState().addBatchDamageCouldHaveBeenFired(true, game);
// Must fire damage batch events now, before SBA (https://github.com/magefree/mage/issues/9129)
game.getState().handleSimultaneousEvent(game);
}

View file

@ -2255,7 +2255,7 @@ public abstract class PlayerImpl implements Player, Serializable {
return doDamage(damage, attackerId, source, game, combatDamage, preventable, appliedEffects);
}
private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List<UUID> appliedEffects) {
private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combat, boolean preventable, List<UUID> appliedEffects) {
if (!this.isInGame()) {
return 0;
}
@ -2263,8 +2263,11 @@ public abstract class PlayerImpl implements Player, Serializable {
if (damage < 1) {
return 0;
}
DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combatDamage);
DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combat);
event.setAppliedEffects(appliedEffects);
// Even if no damage was dealt, some watchers would need a reset next time actions are processed.
// For instance PhantomPreventionWatcher used by the [[Phantom Wurm]] type of replacement effect.
game.getState().addBatchDamageCouldHaveBeenFired(combat, game);
if (game.replaceEvent(event)) {
return 0;
}
@ -2300,20 +2303,20 @@ public abstract class PlayerImpl implements Player, Serializable {
addCounters(CounterType.POISON.createInstance(actualDamage), sourceControllerId, source, game);
} else {
GameEvent damageToLifeLossEvent = new GameEvent(GameEvent.EventType.DAMAGE_CAUSES_LIFE_LOSS,
playerId, source, playerId, actualDamage, combatDamage);
playerId, source, playerId, actualDamage, combat);
if (!game.replaceEvent(damageToLifeLossEvent)) {
this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combatDamage, attackerId);
this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combat, attackerId);
}
}
if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) {
if (combatDamage) {
if (combat) {
game.getPermanent(attackerId).markLifelink(actualDamage);
} else {
Player player = game.getPlayer(sourceControllerId);
player.gainLife(actualDamage, game, source);
}
}
if (combatDamage && sourceAbilities != null && sourceAbilities.containsClass(ToxicAbility.class)) {
if (combat && sourceAbilities != null && sourceAbilities.containsClass(ToxicAbility.class)) {
int countersToAdd = CardUtil
.castStream(sourceAbilities.stream(), ToxicAbility.class)
.mapToInt(ToxicAbility::getAmount)
@ -2325,7 +2328,7 @@ public abstract class PlayerImpl implements Player, Serializable {
Player player = game.getPlayer(sourceControllerId);
new SquirrelToken().putOntoBattlefield(actualDamage, game, source, player.getId());
}
DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combatDamage);
DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combat);
game.fireEvent(damagedEvent);
game.getState().addSimultaneousDamage(damagedEvent, game);
return actualDamage;