[CMM] Implement Demon of Fate's Design (#10737)

* refactor SacrificeCostManaValue to be an enum.

* [CMM] Implement Demon of Fates Design

* Add Unit Tests, including one bug on alternative cost.

* fix alternativeCosts made from dynamicCost returning that they were not activated when paid.

* fix small issues, add hint

* cleanup tests and add a couple

* Capitalize enum instances

* Minor fixes

* simplify the ContinuousEffect

* use the ConditionPermanentHint made for the Demon

* fix text
This commit is contained in:
Susucre 2023-08-12 21:49:06 +02:00 committed by GitHub
parent 0ce21d12a5
commit eef8f508e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 707 additions and 111 deletions

View file

@ -0,0 +1,37 @@
package mage.abilities.condition.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.cards.AdventureCardSpell;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCardHalf;
import mage.cards.SplitCardHalf;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.stack.Spell;
import java.util.UUID;
public enum IsBeingCastFromHandCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
MageObject object = game.getObject(source);
if (object instanceof SplitCardHalf || object instanceof AdventureCardSpell || object instanceof ModalDoubleFacedCardHalf) {
UUID mainCardId = ((Card) object).getMainCard().getId();
object = game.getObject(mainCardId);
}
if (object instanceof Spell) { // needed to check if it can be cast by alternate cost
Spell spell = (Spell) object;
return Zone.HAND.equals(spell.getFromZone());
}
if (object instanceof Card) { // needed for the check what's playable
Card card = (Card) object;
return game.getPlayer(card.getOwnerId()).getHand().get(card.getId(), game) != null;
}
return false;
}
}

View file

