mirror of
https://github.com/magefree/mage.git
synced 2025-12-28 06:22:01 -08:00
[MKM] Implement Cases (#11713)
* Implementing "case" mechanic * [MKM] Implement Case of the Burning Masks * [MKM] Implement Case of the Filched Falcon * [MKM] Implement Case of the Crimson Pulse * [MKM] Implement Case of the Locked Hothouse * Address PR comments * some minor adjustments * adjustments to hints --------- Co-authored-by: Matthew Wilson <matthew_w@vaadin.com> Co-authored-by: xenohedron <xenohedron@users.noreply.github.com>
This commit is contained in:
parent
25a08c736f
commit
f8d15cd6ba
18 changed files with 941 additions and 22 deletions
|
|
@ -370,7 +370,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
|
|||
}
|
||||
|
||||
public static boolean isInUseableZoneDiesTrigger(TriggeredAbility source, GameEvent event, Game game) {
|
||||
// Get the source permanent of the ability
|
||||
// Get the source permanent of the ability
|
||||
MageObject sourceObject = null;
|
||||
if (game.getState().getZone(source.getSourceId()) == Zone.BATTLEFIELD) {
|
||||
sourceObject = game.getPermanent(source.getSourceId());
|
||||
|
|
|
|||
166
Mage/src/main/java/mage/abilities/common/CaseAbility.java
Normal file
166
Mage/src/main/java/mage/abilities/common/CaseAbility.java
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
package mage.abilities.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.CompoundCondition;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.abilities.condition.common.SolvedSourceCondition;
|
||||
import mage.abilities.decorator.ConditionalActivatedAbility;
|
||||
import mage.abilities.decorator.ConditionalAsThoughEffect;
|
||||
import mage.abilities.decorator.ConditionalContinuousEffect;
|
||||
import mage.abilities.decorator.ConditionalTriggeredAbility;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.TargetController;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* The Case mechanic was added in Murders at Karlov Manor [MKM].
|
||||
* <ul>
|
||||
* <li>Each Case has two special keyword abilities: to solve and solved.</li>
|
||||
* <li>"To Solve — [condition]" means "At the beginning of your end step,
|
||||
* if [condition] and this Case is not solved, it becomes solved."</li>
|
||||
* <li>The meaning of "solved" differs based on what type of ability follows it.
|
||||
* "Solved — [activated ability]" means "[Activated ability].
|
||||
* Activate only if this Case is solved." Activated abilities contain a colon.
|
||||
* They're generally written "[Cost]: [Effect]."</li>
|
||||
* <li>"Solved — [Triggered ability]" means "[Triggered ability].
|
||||
* This ability triggers only if this Case is solved."
|
||||
* Triggered abilities use the word "when," "whenever," or "at."
|
||||
* They're often written as "[Trigger condition], [effect]."</li>
|
||||
* <li>"Solved — [static ability]" means "As long as this Case is solved, [static ability]."
|
||||
* Static abilities are written as statements, such as "Creatures you control get +1/+1"
|
||||
* or "Instant and sorcery spells you cast cost {1} less to cast."</li>
|
||||
* <li>"To solve" abilities will check for their condition twice:
|
||||
* once when the ability would trigger, and once when it resolves.
|
||||
* If the condition isn't true at the beginning of your end step,
|
||||
* the ability won't trigger at all.
|
||||
* If the condition isn't true when the ability resolves, the Case won't become solved.</li>
|
||||
* <li>Once a Case becomes solved, it stays solved until it leaves the battlefield.</li>
|
||||
* <li>Cases don't lose their other abilities when they become solved.</li>
|
||||
* <li>Being solved is not part of a permanent's copiable values.
|
||||
* A permanent that becomes a copy of a solved Case is not solved.
|
||||
* A solved Case that somehow becomes a copy of a different Case stays solved.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author DominionSpy
|
||||
*/
|
||||
public class CaseAbility extends SimpleStaticAbility {
|
||||
|
||||
/**
|
||||
* Constructs a Case with three abilities:
|
||||
* <ul>
|
||||
* <li>A initial ability the Case has at all times</li>
|
||||
* <li>A "To solve" ability that will conditionally solve the Case
|
||||
* at the beginning of the controller's end step</li>
|
||||
* <li>A "Solved" ability the Case has when solved</li>
|
||||
* </ul>
|
||||
* The "Solved" ability must be one of the following:
|
||||
* <ul>
|
||||
* <li>{@link ConditionalActivatedAbility} using the condition {@link SolvedSourceCondition}.SOLVED</li>
|
||||
* <li>{@link ConditionalTriggeredAbility} using the condition {@link SolvedSourceCondition}.SOLVED</li>
|
||||
* <li>{@link SimpleStaticAbility} with only {@link ConditionalAsThoughEffect} or {@link ConditionalContinuousEffect} effects</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param initialAbility The ability that a Case has at all times
|
||||
* @param toSolveCondition The condition to be checked when solving
|
||||
* @param solvedAbility The ability that a solved Case has
|
||||
*/
|
||||
public CaseAbility(Ability initialAbility, Condition toSolveCondition, Ability solvedAbility) {
|
||||
super(Zone.ALL, null);
|
||||
|
||||
if (initialAbility instanceof EntersBattlefieldTriggeredAbility) {
|
||||
((EntersBattlefieldTriggeredAbility) initialAbility).setTriggerPhrase("When this Case enters the battlefield, ");
|
||||
}
|
||||
addSubAbility(initialAbility);
|
||||
|
||||
addSubAbility(new CaseSolveAbility(toSolveCondition));
|
||||
|
||||
if (solvedAbility instanceof ConditionalActivatedAbility) {
|
||||
((ConditionalActivatedAbility) solvedAbility).hideCondition();
|
||||
} else if (!(solvedAbility instanceof ConditionalTriggeredAbility)) {
|
||||
if (solvedAbility instanceof SimpleStaticAbility) {
|
||||
for (Effect effect : solvedAbility.getEffects()) {
|
||||
if (!(effect instanceof ConditionalContinuousEffect ||
|
||||
effect instanceof ConditionalAsThoughEffect)) {
|
||||
throw new IllegalArgumentException("solvedAbility must be one of ConditionalActivatedAbility, " +
|
||||
"ConditionalTriggeredAbility, or StaticAbility with conditional effects.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("solvedAbility must be one of ConditionalActivatedAbility, " +
|
||||
"ConditionalTriggeredAbility, or StaticAbility with conditional effects.");
|
||||
}
|
||||
}
|
||||
addSubAbility(solvedAbility.withFlavorWord("Solved")); // TODO: Technically this shouldn't be italicized
|
||||
}
|
||||
|
||||
protected CaseAbility(final CaseAbility ability) {
|
||||
super(ability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaseAbility copy() {
|
||||
return new CaseAbility(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CaseSolveAbility extends BeginningOfEndStepTriggeredAbility {
|
||||
|
||||
CaseSolveAbility(Condition condition) {
|
||||
super(new SolveEffect(), TargetController.YOU,
|
||||
new CompoundCondition(condition, SolvedSourceCondition.UNSOLVED), false);
|
||||
withFlavorWord("To solve"); // TODO: technically this shouldn't be italicized
|
||||
setTriggerPhrase(CardUtil.getTextWithFirstCharUpperCase(trimIf(condition.toString())));
|
||||
}
|
||||
|
||||
private CaseSolveAbility(final CaseSolveAbility ability) {
|
||||
super(ability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaseSolveAbility copy() {
|
||||
return new CaseSolveAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
return super.getRule() + ". <i>(If unsolved, solve at the beginning of your end step.)</i>";
|
||||
}
|
||||
|
||||
private static String trimIf(String text) {
|
||||
if (text.startsWith("if ")) {
|
||||
return text.substring(3);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
class SolveEffect extends OneShotEffect {
|
||||
|
||||
SolveEffect() {
|
||||
super(Outcome.Benefit);
|
||||
}
|
||||
|
||||
private SolveEffect(final SolveEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SolveEffect copy() {
|
||||
return new SolveEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
|
||||
if (permanent == null || permanent.isSolved()) {
|
||||
return false;
|
||||
}
|
||||
return permanent.solve(game, source);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package mage.abilities.condition.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
||||
/**
|
||||
* Checks if a Permanent is solved
|
||||
*
|
||||
* @author DominionSpy
|
||||
*/
|
||||
public enum SolvedSourceCondition implements Condition {
|
||||
SOLVED(true),
|
||||
UNSOLVED(false);
|
||||
private final boolean solved;
|
||||
|
||||
SolvedSourceCondition(boolean solved) {
|
||||
this.solved = solved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Permanent permanent = game.getPermanent(source.getSourceId());
|
||||
return permanent != null && permanent.isSolved() == solved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{this} is " + (solved ? "solved" : "unsolved");
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ public class ConditionalActivatedAbility extends ActivatedAbilityImpl {
|
|||
private static final Effects emptyEffects = new Effects();
|
||||
|
||||
private String ruleText = null;
|
||||
private boolean showCondition = true;
|
||||
|
||||
public ConditionalActivatedAbility(Effect effect, Cost cost, Condition condition) {
|
||||
this(Zone.BATTLEFIELD, effect, cost, condition);
|
||||
|
|
@ -36,6 +37,7 @@ public class ConditionalActivatedAbility extends ActivatedAbilityImpl {
|
|||
protected ConditionalActivatedAbility(final ConditionalActivatedAbility ability) {
|
||||
super(ability);
|
||||
this.ruleText = ability.ruleText;
|
||||
this.showCondition = ability.showCondition;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -51,22 +53,29 @@ public class ConditionalActivatedAbility extends ActivatedAbilityImpl {
|
|||
return new ConditionalActivatedAbility(this);
|
||||
}
|
||||
|
||||
public ConditionalActivatedAbility hideCondition() {
|
||||
this.showCondition = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
if (ruleText != null && !ruleText.isEmpty()) {
|
||||
return ruleText;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(super.getRule());
|
||||
sb.append(" Activate only ");
|
||||
if (timing == TimingRule.SORCERY) {
|
||||
sb.append("as a sorcery and only ");
|
||||
if (showCondition) {
|
||||
sb.append(" Activate only ");
|
||||
if (timing == TimingRule.SORCERY) {
|
||||
sb.append("as a sorcery and only ");
|
||||
}
|
||||
String conditionText = condition.toString();
|
||||
if (!conditionText.startsWith("during") && !conditionText.startsWith("before") && !conditionText.startsWith("if")) {
|
||||
sb.append("if ");
|
||||
}
|
||||
sb.append(conditionText);
|
||||
sb.append('.');
|
||||
}
|
||||
String conditionText = condition.toString();
|
||||
if (!conditionText.startsWith("during") && !conditionText.startsWith("before") && !conditionText.startsWith("if")) {
|
||||
sb.append("if ");
|
||||
}
|
||||
sb.append(conditionText);
|
||||
sb.append('.');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package mage.abilities.decorator;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.Modes;
|
||||
import mage.abilities.TriggeredAbility;
|
||||
import mage.abilities.TriggeredAbilityImpl;
|
||||
|
|
@ -108,4 +109,10 @@ public class ConditionalTriggeredAbility extends TriggeredAbilityImpl {
|
|||
return ability.isOptional();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Ability withFlavorWord(String flavorWord) {
|
||||
ability.withFlavorWord(flavorWord);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import java.awt.*;
|
|||
*/
|
||||
public class ConditionHint implements Hint {
|
||||
|
||||
private Condition condition;
|
||||
private String trueText;
|
||||
private Color trueColor;
|
||||
private String falseText;
|
||||
private Color falseColor;
|
||||
private Boolean useIcons;
|
||||
private final Condition condition;
|
||||
private final String trueText;
|
||||
private final Color trueColor;
|
||||
private final String falseText;
|
||||
private final Color falseColor;
|
||||
private final boolean useIcons;
|
||||
|
||||
public ConditionHint(Condition condition) {
|
||||
this(condition, condition.toString());
|
||||
|
|
@ -27,7 +27,7 @@ public class ConditionHint implements Hint {
|
|||
this(condition, textWithIcons, null, textWithIcons, null, true);
|
||||
}
|
||||
|
||||
public ConditionHint(Condition condition, String trueText, Color trueColor, String falseText, Color falseColor, Boolean useIcons) {
|
||||
public ConditionHint(Condition condition, String trueText, Color trueColor, String falseText, Color falseColor, boolean useIcons) {
|
||||
this.condition = condition;
|
||||
this.trueText = CardUtil.getTextWithFirstCharUpperCase(trueText);
|
||||
this.trueColor = trueColor;
|
||||
|
|
@ -58,7 +58,7 @@ public class ConditionHint implements Hint {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Hint copy() {
|
||||
public ConditionHint copy() {
|
||||
return new ConditionHint(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package mage.abilities.hint.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.abilities.condition.common.SolvedSourceCondition;
|
||||
import mage.abilities.hint.ConditionHint;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
||||
public class CaseSolvedHint extends ConditionHint {
|
||||
|
||||
private final Condition condition;
|
||||
|
||||
/**
|
||||
* Hint for use with CaseAbility
|
||||
* @param condition Same condition added to CaseAbility
|
||||
*/
|
||||
public CaseSolvedHint(Condition condition) {
|
||||
super(SolvedSourceCondition.SOLVED, "Case is solved.", null, "Case is unsolved.", null, true);
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
protected CaseSolvedHint(final CaseSolvedHint hint) {
|
||||
super(hint);
|
||||
this.condition = hint.condition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText(Game game, Ability ability) {
|
||||
Permanent permanent = game.getPermanent(ability.getSourceId());
|
||||
if (permanent == null) {
|
||||
return "";
|
||||
}
|
||||
String text = super.getText(game, ability);
|
||||
if (!permanent.isSolved()) {
|
||||
text += " " + getConditionText(game, ability);
|
||||
if (condition.apply(game, ability) && game.isActivePlayer(ability.getControllerId())) {
|
||||
text += " Case will be solved at the end step.";
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to add specific information on satisfying the condition.
|
||||
*/
|
||||
protected String getConditionText(Game game, Ability ability) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ 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.*;
|
||||
|
|
@ -23,7 +22,7 @@ public class ConditionPermanentHint extends ConditionHint {
|
|||
super(condition, textWithIcons);
|
||||
}
|
||||
|
||||
public ConditionPermanentHint(Condition condition, String trueText, Color trueColor, String falseText, Color falseColor, Boolean useIcons) {
|
||||
public ConditionPermanentHint(Condition condition, String trueText, Color trueColor, String falseText, Color falseColor, boolean useIcons) {
|
||||
super(condition, trueText, trueColor, falseText, falseColor, useIcons);
|
||||
}
|
||||
|
||||
|
|
@ -36,12 +35,11 @@ public class ConditionPermanentHint extends ConditionHint {
|
|||
if (game.getPermanent(ability.getSourceId()) == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return super.getText(game, ability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Hint copy() {
|
||||
public ConditionPermanentHint copy() {
|
||||
return new ConditionPermanentHint(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ public enum SubType {
|
|||
AURA("Aura", SubTypeSet.EnchantmentType),
|
||||
BACKGROUND("Background", SubTypeSet.EnchantmentType),
|
||||
CARTOUCHE("Cartouche", SubTypeSet.EnchantmentType),
|
||||
CASE("Case", SubTypeSet.EnchantmentType),
|
||||
CLASS("Class", SubTypeSet.EnchantmentType),
|
||||
CURSE("Curse", SubTypeSet.EnchantmentType),
|
||||
ROLE("Role", SubTypeSet.EnchantmentType),
|
||||
|
|
|
|||
|
|
@ -567,6 +567,12 @@ public class GameEvent implements Serializable {
|
|||
playerId the player crafting
|
||||
*/
|
||||
EXILED_WHILE_CRAFTING,
|
||||
/* Solving a Case
|
||||
targetId the permanent being solved
|
||||
sourceId of the ability solving
|
||||
playerId the player solving
|
||||
*/
|
||||
SOLVE_CASE, CASE_SOLVED,
|
||||
/* Become suspected
|
||||
targetId the permanent being suspected
|
||||
sourceId of the ability suspecting
|
||||
|
|
|
|||
|
|
@ -446,6 +446,10 @@ public interface Permanent extends Card, Controllable {
|
|||
|
||||
void setRingBearer(Game game, boolean value);
|
||||
|
||||
boolean isSolved();
|
||||
|
||||
boolean solve(Game game, Ability source);
|
||||
|
||||
@Override
|
||||
Permanent copy();
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
// maximal number of creatures the creature can be blocked by 0 = no restriction
|
||||
protected int maxBlockedBy = 0;
|
||||
protected boolean deathtouched;
|
||||
protected boolean solved = false;
|
||||
|
||||
protected Map<String, List<UUID>> connectedCards = new HashMap<>();
|
||||
protected Set<MageObjectReference> dealtDamageByThisTurn;
|
||||
|
|
@ -145,6 +146,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
this.blocking = permanent.blocking;
|
||||
this.maxBlocks = permanent.maxBlocks;
|
||||
this.deathtouched = permanent.deathtouched;
|
||||
this.solved = permanent.solved;
|
||||
this.markedLifelink = permanent.markedLifelink;
|
||||
this.connectedCards = CardUtil.deepCopyObject(permanent.connectedCards);
|
||||
this.dealtDamageByThisTurn = CardUtil.deepCopyObject(permanent.dealtDamageByThisTurn);
|
||||
|
|
@ -1913,6 +1915,33 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
return ringBearerFlag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolved() {
|
||||
return solved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean solve(Game game, Ability source) {
|
||||
if (this.solved) {
|
||||
return false;
|
||||
}
|
||||
GameEvent event = new GameEvent(GameEvent.EventType.SOLVE_CASE, getId(),
|
||||
source, source.getControllerId());
|
||||
if (game.replaceEvent(event)) {
|
||||
return false;
|
||||
}
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller != null) {
|
||||
game.informPlayers(controller.getLogName() + " solved " + this.getLogName() +
|
||||
CardUtil.getSourceLogName(game, source));
|
||||
}
|
||||
|
||||
this.solved = true;
|
||||
game.fireEvent(new GameEvent(EventType.CASE_SOLVED, getId(), source,
|
||||
source.getControllerId()));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fight(Permanent fightTarget, Ability source, Game game) {
|
||||
return this.fight(fightTarget, source, game, true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue