Rework sacrifice effects to support "can't be sacrificed" (#11587)

* add TargetSacrifice and canBeSacrificed

* SacrificeTargetCost refactor, now uses TargetSacrifice, constructors simplified, subclasses aligned

* fix text errors introduced by refactor

* refactor SacrificeEffect, SacrificeAllEffect, SacrificeOpponentsEffect

* cleanup keyword abilities involving sacrifice

* fix a bunch of custom effect classes involving sacrifice

* fix test choices

* update Assault Suit implementation

* fix filter check arguments

* add documentation to refactored common classes

* [CLB] Implement Jon Irenicus, Shattered One

* implement "{this} can't be sacrificed"

* add tests for Assault Suit and Jon Irenicus

* refactor out PlayerToRightGainsControlOfSourceEffect

* implement [LTC] Hithlain Rope

* add choose hint to all TargetSacrifice

---------

Co-authored-by: Evan Kranzler <theelk801@gmail.com>
Co-authored-by: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com>
This commit is contained in:
xenohedron 2023-12-31 14:10:37 -05:00 committed by GitHub
parent f28c5c4fc5
commit 9b3ff32a33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
699 changed files with 1837 additions and 1619 deletions

View file

@ -50,10 +50,7 @@ public class SacrificeAllCost extends CostImpl implements SacrificeCost {
if (ability.getAbilityType() == AbilityType.ACTIVATED || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) {
if (((ActivatedAbilityImpl) ability).getActivatorId() != null) {
activator = ((ActivatedAbilityImpl) ability).getActivatorId();
} else {
// Aktivator not filled?
activator = controllerId;
}
} // else, Activator not filled?
}
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, controllerId, game)) {

View file

@ -47,6 +47,14 @@ public class SacrificeAttachmentCost extends UseAttachedCost implements Sacrific
return paid;
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
if (!super.canPay(ability, source, controllerId, game)) {
return false;
}
return game.getPermanent(source.getSourceId()).canBeSacrificed();
}
@Override
public SacrificeAttachmentCost copy() {
return new SacrificeAttachmentCost(this);

View file

@ -7,10 +7,12 @@ import mage.abilities.costs.CostImpl;
import mage.abilities.costs.SacrificeCost;
import mage.constants.AbilityType;
import mage.constants.Outcome;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.TargetPermanent;
import mage.target.common.TargetControlledPermanent;
import mage.target.common.TargetSacrifice;
import mage.util.CardUtil;
import java.util.ArrayList;
@ -24,20 +26,32 @@ public class SacrificeTargetCost extends CostImpl implements SacrificeCost {
private final List<Permanent> permanents = new ArrayList<>();
public SacrificeTargetCost(FilterControlledPermanent filter) {
this(new TargetControlledPermanent(filter));
/**
* Sacrifice a permanent matching the filter:
* @param filter can be generic, will automatically add article and sacrifice predicates
*/
public SacrificeTargetCost(FilterPermanent filter) {
this(1, filter);
}
/**
* Sacrifice N permanents matching the filter:
* @param filter can be generic, will automatically add sacrifice predicates
*/
public SacrificeTargetCost(int numToSac, FilterPermanent filter) {
this(new TargetSacrifice(numToSac, filter));
}
// remove once merge complete
@Deprecated
public SacrificeTargetCost(TargetControlledPermanent target) {
throw new UnsupportedOperationException("Wrong code usage, refactor to TargetSacrifice");
}
public SacrificeTargetCost(TargetSacrifice target) {
this.addTarget(target);
target.withNotTarget(true); // sacrifice is never targeted
target.setRequired(false); // can be canceled
this.text = "sacrifice " + makeText(target);
target.setTargetName(target.getTargetName() + " (to sacrifice)");
}
public SacrificeTargetCost(TargetControlledPermanent target, boolean noText) {
this.addTarget(target);
}
public SacrificeTargetCost(SacrificeTargetCost cost) {
@ -70,6 +84,9 @@ public class SacrificeTargetCost extends CostImpl implements SacrificeCost {
return paid;
}
/**
* For storing additional info upon selecting permanents to sacrifice
*/
protected void addSacrificeTarget(Game game, Permanent permanent) {
permanents.add(permanent.copy());
}
@ -80,18 +97,15 @@ public class SacrificeTargetCost extends CostImpl implements SacrificeCost {
if (ability.getAbilityType() == AbilityType.ACTIVATED || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) {
if (((ActivatedAbilityImpl) ability).getActivatorId() != null) {
activator = ((ActivatedAbilityImpl) ability).getActivatorId();
} else {
// Activator not filled?
activator = controllerId;
}
} // else, Activator not filled?
}
int validTargets = 0;
int neededtargets = this.getTargets().get(0).getNumberOfTargets();
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(((TargetControlledPermanent) this.getTargets().get(0)).getFilter(), controllerId, game)) {
int neededTargets = this.getTargets().get(0).getNumberOfTargets();
for (Permanent permanent : game.getBattlefield().getActivePermanents(((TargetPermanent) this.getTargets().get(0)).getFilter(), controllerId, source, game)) {
if (game.getPlayer(activator).canPaySacrificeCost(permanent, source, controllerId, game)) {
validTargets++;
if (validTargets >= neededtargets) {
if (validTargets >= neededTargets) {
return true;
}
}
@ -112,7 +126,7 @@ public class SacrificeTargetCost extends CostImpl implements SacrificeCost {
return permanents;
}
private static String makeText(TargetControlledPermanent target) {
private static String makeText(TargetSacrifice target) {
if (target.getMinNumberOfTargets() != target.getMaxNumberOfTargets()) {
return target.getTargetName();
}

View file

@ -5,28 +5,27 @@ import mage.abilities.costs.Cost;
import mage.abilities.costs.SacrificeCost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.filter.Filter;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.target.common.TargetControlledPermanent;
import mage.target.common.TargetSacrifice;
/**
* @author LevelX2
*/
public class SacrificeXTargetCost extends VariableCostImpl implements SacrificeCost {
protected final FilterControlledPermanent filter;
protected final FilterPermanent filter;
private final int minValue;
public SacrificeXTargetCost(FilterControlledPermanent filter) {
public SacrificeXTargetCost(FilterPermanent filter) {
this(filter, false);
}
public SacrificeXTargetCost(FilterControlledPermanent filter, boolean useAsAdditionalCost) {
public SacrificeXTargetCost(FilterPermanent filter, boolean useAsAdditionalCost) {
this(filter, useAsAdditionalCost, 0);
}
public SacrificeXTargetCost(FilterControlledPermanent filter, boolean useAsAdditionalCost, int minValue) {
public SacrificeXTargetCost(FilterPermanent filter, boolean useAsAdditionalCost, int minValue) {
super(useAsAdditionalCost ? VariableCostType.ADDITIONAL : VariableCostType.NORMAL,
filter.getMessage() + " to sacrifice");
this.text = (useAsAdditionalCost ? "as an additional cost to cast this spell, sacrifice " : "Sacrifice ") + xText + ' ' + filter.getMessage();
@ -57,11 +56,10 @@ public class SacrificeXTargetCost extends VariableCostImpl implements SacrificeC
@Override
public Cost getFixedCostsFromAnnouncedValue(int xValue) {
TargetControlledPermanent target = new TargetControlledPermanent(xValue, xValue, filter, true);
return new SacrificeTargetCost(target);
return new SacrificeTargetCost(new TargetSacrifice(xValue, filter));
}
public Filter getFilter() {
public FilterPermanent getFilter() {
return filter;
}

View file

@ -15,7 +15,7 @@ import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetControlledPermanent;
import mage.target.common.TargetSacrifice;
import mage.util.CardUtil;
import java.util.ArrayList;
@ -88,8 +88,7 @@ public class DevourEffect extends ReplacementEffectImpl {
}
filter.add(AnotherPredicate.instance);
Target target = new TargetControlledPermanent(1, Integer.MAX_VALUE, filter, true);
target.setRequired(false);
Target target = new TargetSacrifice(1, Integer.MAX_VALUE, filter);
if (!target.canChoose(source.getControllerId(), source, game)) {
return false;
}

View file

@ -0,0 +1,53 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.continuous.GainControlTargetEffect;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
* @author xenohedron
*/
public class PlayerToRightGainsControlOfSourceEffect extends OneShotEffect {
public PlayerToRightGainsControlOfSourceEffect() {
super(Outcome.Detriment);
this.staticText = "the player to your right gains control of {this}";
}
protected PlayerToRightGainsControlOfSourceEffect(final PlayerToRightGainsControlOfSourceEffect effect) {
super(effect);
}
@Override
public PlayerToRightGainsControlOfSourceEffect copy() {
return new PlayerToRightGainsControlOfSourceEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
return true;
}
UUID playerToRightId = game
.getState()
.getPlayersInRange(source.getControllerId(), game)
.stream()
.reduce((u1, u2) -> u2)
.orElse(null);
if (playerToRightId == null) {
return false;
}
game.addEffect(new GainControlTargetEffect(
Duration.Custom, true, playerToRightId
).setTargetPointer(new FixedTarget(permanent, game)), source);
return true;
}
}

View file

@ -5,15 +5,15 @@ import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetControlledPermanent;
import mage.target.common.TargetSacrifice;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
@ -21,21 +21,42 @@ import java.util.UUID;
*/
public class SacrificeAllEffect extends OneShotEffect {
protected DynamicValue amount;
protected FilterControlledPermanent filter;
private final DynamicValue amount;
private final FilterPermanent filter;
private final boolean onlyOpponents;
public SacrificeAllEffect(FilterControlledPermanent filter) {
/**
* Each player sacrifices a permanent
* @param filter can be generic, will automatically add article and necessary sacrifice predicates
*/
public SacrificeAllEffect(FilterPermanent filter) {
this(1, filter);
}
public SacrificeAllEffect(int amount, FilterControlledPermanent filter) {
/**
* Each player sacrifices N permanents
* @param filter can be generic, will automatically add necessary sacrifice predicates
*/
public SacrificeAllEffect(int amount, FilterPermanent filter) {
this(StaticValue.get(amount), filter);
}
public SacrificeAllEffect(DynamicValue amount, FilterControlledPermanent filter) {
/**
* Each player sacrifices X permanents
* @param filter can be generic, will automatically add necessary sacrifice predicates
*/
public SacrificeAllEffect(DynamicValue amount, FilterPermanent filter) {
this(amount, filter, false);
}
/**
* Internal use for this and SacrificeOpponentsEffect
*/
protected SacrificeAllEffect(DynamicValue amount, FilterPermanent filter, boolean onlyOpponents) {
super(Outcome.Sacrifice);
this.amount = amount;
this.filter = filter;
this.onlyOpponents = onlyOpponents;
setText();
}
@ -43,6 +64,7 @@ public class SacrificeAllEffect extends OneShotEffect {
super(effect);
this.amount = effect.amount;
this.filter = effect.filter.copy();
this.onlyOpponents = effect.onlyOpponents;
}
@Override
@ -52,24 +74,27 @@ public class SacrificeAllEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
int num = amount.calculate(game, source, this);
if (num < 1) {
return false;
}
List<UUID> perms = new ArrayList<>();
for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) {
Set<UUID> perms = new HashSet<>();
for (UUID playerId : onlyOpponents ?
game.getOpponents(source.getControllerId()) :
game.getState().getPlayersInRange(source.getControllerId(), game)) {
Player player = game.getPlayer(playerId);
if (player != null) {
int numTargets = Math.min(amount.calculate(game, source, this), game.getBattlefield().countAll(filter, player.getId(), game));
TargetControlledPermanent target = new TargetControlledPermanent(numTargets, numTargets, filter, true);
if (target.canChoose(player.getId(), source, game)) {
while (!target.isChosen() && player.canRespond()) {
player.choose(Outcome.Sacrifice, target, source, game);
}
perms.addAll(target.getTargets());
}
if (player == null) {
continue;
}
int numTargets = Math.min(num, game.getBattlefield().count(TargetSacrifice.makeFilter(filter), player.getId(), source, game));
if (numTargets < 1) {
continue;
}
TargetSacrifice target = new TargetSacrifice(numTargets, filter);
while (!target.isChosen() && target.canChoose(player.getId(), source, game) && player.canRespond()) {
player.choose(Outcome.Sacrifice, target, source, game);
}
perms.addAll(target.getTargets());
}
for (UUID permID : perms) {
Permanent permanent = game.getPermanent(permID);
@ -82,17 +107,20 @@ public class SacrificeAllEffect extends OneShotEffect {
private void setText() {
StringBuilder sb = new StringBuilder();
sb.append("each player sacrifices ");
if (amount.toString().equals("X")) {
sb.append(amount.toString());
sb.append(' ');
sb.append(filter.getMessage());
} else if (amount.toString().equals("1")) {
sb.append(CardUtil.addArticle(filter.getMessage()));
} else {
sb.append(CardUtil.numberToText(amount.toString(), "a"));
sb.append(' ');
sb.append(filter.getMessage());
sb.append(onlyOpponents ? "each opponent sacrifices " : "each player sacrifices ");
switch (amount.toString()) {
case "X":
sb.append(amount.toString());
sb.append(' ');
sb.append(filter.getMessage());
break;
case "1":
sb.append(CardUtil.addArticle(filter.getMessage()));
break;
default:
sb.append(CardUtil.numberToText(amount.toString(), "a"));
sb.append(' ');
sb.append(filter.getMessage());
}
staticText = sb.toString();
}

View file

@ -6,12 +6,10 @@ import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.filter.FilterPermanent;
import mage.filter.predicate.permanent.ControllerIdPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetPermanent;
import mage.target.common.TargetSacrifice;
import mage.util.CardUtil;
import java.util.UUID;
@ -21,14 +19,20 @@ import java.util.UUID;
*/
public class SacrificeEffect extends OneShotEffect {
private FilterPermanent filter;
private String preText;
private final FilterPermanent filter;
private final String preText;
private DynamicValue count;
/**
* Target player sacrifices N permanents matching the filter
*/
public SacrificeEffect(FilterPermanent filter, int count, String preText) {
this(filter, StaticValue.get(count), preText);
}
/**
* Target player sacrifices X permanents matching the filter
*/
public SacrificeEffect(FilterPermanent filter, DynamicValue count, String preText) {
super(Outcome.Sacrifice);
this.filter = filter;
@ -49,26 +53,24 @@ public class SacrificeEffect extends OneShotEffect {
boolean applied = false;
for (UUID playerId : targetPointer.getTargets(game, source)) {
Player player = game.getPlayer(playerId);
if (player != null) {
FilterPermanent newFilter = filter.copy(); // filter can be static, so it's important to copy here
newFilter.add(new ControllerIdPredicate(player.getId()));
int amount = count.calculate(game, source, this);
int realCount = game.getBattlefield().countAll(newFilter, player.getId(), game);
amount = Math.min(amount, realCount);
Target target = new TargetPermanent(amount, amount, newFilter, true);
if (amount > 0 && target.canChoose(player.getId(), source, game)) {
while (!target.isChosen()
&& target.canChoose(player.getId(), source, game)
&& player.canRespond()) {
player.chooseTarget(Outcome.Sacrifice, target, source, game);
}
for (int idx = 0; idx < target.getTargets().size(); idx++) {
Permanent permanent = game.getPermanent(target.getTargets().get(idx));
if (permanent != null
&& permanent.sacrifice(source, game)) {
applied = true;
}
}
if (player == null) {
continue;
}
int amount = Math.min(
count.calculate(game, source, this),
game.getBattlefield().count(TargetSacrifice.makeFilter(filter), player.getId(), source, game)
);
if (amount < 1) {
continue;
}
TargetSacrifice target = new TargetSacrifice(amount, filter);
while (!target.isChosen() && target.canChoose(player.getId(), source, game) && player.canRespond()) {
player.choose(Outcome.Sacrifice, target, source, game);
}
for (UUID targetId : target.getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null && permanent.sacrifice(source, game)) {
applied = true;
}
}
}

View file

@ -1,52 +1,37 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.constants.TargetController;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* All opponents have to sacrifice [amount] permanents that match the [filter].
*
* @author LevelX2
*/
public class SacrificeOpponentsEffect extends OneShotEffect {
protected DynamicValue amount;
protected FilterPermanent filter;
public class SacrificeOpponentsEffect extends SacrificeAllEffect {
/**
* Each opponent sacrifices a permanent
* @param filter can be generic, will automatically add article and necessary sacrifice predicates
*/
public SacrificeOpponentsEffect(FilterPermanent filter) {
this(1, filter);
}
/**
* Each opponent sacrifices N permanents
* @param filter can be generic, will automatically add necessary sacrifice predicates
*/
public SacrificeOpponentsEffect(int amount, FilterPermanent filter) {
this(StaticValue.get(amount), filter);
}
/**
* Each opponent sacrifices X permanents
* @param filter can be generic, will automatically add necessary sacrifice predicates
*/
public SacrificeOpponentsEffect(DynamicValue amount, FilterPermanent filter) {
super(Outcome.Sacrifice);
this.amount = amount;
this.filter = filter.copy();
this.filter.add(TargetController.YOU.getControllerPredicate());
super(amount, filter, true);
}
protected SacrificeOpponentsEffect(final SacrificeOpponentsEffect effect) {
super(effect);
this.amount = effect.amount;
this.filter = effect.filter.copy();
}
@Override
@ -54,48 +39,4 @@ public class SacrificeOpponentsEffect extends OneShotEffect {
return new SacrificeOpponentsEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
List<UUID> perms = new ArrayList<>();
for (UUID playerId : game.getOpponents(source.getControllerId())) {
Player player = game.getPlayer(playerId);
if (player != null) {
int numTargets = Math.min(amount.calculate(game, source, this), game.getBattlefield().countAll(filter, player.getId(), game));
if (numTargets > 0) {
TargetPermanent target = new TargetPermanent(numTargets, numTargets, filter, true);
if (target.canChoose(player.getId(), source, game)) {
player.chooseTarget(Outcome.Sacrifice, target, source, game);
perms.addAll(target.getTargets());
}
}
}
}
for (UUID permID : perms) {
Permanent permanent = game.getPermanent(permID);
if (permanent != null) {
permanent.sacrifice(source, game);
}
}
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
StringBuilder sb = new StringBuilder();
sb.append("each opponent sacrifices ");
switch (amount.toString()) {
case "X":
sb.append(amount.toString()).append(' ');
break;
case "1":
sb.append(CardUtil.addArticle(filter.getMessage()));
break;
default:
sb.append(CardUtil.numberToText(amount.toString())).append(' ').append(filter.getMessage());
}
return sb.toString();
}
}

View file

@ -1,26 +1,21 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.costs.Cost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.constants.TargetController;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.common.TargetSacrifice;
import mage.util.CardUtil;
import mage.util.ManaUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
@ -60,45 +55,41 @@ public class SacrificeOpponentsUnlessPayEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
List<UUID> permsToSacrifice = new ArrayList<>();
filter.add(TargetController.YOU.getControllerPredicate());
Set<UUID> permsToSacrifice = new HashSet<>();
for (UUID playerId : game.getOpponents(source.getControllerId())) {
Player player = game.getPlayer(playerId);
if (player == null) {
continue;
}
if (player != null) {
Cost costToPay = cost.copy();
String costValueMessage = costToPay.getText();
String message = ((costToPay instanceof ManaCost) ? "Pay " : "") + costValueMessage + '?';
Cost costToPay = cost.copy();
String costValueMessage = costToPay.getText();
String message = ((costToPay instanceof ManaCost) ? "Pay " : "") + costValueMessage + '?';
costToPay.clearPaid();
if (!(player.chooseUse(Outcome.Benefit, message, source, game)
&& costToPay.pay(source, game, source, player.getId(), false, null))) {
game.informPlayers(player.getLogName() + " chooses not to pay " + costValueMessage + " to prevent the sacrifice effect");
costToPay.clearPaid();
if (!(player.chooseUse(Outcome.Benefit, message, source, game)
&& costToPay.pay(source, game, source, player.getId(), false, null))) {
game.informPlayers(player.getLogName() + " chooses not to pay " + costValueMessage + " to prevent the sacrifice effect");
int numTargets = Math.min(1, game.getBattlefield().countAll(filter, player.getId(), game));
if (numTargets > 0) {
TargetPermanent target = new TargetPermanent(numTargets, numTargets, filter, true);
if (target.canChoose(player.getId(), source, game)) {
player.chooseTarget(Outcome.Sacrifice, target, source, game);
permsToSacrifice.addAll(target.getTargets());
}
if (game.getBattlefield().count(TargetSacrifice.makeFilter(filter), player.getId(), source, game) > 0) {
TargetSacrifice target = new TargetSacrifice(1, filter);
if (target.canChoose(player.getId(), source, game)) {
player.choose(Outcome.Sacrifice, target, source, game);
permsToSacrifice.addAll(target.getTargets());
}
} else {
game.informPlayers(player.getLogName() + " chooses to pay " + costValueMessage + " to prevent the sacrifice effect");
}
} else {
game.informPlayers(player.getLogName() + " chooses to pay " + costValueMessage + " to prevent the sacrifice effect");
}
}
for (UUID permID : permsToSacrifice) {
Permanent permanent = game.getPermanent(permID);
if (permanent != null) {
permanent.sacrifice(source, game);
}
}
return true;
}
}

View file

@ -1,8 +1,6 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.condition.Condition;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
@ -20,6 +18,7 @@ public class SacrificeSourceUnlessConditionEffect extends OneShotEffect {
public SacrificeSourceUnlessConditionEffect(Condition condition) {
super(Outcome.Sacrifice);
this.condition = condition;
this.staticText = "sacrifice {this} unless " + condition.toString();
}
protected SacrificeSourceUnlessConditionEffect(final SacrificeSourceUnlessConditionEffect effect) {
@ -46,13 +45,4 @@ public class SacrificeSourceUnlessConditionEffect extends OneShotEffect {
return new SacrificeSourceUnlessConditionEffect(this);
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
StringBuilder sb = new StringBuilder("sacrifice {this} unless ");
sb.append(condition.toString());
return sb.toString();
}
}

View file

@ -0,0 +1,45 @@
package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author xenohedron
*/
public class CantBeSacrificedSourceEffect extends ContinuousEffectImpl {
public CantBeSacrificedSourceEffect() {
super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit);
staticText = "{this} can't be sacrificed";
}
protected CantBeSacrificedSourceEffect(final CantBeSacrificedSourceEffect effect) {
super(effect);
}
@Override
public CantBeSacrificedSourceEffect copy() {
return new CantBeSacrificedSourceEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanentEntering(source.getSourceId());
if (permanent == null) {
permanent = source.getSourcePermanentIfItStillExists(game);
}
if (permanent == null) {
discard();
return false;
}
permanent.setCanBeSacrificed(false);
return true;
}
}

View file

@ -1,20 +1,14 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.abilities.effects.common.SacrificeEffect;
import mage.constants.Zone;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetControlledPermanent;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.UUID;
/**
@ -29,16 +23,17 @@ import java.util.UUID;
*/
public class AnnihilatorAbility extends TriggeredAbilityImpl {
int count;
String rule;
public AnnihilatorAbility(int count) {
super(Zone.BATTLEFIELD, new AnnihilatorEffect(count), false);
this.count = count;
super(Zone.BATTLEFIELD, new SacrificeEffect(StaticFilters.FILTER_CONTROLLED_PERMANENTS, count, ""), false);
this.rule = "Annihilator " + count + " <i>(Whenever this creature attacks, defending player sacrifices "
+ (count == 1 ? "a permanent" : CardUtil.numberToText(count) + " permanents") + ".)</i>";
}
protected AnnihilatorAbility(final AnnihilatorAbility ability) {
super(ability);
this.count = ability.count;
this.rule = ability.rule;
}
@Override
@ -52,9 +47,7 @@ public class AnnihilatorAbility extends TriggeredAbilityImpl {
UUID defendingPlayerId = game.getCombat().getDefendingPlayerId(sourceId, game);
if (defendingPlayerId != null) {
// the id has to be set here because the source can be leave battlefield
getEffects().forEach((effect) -> {
effect.setValue("defendingPlayerId", defendingPlayerId);
});
getEffects().setTargetPointer(new FixedTarget(defendingPlayerId));
return true;
}
}
@ -63,8 +56,7 @@ public class AnnihilatorAbility extends TriggeredAbilityImpl {
@Override
public String getRule() {
return "Annihilator " + count + " <i>(Whenever this creature attacks, defending player sacrifices "
+ (count == 1 ? "a permanent" : CardUtil.numberToText(count) + " permanents") + ".)</i>";
return rule;
}
@Override
@ -73,52 +65,3 @@ public class AnnihilatorAbility extends TriggeredAbilityImpl {
}
}
class AnnihilatorEffect extends OneShotEffect {
private final int count;
AnnihilatorEffect(int count) {
super(Outcome.Sacrifice);
this.count = count;
}
AnnihilatorEffect(AnnihilatorEffect effect) {
super(effect);
this.count = effect.count;
}
@Override
public boolean apply(Game game, Ability source) {
UUID defendingPlayerId = (UUID) getValue("defendingPlayerId");
Player player = null;
if (defendingPlayerId != null) {
player = game.getPlayer(defendingPlayerId);
}
if (player != null) {
int amount = Math.min(count, game.getBattlefield().countAll(new FilterControlledPermanent(), player.getId(), game));
if (amount > 0) {
Target target = new TargetControlledPermanent(amount, amount, new FilterControlledPermanent(), true);
if (target.canChoose(player.getId(), source, game)) {
while (player.canRespond()
&& target.canChoose(player.getId(), source, game)
&& !target.isChosen()) {
player.choose(Outcome.Sacrifice, target, source, game);
}
target.getTargets().stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.forEach(permanent -> permanent.sacrifice(source, game));
}
return true;
}
}
return false;
}
@Override
public AnnihilatorEffect copy() {
return new AnnihilatorEffect(this);
}
}

View file

@ -15,7 +15,7 @@ import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.mageobject.PowerPredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetControlledPermanent;
import mage.target.common.TargetSacrifice;
/**
* @author TheElk801, Alex-Vasile
@ -28,12 +28,12 @@ public class CasualtyAbility extends StaticAbility implements OptionalAdditional
protected OptionalAdditionalCost additionalCost;
private static TargetControlledPermanent makeFilter(int number) {
private static TargetSacrifice makeFilter(int number) {
FilterControlledPermanent filter = new FilterControlledCreaturePermanent(
"creature with power " + number + " or greater"
);
filter.add(new PowerPredicate(ComparisonType.MORE_THAN, number - 1));
return new TargetControlledPermanent(1, 1, filter, true);
return new TargetSacrifice(1, filter);
}
public CasualtyAbility(int number) {

View file

@ -12,12 +12,14 @@ import mage.cards.Card;
import mage.constants.Outcome;
import mage.constants.SpellAbilityType;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.predicate.permanent.CanBeSacrificedPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetSacrifice;
import mage.util.CardUtil;
import java.util.UUID;
@ -30,6 +32,11 @@ public class EmergeAbility extends SpellAbility {
private final ManaCosts<ManaCost> emergeCost;
public static final String EMERGE_ACTIVATION_CREATURE_REFERENCE = "emergeActivationMOR";
private static final FilterPermanent SAC_FILTER = new FilterControlledCreaturePermanent();
static {
SAC_FILTER.add(CanBeSacrificedPredicate.instance);
}
public EmergeAbility(Card card, String emergeString) {
super(card.getSpellAbility());
this.emergeCost = new ManaCostsImpl<>(emergeString);
@ -56,7 +63,7 @@ public class EmergeAbility extends SpellAbility {
Player controller = game.getPlayer(this.getControllerId());
if (controller != null) {
for (Permanent creature : game.getBattlefield().getActivePermanents(
new FilterControlledCreaturePermanent(), this.getControllerId(), this, game)) {
SAC_FILTER, this.getControllerId(), this, game)) {
ManaCost costToPay = CardUtil.reduceCost(emergeCost.copy(), creature.getManaValue());
if (costToPay.canPay(this, this, this.getControllerId(), game)) {
return new ActivationStatus(new ApprovingObject(this, game));
@ -91,7 +98,8 @@ public class EmergeAbility extends SpellAbility {
public boolean activate(Game game, boolean noMana) {
Player controller = game.getPlayer(this.getControllerId());
if (controller != null) {
TargetPermanent target = new TargetControlledCreaturePermanent(new FilterControlledCreaturePermanent("creature to sacrifice for emerge"));
TargetSacrifice target = new TargetSacrifice(StaticFilters.FILTER_PERMANENT_A_CREATURE);
target.withChooseHint("to sacrifice for emerge");
if (controller.choose(Outcome.Sacrifice, target, this, game)) {
Permanent creature = game.getPermanent(target.getFirstTarget());
if (creature != null) {

View file

@ -5,13 +5,13 @@ import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetPermanent;
import mage.target.common.TargetSacrifice;
/**
* Exploit is the signature ability of the blue-black Silumgar clan. When a creature with exploit
@ -71,14 +71,13 @@ class ExploitEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
Target target = new TargetPermanent(1, 1, new FilterControlledCreaturePermanent("creature to exploit"), true);
Target target = new TargetSacrifice(StaticFilters.FILTER_PERMANENT_A_CREATURE);
target.withChooseHint("to exploit");
if (target.canChoose(controller.getId(), source, game)) {
controller.chooseTarget(Outcome.Sacrifice, target, source, game);
controller.choose(Outcome.Sacrifice, target, source, game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent != null) {
if (permanent.sacrifice(source, game)) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.EXPLOITED_CREATURE, permanent.getId(), source, controller.getId()));
}
if (permanent != null && (permanent.sacrifice(source, game))) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.EXPLOITED_CREATURE, permanent.getId(), source, controller.getId()));
}
}
return true;

View file

@ -17,7 +17,7 @@ import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetPermanent;
import mage.target.common.TargetSacrifice;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import mage.util.GameLog;
@ -175,8 +175,8 @@ class OfferingAsThoughEffect extends AsThoughEffectImpl {
Player player = game.getPlayer(source.getControllerId());
if (player != null
&& player.chooseUse(Outcome.Benefit, "Offer a " + filter.getMessage() + " to cast " + spellToCast.getName() + '?', source, game)) {
Target target = new TargetPermanent(1, 1, filter, true);
player.chooseTarget(Outcome.Sacrifice, target, source, game);
Target target = new TargetSacrifice(filter);
player.choose(Outcome.Sacrifice, target, source, game);
if (!target.isChosen()) {
return false;
}

View file

@ -0,0 +1,17 @@
package mage.filter.predicate.permanent;
import mage.filter.predicate.Predicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
*/
public enum CanBeSacrificedPredicate implements Predicate<Permanent> {
instance;
@Override
public boolean apply(Permanent input, Game game) {
return input.canBeSacrificed();
}
}

View file

@ -22,6 +22,7 @@ import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.common.TargetControlledPermanent;
import mage.target.common.TargetDiscard;
import mage.target.common.TargetSacrifice;
import java.util.*;
@ -162,7 +163,7 @@ class OublietteEffect extends OneShotEffect {
}
}
class OublietteTarget extends TargetControlledPermanent {
class OublietteTarget extends TargetSacrifice {
private static final CardTypeAssignment cardTypeAssigner = new CardTypeAssignment(
CardType.ARTIFACT,
@ -176,7 +177,7 @@ class OublietteTarget extends TargetControlledPermanent {
}
OublietteTarget(int numTargets) {
super(numTargets, numTargets, filter, true);
super(numTargets, filter);
}
private OublietteTarget(final OublietteTarget target) {
@ -263,4 +264,3 @@ class SandfallCellEffect extends OneShotEffect {
return true;
}
}

View file

@ -101,6 +101,10 @@ public interface Permanent extends Card, Controllable {
boolean isProtectedBy(UUID playerId);
void setCanBeSacrificed(boolean canBeSacrificed);
boolean canBeSacrificed();
void setCardNumber(String cid);
void setExpansionSetCode(String expansionSetCode);

View file

@ -72,6 +72,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected boolean manifested = false;
protected boolean morphed = false;
protected boolean ringBearerFlag = false;
protected boolean canBeSacrificed = true;
protected int classLevel = 1;
protected final Set<UUID> goadingPlayers = new HashSet<>();
protected UUID originalControllerId;
@ -176,6 +177,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.manifested = permanent.manifested;
this.createOrder = permanent.createOrder;
this.prototyped = permanent.prototyped;
this.canBeSacrificed = permanent.canBeSacrificed;
}
@Override
@ -215,6 +217,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.goadingPlayers.clear();
this.loyaltyActivationsAvailable = 1;
this.legendRuleApplies = true;
this.canBeSacrificed = true;
}
@Override
@ -1361,7 +1364,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override
public boolean sacrifice(Ability source, Game game) {
//20091005 - 701.13
if (isPhasedIn() && !game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SACRIFICE_PERMANENT, objectId, source, controllerId))) {
if (isPhasedIn() && canBeSacrificed && !game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SACRIFICE_PERMANENT, objectId, source, controllerId))) {
// Commander replacement effect or Rest in Peace (exile instead of graveyard) in play does not prevent successful sacrifice
// so the return value of the moveToZone is not taken into account here
moveToZone(Zone.GRAVEYARD, source, game, false);
@ -1773,6 +1776,16 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return protectorId != null && protectorId.equals(playerId);
}
@Override
public void setCanBeSacrificed(boolean canBeSacrificed) {
this.canBeSacrificed = canBeSacrificed;
}
@Override
public boolean canBeSacrificed() {
return canBeSacrificed;
}
@Override
public void setPairedCard(MageObjectReference pairedCard) {
this.pairedPermanent = pairedCard;

View file

@ -4437,7 +4437,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override
public boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game) {
return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, controllerId, source, game);
return permanent.canBeSacrificed() &&
(sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, controllerId, source, game));
}
@Override

View file

@ -0,0 +1,49 @@
package mage.target.common;
import mage.constants.TargetController;
import mage.filter.FilterPermanent;
import mage.filter.predicate.permanent.CanBeSacrificedPredicate;
import mage.target.TargetPermanent;
/**
* @author TheElk801
*/
public class TargetSacrifice extends TargetPermanent {
public TargetSacrifice(FilterPermanent filter) {
this(1, filter);
}
public TargetSacrifice(int numTargets, FilterPermanent filter) {
this(numTargets, numTargets, filter);
}
public TargetSacrifice(int minNumTargets, int maxNumTargets, FilterPermanent filter) {
super(minNumTargets, maxNumTargets, makeFilter(filter), true);
this.withChooseHint("to sacrifice");
}
protected TargetSacrifice(final TargetSacrifice target) {
super(target);
}
@Override
public TargetSacrifice copy() {
return new TargetSacrifice(this);
}
/**
* Creates a new filter with necessary constraints for sacrificing
* @param filter input generic filter
* @return new filter with "you control" and CanBeSacrificedPredicate added
*/
public static FilterPermanent makeFilter(FilterPermanent filter) {
FilterPermanent newFilter = filter.copy();
newFilter.add(TargetController.YOU.getControllerPredicate());
newFilter.add(CanBeSacrificedPredicate.instance);
if (filter.getMessage().contains(" you control")) {
newFilter.setMessage(filter.getMessage().replace(" you control", ""));
}
return newFilter;
}
}

View file

@ -21,11 +21,11 @@ import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public class TargetControlledCreatureEachColor extends TargetControlledPermanent {
public class TargetSacrificeCreatureEachColor extends TargetSacrifice {
private final ColorAssignment colorAssigner;
private static final FilterControlledPermanent makeFilter(String colors) {
private static FilterControlledPermanent makeFilter(String colors) {
List<ObjectColor> objectColors
= Arrays.stream(colors.split(""))
.map(ObjectColor::new)
@ -42,12 +42,12 @@ public class TargetControlledCreatureEachColor extends TargetControlledPermanent
return filter;
}
public TargetControlledCreatureEachColor(String colors) {
public TargetSacrificeCreatureEachColor(String colors) {
super(colors.length(), makeFilter(colors));
colorAssigner = new ColorAssignment(colors.split(""));
}
private TargetControlledCreatureEachColor(final TargetControlledCreatureEachColor target) {
private TargetSacrificeCreatureEachColor(final TargetSacrificeCreatureEachColor target) {
super(target);
this.colorAssigner = target.colorAssigner;
}
@ -70,7 +70,7 @@ public class TargetControlledCreatureEachColor extends TargetControlledPermanent
}
@Override
public TargetControlledCreatureEachColor copy() {
return new TargetControlledCreatureEachColor(this);
public TargetSacrificeCreatureEachColor copy() {
return new TargetSacrificeCreatureEachColor(this);
}
}