Implementing "suspected" mechanic (#11670)

* [MKM] Implement Agrus Kos, Spirit of Justice

* rework effects

* [MKM] Implement Airtight Alibi

* [MKM] Implement Convenient Target

* [MKM] Implement Repeat Offender

* add test

* add more tests

* add tooltip for suspected

* implement requested changes
This commit is contained in:
Evan Kranzler 2024-01-25 20:30:51 -05:00 committed by GitHub
parent ea814ecf0c
commit 5a809f6fe4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 594 additions and 23 deletions

View file

@ -1,6 +1,7 @@
package mage.abilities.effects;
import mage.abilities.Ability;
import mage.abilities.keyword.MenaceAbility;
import mage.constants.*;
import mage.counters.AbilityCounter;
import mage.counters.BoostCounter;
@ -9,14 +10,16 @@ import mage.game.permanent.Permanent;
/**
* @author BetaSteward_at_googlemail.com
* <p>
* Applies boost from from boost counters and also adds abilities from ability counters and suspected mechanic
*/
public class ApplyCountersEffect extends ContinuousEffectImpl {
public class ApplyStatusEffect extends ContinuousEffectImpl {
ApplyCountersEffect() {
ApplyStatusEffect() {
super(Duration.EndOfGame, Outcome.BoostCreature);
}
private ApplyCountersEffect(ApplyCountersEffect effect) {
private ApplyStatusEffect(ApplyStatusEffect effect) {
super(effect);
}
@ -32,6 +35,9 @@ public class ApplyCountersEffect extends ContinuousEffectImpl {
for (AbilityCounter counter : permanent.getCounters(game).getAbilityCounters()) {
permanent.addAbility(counter.getAbility(), source == null ? permanent.getId() : source.getSourceId(), game);
}
if (permanent.isSuspected()) {
permanent.addAbility(new MenaceAbility(false), source == null ? permanent.getId() : source.getSourceId(), game);
}
}
}
if (layer == Layer.PTChangingEffects_7 && sublayer == SubLayer.Counters_7d) {
@ -51,7 +57,7 @@ public class ApplyCountersEffect extends ContinuousEffectImpl {
}
@Override
public ApplyCountersEffect copy() {
return new ApplyCountersEffect(this);
public ApplyStatusEffect copy() {
return new ApplyStatusEffect(this);
}
}

View file

@ -1,7 +1,6 @@
package mage.abilities.effects;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.MageSingleton;
@ -9,8 +8,6 @@ import mage.abilities.StaticAbility;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.abilities.effects.common.continuous.CommanderReplacementEffect;
import mage.cards.*;
import mage.choices.Choice;
import mage.choices.ChoiceImpl;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.predicate.Predicate;
@ -55,19 +52,19 @@ public class ContinuousEffects implements Serializable {
private final Map<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> asThoughEffectsMap = new EnumMap<>(AsThoughEffectType.class);
public final List<ContinuousEffectsList<?>> allEffectsLists = new ArrayList<>(); // contains refs to real effect's list
private final ApplyCountersEffect applyCounters;
private final ApplyStatusEffect applyStatus;
private final AuraReplacementEffect auraReplacementEffect;
private final Map<String, ContinuousEffectsList<ContinuousEffect>> lastEffectsListOnLayer = new HashMap<>(); // helps to find out new effect timestamps on layers
public ContinuousEffects() {
applyCounters = new ApplyCountersEffect();
applyStatus = new ApplyStatusEffect();
auraReplacementEffect = new AuraReplacementEffect();
collectAllEffects();
}
protected ContinuousEffects(final ContinuousEffects effect) {
applyCounters = effect.applyCounters.copy();
applyStatus = effect.applyStatus.copy();
auraReplacementEffect = effect.auraReplacementEffect.copy();
layeredEffects = effect.layeredEffects.copy();
continuousRuleModifyingEffects = effect.continuousRuleModifyingEffects.copy();
@ -995,7 +992,7 @@ public class ContinuousEffects implements Serializable {
boolean done = false;
Map<ContinuousEffect, Set<UUID>> waitingEffects = new LinkedHashMap<>();
Set<UUID> appliedEffects = new HashSet<>();
applyCounters.apply(Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, null, game);
applyStatus.apply(Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, null, game);
activeLayerEffects = getLayeredEffects(game, "layer_6");
while (!done) { // loop needed if a added effect adds again an effect (e.g. Level 5- of Joraga Treespeaker)
@ -1108,7 +1105,7 @@ public class ContinuousEffects implements Serializable {
}
}
applyCounters.apply(Layer.PTChangingEffects_7, SubLayer.Counters_7d, null, game);
applyStatus.apply(Layer.PTChangingEffects_7, SubLayer.Counters_7d, null, game);
for (ContinuousEffect effect : layer) {
Set<Ability> abilities = layeredEffects.getAbility(effect.getId());
@ -1410,7 +1407,7 @@ public class ContinuousEffects implements Serializable {
for (Map.Entry<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> entry : asThoughEffectsMap.entrySet()) {
logger.info("... " + entry.getKey().toString() + ": " + entry.getValue().size());
}
logger.info("applyCounters ....................: " + (applyCounters != null ? "exists" : "null"));
logger.info("applyStatus ....................: " + (applyStatus != null ? "exists" : "null"));
logger.info("auraReplacementEffect ............: " + (continuousRuleModifyingEffects != null ? "exists" : "null"));
Map<String, TraceInfo> orderedEffects = new TreeMap<>();
traceAddContinuousEffects(orderedEffects, layeredEffects, game, "layeredEffects................");

View file

@ -0,0 +1,17 @@
package mage.filter.predicate.permanent;
import mage.filter.predicate.Predicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
*/
public enum SuspectedPredicate implements Predicate<Permanent> {
instance;
@Override
public boolean apply(Permanent input, Game game) {
return input.isSuspected();
}
}

View file

@ -1993,7 +1993,7 @@ public abstract class GameImpl implements Game {
}
newBluePrint.assignNewId();
if (copyFromPermanent.isTransformed()) {
TransformAbility.transformPermanent(newBluePrint,this, source);
TransformAbility.transformPermanent(newBluePrint, this, source);
}
if (copyFromPermanent.isPrototyped()) {
Abilities<Ability> abilities = copyFromPermanent.getAbilities();
@ -3552,8 +3552,9 @@ public abstract class GameImpl implements Game {
public Map<MageObjectReference, Map<String, Object>> getPermanentCostsTags() {
return state.getPermanentCostsTags();
}
@Override
public void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source){
public void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source) {
state.storePermanentCostsTags(permanentMOR, source);
}

View file

@ -567,6 +567,12 @@ public class GameEvent implements Serializable {
playerId the player crafting
*/
EXILED_WHILE_CRAFTING,
/* Become suspected
targetId the permanent being suspected
sourceId of the ability suspecting
playerId the player suspecting
*/
BECOME_SUSPECTED,
//custom events
CUSTOM_EVENT
}

View file

@ -75,6 +75,10 @@ public interface Permanent extends Card, Controllable {
void setRenowned(boolean value);
boolean isSuspected();
void setSuspected(boolean value, Game game, Ability source);
boolean isPrototyped();
void setPrototyped(boolean value);
@ -222,6 +226,7 @@ public interface Permanent extends Card, Controllable {
* @return can be null for exists abilities
*/
Ability addAbility(Ability ability, UUID sourceId, Game game);
Ability addAbility(Ability ability, UUID sourceId, Game game, boolean fromExistingObject);
void removeAllAbilities(UUID sourceId, Game game);
@ -313,7 +318,7 @@ public interface Permanent extends Card, Controllable {
/**
* Fast check for attacking possibilities (is it possible to attack permanent/planeswalker/battle)
*
* @param attackerId creature to attack, can be null
* @param attackerId creature to attack, can be null
* @param defendingPlayerId defending player
* @param game
* @return

View file

@ -69,6 +69,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected boolean transformed;
protected boolean monstrous;
protected boolean renowned;
protected boolean suspected;
protected boolean manifested = false;
protected boolean morphed = false;
protected boolean ringBearerFlag = false;
@ -162,6 +163,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.transformed = permanent.transformed;
this.monstrous = permanent.monstrous;
this.renowned = permanent.renowned;
this.suspected = permanent.suspected;
this.ringBearerFlag = permanent.ringBearerFlag;
this.classLevel = permanent.classLevel;
this.goadingPlayers.addAll(permanent.goadingPlayers);
@ -392,8 +394,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
/**
* Add an ability to the permanent. When copying from an existing source
* you should use the fromExistingObject variant of this function to prevent double-copying subabilities
* @param ability The ability to be added
* @param sourceId id of the source doing the added (for the effect created to add it)
*
* @param ability The ability to be added
* @param sourceId id of the source doing the added (for the effect created to add it)
* @param game
* @return The newly added ability copy
*/
@ -403,11 +406,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
}
/**
* @param ability The ability to be added
* @param sourceId id of the source doing the added (for the effect created to add it)
* @param ability The ability to be added
* @param sourceId id of the source doing the added (for the effect created to add it)
* @param game
* @param fromExistingObject if copying abilities from an existing source then must ignore sub-abilities because they're already on the source object
* Otherwise sub-abilities will be added twice to the resulting object
* Otherwise sub-abilities will be added twice to the resulting object
* @return The newly added ability copy
*/
@Override
@ -1490,7 +1493,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override
public boolean canBlock(UUID attackerId, Game game) {
if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty() || isBattle(game)) {
if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty() || isBattle(game) || isSuspected()) {
return false;
}
Permanent attacker = game.getPermanent(attackerId);
@ -1671,6 +1674,28 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.renowned = value;
}
@Override
public boolean isSuspected() {
return suspected;
}
private static final String suspectedInfoKey = "IS_SUSPECTED";
@Override
public void setSuspected(boolean value, Game game, Ability source) {
if (!value || !game.replaceEvent(GameEvent.getEvent(
EventType.BECOME_SUSPECTED, getId(),
source, source.getControllerId()
))) {
this.suspected = value;
}
if (this.suspected) {
addInfo(suspectedInfoKey, CardUtil.addToolTipMarkTags("Suspected (has menace and can't block)"), game);
} else {
addInfo(suspectedInfoKey, null, game);
}
}
// Used as key for the ring bearer info.
private static final String ringbearerInfoKey = "IS_RINGBEARER";