forked from External/mage
rework more Prevention Effects involving counters. Implement [PIP] Bloatfly Swarm (#12205)
This commit is contained in:
parent
e3e34dae33
commit
bcff245a31
23 changed files with 545 additions and 189 deletions
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue