Introduce new batch event for life lost for a specific player (#13071)

* Introduce new batch event for life lost for a specific player

closes #12202, fix #10805

* implement [DSC] Valgavoth, Harrower of Souls

* text fixes
This commit is contained in:
xenohedron 2024-11-19 23:41:34 -05:00 committed by GitHub
parent 95e986dee7
commit d6cf207a8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 693 additions and 707 deletions

View file

@ -28,12 +28,12 @@ public class GainLoseLifeYourTurnTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.GAINED_LIFE
|| event.getType() == GameEvent.EventType.LOST_LIFE;
|| event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return isControlledBy(game.getActivePlayerId())
&& isControlledBy(event.getPlayerId());
&& isControlledBy(event.getTargetId());
}
}

View file

@ -0,0 +1,51 @@
package mage.abilities.common;
import mage.abilities.effects.Effect;
import mage.constants.TargetController;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.watchers.common.LifeLostThisTurnWatcher;
/**
* @author Susucr
*/
public class LoseLifeFirstTimeEachTurnTriggeredAbility extends LoseLifeTriggeredAbility {
public LoseLifeFirstTimeEachTurnTriggeredAbility(Effect effect) {
this(effect, TargetController.YOU, false, false);
}
public LoseLifeFirstTimeEachTurnTriggeredAbility(Effect effect, TargetController targetController, boolean optional, boolean setTargetPointer) {
super(effect, targetController, optional, setTargetPointer);
addWatcher(new LifeLostThisTurnWatcher());
}
protected LoseLifeFirstTimeEachTurnTriggeredAbility(final LoseLifeFirstTimeEachTurnTriggeredAbility ability) {
super(ability);
}
@Override
public LoseLifeFirstTimeEachTurnTriggeredAbility copy() {
return new LoseLifeFirstTimeEachTurnTriggeredAbility(this);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
LifeLostThisTurnWatcher watcher = game.getState().getWatcher(LifeLostThisTurnWatcher.class);
return watcher != null
&& watcher.timesLostLifeThisTurn(event.getTargetId()) <= 1
&& super.checkTrigger(event, game);
}
@Override
protected String generateTriggerPhrase() {
switch (targetController) {
case YOU:
return "Whenever you lose life for the first time each turn, ";
case OPPONENT:
return "Whenever an opponent loses life for the first time each turn, ";
default:
throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController);
}
}
}

View file

@ -0,0 +1,85 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.Effect;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.LifeLostEvent;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
* @author Susucr
*/
public class LoseLifeTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<LifeLostEvent> {
protected final TargetController targetController;
private final boolean setTargetPointer;
public LoseLifeTriggeredAbility(Effect effect) {
this(effect, TargetController.YOU, false, false);
}
public LoseLifeTriggeredAbility(Effect effect, TargetController targetController, boolean optional, boolean setTargetPointer) {
super(Zone.BATTLEFIELD, effect, optional);
this.targetController = targetController;
this.setTargetPointer = setTargetPointer;
setTriggerPhrase(generateTriggerPhrase());
}
protected LoseLifeTriggeredAbility(final LoseLifeTriggeredAbility ability) {
super(ability);
this.targetController = ability.targetController;
this.setTargetPointer = ability.setTargetPointer;
}
@Override
public LoseLifeTriggeredAbility copy() {
return new LoseLifeTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER;
}
private boolean filterPlayer(UUID playerId, Game game) {
switch (targetController) {
case YOU:
return isControlledBy(playerId);
case OPPONENT:
return game.getOpponents(getControllerId()).contains(playerId);
default:
throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController);
}
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!filterPlayer(event.getTargetId(), game)) {
return false;
}
// if target id matches, all events in the batch are relevant
this.getEffects().setValue(SavedLifeLossValue.getValueKey(), event.getAmount());
if (setTargetPointer) {
this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId()));
}
return true;
}
protected String generateTriggerPhrase() {
switch (targetController) {
case YOU:
return "Whenever you lose life, ";
case OPPONENT:
return "Whenever an opponent loses life, ";
default:
throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController);
}
}
}

View file

@ -0,0 +1,49 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
/**
* @author Susucr
*/
public enum SavedLifeLossValue implements DynamicValue {
MANY("many"),
MUCH("much");
private final String message;
private static final String key = "SavedLifeLoss";
/**
* value key used to store the amount of life lost
*/
public static String getValueKey() {
return key;
}
SavedLifeLossValue(String message) {
this.message = "that " + message;
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return (Integer) effect.getValue(getValueKey());
}
@Override
public SavedLifeLossValue copy() {
return this;
}
@Override
public String toString() {
return message;
}
@Override
public String getMessage() {
return "";
}
}

View file

@ -975,12 +975,17 @@ public class GameState implements Serializable, Copyable<GameState> {
// Combine multiple life loss events in the single event (batch)
// see GameEvent.LOST_LIFE_BATCH
// existing batch
// existing batchs
boolean isLifeLostBatchUsed = false;
boolean isSingleBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (event instanceof LifeLostBatchEvent) {
((LifeLostBatchEvent) event).addEvent(lifeLossEvent);
isLifeLostBatchUsed = true;
} else if (event instanceof LifeLostBatchForOnePlayerEvent
&& event.getTargetId().equals(lifeLossEvent.getTargetId())) {
((LifeLostBatchForOnePlayerEvent) event).addEvent(lifeLossEvent);
isSingleBatchUsed = true;
}
}
@ -988,6 +993,9 @@ public class GameState implements Serializable, Copyable<GameState> {
if (!isLifeLostBatchUsed) {
addSimultaneousEvent(new LifeLostBatchEvent(lifeLossEvent), game);
}
if (!isSingleBatchUsed) {
addSimultaneousEvent(new LifeLostBatchForOnePlayerEvent(lifeLossEvent), game);
}
}
public void addSimultaneousTappedToBatch(TappedEvent tappedEvent, Game game) {

View file

@ -166,7 +166,6 @@ public class GameEvent implements Serializable {
DAMAGE_CAUSES_LIFE_LOSS,
PLAYER_LIFE_CHANGE,
GAIN_LIFE, GAINED_LIFE,
LOSE_LIFE, LOST_LIFE,
/* LOSE_LIFE + LOST_LIFE
targetId the id of the player loosing life
sourceId sourceId of the ability which caused the lose
@ -174,10 +173,17 @@ public class GameEvent implements Serializable {
amount amount of life loss
flag true = from combat damage - other from non combat damage
*/
LOST_LIFE_BATCH(true),
LOSE_LIFE, LOST_LIFE,
/* LOST_LIFE_BATCH_FOR_ONE_PLAYER
combines all life lost events for a player to a single batch (event)
*/
LOST_LIFE_BATCH_FOR_ONE_PLAYER(true),
/* LOST_LIFE_BATCH
combines all player life lost events to a single batch (event)
*/
LOST_LIFE_BATCH(true),
PLAY_LAND, LAND_PLAYED,
CREATURE_CHAMPIONED,
/* CREATURE_CHAMPIONED

View file

@ -0,0 +1,11 @@
package mage.game.events;
/**
* @author Susucr
*/
public class LifeLostBatchForOnePlayerEvent extends BatchEvent<LifeLostEvent> {
public LifeLostBatchForOnePlayerEvent(LifeLostEvent firstEvent) {
super(EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER, true, false, false, firstEvent);
}
}

View file

@ -0,0 +1,42 @@
package mage.watchers.common;
import mage.constants.WatcherScope;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.util.CardUtil;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author Susucr
*/
public class LifeLostThisTurnWatcher extends Watcher {
// player -> number of times (not amount!) that player lost life this turn.
private final Map<UUID, Integer> playersLostLife = new HashMap<>();
public LifeLostThisTurnWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER) {
playersLostLife.compute(event.getTargetId(), CardUtil::setOrIncrementValue);
}
}
@Override
public void reset() {
super.reset();
playersLostLife.clear();
}
public int timesLostLifeThisTurn(UUID playerId) {
return playersLostLife.getOrDefault(playerId, 0);
}
}