Refactor skip phase/step using replacement effect. Also add Fasting.

This commit is contained in:
Alumi
2021-03-17 05:43:32 +00:00
committed by Michael Kamensky
parent 10c9d153fc
commit b6b1ae852d
49 changed files with 318 additions and 129 deletions

View File

@@ -153,6 +153,7 @@ public enum ApiType {
SetLife (LifeSetEffect.class),
SetState (SetStateEffect.class),
Shuffle (ShuffleEffect.class),
SkipPhase (SkipPhaseEffect.class),
SkipTurn (SkipTurnEffect.class),
StoreSVar (StoreSVarEffect.class),
Subgame (SubgameEffect.class),

View File

@@ -57,6 +57,7 @@ public class AddTurnEffect extends SpellAbilityEffect {
}
if (sa.hasParam("SkipUntap")) {
extra.setSkipUntap(true);
extra.setSkipUntapSA(sa);
}
if (sa.hasParam("NoSchemes")) {
extra.setCantSetSchemesInMotion(true);

View File

@@ -0,0 +1,126 @@
package forge.game.ability.effects;
import java.util.List;
import forge.GameCommand;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
public class SkipPhaseEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
final String duration = sa.getParam("Duration");
final String phase = sa.getParam("Phase");
final String step = sa.getParam("Step");
List<Player> tgtPlayers = getTargetPlayers(sa);
for (final Player player : tgtPlayers) {
sb.append(player).append(" ");
sb.append("skips their ");
if (duration == null) {
sb.append("next ");
}
if (phase != null) {
sb.append(phase.toLowerCase()).append(" phase.");
} else {
sb.append(step.toLowerCase()).append(" step.");
}
}
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
final String duration = sa.getParam("Duration");
final String phase = sa.getParam("Phase");
final String step = sa.getParam("Step");
List<Player> tgtPlayers = getTargetPlayers(sa);
for (final Player player : tgtPlayers) {
createSkipPhaseEffect(sa, player, duration, phase, step);
}
}
public static void createSkipPhaseEffect(SpellAbility sa, final Player player,
final String duration, final String phase, final String step) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
final String name = hostCard.getName() + "'s Effect";
final String image = hostCard.getImageKey();
final boolean isNextThisTurn = duration != null && duration.equals("NextThisTurn");
final Card eff = createEffect(sa, player, name, image);
final StringBuilder sb = new StringBuilder();
sb.append("Event$ BeginPhase | ActiveZones$ Command | ValidPlayer$ You | Phase$ ");
sb.append(phase != null ? phase : step);
if (duration != null && !isNextThisTurn) {
sb.append(" | Skip$ True");
}
sb.append("| Description$ Skip ");
if (duration == null || isNextThisTurn) {
sb.append("your next ");
} else {
sb.append("each ");
}
if (phase != null) {
sb.append(phase.toLowerCase()).append(" phase");
} else {
sb.append(step.toLowerCase()).append(" step");
}
if (duration == null) {
sb.append(".");
} else {
if (game.getPhaseHandler().getPlayerTurn().equals(player)) {
sb.append(" of this turn.");
} else {
sb.append(" of your next turn.");
}
}
final String repeffstr = sb.toString();
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true);
// Set to layer to Control so it will be applied before "would begin your X phase/step" replacement effects
// (Any layer before Other is OK, since default layer is Other.)
re.setLayer(ReplacementLayer.Control);
if (duration == null || isNextThisTurn) {
String exilestr = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile";
SpellAbility exile = AbilityFactory.getAbility(exilestr, eff);
re.setOverridingAbility(exile);
}
if (duration != null) {
final GameCommand endEffect = new GameCommand() {
private static final long serialVersionUID = -5861759814760561373L;
@Override
public void run() {
game.getAction().exile(eff, null);
}
};
if (duration.equals("EndOfTurn") || isNextThisTurn) {
game.getEndOfTurn().addUntil(endEffect);
} else if (duration.equals("UntilTheEndOfYourNextTurn")) {
game.getEndOfTurn().addUntilEnd(player, endEffect);
}
}
eff.addReplacementEffect(re);
eff.updateStateForView();
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
}
}

View File

@@ -54,7 +54,9 @@ public class SkipTurnEffect extends SpellAbilityEffect {
calcTurn.setSubAbility((AbilitySub) AbilityFactory.getAbility(exile, eff));
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true);
re.setLayer(ReplacementLayer.Other);
// Set to layer to Control so it will be applied before "would begin your turn" replacement effects
// (Any layer before Other is OK, since default layer is Other.)
re.setLayer(ReplacementLayer.Control);
re.setOverridingAbility(calcTurn);
eff.addReplacementEffect(re);
eff.updateStateForView();

View File

@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.List;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
/**
@@ -38,6 +39,7 @@ public class ExtraTurn {
private Player player = null;
private List<Trigger> delTrig = Collections.synchronizedList(new ArrayList<>());
private boolean skipUntap = false;
private SpellAbility skipUntapSA;
private boolean cantSetSchemesInMotion = false;
/**
* TODO: Write javadoc for Constructor.
@@ -89,6 +91,20 @@ public class ExtraTurn {
this.skipUntap = skipUntap;
}
/**
* @return the skipUntapSA;
*/
public SpellAbility getSkipUntapSA() {
return skipUntapSA;
}
/**
* @param skipUntapSA the skipUntapSA to set
*/
public void setSkipUntapSA(SpellAbility skipUntapSA) {
this.skipUntapSA = skipUntapSA;
}
/**
* @return true if Schemes can't be played during the extra turn
*/

View File

@@ -24,6 +24,7 @@ import com.google.common.collect.Multimap;
import forge.card.mana.ManaCost;
import forge.game.*;
import forge.game.ability.AbilityKey;
import forge.game.ability.effects.SkipPhaseEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
@@ -166,6 +167,21 @@ public class PhaseHandler implements java.io.Serializable {
turnEnded = PhaseType.isLast(phase, isTopsy);
setPhase(PhaseType.getNext(phase, isTopsy));
}
// Replacement effects
final Map<AbilityKey, Object> repRunParams = AbilityKey.mapFromAffected(playerTurn);
repRunParams.put(AbilityKey.Phase, phase.nameForScripts);
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.BeginPhase, repRunParams);
if (repres != ReplacementResult.NotReplaced) {
// Currently there is no effect to skip entire beginning phase
// If in the future that kind of effect is added, need to handle it too.
// Handle skipping of entire combat phase
if (phase == PhaseType.COMBAT_BEGIN) {
setPhase(PhaseType.COMBAT_END);
}
advanceToNextPhase();
return;
}
}
game.getStack().clearUndoStack(); //can't undo action from previous phase
@@ -197,30 +213,19 @@ public class PhaseHandler implements java.io.Serializable {
}
private boolean isSkippingPhase(final PhaseType phase) {
// TODO: Refactor this method to replacement effect
switch (phase) {
case UNTAP:
if (playerTurn.hasKeyword("Skip your next untap step.")) {
playerTurn.removeKeyword("Skip your next untap step.", false); // Skipping your "next" untap step is cumulative.
return true;
}
return playerTurn.hasKeyword("Skip the untap step of this turn.") || playerTurn.hasKeyword("Skip your untap step.");
case UPKEEP:
return playerTurn.hasKeyword("Skip your upkeep step.");
case DRAW:
return playerTurn.isSkippingDraw() || turn == 1 && game.getPlayers().size() == 2;
case MAIN1:
case MAIN2:
return playerTurn.isSkippingMain();
return turn == 1 && game.getPlayers().size() == 2;
case COMBAT_BEGIN:
case COMBAT_DECLARE_ATTACKERS:
return playerTurn.isSkippingCombat();
case COMBAT_DECLARE_BLOCKERS:
if (combat != null && combat.getAttackers().isEmpty()) {
endCombat();
}
// Fall through
case COMBAT_FIRST_STRIKE_DAMAGE:
case COMBAT_DAMAGE:
return !inCombat();
@@ -237,9 +242,6 @@ public class PhaseHandler implements java.io.Serializable {
if (isSkippingPhase(phase)) {
skipped = true;
givePriorityToPlayer = false;
if (phase == PhaseType.COMBAT_DECLARE_ATTACKERS) {
playerTurn.removeKeyword("Skip your next combat phase.");
}
}
else {
// Perform turn-based actions
@@ -289,11 +291,6 @@ public class PhaseHandler implements java.io.Serializable {
declareAttackersTurnBasedAction();
game.getStack().unfreezeStack();
if (combat != null && combat.getAttackers().isEmpty()
&& !game.getTriggerHandler().hasDelayedTriggers()) {
endCombat();
}
if (combat != null) {
for (Card c : combat.getAttackers()) {
if (combat.getDefenderByAttacker(c) instanceof Player) {
@@ -408,7 +405,6 @@ public class PhaseHandler implements java.io.Serializable {
player.clearAssignedDamage();
}
playerTurn.removeKeyword("Skip all combat phases of this turn.");
nUpkeepsThisTurn = 0;
nMain1sThisTurn = 0;
game.getStack().resetMaxDistinctSources();
@@ -861,7 +857,7 @@ public class PhaseHandler implements java.io.Serializable {
game.getTriggerHandler().registerThisTurnDelayedTrigger(deltrig);
}
if (extraTurn.isSkipUntap()) {
nextPlayer.addKeyword("Skip the untap step of this turn.");
SkipPhaseEffect.createSkipPhaseEffect(extraTurn.getSkipUntapSA(), nextPlayer, null, null, "Untap");
}
if (extraTurn.isCantSetSchemesInMotion()) {
nextPlayer.addKeyword("Schemes can't be set in motion this turn.");

View File

@@ -2698,28 +2698,9 @@ public class Player extends GameEntity implements Comparable<Player> {
if (hasLost()) {
return true;
}
if (hasKeyword("Skip your next combat phase.")) {
return true;
}
if (hasKeyword("Skip your combat phase.")) {
return true;
}
if (hasKeyword("Skip all combat phases of your next turn.")) {
replaceAllKeywordInstances("Skip all combat phases of your next turn.",
"Skip all combat phases of this turn.");
return true;
}
if (hasKeyword("Skip all combat phases of this turn.")) {
return true;
}
return false;
}
public boolean isSkippingMain() {
return hasKeyword("Skip your main phase.");
}
public int getStartingHandSize() {
return startingHandSize;
}
@@ -2829,17 +2810,6 @@ public class Player extends GameEntity implements Comparable<Player> {
}
}
public boolean isSkippingDraw() {
if (hasKeyword("Skip your next draw step.")) {
removeKeyword("Skip your next draw step.");
return true;
}
if (hasKeyword("Skip your draw step.")) {
return true;
}
return false;
}
public CardCollectionView getInboundTokens() {
return inboundTokens;
}
@@ -3393,7 +3363,6 @@ public class Player extends GameEntity implements Comparable<Player> {
setLibrarySearched(0);
setNumManaConversion(0);
removeKeyword("Skip the untap step of this turn.");
removeKeyword("Schemes can't be set in motion this turn.");
}

View File

@@ -0,0 +1,51 @@
package forge.game.replacement;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.Map;
public class ReplaceBeginPhase extends ReplacementEffect {
public ReplaceBeginPhase(final Map<String, String> mapParams, final Card host, final boolean intrinsic) {
super(mapParams, host, intrinsic);
// set default layer to control
if (!mapParams.containsKey("Layer")) {
this.setLayer(ReplacementLayer.Control);
}
}
@Override
public boolean canReplace(Map<AbilityKey, Object> runParams) {
Player affected = (Player) runParams.get(AbilityKey.Affected);
if (!matchesValidParam("ValidPlayer", affected)) {
return false;
}
if (hasParam("Phase")) {
final String phase = getParam("Phase");
final String currentPhase = (String) runParams.get(AbilityKey.Phase);
if (phase.equals("Combat") && currentPhase.equals("BeginCombat")) {
return true;
}
if (phase.equals("Main") && (currentPhase.equals("Main1") || currentPhase.equals("Main2"))) {
return true;
}
if (!phase.equals(currentPhase)) {
return false;
}
}
if (hasParam("Condition")) {
if (getParam("Condition").equals("Hellbent") && !affected.hasHellbent()) {
return false;
}
}
return true;
}
@Override
public void setReplacingObjects(Map<AbilityKey, Object> runParams, SpellAbility sa) {
sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Affected));
}
}

View File

@@ -420,4 +420,17 @@ public class ReplacementHandler {
}
return ret;
}
/**
* Helper function to check if a phase would be skipped for AI.
*/
public boolean wouldPhaseBeSkipped(final Player player, final String phase) {
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(player);
repParams.put(AbilityKey.Phase, phase);
List<ReplacementEffect> list = getReplacementList(ReplacementType.BeginPhase, repParams, ReplacementLayer.Control);
if (!list.isEmpty()) {
return false;
}
return true;
}
}

View File

@@ -14,6 +14,7 @@ public enum ReplacementType {
AddCounter(ReplaceAddCounter.class),
AssignDealDamage(ReplaceAssignDealDamage.class),
Attached(ReplaceAttached.class),
BeginPhase(ReplaceBeginPhase.class),
BeginTurn(ReplaceBeginTurn.class),
Counter(ReplaceCounter.class),
CopySpell(ReplaceCopySpell.class),