rework more Prevention Effects involving counters. Implement [PIP] Bloatfly Swarm (#12205)

This commit is contained in:
Susucre 2024-05-23 19:48:44 +02:00 committed by GitHub
parent e3e34dae33
commit bcff245a31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 545 additions and 189 deletions

View file

@ -0,0 +1,46 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
import java.util.Optional;
/**
* For trigger/prevention effects that save a value of removed counters.
* Retrieve the value in resulting effects without need for custom ones.
*
* @author Susucr
*/
public enum SavedCounterRemovedValue implements DynamicValue {
MANY("many"),
MUCH("much");
private final String message;
public static final String VALUE_KEY = "CounterRemoved";
SavedCounterRemovedValue(String message) {
this.message = "that " + message;
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return Optional.ofNullable((Integer) effect.getValue(VALUE_KEY)).orElse(0);
}
@Override
public SavedCounterRemovedValue copy() {
return this;
}
@Override
public String toString() {
return message;
}
@Override
public String getMessage() {
return "";
}
}

View file

@ -1,105 +0,0 @@
package mage.abilities.effects;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.constants.Duration;
import mage.constants.WatcherScope;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.watchers.Watcher;
import java.util.HashSet;
import java.util.Set;
/**
* This requires to add the PhantomPreventionWatcher
*
* @author Susucr
*/
public class PhantomPreventionEffect extends PreventionEffectImpl {
public static PhantomPreventionWatcher createWatcher() {
return new PhantomPreventionWatcher();
}
public PhantomPreventionEffect() {
super(Duration.WhileOnBattlefield);
staticText = "If damage would be dealt to {this}, prevent that damage. Remove a +1/+1 counter from {this}";
}
protected PhantomPreventionEffect(final PhantomPreventionEffect effect) {
super(effect);
}
@Override
public PhantomPreventionEffect copy() {
return new PhantomPreventionEffect(this);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
preventDamageAction(event, source, game);
Permanent permanent = game.getPermanent(source.getSourceId());
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());
}
}
}
return false;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
if (super.applies(event, source, game)) {
if (event.getTargetId().equals(source.getSourceId())) {
return true;
}
}
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