@ -24,6 +24,7 @@ import java.util.stream.Collectors;
public class AlternativeCostSourceAbility extends StaticAbility implements AlternativeSourceCosts {
private static final String ALTERNATIVE_COST_ACTIVATION_KEY = "AlternativeCostActivated";
private static final String ALTERNATIVE_DYNAMIC_COST_ACTIVATED_KEY = "AlternativeDynamicCostActivated";
private Costs<AlternativeCost> alternateCosts = new CostsImpl<>();
protected Condition condition;
@ -162,8 +163,13 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter
}
}
// Those cost have been paid, we want to store them.
if (dynamicCost != null) {
rememberDynamicCost(game, ability, alternativeCostsToCheck);
}
// save activated status
game.getState().setValue(getActivatedKey(ability), Boolean.TRUE);
doActivate(game, ability);
} else {
return false;
}
@ -174,6 +180,10 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter
return isActivated(ability, game);
}
protected void doActivate(Game game, Ability ability) {
game.getState().setValue(getActivatedKey(ability), Boolean.TRUE);
}
private String getActivatedKey(Ability source) {
return getActivatedKey(this.getOriginalId(), source.getSourceId(), source.getSourceObjectZoneChangeCounter());
}
@ -184,6 +194,20 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter
return ALTERNATIVE_COST_ACTIVATION_KEY + "_" + alternativeCostOriginalId + "_" /*+ sourceId + "_"*/ + sourceZCC;
}
private void rememberDynamicCost(Game game, Ability ability, Costs<AlternativeCost> costs) {
game.getState().setValue(getDynamicCostActivatedKey(ability), costs);
}
private String getDynamicCostActivatedKey(Ability source) {
return getDynamicCostActivatedKey(this.getOriginalId(), source.getSourceId(), source.getSourceObjectZoneChangeCounter());
}
private static String getDynamicCostActivatedKey(UUID alternativeCostOriginalId, UUID sourceId, int sourceZCC) {
// can't use sourceId cause copied cards are different...
// TODO: enable sourceId after copy card fix (it must copy cards with all related game state values)
return ALTERNATIVE_DYNAMIC_COST_ACTIVATED_KEY + "_" + alternativeCostOriginalId + "_" /*+ sourceId + "_"*/ + sourceZCC;
}
/**
* Search activated status of alternative cost.
* <p>
@ -210,8 +234,10 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter
public boolean isActivated(Ability source, Game game) {
Costs<AlternativeCost> alternativeCostsToCheck;
if (dynamicCost != null) {
alternativeCostsToCheck = new CostsImpl<>();
alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(source, game)));
alternativeCostsToCheck = (Costs<AlternativeCost>) game.getState().getValue(getDynamicCostActivatedKey(source));
if (alternativeCostsToCheck == null) {
return false;
}
} else {
alternativeCostsToCheck = this.alternateCosts;
}

View file

@ -28,7 +28,7 @@ public class PayLifeCost extends CostImpl {
this.text = "pay " + text;
}
public PayLifeCost(PayLifeCost cost) {
protected PayLifeCost(final PayLifeCost cost) {
super(cost);
this.amount = cost.amount.copy();
}

View file

@ -10,27 +10,27 @@ import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author LoneFox
* @author LoneFox, Susucr
*/
public class SacrificeCostConvertedMana implements DynamicValue {
public enum SacrificeCostManaValue implements DynamicValue {
CREATURE("creature"),
ENCHANTMENT("enchantment"),
ARTIFACT("artifact"),
PERMANENT("permanent");
private final String type;
public SacrificeCostConvertedMana(String type) {
private SacrificeCostManaValue(String type) {
this.type = type;
}
public SacrificeCostConvertedMana(SacrificeCostConvertedMana value) {
this.type = value.type;
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
for(Cost cost : sourceAbility.getCosts()) {
if(cost instanceof SacrificeTargetCost) {
for (Cost cost : sourceAbility.getCosts()) {
if (cost instanceof SacrificeTargetCost) {
SacrificeTargetCost sacrificeCost = (SacrificeTargetCost) cost;
int totalCMC = 0;
for(Permanent permanent : sacrificeCost.getPermanents()) {
for (Permanent permanent : sacrificeCost.getPermanents()) {
totalCMC += permanent.getManaValue();
}
return totalCMC;
@ -40,8 +40,8 @@ public class SacrificeCostConvertedMana implements DynamicValue {
}
@Override
public SacrificeCostConvertedMana copy() {
return new SacrificeCostConvertedMana(this);
public SacrificeCostManaValue copy() {
return this;
}
@Override

View file

@ -1,21 +1,19 @@
package mage.abilities.effects.common.continuous;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.condition.CompoundCondition;
import mage.abilities.condition.Condition;
import mage.abilities.condition.common.IsBeingCastFromHandCondition;
import mage.abilities.condition.common.SourceIsSpellCondition;
import mage.abilities.costs.AlternativeCostSourceAbility;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.cards.AdventureCardSpell;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCardHalf;
import mage.cards.SplitCardHalf;
import mage.constants.*;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.stack.Spell;
import mage.players.Player;
import java.util.UUID;
@ -81,26 +79,4 @@ public class CastFromHandWithoutPayingManaCostEffect extends ContinuousEffectImp
public boolean hasLayer(Layer layer) {
return layer == Layer.RulesEffects;
}
}
enum IsBeingCastFromHandCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
MageObject object = game.getObject(source);
if (object instanceof SplitCardHalf || object instanceof AdventureCardSpell || object instanceof ModalDoubleFacedCardHalf) {
UUID mainCardId = ((Card) object).getMainCard().getId();
object = game.getObject(mainCardId);
}
if (object instanceof Spell) { // needed to check if it can be cast by alternate cost
Spell spell = (Spell) object;
return Zone.HAND.equals(spell.getFromZone());
}
if (object instanceof Card) { // needed for the check what's playable
Card card = (Card) object;
return game.getPlayer(card.getOwnerId()).getHand().get(card.getId(), game) != null;
}
return false;
}
}
}

View file

@ -36,7 +36,7 @@ public class ConditionHint implements Hint {
this.useIcons = useIcons;
}
private ConditionHint(final ConditionHint hint) {
protected ConditionHint(final ConditionHint hint) {
this.condition = hint.condition;
this.trueText = hint.trueText;
this.trueColor = hint.trueColor;

View file

@ -0,0 +1,47 @@
package mage.abilities.hint.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.game.Game;
import java.awt.*;
/**
* ConditionHint that is only shown on permanents.
*
* @author Susucr
*/
public class ConditionPermanentHint extends ConditionHint {
public ConditionPermanentHint(Condition condition) {
super(condition);
}
public ConditionPermanentHint(Condition condition, String textWithIcons) {
super(condition, textWithIcons);
}
public ConditionPermanentHint(Condition condition, String trueText, Color trueColor, String falseText, Color falseColor, Boolean useIcons) {
super(condition, trueText, trueColor, falseText, falseColor, useIcons);
}
private ConditionPermanentHint(final ConditionPermanentHint hint) {
super(hint);
}
@Override
public String getText(Game game, Ability ability) {
if (game.getPermanent(ability.getSourceId()) == null) {
return "";
}
return super.getText(game, ability);
}
@Override
public Hint copy() {
return new ConditionPermanentHint(this);
}
}

View file

@ -1212,7 +1212,7 @@ public abstract class PlayerImpl implements Player, Serializable {
game.fireEvent(castEvent);
if (spell.activate(game, noMana)) {
GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST,
spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject);
ability.getId(), ability, playerId, approvingObject);
castedEvent.setZone(fromZone);
game.fireEvent(castedEvent);
if (!game.isSimulation()) {
@ -4721,7 +4721,7 @@ public abstract class PlayerImpl implements Player, Serializable {
boolean chooseOrder = false;
if (userData.askMoveToGraveOrder() && (cards.size() > 1)) {
chooseOrder = choosingPlayer.chooseUse(Outcome.Neutral,
"Choose the order in which the cards go to the graveyard?", source, game);
"Choose the order in which the cards go to the graveyard?", source, game);
}
if (chooseOrder) {
TargetCard target = new TargetCard(fromZone,
@ -5145,13 +5145,13 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override
public Permanent getRingBearer(Game game) {
return game.getBattlefield()
.getActivePermanents(
StaticFilters.FILTER_CONTROLLED_RINGBEARER,
getId(),null, game)
.stream()
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
.getActivePermanents(
StaticFilters.FILTER_CONTROLLED_RINGBEARER,
getId(), null, game)
.stream()
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
// 701.52a Certain spells and abilities have the text the Ring tempts you. Each time the Ring tempts
@ -5163,11 +5163,11 @@ public abstract class PlayerImpl implements Player, Serializable {
UUID currentBearerId = currentBearer == null ? null : currentBearer.getId();
List<UUID> ids = game.getBattlefield()
.getActivePermanents(StaticFilters.FILTER_CONTROLLED_CREATURE, getId(), null, game)
.stream()
.filter(Objects::nonNull)
.map(p -> p.getId())
.collect(Collectors.toList());
.getActivePermanents(StaticFilters.FILTER_CONTROLLED_CREATURE, getId(), null, game)
.stream()
.filter(Objects::nonNull)
.map(p -> p.getId())
.collect(Collectors.toList());
if (ids.isEmpty()) {
game.informPlayers(getLogName() + " has no creature to be Ring-bearer.");
@ -5196,8 +5196,7 @@ public abstract class PlayerImpl implements Player, Serializable {
choose(Outcome.Neutral, target, null, game);
newBearerId = target.getFirstTarget();
}
else {
} else {
newBearerId = currentBearerId;
}
}
@ -5211,11 +5210,11 @@ public abstract class PlayerImpl implements Player, Serializable {
// or abilities that care about which creature was chosen as your Ring-bearer.
// (2023-06-16)
game.informPlayers(getLogName() + " did not choose a new Ring-bearer. " +
"It is still " + (currentBearer == null ? "" : currentBearer.getLogName()) + ".");
"It is still " + (currentBearer == null ? "" : currentBearer.getLogName()) + ".");
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.RING_BEARER_CHOSEN, currentBearerId, null, getId()));
} else {
Permanent ringBearer = game.getPermanent(newBearerId);
if(ringBearer != null){
if (ringBearer != null) {
// The setRingBearer method is taking care of removing
// the status from the current ring bearer, if existing.
ringBearer.setRingBearer(game, true);