rework batch events (#13066)

* add new framework for batch triggers

apply for tapped, untapped, sacrificed, milled

simplify Ob Nixilis, Captive Kingpin

* add a verify check

* fix mistakes

* add simple tests

* another test

* zone change - enters battlefield

* zone change: not battlefield

* zone change - leaves battlefield

* fix Kaya Spirit's Justice

* rename OneOrMoreCombatDamagePlayerTriggeredAbility

* refactor OneOrMoreDamagePlayerTriggeredAbility

* new YoureDealtDamageTriggeredAbility

* new OpponentDealtNoncombatDamageTriggeredAbility

* rework Risona, Asari Commander

* simplify War Elemental

* Add damage batch by source

rework some delayed triggered abilities

* fix Mindblade Render

* rework Initiative and a few others

* [temp] initiative test

* refactor: common style for DealsDamageSourceTriggeredAbility

* refactor cards to use common DealsDamageSourceTriggeredAbility

* update damage players batch triggers

* fix mistake in initiative

* new DealtDamageAnyTriggeredAbility

* new DealtCombatDamageToSourceTriggeredAbility

* update dealt damage to permanent batch triggered abilities

* refactor Hot Soup and param in DealtDamageAttachedTriggeredAbility

* a few more permanent batch triggered abilities

* fix mistake

* update some more damage batch triggers

* add test for Phyrexian Negator

* update Felix Five-Boots and enable test

update Wayta, Trainer Prodigy to align

* update damage batch by source triggers

* undo mistaken change

* fix verify

* cleanup unused methods

* Revert "[temp] initiative test"

This reverts commit 11ed19295f.

* Revert "add a verify check"

This reverts commit e7de47a656.

* fixes from checking text discrepancies

* fix Shriekwood Devourer

* merge fix

---------

Co-authored-by: Susucre <34709007+Susucre@users.noreply.github.com>
This commit is contained in:
xenohedron 2024-11-19 21:23:08 -05:00 committed by GitHub
parent cef2a1edc8
commit d06d594934
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
192 changed files with 2411 additions and 3363 deletions

View file

@ -0,0 +1,38 @@
package mage.abilities;
import mage.game.Game;
import mage.game.events.BatchEvent;
import mage.game.events.GameEvent;
import java.util.List;
import java.util.stream.Collectors;
/**
* Batch triggers (e.g. 'When... one or more ..., ')
* require additional logic to check the events in the batch.
* Parametrized on the individual game event type.
*
* @see mage.game.events.BatchEvent
*
* @author Susucr, xenohedron
*/
public interface BatchTriggeredAbility<T extends GameEvent> extends TriggeredAbility {
/**
* Some events in the batch may not be relevant to the trigger logic.
* If so, use this method to exclude them.
*/
default boolean checkEvent(T event, Game game) {
return true;
}
/**
* For use in checkTrigger - streams all events that pass the event check
*/
default List<T> getFilteredEvents(BatchEvent<T> event, Game game) {
return event.getEvents()
.stream()
.filter(e -> checkEvent(e, game))
.collect(Collectors.toList());
}
}

View file

@ -1,5 +1,6 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
@ -7,13 +8,15 @@ import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.TappedBatchEvent;
import mage.game.events.TappedEvent;
import mage.game.permanent.Permanent;
/**
* @author Susucr
*/
public class BecomesTappedOneOrMoreTriggeredAbility extends TriggeredAbilityImpl {
public class BecomesTappedOneOrMoreTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<TappedEvent> {
protected FilterPermanent filter;
private final FilterPermanent filter;
public BecomesTappedOneOrMoreTriggeredAbility(Zone zone, Effect effect, boolean optional, FilterPermanent filter) {
super(zone, effect, optional);
@ -36,13 +39,14 @@ public class BecomesTappedOneOrMoreTriggeredAbility extends TriggeredAbilityImpl
return event.getType() == GameEvent.EventType.TAPPED_BATCH;
}
@Override
public boolean checkEvent(TappedEvent event, Game game) {
Permanent permanent = game.getPermanent(event.getTargetId());
return permanent != null && filter.match(permanent, getControllerId(), this, game);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
TappedBatchEvent batchEvent = (TappedBatchEvent) event;
return batchEvent
.getTargetIds()
.stream()
.map(game::getPermanent)
.anyMatch(p -> filter.match(p, getControllerId(), this, game));
return !getFilteredEvents((TappedBatchEvent) event, game).isEmpty();
}
}

View file

@ -1,17 +1,15 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedBatchForPlayersEvent;
import mage.game.events.DamagedEvent;
import mage.game.events.DamagedBatchForOnePlayerEvent;
import mage.game.events.DamagedPlayerEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
* A triggered ability for whenever one or more creatures deal combat damage to
* you. Has an optional component for setting the target pointer to the opponent
@ -19,87 +17,47 @@ import java.util.UUID;
*
* @author alexander-novo
*/
public class CombatDamageDealtToYouTriggeredAbility extends TriggeredAbilityImpl {
public class CombatDamageDealtToYouTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPlayerEvent> {
// Whether the ability should set a target targetting the opponent who
// Whether the ability should set a target targeting the opponent who
// controls the creatures who dealt damage to you
private final boolean setTarget;
private final boolean setTargetPointer;
/**
* @param effect The effect that should happen when the ability resolves
*/
public CombatDamageDealtToYouTriggeredAbility(Effect effect) {
this(effect, false);
this(Zone.BATTLEFIELD, effect, false, false);
}
/**
* @param effect The effect that should happen when the ability resolves
* @param setTarget Whether or not the ability should set a target targetting
* the opponent who controls the creatures who dealt damage to
* you
*/
public CombatDamageDealtToYouTriggeredAbility(Effect effect, boolean setTarget) {
this(Zone.BATTLEFIELD, effect, setTarget, false);
}
/**
* @param zone Which zone the ability shoudl take effect in
* @param effect The effect that should happen when the ability resolves
* @param setTarget Whether or not the ability should set a target targetting
* the opponent who controls the creatures who dealt damage to
* you
* @param optional Whether or not the ability is optional
*/
public CombatDamageDealtToYouTriggeredAbility(Zone zone, Effect effect, boolean setTarget,
boolean optional) {
public CombatDamageDealtToYouTriggeredAbility(Zone zone, Effect effect, boolean setTargetPointer, boolean optional) {
super(zone, effect, optional);
this.setTarget = setTarget;
this.setTargetPointer = setTargetPointer;
setTriggerPhrase(generateTriggerPhrase());
}
private CombatDamageDealtToYouTriggeredAbility(final CombatDamageDealtToYouTriggeredAbility ability) {
super(ability);
this.setTarget = ability.setTarget;
this.setTargetPointer = ability.setTargetPointer;
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PLAYERS;
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
DamagedBatchForPlayersEvent dEvent = (DamagedBatchForPlayersEvent) event;
boolean isDamaged = false;
UUID damageSourceControllerID = null;
for (DamagedEvent damagedEvent : dEvent.getEvents()) {
if (damagedEvent.isCombatDamage() && damagedEvent.getPlayerId() == this.controllerId) {
isDamaged = true;
// TODO: current code support only one controller
// (it's can be potentially bugged in team mode with multiple attack players)
Permanent damageSource = game.getPermanent(damagedEvent.getSourceId());
if (damageSource != null) {
damageSourceControllerID = damageSource.getControllerId();
}
}
if (!isControlledBy(event.getTargetId()) || !((DamagedBatchForOnePlayerEvent) event).isCombatDamage()) {
return false;
}
if (isDamaged) {
if (this.setTarget && damageSourceControllerID != null) {
this.getEffects().setTargetPointer(new FixedTarget(damageSourceControllerID));
}
return true;
if (setTargetPointer) {
// attacking player is active player
this.getEffects().setTargetPointer(new FixedTarget(game.getActivePlayerId()));
}
return true;
return false;
}
private String generateTriggerPhrase() {
if (setTarget) {
if (setTargetPointer) {
return "Whenever one or more creatures an opponent controls deal combat damage to you, ";
} else {
return "Whenever one or more creatures deal combat damage to you, ";
@ -110,4 +68,4 @@ public class CombatDamageDealtToYouTriggeredAbility extends TriggeredAbilityImpl
public CombatDamageDealtToYouTriggeredAbility copy() {
return new CombatDamageDealtToYouTriggeredAbility(this);
}
}
}

View file

@ -1,39 +0,0 @@
package mage.abilities.common;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.filter.common.FilterCreaturePermanent;
/**
* @author Xanderhall, xenohedron
*/
public class DealCombatDamageControlledTriggeredAbility extends OneOrMoreDealDamageTriggeredAbility {
public DealCombatDamageControlledTriggeredAbility(Effect effect) {
this(effect, SetTargetPointer.NONE);
}
public DealCombatDamageControlledTriggeredAbility(Effect effect, FilterCreaturePermanent filter) {
this(Zone.BATTLEFIELD, effect, filter, SetTargetPointer.NONE, false);
}
public DealCombatDamageControlledTriggeredAbility(Effect effect, SetTargetPointer setTargetPointer) {
this(Zone.BATTLEFIELD, effect, StaticFilters.FILTER_PERMANENT_CREATURES, setTargetPointer, false);
}
public DealCombatDamageControlledTriggeredAbility(Zone zone, Effect effect, FilterCreaturePermanent filter,
SetTargetPointer setTargetPointer, boolean optional) {
super(zone, effect, filter, true, true, setTargetPointer, optional);
}
protected DealCombatDamageControlledTriggeredAbility(final DealCombatDamageControlledTriggeredAbility ability) {
super(ability);
}
@Override
public DealCombatDamageControlledTriggeredAbility copy() {
return new DealCombatDamageControlledTriggeredAbility(this);
}
}

View file

@ -1,10 +1,11 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedBatchAllEvent;
import mage.game.events.DamagedBatchBySourceEvent;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
@ -12,7 +13,7 @@ import mage.game.permanent.Permanent;
/**
* @author TheElk801, xenohedron
*/
public class DealsCombatDamageEquippedTriggeredAbility extends TriggeredAbilityImpl {
public class DealsCombatDamageEquippedTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedEvent> {
public DealsCombatDamageEquippedTriggeredAbility(Effect effect) {
this(effect, false);
@ -34,26 +35,19 @@ public class DealsCombatDamageEquippedTriggeredAbility extends TriggeredAbilityI
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ALL;
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_BY_SOURCE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!((DamagedBatchBySourceEvent) event).isCombatDamage()) {
return false;
}
Permanent sourcePermanent = getSourcePermanentOrLKI(game);
if (sourcePermanent == null || sourcePermanent.getAttachedTo() == null) {
if (sourcePermanent == null || !event.getSourceId().equals(sourcePermanent.getAttachedTo())) {
return false;
}
int amount = ((DamagedBatchAllEvent) event)
.getEvents()
.stream()
.filter(DamagedEvent::isCombatDamage)
.filter(e -> e.getAttackerId().equals(sourcePermanent.getAttachedTo()))
.mapToInt(GameEvent::getAmount)
.sum();
if (amount < 1) {
return false;
}
this.getEffects().setValue("damage", amount);
this.getEffects().setValue("damage", event.getAmount());
return true;
}
}

View file

@ -1,10 +1,11 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedBatchAllEvent;
import mage.game.events.DamagedBatchBySourceEvent;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
@ -15,7 +16,7 @@ import mage.game.events.GameEvent;
*
* @author LevelX, xenohedron
*/
public class DealsCombatDamageTriggeredAbility extends TriggeredAbilityImpl {
public class DealsCombatDamageTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedEvent> {
public DealsCombatDamageTriggeredAbility(Effect effect, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
@ -34,22 +35,15 @@ public class DealsCombatDamageTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ALL;
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_BY_SOURCE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
int amount = ((DamagedBatchAllEvent) event)
.getEvents()
.stream()
.filter(DamagedEvent::isCombatDamage)
.filter(e -> e.getAttackerId().equals(getSourceId()))
.mapToInt(GameEvent::getAmount)
.sum();
if (amount < 1) {
if (!event.getSourceId().equals(getSourceId()) || !((DamagedBatchBySourceEvent) event).isCombatDamage() ) {
return false;
}
this.getEffects().setValue("damage", amount);
this.getEffects().setValue("damage", event.getAmount());
return true;
}
}

View file

@ -1,47 +0,0 @@
package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.dynamicvalue.common.SavedDamageValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.GainLifeEffect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
/**
* @author LevelX2
*/
public class DealsDamageGainLifeSourceTriggeredAbility extends TriggeredAbilityImpl {
public DealsDamageGainLifeSourceTriggeredAbility() {
super(Zone.BATTLEFIELD, new GainLifeEffect(SavedDamageValue.MUCH), false);
setTriggerPhrase("Whenever {this} deals damage, ");
}
protected DealsDamageGainLifeSourceTriggeredAbility(final DealsDamageGainLifeSourceTriggeredAbility ability) {
super(ability);
}
@Override
public DealsDamageGainLifeSourceTriggeredAbility copy() {
return new DealsDamageGainLifeSourceTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT
|| event.getType() == GameEvent.EventType.DAMAGED_PLAYER;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getSourceId().equals(this.getSourceId())) {
for (Effect effect : this.getEffects()) {
effect.setValue("damage", event.getAmount());
}
return true;
}
return false;
}
}

View file

@ -0,0 +1,50 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
/**
* @author xenohedron
*/
public class DealsDamageSourceTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedEvent> {
public DealsDamageSourceTriggeredAbility(Effect effect) {
this(effect, false);
}
public DealsDamageSourceTriggeredAbility(Effect effect, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
setTriggerPhrase("Whenever {this} deals damage, ");
this.withRuleTextReplacement(true);
}
protected DealsDamageSourceTriggeredAbility(final DealsDamageSourceTriggeredAbility ability) {
super(ability);
}
@Override
public DealsDamageSourceTriggeredAbility copy() {
return new DealsDamageSourceTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_BY_SOURCE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
// all events in the batch are always relevant
if (event.getSourceId().equals(this.getSourceId())) {
this.getEffects().setValue("damage", event.getAmount());
return true;
}
return false;
}
}

View file

@ -0,0 +1,77 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.DamagedBatchBySourceEvent;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
/**
* @author xenohedron
*/
public class DealsDamageToAnyTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedEvent> {
private final FilterPermanent filter;
private final boolean onlyCombat;
private final SetTargetPointer setTargetPointer;
public DealsDamageToAnyTriggeredAbility(Zone zone, Effect effect, FilterPermanent filter,
SetTargetPointer setTargetPointer, boolean onlyCombat, boolean optional) {
super(zone, effect, optional);
this.filter = filter;
this.onlyCombat = onlyCombat;
this.setTargetPointer = setTargetPointer;
setTriggerPhrase(getWhen() + CardUtil.addArticle(filter.getMessage()) + " deals "
+ (onlyCombat ? "combat " : "") + "damage, ");
}
protected DealsDamageToAnyTriggeredAbility(final DealsDamageToAnyTriggeredAbility ability) {
super(ability);
this.filter = ability.filter;
this.onlyCombat = ability.onlyCombat;
this.setTargetPointer = ability.setTargetPointer;
}
@Override
public DealsDamageToAnyTriggeredAbility copy() {
return new DealsDamageToAnyTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_BY_SOURCE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
// all events in the batch are always relevant if triggers at all
if (onlyCombat && !((DamagedBatchBySourceEvent) event).isCombatDamage()) {
return false;
}
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId());
if (permanent == null || !filter.match(permanent, getControllerId(), this, game)) {
return false;
}
getEffects().setValue("damage", event.getAmount());
switch (setTargetPointer) {
case PERMANENT:
getEffects().setTargetPointer(new FixedTarget(permanent, game));
return true;
case PLAYER:
getEffects().setTargetPointer(new FixedTarget(permanent.getControllerId()));
return true;
case NONE:
return true;
default:
throw new IllegalArgumentException("Unsupported SetTargetPointer in DealtDamageAttachedTriggeredAbility");
}
}
}

View file

@ -0,0 +1,46 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedBatchForOnePermanentEvent;
import mage.game.events.DamagedPermanentEvent;
import mage.game.events.GameEvent;
/**
* @author xenohedron
*/
public class DealtCombatDamageToSourceTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPermanentEvent> {
public DealtCombatDamageToSourceTriggeredAbility(Effect effect, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
setTriggerPhrase("Whenever {this} is dealt combat damage, ");
this.withRuleTextReplacement(true);
}
protected DealtCombatDamageToSourceTriggeredAbility(final DealtCombatDamageToSourceTriggeredAbility ability) {
super(ability);
}
@Override
public DealtCombatDamageToSourceTriggeredAbility copy() {
return new DealtCombatDamageToSourceTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
// all events in the batch are always relevant if triggers at all
if (!getSourceId().equals(event.getTargetId()) || !((DamagedBatchForOnePermanentEvent) event).isCombatDamage()) {
return false;
}
this.getEffects().setValue("damage", event.getAmount());
return true;
}
}

View file

@ -0,0 +1,72 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.DamagedPermanentEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
/**
* @author xenohedron
*/
public class DealtDamageAnyTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPermanentEvent> {
private final FilterPermanent filter;
private final SetTargetPointer setTargetPointer;
public DealtDamageAnyTriggeredAbility(Effect effect, FilterPermanent filter, SetTargetPointer setTargetPointer, boolean optional) {
this(Zone.BATTLEFIELD, effect, filter, setTargetPointer, optional);
}
public DealtDamageAnyTriggeredAbility(Zone zone, Effect effect, FilterPermanent filter, SetTargetPointer setTargetPointer, boolean optional) {
super(zone, effect, optional);
this.filter = filter;
this.setTargetPointer = setTargetPointer;
setTriggerPhrase(getWhen() + CardUtil.addArticle(filter.getMessage()) + " is dealt damage, ");
}
protected DealtDamageAnyTriggeredAbility(final DealtDamageAnyTriggeredAbility ability) {
super(ability);
this.filter = ability.filter;
this.setTargetPointer = ability.setTargetPointer;
}
@Override
public DealtDamageAnyTriggeredAbility copy() {
return new DealtDamageAnyTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
// all events in the batch are always relevant if triggers at all
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId());
if (permanent == null || !filter.match(permanent, getControllerId(), this, game)) {
return false;
}
getEffects().setValue("damage", event.getAmount());
switch (setTargetPointer) {
case PERMANENT:
getEffects().setTargetPointer(new FixedTarget(permanent, game));
return true;
case PLAYER:
getEffects().setTargetPointer(new FixedTarget(permanent.getControllerId()));
return true;
case NONE:
return true;
default:
throw new IllegalArgumentException("Unsupported SetTargetPointer in DealtDamageAttachedTriggeredAbility");
}
}
}

View file

@ -1,10 +1,12 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedPermanentEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
@ -14,12 +16,12 @@ import java.util.UUID;
/**
* @author LoneFox
*/
public class DealtDamageAttachedTriggeredAbility extends TriggeredAbilityImpl {
public class DealtDamageAttachedTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPermanentEvent> {
protected SetTargetPointer setTargetPointer;
private final SetTargetPointer setTargetPointer;
public DealtDamageAttachedTriggeredAbility(Effect effect, boolean optional) {
this(Zone.BATTLEFIELD, effect, optional, SetTargetPointer.NONE);
public DealtDamageAttachedTriggeredAbility(Effect effect) {
this(Zone.BATTLEFIELD, effect, false, SetTargetPointer.NONE);
}
public DealtDamageAttachedTriggeredAbility(Zone zone, Effect effect, boolean optional, SetTargetPointer setTargetPointer) {
@ -45,21 +47,23 @@ public class DealtDamageAttachedTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Permanent enchantment = game.getPermanent(sourceId);
// all events in the batch are always relevant if triggers at all
Permanent attachment = getSourcePermanentOrLKI(game);
UUID targetId = event.getTargetId();
if (enchantment != null && enchantment.getAttachedTo() != null && targetId.equals(enchantment.getAttachedTo())) {
for (Effect effect : this.getEffects()) {
effect.setValue("damage", event.getAmount());
switch (setTargetPointer) {
case PERMANENT:
effect.setTargetPointer(new FixedTarget(targetId, game));
break;
case PLAYER:
effect.setTargetPointer(new FixedTarget(game.getPermanentOrLKIBattlefield(targetId).getControllerId()));
break;
}
if (attachment != null && attachment.getAttachedTo() != null && targetId.equals(attachment.getAttachedTo())) {
getEffects().setValue("damage", event.getAmount());
switch (setTargetPointer) {
case PERMANENT:
getEffects().setTargetPointer(new FixedTarget(targetId, game));
return true;
case PLAYER:
getEffects().setTargetPointer(new FixedTarget(attachment.getControllerId()));
return true;
case NONE:
return true;
default:
throw new IllegalArgumentException("Unsupported SetTargetPointer in DealtDamageAttachedTriggeredAbility");
}
return true;
}
return false;
}

View file

@ -1,17 +1,18 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.AbilityWord;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedBatchForPermanentsEvent;
import mage.game.events.DamagedPermanentEvent;
import mage.game.events.GameEvent;
/**
* @author LevelX2
*/
public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl {
public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPermanentEvent> {
public DealtDamageToSourceTriggeredAbility(Effect effect, boolean optional) {
this(effect, optional, false);
@ -37,22 +38,16 @@ public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PERMANENTS;
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
DamagedBatchForPermanentsEvent dEvent = (DamagedBatchForPermanentsEvent) event;
int damage = dEvent
.getEvents()
.stream()
.filter(damagedEvent -> getSourceId().equals(damagedEvent.getTargetId()))
.mapToInt(GameEvent::getAmount)
.sum();
if (damage < 1) {
// all events in the batch are always relevant if triggers at all
if (!getSourceId().equals(event.getTargetId())) {
return false;
}
this.getEffects().setValue("damage", damage);
this.getEffects().setValue("damage", event.getAmount());
return true;
}
}

View file

@ -1,53 +0,0 @@
package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeBatchEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
/**
* @author Cguy7777
*/
public class DiesOneOrMoreCreaturesTriggeredAbility extends TriggeredAbilityImpl {
public DiesOneOrMoreCreaturesTriggeredAbility(Effect effect) {
this(effect, false);
}
public DiesOneOrMoreCreaturesTriggeredAbility(Effect effect, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
setTriggerPhrase("Whenever one or more creatures die, ");
}
private DiesOneOrMoreCreaturesTriggeredAbility(final DiesOneOrMoreCreaturesTriggeredAbility ability) {
super(ability);
}
@Override
public DiesOneOrMoreCreaturesTriggeredAbility copy() {
return new DiesOneOrMoreCreaturesTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
for (ZoneChangeEvent zEvent : ((ZoneChangeBatchEvent) event).getEvents()) {
if (!zEvent.isDiesEvent()) {
continue;
}
Permanent permanent = game.getPermanentOrLKIBattlefield(zEvent.getTargetId());
if (permanent != null && permanent.isCreature(game)) {
return true;
}
}
return false;
}
}

View file

@ -1,6 +1,7 @@
package mage.abilities.common;
import mage.MageObject;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
@ -9,30 +10,29 @@ import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeBatchEvent;
import mage.game.events.ZoneChangeEvent;
import java.util.Objects;
import mage.game.permanent.Permanent;
/**
* @author Susucr
*/
public class DiesOneOrMoreCreatureTriggeredAbility extends TriggeredAbilityImpl {
public class DiesOneOrMoreTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<ZoneChangeEvent> {
private final FilterCreaturePermanent filter;
public DiesOneOrMoreCreatureTriggeredAbility(Effect effect, FilterCreaturePermanent filter, boolean optional) {
public DiesOneOrMoreTriggeredAbility(Effect effect, FilterCreaturePermanent filter, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
this.filter = filter;
this.setTriggerPhrase("Whenever one or more " + filter.getMessage() + " die, ");
}
private DiesOneOrMoreCreatureTriggeredAbility(final DiesOneOrMoreCreatureTriggeredAbility ability) {
private DiesOneOrMoreTriggeredAbility(final DiesOneOrMoreTriggeredAbility ability) {
super(ability);
this.filter = ability.filter;
}
@Override
public DiesOneOrMoreCreatureTriggeredAbility copy() {
return new DiesOneOrMoreCreatureTriggeredAbility(this);
public DiesOneOrMoreTriggeredAbility copy() {
return new DiesOneOrMoreTriggeredAbility(this);
}
@Override
@ -40,16 +40,18 @@ public class DiesOneOrMoreCreatureTriggeredAbility extends TriggeredAbilityImpl
return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH;
}
@Override
public boolean checkEvent(ZoneChangeEvent event, Game game) {
if (!event.isDiesEvent()) {
return false;
}
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId());
return permanent != null && filter.match(permanent, getControllerId(), this, game);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return ((ZoneChangeBatchEvent) event)
.getEvents()
.stream()
.filter(ZoneChangeEvent::isDiesEvent)
.map(ZoneChangeEvent::getTargetId)
.map(game::getPermanentOrLKIBattlefield)
.filter(Objects::nonNull)
.anyMatch(p -> filter.match(p, getControllerId(), this, game));
return !getFilteredEvents((ZoneChangeBatchEvent) event, game).isEmpty();
}
@Override

View file

@ -1,5 +1,6 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.TargetController;
@ -8,30 +9,29 @@ import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeBatchEvent;
import mage.players.Player;
import java.util.UUID;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
/**
* "Whenever one or more {filter} enter the battlefield under {target controller} control,
*
* @author Alex-Vasile
* @author Alex-Vasile, xenohedron
*/
public class EntersBattlefieldOneOrMoreTriggeredAbility extends TriggeredAbilityImpl {
public class EntersBattlefieldOneOrMoreTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<ZoneChangeEvent> {
private final FilterPermanent filterPermanent;
private final FilterPermanent filter;
private final TargetController targetController;
public EntersBattlefieldOneOrMoreTriggeredAbility(Effect effect, FilterPermanent filter, TargetController targetController) {
super(Zone.BATTLEFIELD, effect);
this.filterPermanent = filter;
this.filter = filter;
this.targetController = targetController;
setTriggerPhrase(generateTriggerPhrase());
}
private EntersBattlefieldOneOrMoreTriggeredAbility(final EntersBattlefieldOneOrMoreTriggeredAbility ability) {
super(ability);
this.filterPermanent = ability.filterPermanent;
this.filter = ability.filter;
this.targetController = ability.targetController;
}
@ -41,28 +41,27 @@ public class EntersBattlefieldOneOrMoreTriggeredAbility extends TriggeredAbility
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Player controller = game.getPlayer(this.controllerId);
if (controller == null) {
public boolean checkEvent(ZoneChangeEvent event, Game game) {
if (event.getToZone() != Zone.BATTLEFIELD) {
return false;
}
Permanent permanent = event.getTarget();
if (permanent == null || !filter.match(permanent, getControllerId(), this, game)) {
return false;
}
switch (targetController) {
case YOU:
return isControlledBy(permanent.getControllerId());
case OPPONENT:
return game.getOpponents(getControllerId()).contains(permanent.getControllerId());
default:
throw new IllegalArgumentException("Unsupported TargetController in EntersBattlefieldOneOrMoreTriggeredAbility: " + targetController);
}
}
ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event;
return zEvent.getEvents().stream()
.filter(z -> z.getToZone() == Zone.BATTLEFIELD)
.filter(z -> filterPermanent.match(z.getTarget(), this.controllerId, this, game))
.anyMatch(z -> {
UUID enteringPermanentControllerID = z.getTarget().getControllerId();
switch (this.targetController) {
case YOU:
return enteringPermanentControllerID.equals(this.controllerId);
case OPPONENT:
return controller.hasOpponent(enteringPermanentControllerID, game);
default:
throw new IllegalArgumentException("Unsupported target: " + this.targetController);
}
});
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return !getFilteredEvents((ZoneChangeBatchEvent) event, game).isEmpty();
}
@Override
@ -71,10 +70,10 @@ public class EntersBattlefieldOneOrMoreTriggeredAbility extends TriggeredAbility
}
private String generateTriggerPhrase() {
StringBuilder sb = new StringBuilder("Whenever one or more " + filterPermanent.getMessage());
StringBuilder sb = new StringBuilder("Whenever one or more " + filter.getMessage());
switch (targetController) {
case YOU:
if (filterPermanent.getMessage().contains("you control")) {
if (filter.getMessage().contains("you control")) {
sb.append(" enter, ");
} else {
sb.append(" you control enter, ");
@ -84,7 +83,7 @@ public class EntersBattlefieldOneOrMoreTriggeredAbility extends TriggeredAbility
sb.append(" enter under an opponent's control, ");
break;
default:
throw new IllegalArgumentException("Unsupported TargetController in EntersBattlefieldOneOrMoreTriggeredAbility");
throw new IllegalArgumentException("Unsupported TargetController in EntersBattlefieldOneOrMoreTriggeredAbility: " + targetController);
}
return sb.toString();
}

View file

@ -0,0 +1,39 @@
package mage.abilities.common;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.filter.common.FilterCreaturePermanent;
/**
* @author Xanderhall, xenohedron
*/
public class OneOrMoreCombatDamagePlayerTriggeredAbility extends OneOrMoreDamagePlayerTriggeredAbility {
public OneOrMoreCombatDamagePlayerTriggeredAbility(Effect effect) {
this(effect, SetTargetPointer.NONE);
}
public OneOrMoreCombatDamagePlayerTriggeredAbility(Effect effect, FilterCreaturePermanent filter) {
this(Zone.BATTLEFIELD, effect, filter, SetTargetPointer.NONE, false);
}
public OneOrMoreCombatDamagePlayerTriggeredAbility(Effect effect, SetTargetPointer setTargetPointer) {
this(Zone.BATTLEFIELD, effect, StaticFilters.FILTER_PERMANENT_CREATURES, setTargetPointer, false);
}
public OneOrMoreCombatDamagePlayerTriggeredAbility(Zone zone, Effect effect, FilterCreaturePermanent filter,
SetTargetPointer setTargetPointer, boolean optional) {
super(zone, effect, filter, true, true, setTargetPointer, optional);
}
protected OneOrMoreCombatDamagePlayerTriggeredAbility(final OneOrMoreCombatDamagePlayerTriggeredAbility ability) {
super(ability);
}
@Override
public OneOrMoreCombatDamagePlayerTriggeredAbility copy() {
return new OneOrMoreCombatDamagePlayerTriggeredAbility(this);
}
}

View file

@ -1,5 +1,6 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
@ -8,30 +9,30 @@ import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.DamagedBatchForOnePlayerEvent;
import mage.game.events.DamagedEvent;
import mage.game.events.DamagedPlayerEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Xanderhall, xenohedron
*/
public class OneOrMoreDealDamageTriggeredAbility extends TriggeredAbilityImpl {
public class OneOrMoreDamagePlayerTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPlayerEvent> {
private final SetTargetPointer setTargetPointer;
private final FilterPermanent filter;
private final boolean onlyCombat;
private final boolean onlyControlled;
public OneOrMoreDealDamageTriggeredAbility(Effect effect, FilterPermanent filter, boolean onlyCombat, boolean onlyControlled) {
public OneOrMoreDamagePlayerTriggeredAbility(Effect effect, FilterPermanent filter, boolean onlyCombat, boolean onlyControlled) {
this(Zone.BATTLEFIELD, effect, filter, onlyCombat, onlyControlled, SetTargetPointer.NONE, false);
}
public OneOrMoreDealDamageTriggeredAbility(Zone zone, Effect effect, FilterPermanent filter,
boolean onlyCombat, boolean onlyControlled,
SetTargetPointer setTargetPointer, boolean optional) {
public OneOrMoreDamagePlayerTriggeredAbility(Zone zone, Effect effect, FilterPermanent filter,
boolean onlyCombat, boolean onlyControlled,
SetTargetPointer setTargetPointer, boolean optional) {
super(zone, effect, optional);
this.setTargetPointer = setTargetPointer;
this.filter = filter;
@ -40,7 +41,7 @@ public class OneOrMoreDealDamageTriggeredAbility extends TriggeredAbilityImpl {
makeTriggerPhrase();
}
protected OneOrMoreDealDamageTriggeredAbility(final OneOrMoreDealDamageTriggeredAbility ability) {
protected OneOrMoreDamagePlayerTriggeredAbility(final OneOrMoreDamagePlayerTriggeredAbility ability) {
super(ability);
this.setTargetPointer = ability.setTargetPointer;
this.filter = ability.filter;
@ -49,8 +50,8 @@ public class OneOrMoreDealDamageTriggeredAbility extends TriggeredAbilityImpl {
}
@Override
public OneOrMoreDealDamageTriggeredAbility copy() {
return new OneOrMoreDealDamageTriggeredAbility(this);
public OneOrMoreDamagePlayerTriggeredAbility copy() {
return new OneOrMoreDamagePlayerTriggeredAbility(this);
}
@Override
@ -59,30 +60,26 @@ public class OneOrMoreDealDamageTriggeredAbility extends TriggeredAbilityImpl {
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event;
if (onlyCombat && !dEvent.isCombatDamage()) {
public boolean checkEvent(DamagedPlayerEvent event, Game game) {
if (onlyCombat && !event.isCombatDamage()) {
return false;
}
List<DamagedEvent> events = dEvent
.getEvents()
.stream()
.filter(e -> {
Permanent permanent = game.getPermanentOrLKIBattlefield(e.getSourceId());
if (permanent == null) {
return false;
}
if (onlyControlled && !permanent.isControlledBy(this.getControllerId())) {
return false;
}
return filter.match(permanent, this.getControllerId(), this, game);
})
.collect(Collectors.toList());
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId());
if (permanent == null) {
return false;
}
if (onlyControlled && !permanent.isControlledBy(this.getControllerId())) {
return false;
}
return filter.match(permanent, this.getControllerId(), this, game);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
List<DamagedPlayerEvent> events = getFilteredEvents((DamagedBatchForOnePlayerEvent) event, game);
if (events.isEmpty()) {
return false;
}
this.getAllEffects().setValue("damage", events.stream().mapToInt(DamagedEvent::getAmount).sum());
switch (setTargetPointer) {
case PLAYER:
@ -91,9 +88,8 @@ public class OneOrMoreDealDamageTriggeredAbility extends TriggeredAbilityImpl {
case NONE:
break;
default:
throw new IllegalArgumentException("Invalid SetTargetPointer option");
throw new IllegalArgumentException("Unsupported SetTargetPointer in OneOrMoreDamagePlayerTriggeredAbility");
}
return true;
}

View file

@ -1,19 +1,21 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.dynamicvalue.common.SavedMilledValue;
import mage.abilities.effects.Effect;
import mage.cards.Card;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.MilledBatchAllEvent;
import mage.game.events.MilledCardEvent;
/**
* @author Susucr
*/
public class OneOrMoreMilledTriggeredAbility extends TriggeredAbilityImpl {
public class OneOrMoreMilledTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<MilledCardEvent> {
private final FilterCard filter;
@ -42,10 +44,16 @@ public class OneOrMoreMilledTriggeredAbility extends TriggeredAbilityImpl {
return event.getType() == GameEvent.EventType.MILLED_CARDS_BATCH_FOR_ALL;
}
@Override
public boolean checkEvent(MilledCardEvent event, Game game) {
Card card = event.getCard(game);
return card != null && filter.match(card, getControllerId(), this, game);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
int count = ((MilledBatchAllEvent) event).getCards(game).count(filter, getControllerId(), this, game);
if (count <= 0) {
int count = getFilteredEvents((MilledBatchAllEvent) event, game).size();
if (count == 0) {
return false;
}
getEffects().setValue(SavedMilledValue.VALUE_KEY, count);

View file

@ -0,0 +1,49 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.*;
/**
* @author xenohedron
*/
public class OpponentDealtNoncombatDamageTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPlayerEvent> {
public OpponentDealtNoncombatDamageTriggeredAbility(Effect effect) {
this(Zone.BATTLEFIELD, effect, false);
}
public OpponentDealtNoncombatDamageTriggeredAbility(Zone zone, Effect effect, boolean optional) {
super(zone, effect, optional);
setTriggerPhrase("Whenever an opponent is dealt noncombat damage, ");
}
protected OpponentDealtNoncombatDamageTriggeredAbility(final OpponentDealtNoncombatDamageTriggeredAbility ability) {
super(ability);
}
@Override
public OpponentDealtNoncombatDamageTriggeredAbility copy() {
return new OpponentDealtNoncombatDamageTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
// all events in the batch are always relevant if triggers at all
if (game.getOpponents(getControllerId()).contains(event.getTargetId())
&& !((DamagedBatchForOnePlayerEvent) event).isCombatDamage()) {
this.getAllEffects().setValue("damage", event.getAmount());
return true;
}
return false;
}
}

View file

@ -1,5 +1,6 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
@ -9,16 +10,18 @@ import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.SacrificedPermanentBatchEvent;
import mage.game.events.SacrificedPermanentEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.targetpointer.FixedTargets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author TheElk801, xenohedron
*/
public class SacrificeOneOrMorePermanentsTriggeredAbility extends TriggeredAbilityImpl {
public class SacrificeOneOrMorePermanentsTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<SacrificedPermanentEvent> {
private final FilterPermanent filter;
private final SetTargetPointer setTargetPointer;
@ -64,31 +67,30 @@ public class SacrificeOneOrMorePermanentsTriggeredAbility extends TriggeredAbili
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
ArrayList<Permanent> matchingPermanents = new ArrayList<>();
for (GameEvent sEvent : ((SacrificedPermanentBatchEvent) event).getEvents()) {
Permanent permanent = game.getPermanentOrLKIBattlefield(sEvent.getTargetId());
if (permanent != null && filter.match(permanent, getControllerId(), this, game)) {
switch (sacrificingPlayer) {
case YOU:
if (!sEvent.getPlayerId().equals(getControllerId())) {
continue;
}
break;
case OPPONENT:
Player controller = game.getPlayer(getControllerId());
if (controller == null || !controller.hasOpponent(sEvent.getPlayerId(), game)) {
continue;
}
break;
case ANY:
break;
default:
throw new IllegalArgumentException("Unsupported TargetController in SacrificePermanentTriggeredAbility: " + sacrificingPlayer);
}
matchingPermanents.add(permanent);
}
public boolean checkEvent(SacrificedPermanentEvent event, Game game) {
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId());
if (permanent == null || !filter.match(permanent, getControllerId(), this, game)) {
return false;
}
switch (sacrificingPlayer) {
case YOU:
return isControlledBy(event.getPlayerId());
case OPPONENT:
return game.getOpponents(getControllerId()).contains(event.getPlayerId());
case ANY:
return true;
default:
throw new IllegalArgumentException("Unsupported TargetController in SacrificePermanentTriggeredAbility: " + sacrificingPlayer); }
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
List<Permanent> matchingPermanents = getFilteredEvents((SacrificedPermanentBatchEvent) event, game)
.stream()
.map(GameEvent::getTargetId)
.map(game::getPermanentOrLKIBattlefield)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (matchingPermanents.isEmpty()) {
return false;
}

View file

@ -0,0 +1,54 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.filter.FilterSpell;
import mage.game.Game;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
import mage.game.stack.Spell;
import mage.util.CardUtil;
/**
* @author xenohedron
*/
public class SpellControlledDealsDamageTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedEvent> {
private final FilterSpell filter;
public SpellControlledDealsDamageTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional) {
super(zone, effect, optional);
this.filter = filter;
setTriggerPhrase(getWhen() + CardUtil.addArticle(filter.getMessage()) + " you control deals damage, ");
}
protected SpellControlledDealsDamageTriggeredAbility(final SpellControlledDealsDamageTriggeredAbility ability) {
super(ability);
this.filter = ability.filter;
}
@Override
public SpellControlledDealsDamageTriggeredAbility copy() {
return new SpellControlledDealsDamageTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_BY_SOURCE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
// all events in the batch are always relevant if triggers at all
Spell spell = game.getSpellOrLKIStack(event.getSourceId());
if (spell == null
|| !isControlledBy(spell.getControllerId())
|| !filter.match(spell, getControllerId(), this, game)) {
return false;
}
getEffects().setValue("damage", event.getAmount());
return true;
}
}

View file

@ -0,0 +1,49 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedPlayerEvent;
import mage.game.events.GameEvent;
/**
* @author xenohedron
*/
public class YoureDealtDamageTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPlayerEvent> {
public YoureDealtDamageTriggeredAbility(Effect effect, boolean optional) {
this(Zone.BATTLEFIELD, effect, optional);
}
public YoureDealtDamageTriggeredAbility(Zone zone, Effect effect, boolean optional) {
super(zone, effect, optional);
setTriggerPhrase("Whenever you're dealt damage, ");
}
protected YoureDealtDamageTriggeredAbility(final YoureDealtDamageTriggeredAbility ability) {
super(ability);
}
@Override
public YoureDealtDamageTriggeredAbility copy() {
return new YoureDealtDamageTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
// all events in the batch are always relevant
if (isControlledBy(event.getTargetId())) {
this.getAllEffects().setValue("damage", event.getAmount());
return true;
}
return false;
}
}

View file

@ -1,6 +1,7 @@
package mage.designations;
import mage.abilities.Ability;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.hint.common.CurrentDungeonHint;
@ -8,9 +9,10 @@ import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.events.DamagedEvent;
import mage.game.events.DamagedBatchForPlayersEvent;
import mage.game.events.DamagedBatchForOnePlayerEvent;
import mage.game.events.DamagedPlayerEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
import java.util.Objects;
@ -41,7 +43,7 @@ public class Initiative extends Designation {
}
}
class InitiativeDamageTriggeredAbility extends TriggeredAbilityImpl {
class InitiativeDamageTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPlayerEvent> {
InitiativeDamageTriggeredAbility() {
super(Zone.ALL, new InitiativeTakeEffect());
@ -58,17 +60,22 @@ class InitiativeDamageTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PLAYERS;
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER;
}
@Override
public boolean checkEvent(DamagedPlayerEvent event, Game game) {
if (!event.isCombatDamage() || !event.getTargetId().equals(game.getInitiativeId())) {
return false;
}
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId());
return permanent != null && permanent.isCreature(game);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
DamagedBatchForPlayersEvent dEvent = (DamagedBatchForPlayersEvent) event;
UUID playerId = dEvent
.getEvents()
UUID playerId = getFilteredEvents((DamagedBatchForOnePlayerEvent) event, game)
.stream()
.filter(DamagedEvent::isCombatDamage)
.filter(e -> e.getTargetId().equals(game.getInitiativeId()))
.map(GameEvent::getSourceId)
.map(game::getPermanent)
.filter(Objects::nonNull)
@ -84,7 +91,7 @@ class InitiativeDamageTriggeredAbility extends TriggeredAbilityImpl {
@Override
public String getRule() {
return "Whenever one or more creatures a player controls deals combat damage to you, that player takes the initiative.";
return "Whenever one or more creatures a player controls deal combat damage to the player who has the initiative, the controller of those creatures takes the initiative.";
}
}

View file

@ -845,6 +845,8 @@ public class GameState implements Serializable, Copyable<GameState> {
// DAMAGED_BATCH_FOR_PERMANENTS + DAMAGED_BATCH_FOR_ONE_PERMANENT
addSimultaneousDamageToPermanentBatches((DamagedPermanentEvent) damagedEvent, game);
}
// DAMAGED_BATCH_BY_SOURCE
addSimultaneousDamageBySourceBatched(damagedEvent, game);
// DAMAGED_BATCH_FOR_ALL
addSimultaneousDamageToBatchForAll(damagedEvent, game);
}
@ -895,6 +897,22 @@ public class GameState implements Serializable, Copyable<GameState> {
}
}
public void addSimultaneousDamageBySourceBatched(DamagedEvent damageEvent, Game game) {
// find existing batch first
boolean isBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (event instanceof DamagedBatchBySourceEvent
&& damageEvent.getSourceId().equals(event.getSourceId())) {
((DamagedBatchBySourceEvent) event).addEvent(damageEvent);
isBatchUsed = true;
}
}
// new batch if necessary
if (!isBatchUsed) {
addSimultaneousEvent(new DamagedBatchBySourceEvent(damageEvent), game);
}
}
public void addSimultaneousDamageToBatchForAll(DamagedEvent damagedEvent, Game game) {
boolean isBatchUsed = false;
for (GameEvent event : simultaneousEvents) {

View file

@ -1,7 +1,7 @@
package mage.game.command.emblems;
import mage.abilities.Ability;
import mage.abilities.common.DealCombatDamageControlledTriggeredAbility;
import mage.abilities.common.OneOrMoreCombatDamagePlayerTriggeredAbility;
import mage.abilities.condition.Condition;
import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility;
import mage.abilities.effects.Effect;
@ -26,7 +26,7 @@ public final class LolthSpiderQueenEmblem extends Emblem {
public LolthSpiderQueenEmblem() {
super("Emblem Lolth");
this.getAbilities().add(new ConditionalInterveningIfTriggeredAbility(
new DealCombatDamageControlledTriggeredAbility(
new OneOrMoreCombatDamagePlayerTriggeredAbility(
Zone.COMMAND, new LolthSpiderQueenEmblemEffect(), StaticFilters.FILTER_PERMANENT_CREATURES, SetTargetPointer.PLAYER, false
), LolthSpiderQueenEmblemCondition.instance, "Whenever an opponent " +
"is dealt combat damage by one or more creatures you control, " +

View file

@ -39,6 +39,9 @@ public abstract class BatchEvent<T extends GameEvent> extends GameEvent {
if (firstEvent instanceof BatchEvent) { // sanity check, if you need it then think twice and research carefully
throw new UnsupportedOperationException("Wrong code usage: nesting batch events not supported");
}
if (!eventType.isBatch()) { // sanity check, a batch event should use a batch EventType
throw new IllegalArgumentException("Wrong code usage: batch event should use batch EventType: " + eventType);
}
this.addEvent(firstEvent);
}
@ -50,6 +53,9 @@ public abstract class BatchEvent<T extends GameEvent> extends GameEvent {
this.singleTargetId = false;
this.singleSourceId = false;
this.singlePlayerId = false;
if (!eventType.isBatch()) { // sanity check, a batch event should use a batch EventType
throw new IllegalArgumentException("Wrong code usage: batch event should use batch EventType: " + eventType);
}
}
public void addEvent(T event) {

View file

@ -0,0 +1,19 @@
package mage.game.events;
/**
* Batch all simultaneous damage events dealt by a single source.
*
* @author Susucr
*/
public class DamagedBatchBySourceEvent extends BatchEvent<DamagedEvent> {
public DamagedBatchBySourceEvent(DamagedEvent firstEvent) {
super(EventType.DAMAGED_BATCH_BY_SOURCE, false, true, false, firstEvent);
}
public boolean isCombatDamage() {
return getEvents()
.stream()
.anyMatch(DamagedEvent::isCombatDamage);
}
}

View file

@ -10,7 +10,7 @@ public abstract class DamagedEvent extends GameEvent {
protected boolean combat;
protected int excess;
public DamagedEvent(EventType type, UUID targetId, UUID attackerId, UUID playerId, int amount, boolean combat) {
protected DamagedEvent(EventType type, UUID targetId, UUID attackerId, UUID playerId, int amount, boolean combat) {
super(type, targetId, null, playerId, amount, false);
this.combat = combat;
this.excess = 0;
@ -21,10 +21,6 @@ public abstract class DamagedEvent extends GameEvent {
return combat;
}
public boolean isPreventable() {
return flag;
}
public void setExcess(int excess) {
this.excess = Math.max(excess, 0);
}
@ -33,7 +29,4 @@ public abstract class DamagedEvent extends GameEvent {
return excess;
}
public UUID getAttackerId() {
return getSourceId();
}
}

View file

@ -73,7 +73,7 @@ public class GameEvent implements Serializable {
*/
ZONE_CHANGE,
ZONE_CHANGE_GROUP, // between two specific zones only; TODO: rework all usages to ZONE_CHANGE_BATCH instead, see #11895
ZONE_CHANGE_BATCH, // all zone changes that occurred from a single effect
ZONE_CHANGE_BATCH(true), // all zone changes that occurred from a single effect
DRAW_TWO_OR_MORE_CARDS, // event calls for multi draws only (if player draws 2+ cards at once)
DRAW_CARD, DREW_CARD,
EXPLORE, EXPLORED, // targetId is exploring permanent, playerId is its controller
@ -116,11 +116,11 @@ public class GameEvent implements Serializable {
combines all MILLED_CARD events for a player milling card at the same time in a single batch
playerId the id of the player whose batch it is
*/
MILLED_CARDS_BATCH_FOR_ONE_PLAYER,
MILLED_CARDS_BATCH_FOR_ONE_PLAYER(true),
/* MILLED_CARDS_BATCH_FOR_ALL,
combines all MILLED_CARD events for any player in a single batch
*/
MILLED_CARDS_BATCH_FOR_ALL,
MILLED_CARDS_BATCH_FOR_ALL(true),
/* DAMAGED_PLAYER
targetId the id of the damaged player
@ -134,18 +134,21 @@ public class GameEvent implements Serializable {
/* DAMAGED_BATCH_FOR_PLAYERS,
combines all player damage events to a single batch (event)
*/
DAMAGED_BATCH_FOR_PLAYERS,
DAMAGED_BATCH_FOR_PLAYERS(true),
/* DAMAGED_BATCH_FOR_ONE_PLAYER
combines all player damage events to a single batch (event) and split it per damaged player
targetId the id of the damaged player (playerId won't work for batch)
*/
DAMAGED_BATCH_FOR_ONE_PLAYER,
DAMAGED_BATCH_FOR_ONE_PLAYER(true),
/* DAMAGED_BATCH_BY_SOURCE
combine all damage events from a single source to a single batch (event)
*/
DAMAGED_BATCH_BY_SOURCE(true),
/* DAMAGED_BATCH_FOR_ALL
includes all damage events, both permanent damage and player damage, in single batch event
*/
DAMAGED_BATCH_FOR_ALL,
DAMAGED_BATCH_FOR_ALL(true),
/* 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).
@ -171,7 +174,7 @@ public class GameEvent implements Serializable {
amount amount of life loss
flag true = from combat damage - other from non combat damage
*/
LOST_LIFE_BATCH,
LOST_LIFE_BATCH(true),
/* LOST_LIFE_BATCH
combines all player life lost events to a single batch (event)
*/
@ -426,7 +429,7 @@ public class GameEvent implements Serializable {
/* TAPPED_BATCH
combine all TAPPED events occuring at the same time in a single event
*/
TAPPED_BATCH,
TAPPED_BATCH(true),
UNTAP,
/* UNTAPPED,
targetId untapped permanent
@ -439,7 +442,7 @@ public class GameEvent implements Serializable {
/* UNTAPPED_BATCH
combine all UNTAPPED events occuring at the same time in a single event
*/
UNTAPPED_BATCH,
UNTAPPED_BATCH(true),
FLIP, FLIPPED,
TRANSFORMING, TRANSFORMED,
ADAPT,
@ -494,12 +497,12 @@ public class GameEvent implements Serializable {
/* DAMAGED_BATCH_FOR_PERMANENTS
combine all permanent damage events to a single batch (event)
*/
DAMAGED_BATCH_FOR_PERMANENTS,
DAMAGED_BATCH_FOR_PERMANENTS(true),
/* DAMAGED_BATCH_FOR_ONE_PERMANENT
combines all permanent damage events to a single batch (event) and split it per damaged permanent
*/
DAMAGED_BATCH_FOR_ONE_PERMANENT,
DAMAGED_BATCH_FOR_ONE_PERMANENT(true),
DESTROY_PERMANENT,
/* DESTROY_PERMANENT_BY_LEGENDARY_RULE
@ -515,7 +518,7 @@ public class GameEvent implements Serializable {
flag true if no regeneration is allowed
*/
DESTROYED_PERMANENT,
SACRIFICE_PERMANENT, SACRIFICED_PERMANENT, SACRIFICED_PERMANENT_BATCH,
SACRIFICE_PERMANENT, SACRIFICED_PERMANENT, SACRIFICED_PERMANENT_BATCH(true),
FIGHTED_PERMANENT,
BATCH_FIGHT,
EXPLOITED_CREATURE,
@ -669,7 +672,21 @@ public class GameEvent implements Serializable {
*/
GAVE_GIFT,
// custom events - must store some unique data to track
CUSTOM_EVENT
CUSTOM_EVENT;
private final boolean isBatch;
EventType() {
this(false);
}
EventType(boolean isBatch) {
this.isBatch = isBatch;
}
public boolean isBatch() {
return isBatch;
}
}
public GameEvent(EventType type, UUID targetId, Ability source, UUID playerId) {

View file

@ -1,5 +1,6 @@
package mage.game.events;
import java.util.Collection;
import java.util.UUID;
/**
@ -11,10 +12,9 @@ public class LifeLostBatchEvent extends BatchEvent<LifeLostEvent> {
super(EventType.LOST_LIFE_BATCH, false, false, false, firstEvent);
}
public int getLifeLostByPlayer(UUID playerID) {
return getEvents()
.stream()
.filter(ev -> ev.getTargetId().equals(playerID))
public static int getLifeLostByPlayer(Collection<LifeLostEvent> events, UUID playerId) {
return events.stream()
.filter(ev -> ev.getTargetId().equals(playerId))
.mapToInt(GameEvent::getAmount)
.sum();
}

View file

@ -1,12 +1,5 @@
package mage.game.events;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.game.Game;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Susucr
*/
@ -16,12 +9,4 @@ public class MilledBatchAllEvent extends BatchEvent<MilledCardEvent> {
super(EventType.MILLED_CARDS_BATCH_FOR_ALL, false, false, false, event);
}
public Cards getCards(Game game) {
return new CardsImpl(getEvents()
.stream()
.map(mce -> mce.getCard(game))
.filter(Objects::nonNull)
.collect(Collectors.toSet())
);
}
}

View file

@ -1,12 +1,5 @@
package mage.game.events;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.game.Game;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Susucr
*/
@ -16,12 +9,4 @@ public class MilledBatchForOnePlayerEvent extends BatchEvent<MilledCardEvent> {
super(EventType.MILLED_CARDS_BATCH_FOR_ONE_PLAYER, false, false, true, event);
}
public Cards getCards(Game game) {
return new CardsImpl(getEvents()
.stream()
.map(mce -> mce.getCard(game))
.filter(Objects::nonNull)
.collect(Collectors.toSet())
);
}
}

View file

@ -1,6 +1,7 @@
package mage.target.targetadjustment;
import mage.abilities.Ability;
import mage.abilities.common.OneOrMoreDamagePlayerTriggeredAbility;
import mage.filter.Filter;
import mage.filter.predicate.card.OwnerIdPredicate;
import mage.filter.predicate.permanent.ControllerIdPredicate;
@ -21,7 +22,7 @@ public class DamagedPlayerControlsTargetAdjuster extends GenericTargetAdjuster {
/**
* Use with {@link mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility} with setTargetPointer enabled,
* or {@link mage.abilities.common.OneOrMoreDealDamageTriggeredAbility} with "SetTargetPointer.PLAYER" or similar.
* or {@link OneOrMoreDamagePlayerTriggeredAbility} with "SetTargetPointer.PLAYER" or similar.
* Adjusts the target to only target something the damaged player controls (or owns with alternative constructor)
* And then removes the effects' target pointer that the triggered ability set
*/