@ -1,18 +1,31 @@
package mage.abilities.effects;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.common.SavedCounterRemovedValue;
import mage.constants.Duration;
import mage.constants.WatcherScope;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.watchers.Watcher;
import java.util.HashSet;
import java.util.Set;
/**
* @author antoni-g
* This requires to add the Watcher from createWatcher() together with the Prevention Effect.
*
* @author Susucr
*/
public class PreventDamageAndRemoveCountersEffect extends PreventionEffectImpl {
private final boolean thatMany;
private final boolean whileHasCounter;
private final boolean thatMany; // If true, remove as many counters as damage prevent. If false, remove 1 counter.
private final boolean whileHasCounter; // If true, the creature need a counter for the effect to be active.
public static Watcher createWatcher() {
return new PreventDamageAndRemoveCountersWatcher();
}
public PreventDamageAndRemoveCountersEffect(boolean thatMany, boolean whileHasCounter, boolean textFromIt) {
super(Duration.WhileOnBattlefield, Integer.MAX_VALUE, false, false);
@ -25,6 +38,14 @@ public class PreventDamageAndRemoveCountersEffect extends PreventionEffectImpl {
" from " + (textFromIt ? "it" : "{this}");
}
/**
* A specific wording for the old "Phantom [...]" not covered by the new wording using "and"
*/
public PreventDamageAndRemoveCountersEffect withPhantomText() {
staticText = "If damage would be dealt to {this}, prevent that damage. Remove a +1/+1 counter from {this}.";
return this;
}
protected PreventDamageAndRemoveCountersEffect(final PreventDamageAndRemoveCountersEffect effect) {
super(effect);
this.thatMany = effect.thatMany;
@ -38,25 +59,94 @@ public class PreventDamageAndRemoveCountersEffect extends PreventionEffectImpl {
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
int damage = event.getAmount();
int damageAmount = event.getAmount();
// Prevent the damage.
// Note that removing counters does not care if prevention did work.
// Ruling on Phantom Wurm for instance:
// > If damage that can't be prevented is be dealt to Phantom Wurm,
// > you still remove a counter even though the damage is dealt. (2021-03-19)
preventDamageAction(event, source, game);
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent == null) {
PreventDamageAndRemoveCountersWatcher watcher = game.getState().getWatcher(PreventDamageAndRemoveCountersWatcher.class);
if (permanent == null || watcher == null || damageAmount <= 0) {
return false;
}
if (!thatMany) {
damage = 1;
MageObjectReference mor = new MageObjectReference(source.getId(), source.getSourceObjectZoneChangeCounter(), game);
int beforeCount = permanent.getCounters(game).getCount(CounterType.P1P1);
if (thatMany) {
// Remove them.
permanent.removeCounters(CounterType.P1P1.createInstance(damageAmount), source, game);
} else if (!watcher.hadMORCounterRemovedThisBatch(mor)) {
// Remove one.
permanent.removeCounters(CounterType.P1P1.createInstance(), source, game);
}
permanent.removeCounters(CounterType.P1P1.createInstance(damage), source, game); //MTG ruling (this) loses counters even if the damage isn't prevented
int amountRemovedThisTime = beforeCount - permanent.getCounters(game).getCount(CounterType.P1P1);
int amountRemovedInTotal = amountRemovedThisTime;
if (!watcher.hadMORCounterRemovedThisBatch(mor)) {
watcher.addMOR(mor);
} else {
// Sum the previous added counter
amountRemovedInTotal += (Integer) source.getEffects().get(0).getValue(SavedCounterRemovedValue.VALUE_KEY);
}
onDamagePrevented(event, source, game, amountRemovedInTotal, amountRemovedThisTime);
return false;
}
/**
* Meant to be Overriden if needs be.
*/
protected void onDamagePrevented(GameEvent event, Ability source, Game game, int amountRemovedInTotal, int amountRemovedThisTime) {
source.getEffects().setValue(SavedCounterRemovedValue.VALUE_KEY, amountRemovedInTotal);
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
Permanent permanent = game.getPermanent(event.getTargetId());
return super.applies(event, source, game)
&& permanent != null
&& event.getTargetId().equals(source.getSourceId())
&& (!whileHasCounter || permanent.getCounters(game).containsKey(CounterType.P1P1));
if (!super.applies(event, source, game)
|| permanent == null
|| !event.getTargetId().equals(source.getSourceId())) {
return false;
}
if (whileHasCounter && !permanent.getCounters(game).containsKey(CounterType.P1P1)) {
// If the last counter has already be removed for the same batch of prevention, we still want to prevent the damage.
PreventDamageAndRemoveCountersWatcher watcher = game.getState().getWatcher(PreventDamageAndRemoveCountersWatcher.class);
MageObjectReference mor = new MageObjectReference(source.getId(), source.getSourceObjectZoneChangeCounter(), game);
return watcher != null && watcher.hadMORCounterRemovedThisBatch(mor);
}
return true;
}
}
class PreventDamageAndRemoveCountersWatcher extends Watcher {
// We keep a very short-lived set of which PreventDamageAndRemoveCountersEffect caused
// +1/+1 to get removed during the current damage batch.
private final Set<MageObjectReference> morRemovedCounterThisDamageBatch = new HashSet<>();
PreventDamageAndRemoveCountersWatcher() {
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

@ -29,7 +29,6 @@ import mage.game.match.MatchType;
import mage.game.mulligan.Mulligan;
import mage.game.permanent.Battlefield;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.stack.Spell;
import mage.game.stack.SpellStack;
import mage.game.turn.Phase;
@ -500,6 +499,8 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
UUID fireReflexiveTriggeredAbility(ReflexiveTriggeredAbility reflexiveAbility, Ability source);
UUID fireReflexiveTriggeredAbility(ReflexiveTriggeredAbility reflexiveAbility, Ability source, boolean fireAsSimultaneousEvent);
/**
* Inner game engine call to reset game objects to actual versions
* (reset all objects and apply all effects due layer system)

View file

@ -2192,8 +2192,18 @@ public abstract class GameImpl implements Game {
@Override
public UUID fireReflexiveTriggeredAbility(ReflexiveTriggeredAbility reflexiveAbility, Ability source) {
return fireReflexiveTriggeredAbility(reflexiveAbility, source, false);
}
@Override
public UUID fireReflexiveTriggeredAbility(ReflexiveTriggeredAbility reflexiveAbility, Ability source, boolean fireAsSimultaneousEvent) {
UUID uuid = this.addDelayedTriggeredAbility(reflexiveAbility, source);
this.fireEvent(GameEvent.getEvent(GameEvent.EventType.OPTION_USED, source.getOriginalId(), source, source.getControllerId()));
GameEvent event = GameEvent.getEvent(GameEvent.EventType.OPTION_USED, source.getOriginalId(), source, source.getControllerId());
if (fireAsSimultaneousEvent) {
this.getState().addSimultaneousEvent(event, this);
} else {
this.fireEvent(event);
}
return uuid;
}