mirror of
https://github.com/magefree/mage.git
synced 2026-01-26 21:29:17 -08:00
[PIP] Implement Rad Counters mechanic (#12087)
This commit is contained in:
parent
11373fd75d
commit
9d7bf27d38
14 changed files with 420 additions and 176 deletions
|
|
@ -308,10 +308,9 @@ public class TriggeredAbilities extends LinkedHashMap<String, TriggeredAbility>
|
|||
|
||||
public void removeAbilitiesOfNonExistingSources(Game game) {
|
||||
// e.g. Token that had triggered abilities
|
||||
|
||||
entrySet().removeIf(entry -> game.getObject(entry.getValue().getSourceId()) == null
|
||||
&& game.getState().getInherentEmblems().stream().noneMatch(emblem -> emblem.getId().equals(entry.getValue().getSourceId()))
|
||||
&& game.getState().getDesignations().stream().noneMatch(designation -> designation.getId().equals(entry.getValue().getSourceId())));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public enum TokenRepository {
|
|||
public static final String XMAGE_IMAGE_NAME_DAY = "Day";
|
||||
public static final String XMAGE_IMAGE_NAME_NIGHT = "Night";
|
||||
public static final String XMAGE_IMAGE_NAME_THE_MONARCH = "The Monarch";
|
||||
public static final String XMAGE_IMAGE_NAME_RADIATION = "Radiation";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(TokenRepository.class);
|
||||
|
||||
|
|
@ -296,6 +297,9 @@ public enum TokenRepository {
|
|||
res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_MONARCH, 2, "https://api.scryfall.com/cards/tcn2/1/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_MONARCH, 3, "https://api.scryfall.com/cards/tltc/15/en?format=image"));
|
||||
|
||||
// Radiation (for trigger)
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_RADIATION, 1, "https://api.scryfall.com/cards/tpip/22/en?format=image"));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import mage.game.combat.CombatGroup;
|
|||
import mage.game.command.*;
|
||||
import mage.game.command.dungeons.UndercityDungeon;
|
||||
import mage.game.command.emblems.EmblemOfCard;
|
||||
import mage.game.command.emblems.RadiationEmblem;
|
||||
import mage.game.command.emblems.TheRingEmblem;
|
||||
import mage.game.events.*;
|
||||
import mage.game.events.TableEvent.EventType;
|
||||
|
|
@ -443,6 +444,11 @@ public abstract class GameImpl implements Game {
|
|||
return designation;
|
||||
}
|
||||
}
|
||||
for (Emblem emblem : state.getInherentEmblems()) {
|
||||
if (emblem.getId().equals(objectId)) {
|
||||
return emblem;
|
||||
}
|
||||
}
|
||||
// can be an ability of a sacrificed Token trying to get it's source object
|
||||
object = getLastKnownInformation(objectId, Zone.BATTLEFIELD);
|
||||
}
|
||||
|
|
@ -1366,6 +1372,14 @@ public abstract class GameImpl implements Game {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rad counter mechanic for every player
|
||||
for (UUID playerId : state.getPlayerList(startingPlayerId)) {
|
||||
// This is not a real emblem. Just a fake source for the
|
||||
// inherent trigger ability related to Rad counters
|
||||
// Faking a source just to display something on the stack ability.
|
||||
state.addInherentEmblem(new RadiationEmblem(), playerId);
|
||||
}
|
||||
}
|
||||
|
||||
public void initGameDefaultWatchers() {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import mage.game.combat.Combat;
|
|||
import mage.game.combat.CombatGroup;
|
||||
import mage.game.command.Command;
|
||||
import mage.game.command.CommandObject;
|
||||
import mage.game.command.Emblem;
|
||||
import mage.game.command.Plane;
|
||||
import mage.game.events.*;
|
||||
import mage.game.permanent.Battlefield;
|
||||
|
|
@ -85,6 +86,7 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
private boolean isPlaneChase;
|
||||
private List<String> seenPlanes = new ArrayList<>();
|
||||
private List<Designation> designations = new ArrayList<>();
|
||||
private List<Emblem> inherentEmblems = new ArrayList<>();
|
||||
private Exile exile;
|
||||
private Battlefield battlefield;
|
||||
private int turnNum = 1;
|
||||
|
|
@ -157,6 +159,7 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
this.isPlaneChase = state.isPlaneChase;
|
||||
this.seenPlanes.addAll(state.seenPlanes);
|
||||
this.designations.addAll(state.designations);
|
||||
this.inherentEmblems = CardUtil.deepCopyObject(state.inherentEmblems);
|
||||
this.exile = state.exile.copy();
|
||||
this.battlefield = state.battlefield.copy();
|
||||
this.turnNum = state.turnNum;
|
||||
|
|
@ -204,6 +207,7 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
exile.clear();
|
||||
command.clear();
|
||||
designations.clear();
|
||||
inherentEmblems.clear();
|
||||
seenPlanes.clear();
|
||||
isPlaneChase = false;
|
||||
revealed.clear();
|
||||
|
|
@ -245,6 +249,7 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
this.isPlaneChase = state.isPlaneChase;
|
||||
this.seenPlanes = state.seenPlanes;
|
||||
this.designations = state.designations;
|
||||
this.inherentEmblems = state.inherentEmblems;
|
||||
this.exile = state.exile;
|
||||
this.battlefield = state.battlefield;
|
||||
this.turnNum = state.turnNum;
|
||||
|
|
@ -506,6 +511,10 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
return designations;
|
||||
}
|
||||
|
||||
public List<Emblem> getInherentEmblems() {
|
||||
return inherentEmblems;
|
||||
}
|
||||
|
||||
public Plane getCurrentPlane() {
|
||||
if (command != null && command.size() > 0) {
|
||||
for (CommandObject cobject : command) {
|
||||
|
|
@ -1135,6 +1144,25 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherent triggers (Rad counters) in the rules have no source.
|
||||
* However to fit better with the engine, we make a fake emblem source,
|
||||
* which is not displayed in any game zone. That allows the trigger to
|
||||
* have a source, which helps with a bunch of situation like hosting,
|
||||
* rather than having a trigger.
|
||||
* <p>
|
||||
* Should not be used except in very specific situations
|
||||
*/
|
||||
public void addInherentEmblem(Emblem emblem, UUID controllerId) {
|
||||
getInherentEmblems().add(emblem);
|
||||
emblem.setControllerId(controllerId);
|
||||
for (Ability ability : emblem.getInitAbilities()) {
|
||||
ability.setControllerId(controllerId);
|
||||
ability.setSourceId(emblem.getId());
|
||||
addAbility(ability, null, emblem);
|
||||
}
|
||||
}
|
||||
|
||||
public void addDesignation(Designation designation, Game game, UUID controllerId) {
|
||||
getDesignations().add(designation);
|
||||
for (Ability ability : designation.getInitAbilities()) {
|
||||
|
|
@ -1414,7 +1442,7 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
/**
|
||||
* Store the tags of source ability using the MOR as a reference
|
||||
*/
|
||||
void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source){
|
||||
void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source) {
|
||||
if (source.getCostsTagMap() != null) {
|
||||
permanentCostsTags.put(permanentMOR, CardUtil.deepCopyObject(source.getCostsTagMap()));
|
||||
}
|
||||
|
|
@ -1424,9 +1452,9 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
* Removes the cost tags if the corresponding permanent is no longer on the battlefield.
|
||||
* Only use if the stack is empty and nothing can refer to them anymore (such as at EOT, the current behavior)
|
||||
*/
|
||||
public void cleanupPermanentCostsTags(Game game){
|
||||
public void cleanupPermanentCostsTags(Game game) {
|
||||
getPermanentCostsTags().entrySet().removeIf(entry ->
|
||||
!(entry.getKey().getZoneChangeCounter() == game.getState().getZoneChangeCounter(entry.getKey().getSourceId())-1)
|
||||
!(entry.getKey().getZoneChangeCounter() == game.getState().getZoneChangeCounter(entry.getKey().getSourceId()) - 1)
|
||||
); // The stored MOR is the stack-moment MOR so need to subtract one from the permanent's ZCC for the check
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ public abstract class Emblem extends CommandObjectImpl {
|
|||
this.controllerId = emblem.controllerId;
|
||||
this.sourceObject = emblem.sourceObject;
|
||||
this.copy = emblem.copy;
|
||||
this.copyFrom = (emblem.copyFrom != null ? emblem.copyFrom : null);
|
||||
this.copyFrom = emblem.copyFrom;
|
||||
this.abilites = emblem.abilites.copy();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
package mage.game.command.emblems;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.BeginningOfPreCombatMainTriggeredAbility;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.cards.Cards;
|
||||
import mage.cards.repository.TokenInfo;
|
||||
import mage.cards.repository.TokenRepository;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.TargetController;
|
||||
import mage.constants.Zone;
|
||||
import mage.counters.CounterType;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
import mage.game.command.Emblem;
|
||||
import mage.players.Player;
|
||||
|
||||
/**
|
||||
* Special emblem to enable the Rad Counter inherent trigger
|
||||
* with an actual source, to display image on the stack.
|
||||
*
|
||||
* @author Susucr
|
||||
*/
|
||||
public class RadiationEmblem extends Emblem {
|
||||
|
||||
public RadiationEmblem() {
|
||||
super("Radiation");
|
||||
|
||||
this.getAbilities().add(new ConditionalInterveningIfTriggeredAbility(
|
||||
new BeginningOfPreCombatMainTriggeredAbility(Zone.ALL, new RadiationEffect(), TargetController.YOU, false, false),
|
||||
RadiationCondition.instance,
|
||||
"At the beginning of your precombat main phase, if you have any rad counters, "
|
||||
+ "mill that many cards. For each nonland card milled this way, you lose 1 life and a rad counter."
|
||||
));
|
||||
|
||||
TokenInfo foundInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_RADIATION, null);
|
||||
if (foundInfo != null) {
|
||||
this.setExpansionSetCode(foundInfo.getSetCode());
|
||||
this.setCardNumber("");
|
||||
this.setImageFileName(""); // use default
|
||||
this.setImageNumber(foundInfo.getImageNumber());
|
||||
} else {
|
||||
// how-to fix: add emblem to the tokens-database
|
||||
throw new IllegalArgumentException("Wrong code usage: can't find xmage token info for: " + TokenRepository.XMAGE_IMAGE_NAME_RADIATION);
|
||||
}
|
||||
}
|
||||
|
||||
private RadiationEmblem(final RadiationEmblem card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RadiationEmblem copy() {
|
||||
return new RadiationEmblem(this);
|
||||
}
|
||||
}
|
||||
|
||||
enum RadiationCondition implements Condition {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
return player != null && player.getCounters().getCount(CounterType.RAD) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 725.1. Rad counters are a kind of counter a player can have (see rule 122, "Counters").
|
||||
* There is an inherent triggered ability associated with rad counters. This ability has no
|
||||
* source and is controlled by the active player. This is an exception to rule 113.8. The
|
||||
* full text of this ability is "At the beginning of each player's precombat main phase, if
|
||||
* that player has one or more rad counters, that player mills a number of cards equal to
|
||||
* the number of rad counters they have. For each nonland card milled this way, that player
|
||||
* loses 1 life and removes one rad counter from themselves."
|
||||
*/
|
||||
class RadiationEffect extends OneShotEffect {
|
||||
|
||||
RadiationEffect() {
|
||||
super(Outcome.Neutral);
|
||||
staticText = "mill that many cards. For each nonland card milled this way, "
|
||||
+ "you lose 1 life and remove one rad counter.";
|
||||
}
|
||||
|
||||
private RadiationEffect(final RadiationEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RadiationEffect copy() {
|
||||
return new RadiationEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
int amount = player.getCounters().getCount(CounterType.RAD);
|
||||
Cards milled = player.millCards(amount, source, game);
|
||||
int countNonLand = milled.count(StaticFilters.FILTER_CARD_NON_LAND, player.getId(), source, game);
|
||||
if (countNonLand > 0) {
|
||||
// TODO: support gaining life instead with [[Strong, the Brutish Thespian]]
|
||||
player.loseLife(countNonLand, game, source, false);
|
||||
player.removeCounters(CounterType.RAD.getName(), countNonLand, source, game);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue