Implementing "Start your engines!" mechanic (#13259)

* add initial speed handling

* finish speed implementation

* remove skip list

* add initial test

* add some more tests

* change speed initialization to state-based action

* add opponent speed check

* add control change test

* add check for speed 5
This commit is contained in:
Evan Kranzler 2025-02-01 13:49:47 -05:00 committed by GitHub
parent 655af10b2e
commit ef213b1bef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 393 additions and 15 deletions

View file

@ -38,9 +38,21 @@ class MaxSpeedAbilityEffect extends ContinuousEffectImpl {
private final Ability ability;
private static Duration getDuration(Ability ability) {
switch (ability.getZone()) {
case BATTLEFIELD:
return Duration.WhileOnBattlefield;
case GRAVEYARD:
return Duration.WhileInGraveyard;
default:
return Duration.Custom;
}
}
MaxSpeedAbilityEffect(Ability ability) {
super(Duration.Custom, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
super(getDuration(ability), Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.ability = ability;
this.ability.setRuleVisible(false);
}
private MaxSpeedAbilityEffect(final MaxSpeedAbilityEffect effect) {

View file

@ -4,6 +4,9 @@ import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.players.Player;
import java.util.Optional;
/**
* @author TheElk801
@ -13,8 +16,10 @@ public enum ControllerSpeedCount implements DynamicValue {
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
// TODO: Implement this
return 0;
return Optional
.ofNullable(game.getPlayer(sourceAbility.getControllerId()))
.map(Player::getSpeed)
.orElse(0);
}
@Override

View file

@ -1,17 +1,21 @@
package mage.abilities.keyword;
import mage.abilities.StaticAbility;
import mage.abilities.dynamicvalue.common.ControllerSpeedCount;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueHint;
import mage.constants.Zone;
/**
* TODO: Implement this
*
* @author TheElk801
*/
public class StartYourEnginesAbility extends StaticAbility {
private static final Hint hint = new ValueHint("Your current speed", ControllerSpeedCount.instance);
public StartYourEnginesAbility() {
super(Zone.BATTLEFIELD, null);
this.addHint(hint);
}
private StartYourEnginesAbility(final StartYourEnginesAbility ability) {
@ -25,6 +29,6 @@ public class StartYourEnginesAbility extends StaticAbility {
@Override
public String getRule() {
return "Start your engines!";
return "start your engines!";
}
}

View file

@ -6,8 +6,8 @@ package mage.designations;
public enum DesignationType {
THE_MONARCH("The Monarch"), // global
CITYS_BLESSING("City's Blessing"), // per player
THE_INITIATIVE("The Initiative"); // global
THE_INITIATIVE("The Initiative"), // global
SPEED("Speed"); // per player
private final String text;
DesignationType(String text) {
@ -18,5 +18,4 @@ public enum DesignationType {
public String toString() {
return text;
}
}

View file

@ -0,0 +1,108 @@
package mage.designations;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import java.util.Optional;
/**
* @author TheElk801
*/
public class Speed extends Designation {
public Speed() {
super(DesignationType.SPEED);
addAbility(new SpeedTriggeredAbility());
}
private Speed(final Speed card) {
super(card);
}
@Override
public Speed copy() {
return new Speed(this);
}
}
class SpeedTriggeredAbility extends TriggeredAbilityImpl {
SpeedTriggeredAbility() {
super(Zone.ALL, new SpeedEffect());
setTriggersLimitEachTurn(1);
}
private SpeedTriggeredAbility(final SpeedTriggeredAbility ability) {
super(ability);
}
@Override
public SpeedTriggeredAbility copy() {
return new SpeedTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return game.isActivePlayer(getControllerId())
&& game
.getOpponents(getControllerId())
.contains(event.getTargetId());
}
@Override
public boolean checkInterveningIfClause(Game game) {
return Optional
.ofNullable(getControllerId())
.map(game::getPlayer)
.map(Player::getSpeed)
.map(x -> x < 4)
.orElse(false);
}
@Override
public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
return true;
}
@Override
public String getRule() {
return "Whenever one or more opponents lose life during your turn, if your speed is less than 4, " +
"increase your speed by 1. This ability triggers only once each turn.";
}
}
class SpeedEffect extends OneShotEffect {
SpeedEffect() {
super(Outcome.Benefit);
}
private SpeedEffect(final SpeedEffect effect) {
super(effect);
}
@Override
public SpeedEffect copy() {
return new SpeedEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Optional.ofNullable(source.getControllerId())
.map(game::getPlayer)
.ifPresent(player -> player.increaseSpeed(game));
return true;
}
}

View file

@ -2862,6 +2862,11 @@ public abstract class GameImpl implements Game {
}
}
}
if (perm.getAbilities(this).containsClass(StartYourEnginesAbility.class)) {
Optional.ofNullable(perm.getControllerId())
.map(this::getPlayer)
.ifPresent(player -> player.initSpeed(this));
}
}
//201300713 - 704.5k
// If a player controls two or more legendary permanents with the same name, that player

View file

@ -216,6 +216,12 @@ public interface Player extends MageItem, Copyable<Player> {
boolean isDrawsOnOpponentsTurn();
int getSpeed();
void initSpeed(Game game);
void increaseSpeed(Game game);
/**
* Returns alternative casting costs a player can cast spells for
*

View file

@ -27,6 +27,7 @@ import mage.counters.CounterType;
import mage.counters.Counters;
import mage.designations.Designation;
import mage.designations.DesignationType;
import mage.designations.Speed;
import mage.filter.FilterCard;
import mage.filter.FilterMana;
import mage.filter.FilterPermanent;
@ -153,6 +154,7 @@ public abstract class PlayerImpl implements Player, Serializable {
protected boolean canPlotFromTopOfLibrary = false;
protected boolean drawsFromBottom = false;
protected boolean drawsOnOpponentsTurn = false;
protected int speed = 0;
protected FilterPermanent sacrificeCostFilter;
protected List<AlternativeSourceCosts> alternativeSourceCosts = new ArrayList<>();
@ -252,6 +254,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary;
this.drawsFromBottom = player.drawsFromBottom;
this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn;
this.speed = player.speed;
this.attachments.addAll(player.attachments);
@ -367,6 +370,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.drawsFromBottom = player.isDrawsFromBottom();
this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn();
this.alternativeSourceCosts = CardUtil.deepCopyObject(((PlayerImpl) player).alternativeSourceCosts);
this.speed = player.getSpeed();
this.topCardRevealed = player.isTopCardRevealed();
@ -480,6 +484,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.canPlotFromTopOfLibrary = false;
this.drawsFromBottom = false;
this.drawsOnOpponentsTurn = false;
this.speed = 0;
this.sacrificeCostFilter = null;
this.alternativeSourceCosts.clear();
@ -4674,6 +4679,29 @@ public abstract class PlayerImpl implements Player, Serializable {
return drawsOnOpponentsTurn;
}
@Override
public int getSpeed() {
return speed;
}
@Override
public void initSpeed(Game game) {
if (speed > 0) {
return;
}
speed = 1;
game.getState().addDesignation(new Speed(), game, getId());
game.informPlayers(this.getLogName() + "'s speed is now 1.");
}
@Override
public void increaseSpeed(Game game) {
if (speed < 4) {
speed++;
game.informPlayers(this.getLogName() + "'s speed has increased to " + speed);
}
}
@Override
public boolean autoLoseGame() {
return false;