[NCC] Implement several cards (#9328)

Many associated refactors too. See full PR for detail.
This commit is contained in:
Alex Vasile 2022-09-22 21:38:29 -04:00 committed by GitHub
parent b7151cfa58
commit fd16f2a16b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 6091 additions and 1069 deletions

View file

@ -8,6 +8,9 @@ import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.Set;
import java.util.UUID;
/**
* @author LevelX2
*/
@ -31,10 +34,22 @@ public class EnchantedPlayerAttackedTriggeredAbility extends TriggeredAbilityImp
public boolean checkTrigger(GameEvent event, Game game) {
Permanent enchantment = game.getPermanentOrLKIBattlefield(getSourceId());
Player controller = game.getPlayer(getControllerId());
if (controller != null && enchantment != null) {
return game.getCombat().getPlayerDefenders(game, false).contains(enchantment.getAttachedTo());
Player attacker = game.getPlayer(game.getCombat().getAttackingPlayerId());
if (controller == null || attacker == null || enchantment == null) {
return false;
}
return false;
Player enchantedPlayer = game.getPlayer(enchantment.getAttachedTo());
if (enchantedPlayer == null) {
return false;
}
Set<UUID> opponentIds = game.getOpponents(controller.getId());
if (!opponentIds.contains(attacker.getId()) || !opponentIds.contains(enchantedPlayer.getId())) {
return false;
}
return game.getCombat().getPlayerDefenders(game, false).contains(enchantment.getAttachedTo());
}
@Override

View file

@ -0,0 +1,95 @@
package mage.abilities.common;
import mage.MageItem;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeGroupEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.Objects;
import java.util.stream.Stream;
/**
* "Whenever one or more {filter} enter the battlefield under {target controller} control,
*
* @author Alex-Vasile
*/
public class EntersBattlefieldOneOrMoreTriggeredAbility extends TriggeredAbilityImpl {
private final FilterPermanent filterPermanent;
private final TargetController targetController;
public EntersBattlefieldOneOrMoreTriggeredAbility(Effect effect, FilterPermanent filter, TargetController targetController) {
super(Zone.BATTLEFIELD, effect);
this.filterPermanent = filter;
this.targetController = targetController;
setTriggerPhrase(generateTriggerPhrase());
}
private EntersBattlefieldOneOrMoreTriggeredAbility(final EntersBattlefieldOneOrMoreTriggeredAbility ability) {
super(ability);
this.filterPermanent = ability.filterPermanent;
this.targetController = ability.targetController;
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE_GROUP;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
ZoneChangeGroupEvent zEvent = (ZoneChangeGroupEvent) event;
Player controller = game.getPlayer(this.controllerId);
if (zEvent.getToZone() != Zone.BATTLEFIELD || controller == null) {
return false;
}
switch (this.targetController) {
case YOU:
if (!controller.getId().equals(zEvent.getPlayerId())) {
return false;
}
break;
case OPPONENT:
if (!controller.hasOpponent(zEvent.getPlayerId(), game)) {
return false;
}
break;
}
return Stream.concat(
zEvent.getTokens().stream(),
zEvent.getCards().stream()
.map(MageItem::getId)
.map(game::getPermanent)
.filter(Objects::nonNull)
).anyMatch(permanent -> filterPermanent.match(permanent, this.controllerId, this, game));
}
@Override
public EntersBattlefieldOneOrMoreTriggeredAbility copy() {
return new EntersBattlefieldOneOrMoreTriggeredAbility(this);
}
private String generateTriggerPhrase() {
StringBuilder sb = new StringBuilder("Whenever one or more " + this.filterPermanent.getMessage() + " enter the battlefield under ");
switch (targetController) {
case YOU:
sb.append("your control, ");
break;
case OPPONENT:
sb.append("an opponent's control, ");
break;
default:
throw new UnsupportedOperationException();
}
return sb.toString();
}
}

View file

@ -3,8 +3,11 @@ package mage.abilities.common;
import mage.abilities.Ability;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.SpellAbility;
import mage.abilities.TriggeredAbility;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.EntersBattlefieldEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.continuous.GainAbilitySourceEffect;
import mage.abilities.keyword.EscapeAbility;
import mage.constants.AbilityType;
import mage.constants.Outcome;
@ -23,22 +26,22 @@ import java.util.UUID;
public class EscapesWithAbility extends EntersBattlefieldAbility {
private final int counters;
private final DelayedTriggeredAbility delayedTriggeredAbility;
private final TriggeredAbility triggeredAbility;
public EscapesWithAbility(int counters) {
this(counters, null);
}
public EscapesWithAbility(int counters, DelayedTriggeredAbility delayedTriggeredAbility) {
super(new EscapesWithEffect(counters, delayedTriggeredAbility), false);
public EscapesWithAbility(int counters, TriggeredAbility triggeredAbility) {
super(new EscapesWithEffect(counters, triggeredAbility), false);
this.counters = counters;
this.delayedTriggeredAbility = delayedTriggeredAbility;
this.triggeredAbility = triggeredAbility;
}
private EscapesWithAbility(final EscapesWithAbility ability) {
super(ability);
this.counters = ability.counters;
this.delayedTriggeredAbility = ability.delayedTriggeredAbility;
this.triggeredAbility = ability.triggeredAbility;
}
@Override
@ -48,27 +51,42 @@ public class EscapesWithAbility extends EntersBattlefieldAbility {
@Override
public String getRule() {
return "{this} escapes with " + CardUtil.numberToText(counters, "a")
+ " +1/+1 counter" + (counters > 1 ? 's' : "") + " on it."
+ (this.delayedTriggeredAbility != null ? " " + this.delayedTriggeredAbility.getRule() : "");
StringBuilder sb = new StringBuilder("{this} escapes with ");
if (counters > 0) {
sb.append(CardUtil.numberToText(counters, "a"));
sb.append(" +1/+1 counter");
sb.append((counters > 1 ? 's' : ""));
sb.append(" on it.");
}
if (triggeredAbility == null) {
// Do nothing
} else if (triggeredAbility instanceof DelayedTriggeredAbility) {
sb.append(this.triggeredAbility.getRule());
} else {
sb.append("\"");
sb.append(this.triggeredAbility.getRule());
sb.append("\"");
}
return sb.toString();
}
}
class EscapesWithEffect extends OneShotEffect {
private final int counter;
private final DelayedTriggeredAbility delayedTriggeredAbility;
private final TriggeredAbility triggeredAbility;
EscapesWithEffect(int counter, DelayedTriggeredAbility delayedTriggeredAbility) {
EscapesWithEffect(int counter, TriggeredAbility triggeredAbility) {
super(Outcome.BoostCreature);
this.counter = counter;
this.delayedTriggeredAbility = delayedTriggeredAbility;
this.triggeredAbility = triggeredAbility;
}
private EscapesWithEffect(final EscapesWithEffect effect) {
super(effect);
this.counter = effect.counter;
this.delayedTriggeredAbility = (effect.delayedTriggeredAbility == null ? null : effect.delayedTriggeredAbility.copy());
this.triggeredAbility = (effect.triggeredAbility == null ? null : effect.triggeredAbility.copy());
}
@Override
@ -80,16 +98,26 @@ class EscapesWithEffect extends OneShotEffect {
if (permanent == null) {
return false;
}
SpellAbility spellAbility = (SpellAbility) getValue(EntersBattlefieldEffect.SOURCE_CAST_SPELL_ABILITY);
if (!(spellAbility instanceof EscapeAbility)
|| !spellAbility.getSourceId().equals(source.getSourceId())
|| permanent.getZoneChangeCounter(game) != spellAbility.getSourceObjectZoneChangeCounter()) {
return false;
}
List<UUID> appliedEffects = (ArrayList<UUID>) this.getValue("appliedEffects");
permanent.addCounters(CounterType.P1P1.createInstance(counter), source.getControllerId(), source, game, appliedEffects);
if (this.delayedTriggeredAbility != null) {
game.addDelayedTriggeredAbility(this.delayedTriggeredAbility, source);
if (counter > 0) {
List<UUID> appliedEffects = (ArrayList<UUID>) this.getValue("appliedEffects");
permanent.addCounters(CounterType.P1P1.createInstance(counter), source.getControllerId(), source, game, appliedEffects);
}
if (this.triggeredAbility != null) {
if (triggeredAbility instanceof DelayedTriggeredAbility) {
game.addDelayedTriggeredAbility((DelayedTriggeredAbility) this.triggeredAbility, source);
} else {
ContinuousEffect gainsAbilityEffect = new GainAbilitySourceEffect(triggeredAbility);
game.addEffect(gainsAbilityEffect, source);
}
}
return true;
}

View file

@ -20,8 +20,10 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl {
// The source SPELL that triggered the ability will be set as target to effect
protected boolean rememberSource;
// Use it if you want remember CARD instead spell
// Use it if you want to remember CARD instead spell
protected boolean rememberSourceAsCard;
// Trigger only for spells cast from this zone. Default is from any zone.
private Zone fromZone = Zone.ALL;
public SpellCastControllerTriggeredAbility(Effect effect, boolean optional) {
this(Zone.BATTLEFIELD, effect, StaticFilters.FILTER_SPELL_A, optional, false);
@ -31,6 +33,11 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl {
this(effect, filter, optional, false);
}
public SpellCastControllerTriggeredAbility(Effect effect, FilterSpell filter, boolean optional, Zone fromZone) {
this(effect, filter, optional, false);
this.fromZone = fromZone;
}
public SpellCastControllerTriggeredAbility(Effect effect, FilterSpell filter, boolean optional, String rule) {
this(effect, filter, optional, false);
this.rule = rule;
@ -49,7 +56,7 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl {
this.filter = filter;
this.rememberSource = rememberSource;
this.rememberSourceAsCard = rememberSourceAsCard;
setTriggerPhrase("Whenever you cast " + filter.getMessage() + ", ");
setTriggerPhrase("Whenever you cast " + filter.getMessage() + (fromZone != Zone.ALL ? "from your " + fromZone.toString().toLowerCase() : "") + ", ");
}
public SpellCastControllerTriggeredAbility(final SpellCastControllerTriggeredAbility ability) {
@ -58,6 +65,7 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl {
this.rule = ability.rule;
this.rememberSource = ability.rememberSource;
this.rememberSourceAsCard = ability.rememberSourceAsCard;
this.fromZone = ability.fromZone;
}
@Override
@ -67,22 +75,20 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getPlayerId().equals(this.getControllerId())) {
Spell spell = game.getStack().getSpell(event.getTargetId());
if (filter.match(spell, getControllerId(), this, game)) {
this.getEffects().setValue("spellCast", spell);
if (rememberSource) {
if (rememberSourceAsCard) {
this.getEffects().setTargetPointer(new FixedTarget(spell.getCard().getId(), game));
} else {
this.getEffects().setTargetPointer(new FixedTarget(spell.getId(), game));
}
}
return true;
}
if (!event.getPlayerId().equals(this.getControllerId())) {
return false;
}
return false;
Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell == null
|| !filter.match(spell, getControllerId(), this, game)
|| !(fromZone == Zone.ALL || fromZone == spell.getFromZone())) {
return false;
}
this.getEffects().setValue("spellCast", spell);
if (rememberSource) {
this.getEffects().setTargetPointer(new FixedTarget(rememberSourceAsCard ? spell.getCard().getId() : spell.getId(), game));
}
return true;
}
@Override

View file

@ -18,37 +18,60 @@ public class SpellCastOpponentTriggeredAbility extends TriggeredAbilityImpl {
protected FilterSpell filter;
protected SetTargetPointer setTargetPointer;
private final boolean onlyFromNonHand;
public SpellCastOpponentTriggeredAbility(Effect effect, boolean optional) {
this(effect, StaticFilters.FILTER_SPELL_A, optional);
this(effect, optional, false);
}
public SpellCastOpponentTriggeredAbility(Effect effect, boolean optional, boolean onlyFromNonHand) {
this(effect, StaticFilters.FILTER_SPELL_A, optional, onlyFromNonHand);
}
public SpellCastOpponentTriggeredAbility(Effect effect, FilterSpell filter, boolean optional) {
this(Zone.BATTLEFIELD, effect, filter, optional);
this(effect, filter, optional, false);
}
public SpellCastOpponentTriggeredAbility(Effect effect, FilterSpell filter, boolean optional, boolean onlyFromNonHand) {
this(Zone.BATTLEFIELD, effect, filter, optional, onlyFromNonHand);
}
public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional) {
this(zone, effect, filter, optional, SetTargetPointer.NONE);
this(zone, effect, filter, optional, false);
}
public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, boolean onlyFromNonHand) {
this(zone, effect, filter, optional, SetTargetPointer.NONE, onlyFromNonHand);
}
public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, SetTargetPointer setTargetPointer) {
this(zone, effect, filter, optional, setTargetPointer, false);
}
/**
* @param zone
* @param effect
* @param filter
* @param optional
* @param setTargetPointer Supported: SPELL, PLAYER
* @param zone The zone in which the source permanent has to be in for the ability to trigger
* @param effect The effect to apply if condition is met
* @param filter Filter for matching the spell cast
* @param optional Whether the player can choose to apply the effect
* @param onlyFromNonHand Whether to trigger only when spells are cast from not the hand
* @param setTargetPointer Supported: SPELL, PLAYER
*/
public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, SetTargetPointer setTargetPointer) {
public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, SetTargetPointer setTargetPointer, boolean onlyFromNonHand) {
super(zone, effect, optional);
this.filter = filter;
this.setTargetPointer = setTargetPointer;
setTriggerPhrase("Whenever an opponent casts " + filter.getMessage() + ", ");
this.onlyFromNonHand = onlyFromNonHand;
setTriggerPhrase("Whenever an opponent casts "
+ filter.getMessage()
+ (onlyFromNonHand ? " from anywhere other than their hand" : "")
+ ", ");
}
public SpellCastOpponentTriggeredAbility(final SpellCastOpponentTriggeredAbility ability) {
super(ability);
this.filter = ability.filter;
this.setTargetPointer = ability.setTargetPointer;
this.onlyFromNonHand = ability.onlyFromNonHand;
}
@Override
@ -65,6 +88,11 @@ public class SpellCastOpponentTriggeredAbility extends TriggeredAbilityImpl {
if (!filter.match(spell, getControllerId(), this, game)) {
return false;
}
if (onlyFromNonHand && spell.getFromZone() == Zone.HAND) {
return false;
}
getEffects().setValue("spellCast", spell);
switch (setTargetPointer) {
case NONE:

View file

@ -0,0 +1,91 @@
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.permanent.Permanent;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
/**
* "Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls,"
* AND
* "Whenever you become the target of a spell or ability an opponent controls,"
*
* @author Alex-Vasile
*/
public class TargetOfOpponentsSpellOrAbilityTriggeredAbility extends TriggeredAbilityImpl {
private Boolean onlyController = Boolean.FALSE;
public TargetOfOpponentsSpellOrAbilityTriggeredAbility(Effect effect) {
this(effect, false);
}
// NOTE: Using Boolean instead of boolean in order to have a second constructor with (Effect, "boolean") signature
public TargetOfOpponentsSpellOrAbilityTriggeredAbility(Effect effect, Boolean onlyController) {
this(effect, false, onlyController);
}
public TargetOfOpponentsSpellOrAbilityTriggeredAbility(Effect effect, boolean optional) {
this(effect, optional, Boolean.FALSE);
}
public TargetOfOpponentsSpellOrAbilityTriggeredAbility(Effect effect, boolean optional, Boolean onlyController) {
super(Zone.BATTLEFIELD, effect, optional);
this.onlyController = onlyController;
if (this.onlyController) {
setTriggerPhrase("Whenever you become the target of a spell or ability an opponent controls, ");
} else {
setTriggerPhrase("Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, ");
}
}
private TargetOfOpponentsSpellOrAbilityTriggeredAbility(final TargetOfOpponentsSpellOrAbilityTriggeredAbility ability) {
super(ability);
this.onlyController = ability.onlyController;
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.TARGETED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Player controller = game.getPlayer(this.getControllerId());
Player targetter = game.getPlayer(event.getPlayerId());
if (controller == null || targetter == null || controller.getId().equals(targetter.getId())) {
return false;
}
// Check if player was targeted
if (controller.getId().equals(event.getTargetId())) {
// Add target for effects which need it (e.g. the counter effect from AmuletOfSafekeeping)
this.getEffects().setTargetPointer(new FixedTarget(event.getSourceId(), game));
return true;
}
// If only the controller is
if (this.onlyController) {
return false;
}
// Check if permanent was targeted
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId());
if (permanent == null || !controller.getId().equals(permanent.getControllerId())) {
return false;
}
// Add target for effects which need it (e.g. the counter effect from AmuletOfSafekeeping)
this.getEffects().setTargetPointer(new FixedTarget(event.getSourceId(), game));
return false;
}
@Override
public TargetOfOpponentsSpellOrAbilityTriggeredAbility copy() {
return new TargetOfOpponentsSpellOrAbilityTriggeredAbility(this);
}
}