forked from External/mage
[NCC] Implement several cards (#9328)
Many associated refactors too. See full PR for detail.
This commit is contained in:
parent
b7151cfa58
commit
fd16f2a16b
104 changed files with 6091 additions and 1069 deletions
|
|
@ -0,0 +1,164 @@
|
|||
package mage.abilities.effects.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.Mode;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.Cards;
|
||||
import mage.cards.CardsImpl;
|
||||
import mage.constants.CommanderCardType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.Zone;
|
||||
import mage.filter.FilterCard;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.filter.common.FilterCreatureCard;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
import mage.target.TargetCard;
|
||||
import mage.target.common.TargetCardInCommandZone;
|
||||
import mage.target.common.TargetCardInGraveyard;
|
||||
import mage.target.common.TargetCardInHand;
|
||||
import mage.target.targetpointer.FixedTarget;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* "Put a {filter} card from {zone 1} or {zone 2} onto the battlefield.
|
||||
*
|
||||
* @author TheElk801, Alex-Vasile
|
||||
*/
|
||||
public class PutCardFromOneOfTwoZonesOntoBattlefieldEffect extends OneShotEffect {
|
||||
|
||||
private final FilterCard filterCard;
|
||||
private final boolean tapped;
|
||||
private final Effect effectToApplyOnPermanent;
|
||||
private final Zone zone1;
|
||||
private final Zone zone2;
|
||||
|
||||
public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard) {
|
||||
this(filterCard, false);
|
||||
}
|
||||
|
||||
public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard, boolean tapped) {
|
||||
this(filterCard, tapped, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param filterCard Filter used to filter which cards are valid choices. (no default)
|
||||
* @param tapped If the permanent should enter the battlefield tapped (default is False)
|
||||
* @param effectToApplyOnPermanent An effect to apply to the permanent after it enters (default null)
|
||||
* See "Swift Warkite" or "Nissa of Shadowed Boughs".
|
||||
* @param zone1 The first zone to pick from (default of HAND)
|
||||
* @param zone2 The second zone to pick from (defualt of GRAVEYARD)
|
||||
*/
|
||||
public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard, boolean tapped, Effect effectToApplyOnPermanent, Zone zone1, Zone zone2) {
|
||||
super(filterCard instanceof FilterCreatureCard ? Outcome.PutCreatureInPlay : Outcome.PutCardInPlay);
|
||||
this.filterCard = filterCard;
|
||||
this.tapped = tapped;
|
||||
this.effectToApplyOnPermanent = effectToApplyOnPermanent;
|
||||
this.zone1 = zone1;
|
||||
this.zone2 = zone2;
|
||||
}
|
||||
|
||||
public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard, boolean tapped, Effect effectToApplyOnPermanent) {
|
||||
this(filterCard, tapped, effectToApplyOnPermanent, Zone.HAND, Zone.GRAVEYARD);
|
||||
}
|
||||
|
||||
public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard, Zone zone1, Zone zone2) {
|
||||
this(filterCard, false, null, zone1, zone2);
|
||||
}
|
||||
|
||||
private PutCardFromOneOfTwoZonesOntoBattlefieldEffect(final PutCardFromOneOfTwoZonesOntoBattlefieldEffect effect) {
|
||||
super(effect);
|
||||
this.filterCard = effect.filterCard;
|
||||
this.tapped = effect.tapped;
|
||||
this.zone1 = effect.zone1;
|
||||
this.zone2 = effect.zone2;
|
||||
if (effect.effectToApplyOnPermanent != null) {
|
||||
this.effectToApplyOnPermanent = effect.effectToApplyOnPermanent.copy();
|
||||
} else {
|
||||
this.effectToApplyOnPermanent = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Cards cardsInZone1 = getCardsFromZone(game, controller, zone1);
|
||||
Cards cardsInZone2 = getCardsFromZone(game, controller, zone2);
|
||||
|
||||
boolean cardsAvailableInZone1 = cardsInZone1.count(filterCard, game) > 0;
|
||||
boolean cardsAvailableInZone2 = cardsInZone2.count(filterCard, game) > 0;
|
||||
if (!cardsAvailableInZone1 && !cardsAvailableInZone2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean choose1stZone;
|
||||
if (cardsAvailableInZone1 && cardsAvailableInZone2) {
|
||||
choose1stZone = controller.chooseUse(outcome, "Where do you want to chose the card from?",
|
||||
null, zone1.name(), zone2.name(), source, game);
|
||||
} else {
|
||||
choose1stZone = cardsAvailableInZone1;
|
||||
}
|
||||
|
||||
Zone zone = choose1stZone ? zone1 : zone2;
|
||||
Cards cards = choose1stZone ? cardsInZone1 : cardsInZone2;
|
||||
TargetCard targetCard;
|
||||
|
||||
switch (zone) {
|
||||
case HAND:
|
||||
targetCard = new TargetCardInHand(filterCard);
|
||||
break;
|
||||
case GRAVEYARD:
|
||||
targetCard = new TargetCardInGraveyard(filterCard);
|
||||
break;
|
||||
case COMMAND:
|
||||
targetCard = new TargetCardInCommandZone(filterCard);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
controller.choose(outcome, cards, targetCard, game);
|
||||
Card card = game.getCard(targetCard.getFirstTarget());
|
||||
if (card == null || !controller.moveCards(card, Zone.BATTLEFIELD, source, game, tapped, false, false, null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (effectToApplyOnPermanent != null) {
|
||||
effectToApplyOnPermanent.setTargetPointer(new FixedTarget(card.getId()));
|
||||
effectToApplyOnPermanent.apply(game, source);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Cards getCardsFromZone(Game game, Player player, Zone zone) {
|
||||
switch (zone) {
|
||||
case HAND:
|
||||
return player.getHand();
|
||||
case COMMAND:
|
||||
return new CardsImpl(game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY));
|
||||
case GRAVEYARD:
|
||||
return player.getGraveyard();
|
||||
default:
|
||||
return new CardsImpl();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PutCardFromOneOfTwoZonesOntoBattlefieldEffect copy() {
|
||||
return new PutCardFromOneOfTwoZonesOntoBattlefieldEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText(Mode mode) {
|
||||
return "you may put " + CardUtil.addArticle(this.filterCard.getMessage()) +
|
||||
" from your hand or graveyard onto the battlefield" +
|
||||
(this.tapped ? " tapped" : "") +
|
||||
(effectToApplyOnPermanent == null ? "" : ". " + effectToApplyOnPermanent.getText(mode));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package mage.abilities.effects.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
|
||||
/**
|
||||
* Used for "Return {this} to the battlefield attached to that creature at the beginning of the next end step" abilities.
|
||||
* E.g.g Gift of Immortality and Next of Kin.
|
||||
*
|
||||
* @author LevelX2, Alex-Vasile
|
||||
*/
|
||||
public class ReturnToBattlefieldAttachedEffect extends OneShotEffect {
|
||||
|
||||
public ReturnToBattlefieldAttachedEffect() {
|
||||
super(Outcome.PutCardInPlay);
|
||||
staticText = "Return {this} to the battlefield attached to that creature at the beginning of the next end step";
|
||||
}
|
||||
|
||||
public ReturnToBattlefieldAttachedEffect(final ReturnToBattlefieldAttachedEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
Permanent creature = game.getPermanent(getTargetPointer().getFirst(game, source));
|
||||
Card aura = game.getCard(source.getSourceId());
|
||||
if (controller == null
|
||||
|| creature == null
|
||||
|| aura == null || game.getState().getZone(aura.getId()) != Zone.GRAVEYARD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
game.getState().setValue("attachTo:" + aura.getId(), creature);
|
||||
controller.moveCards(aura, Zone.BATTLEFIELD, source, game);
|
||||
return creature.addAttachment(aura.getId(), source, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReturnToBattlefieldAttachedEffect copy() {
|
||||
return new ReturnToBattlefieldAttachedEffect(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package mage.abilities.effects.common.continuous;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.effects.ContinuousEffectImpl;
|
||||
import mage.abilities.keyword.ReplicateAbility;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Layer;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.SubLayer;
|
||||
import mage.filter.FilterSpell;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.game.stack.Spell;
|
||||
import mage.game.stack.StackObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
/**
|
||||
* @author LevelX2, Alex-Vasile
|
||||
*/
|
||||
public class EachSpellYouCastHasReplicateEffect extends ContinuousEffectImpl {
|
||||
|
||||
private final FilterSpell filter;
|
||||
private final Cost fixedNewCost;
|
||||
private final Map<UUID, ReplicateAbility> replicateAbilities = new HashMap<>();
|
||||
|
||||
public EachSpellYouCastHasReplicateEffect(FilterSpell filter) {
|
||||
this(filter, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param filter Filter used for filtering spells
|
||||
* @param fixedNewCost Fixed new cost to pay as the replication cost
|
||||
*/
|
||||
public EachSpellYouCastHasReplicateEffect(FilterSpell filter, Cost fixedNewCost) {
|
||||
super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
|
||||
this.filter = filter;
|
||||
this.fixedNewCost = fixedNewCost;
|
||||
this.staticText = "Each " + this.filter.getMessage() + " you cast has replicate" +
|
||||
(this.fixedNewCost == null ? ". The replicate cost is equal to its mana cost" : ' ' + this.fixedNewCost.getText())
|
||||
+ ". <i>(When you cast it, copy it for each time you paid its replicate cost. You may choose new targets for the copies.)</i>";
|
||||
}
|
||||
|
||||
private EachSpellYouCastHasReplicateEffect(final EachSpellYouCastHasReplicateEffect effect) {
|
||||
super(effect);
|
||||
this.filter = effect.filter;
|
||||
this.fixedNewCost = effect.fixedNewCost;
|
||||
this.replicateAbilities.putAll(effect.replicateAbilities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Permanent permanent = game.getPermanent(source.getSourceId());
|
||||
if (permanent == null
|
||||
|| !permanent.isControlledBy(source.getControllerId())) { // Verify that the controller of the permanent is the one who cast the spell
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean applied = false;
|
||||
|
||||
for (StackObject stackObject : game.getStack()) {
|
||||
if (!(stackObject instanceof Spell)
|
||||
|| stackObject.isCopy()
|
||||
|| !stackObject.isControlledBy(source.getControllerId())
|
||||
|| (fixedNewCost == null && stackObject.getManaCost().isEmpty())) { // If the spell has no mana cost, it cannot be played by this ability unless an fixed alternative cost (e.g. such as from Threefold Signal) is specified.
|
||||
continue;
|
||||
}
|
||||
Spell spell = (Spell) stackObject;
|
||||
if (filter.match(stackObject, game)) {
|
||||
Cost cost = fixedNewCost != null ? fixedNewCost.copy() : spell.getSpellAbility().getManaCosts().copy();
|
||||
ReplicateAbility replicateAbility = replicateAbilities.computeIfAbsent(spell.getId(), k -> new ReplicateAbility(cost));
|
||||
game.getState().addOtherAbility(spell.getCard(), replicateAbility, false); // Do not copy because paid and # of activations state is handled in the baility
|
||||
applied = true;
|
||||
}
|
||||
}
|
||||
if (game.getStack().isEmpty()) {
|
||||
replicateAbilities.clear();
|
||||
}
|
||||
|
||||
return applied;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EachSpellYouCastHasReplicateEffect copy() {
|
||||
return new EachSpellYouCastHasReplicateEffect(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package mage.abilities.effects.common.continuous;
|
|||
import mage.abilities.Ability;
|
||||
import mage.abilities.ActivatedAbility;
|
||||
import mage.abilities.Mode;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.abilities.effects.ContinuousEffectImpl;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Layer;
|
||||
|
|
@ -24,6 +25,7 @@ public class GainControlTargetEffect extends ContinuousEffectImpl {
|
|||
protected UUID controllingPlayerId;
|
||||
private boolean fixedControl;
|
||||
private boolean firstControlChange = true;
|
||||
private final Condition condition;
|
||||
|
||||
public GainControlTargetEffect(Duration duration) {
|
||||
this(duration, false, null);
|
||||
|
|
@ -48,15 +50,21 @@ public class GainControlTargetEffect extends ContinuousEffectImpl {
|
|||
}
|
||||
|
||||
public GainControlTargetEffect(Duration duration, boolean fixedControl, UUID controllingPlayerId) {
|
||||
this(duration, fixedControl, controllingPlayerId, null);
|
||||
}
|
||||
|
||||
public GainControlTargetEffect(Duration duration, boolean fixedControl, UUID controllingPlayerId, Condition condition) {
|
||||
super(duration, Layer.ControlChangingEffects_2, SubLayer.NA, Outcome.GainControl);
|
||||
this.controllingPlayerId = controllingPlayerId;
|
||||
this.fixedControl = fixedControl;
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
public GainControlTargetEffect(final GainControlTargetEffect effect) {
|
||||
super(effect);
|
||||
this.controllingPlayerId = effect.controllingPlayerId;
|
||||
this.fixedControl = effect.fixedControl;
|
||||
this.condition = effect.condition;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -106,6 +114,9 @@ public class GainControlTargetEffect extends ContinuousEffectImpl {
|
|||
// This does not handle correctly multiple targets at once
|
||||
discard();
|
||||
}
|
||||
if (condition != null && !condition.apply(game, source)) {
|
||||
discard();
|
||||
}
|
||||
}
|
||||
// no valid target exists and the controller is no longer in the game, effect can be discarded
|
||||
if (!oneTargetStillExists || !controller.isInGame()) {
|
||||
|
|
|
|||
|
|
@ -2,34 +2,55 @@ package mage.abilities.effects.common.counter;
|
|||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.choices.Choice;
|
||||
import mage.choices.ChoiceImpl;
|
||||
import mage.constants.Outcome;
|
||||
import mage.counters.Counter;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class AddCounterChoiceSourceEffect extends OneShotEffect {
|
||||
|
||||
private final CounterType counterType1;
|
||||
private final CounterType counterType2;
|
||||
private final List<CounterType> counterTypes;
|
||||
|
||||
public AddCounterChoiceSourceEffect(CounterType counterType1, CounterType counterType2) {
|
||||
public AddCounterChoiceSourceEffect(CounterType ... counterTypes) {
|
||||
super(Outcome.Benefit);
|
||||
this.counterType1 = counterType1;
|
||||
this.counterType2 = counterType2;
|
||||
staticText = "with your choice of a " + counterType1 + " counter or a " + counterType2 + " counter on it";
|
||||
this.counterTypes = Arrays.stream(counterTypes).collect(Collectors.toList());
|
||||
this.createStaticText();
|
||||
}
|
||||
|
||||
private AddCounterChoiceSourceEffect(final AddCounterChoiceSourceEffect effect) {
|
||||
super(effect);
|
||||
this.counterType1 = effect.counterType1;
|
||||
this.counterType2 = effect.counterType2;
|
||||
this.counterTypes = new ArrayList<>(effect.counterTypes);
|
||||
}
|
||||
|
||||
private void createStaticText() {
|
||||
switch (this.counterTypes.size()) {
|
||||
case 0:
|
||||
case 1:
|
||||
throw new IllegalArgumentException("AddCounterChoiceSourceEffect should be called with at least 2 " +
|
||||
"counter types, it was called with: " + this.counterTypes);
|
||||
case 2:
|
||||
this.staticText = "with your choice of a " + this.counterTypes.get(0) +
|
||||
" counter or a " + this.counterTypes.get(1) + " counter on it";
|
||||
break;
|
||||
default:
|
||||
List<String> strings = this.counterTypes.stream().map(CounterType::toString).collect(Collectors.toList());
|
||||
this.staticText = CardUtil.concatWithOr(strings);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -39,19 +60,30 @@ public class AddCounterChoiceSourceEffect extends OneShotEffect {
|
|||
if (player == null || permanent == null) {
|
||||
return false;
|
||||
}
|
||||
Counter counter;
|
||||
if (player.chooseUse(
|
||||
Outcome.Neutral, "Choose " + counterType1 + " or " + counterType2, null,
|
||||
cap(counterType1.getName()), cap(counterType2.getName()), source, game
|
||||
)) {
|
||||
counter = counterType1.createInstance();
|
||||
} else {
|
||||
counter = counterType2.createInstance();
|
||||
|
||||
Choice counterChoice = new ChoiceImpl();
|
||||
counterChoice.setMessage("Choose counter type");
|
||||
counterChoice.setChoices(
|
||||
this.counterTypes
|
||||
.stream()
|
||||
.map(counterType -> AddCounterChoiceSourceEffect.capitalize(counterType.getName()))
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
|
||||
if (!player.choose(Outcome.Neutral, counterChoice, game)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CounterType counterChosen = CounterType.findByName(counterChoice.getChoice().toLowerCase(Locale.ENGLISH));
|
||||
if (counterChosen == null || !this.counterTypes.contains(counterChosen)) {
|
||||
return false;
|
||||
}
|
||||
Counter counter = counterChosen.createInstance();
|
||||
|
||||
return permanent.addCounters(counter, source.getControllerId(), source, game);
|
||||
}
|
||||
|
||||
private static final String cap(String string) {
|
||||
private static String capitalize(String string) {
|
||||
return string != null ? string.substring(0, 1).toUpperCase(Locale.ENGLISH) + string.substring(1) : null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue