Implementing "collect evidence" mechanic (#11671)

* [MKM] Implement Axebane Ferox

* add exile to cost, fix text

* add targeting message copied from crew ability

* [MKM] Implement Forensic Researcher

* [MKM] Implement Izoni, Center of the Web

* implement requested changes

* merge fix

* [MKM] Implement Sample Collector

* [MKM] Implement Evidence Examiner

* [MKM] Implement Surveillance Monitor

* [MKM] Implement Vitu-Ghazi Inspector

* [MKM] Implement Crimestopper Sprite

* [MKM] Implement Urgent Necropsy

* [MKM] Implement Analyze the Pollen

* implement requested changes

* add can pay cost check to counter unless pays effect

* fix test failure

* add tests

* fix prompt message
This commit is contained in:
Evan Kranzler 2024-01-27 20:26:14 -05:00 committed by GitHub
parent 322c49e37f
commit 99c2ffa231
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1162 additions and 4 deletions

View file

@ -0,0 +1,37 @@
package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
/**
* @author TheElk801
*/
public class CollectEvidenceTriggeredAbility extends TriggeredAbilityImpl {
public CollectEvidenceTriggeredAbility(Effect effect, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
setTriggerPhrase("Whenever you collect evidence, ");
}
private CollectEvidenceTriggeredAbility(final CollectEvidenceTriggeredAbility ability) {
super(ability);
}
@Override
public CollectEvidenceTriggeredAbility copy() {
return new CollectEvidenceTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.EVIDENCE_COLLECTED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return isControlledBy(event.getPlayerId());
}
}

View file

@ -0,0 +1,26 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.keyword.CollectEvidenceAbility;
import mage.game.Game;
import mage.util.CardUtil;
/**
* Checks if the spell was cast with the alternate collect evidence cost
*
* @author TheElk801
*/
public enum CollectedEvidenceCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
return CardUtil.checkSourceCostsTagExists(game, source, CollectEvidenceAbility.COLLECT_EVIDENCE_ACTIVATION_VALUE_KEY);
}
@Override
public String toString() {
return "Evidence was collected";
}
}

View file

@ -0,0 +1,99 @@
package mage.abilities.costs.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.hint.HintUtils;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetCardInYourGraveyard;
import java.awt.*;
import java.util.Objects;
import java.util.UUID;
/**
* @author TheElk801
*/
public class CollectEvidenceCost extends CostImpl {
private final int amount;
public CollectEvidenceCost(int amount) {
super();
this.amount = amount;
this.text = "collect evidence " + amount;
}
private CollectEvidenceCost(final CollectEvidenceCost cost) {
super(cost);
this.amount = cost.amount;
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
Player player = game.getPlayer(controllerId);
return player != null && player
.getGraveyard()
.getCards(game)
.stream()
.filter(Objects::nonNull)
.mapToInt(MageObject::getManaValue)
.sum() >= amount;
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Player player = game.getPlayer(controllerId);
if (player == null) {
paid = false;
return paid;
}
// TODO: require target to have minimum selected total mana value (requires refactor)
Target target = new TargetCardInYourGraveyard(1, Integer.MAX_VALUE) {
@Override
public String getMessage() {
// shows selected mana value
int totalMV = this
.getTargets()
.stream()
.map(game::getCard)
.filter(Objects::nonNull)
.mapToInt(MageObject::getManaValue)
.sum();
return super.getMessage() + HintUtils.prepareText(
" (selected mana value " + totalMV + " of " + amount + ")",
totalMV >= amount ? Color.GREEN : Color.RED
);
}
}.withNotTarget(true);
player.choose(Outcome.Exile, target, source, game);
Cards cards = new CardsImpl(target.getTargets());
paid = cards
.getCards(game)
.stream()
.filter(Objects::nonNull)
.mapToInt(MageObject::getManaValue)
.sum() >= amount;
if (paid) {
player.moveCards(cards, Zone.EXILED, source, game);
game.fireEvent(GameEvent.getEvent(
GameEvent.EventType.EVIDENCE_COLLECTED,
source.getSourceId(), source, source.getControllerId(), amount
));
}
return paid;
}
@Override
public CollectEvidenceCost copy() {
return new CollectEvidenceCost(this);
}
}

View file

@ -84,7 +84,8 @@ public class CounterUnlessPaysEffect extends OneShotEffect {
message += costValueMessage + '?';
costToPay.clearPaid();
if (!(player.chooseUse(Outcome.Benefit, message, source, game)
if (!(costToPay.canPay(source, source, player.getId(), game)
&& player.chooseUse(Outcome.Benefit, message, source, game)
&& costToPay.pay(source, game, source, spell.getControllerId(), false, null))) {
game.informPlayers(player.getLogName() + " chooses not to pay " + costValueMessage + " to prevent the counter effect");
game.getStack().counter(spell.getId(), source, game, exile ? PutCards.EXILED : PutCards.GRAVEYARD);

View file

@ -0,0 +1,103 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.condition.common.CollectedEvidenceCondition;
import mage.abilities.costs.*;
import mage.abilities.costs.common.CollectEvidenceCost;
import mage.abilities.hint.ConditionTrueHint;
import mage.abilities.hint.Hint;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.players.Player;
/**
* @author TheElk801
*/
public class CollectEvidenceAbility extends StaticAbility implements OptionalAdditionalSourceCosts {
private static final String promptString = "Collect evidence ";
private static final String keywordText = "As an additional cost to cast this spell, you may collect evidence ";
private static final String reminderText = "Exile cards with total mana value $$$ or greater from your graveyard";
private final String rule;
private final int amount;
public static final String COLLECT_EVIDENCE_ACTIVATION_VALUE_KEY = "collectEvidenceActivation";
protected OptionalAdditionalCost additionalCost;
private static final Hint hint = new ConditionTrueHint(CollectedEvidenceCondition.instance, "evidence was collected");
public static OptionalAdditionalCost makeCost(int amount) {
OptionalAdditionalCost cost = new OptionalAdditionalCostImpl(
keywordText + amount,
reminderText.replace("$$$", "" + amount),
new CollectEvidenceCost(amount)
);
cost.setRepeatable(false);
return cost;
}
public CollectEvidenceAbility(int amount) {
super(Zone.STACK, null);
this.additionalCost = makeCost(amount);
this.rule = additionalCost.getName() + ' ' + additionalCost.getReminderText();
this.setRuleAtTheTop(true);
this.addHint(hint);
this.amount = amount;
}
private CollectEvidenceAbility(final CollectEvidenceAbility ability) {
super(ability);
this.rule = ability.rule;
this.additionalCost = ability.additionalCost.copy();
this.amount = ability.amount;
}
@Override
public CollectEvidenceAbility copy() {
return new CollectEvidenceAbility(this);
}
public void resetCost() {
if (additionalCost != null) {
additionalCost.reset();
}
}
@Override
public void addOptionalAdditionalCosts(Ability ability, Game game) {
if (!(ability instanceof SpellAbility)) {
return;
}
Player player = game.getPlayer(ability.getControllerId());
if (player == null) {
return;
}
this.resetCost();
boolean canPay = additionalCost.canPay(ability, this, ability.getControllerId(), game);
if (!canPay || !player.chooseUse(Outcome.Exile, promptString + amount + '?', ability, game)) {
return;
}
additionalCost.activate();
for (Cost cost : ((Costs<Cost>) additionalCost)) {
ability.getCosts().add(cost.copy());
}
ability.setCostsTag(COLLECT_EVIDENCE_ACTIVATION_VALUE_KEY, null);
}
@Override
public String getCastMessageSuffix() {
return additionalCost.getCastSuffixMessage(0);
}
@Override
public String getRule() {
return rule;
}
}

View file

@ -573,6 +573,12 @@ public class GameEvent implements Serializable {
playerId the player suspecting
*/
BECOME_SUSPECTED,
/* Evidence collected
targetId same as sourceId
sourceId of the ability for the cost
playerId the player paying the cost
*/
EVIDENCE_COLLECTED,
//custom events
CUSTOM_EVENT
}

View file

@ -0,0 +1,37 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.abilities.keyword.MenaceAbility;
import mage.abilities.keyword.ReachAbility;
import mage.constants.CardType;
import mage.constants.SubType;
/**
* @author TheElk801
*/
public final class IzoniSpiderToken extends TokenImpl {
public IzoniSpiderToken() {
super("Spider Token", "2/1 black and green Spider creature token with menace and reach");
cardType.add(CardType.CREATURE);
color.setBlack(true);
color.setGreen(true);
subtype.add(SubType.SPIDER);
power = new MageInt(2);
toughness = new MageInt(1);
// Menace
this.addAbility(new MenaceAbility());
// Reach
this.addAbility(ReachAbility.getInstance());
}
private IzoniSpiderToken(final IzoniSpiderToken token) {
super(token);
}
public IzoniSpiderToken copy() {
return new IzoniSpiderToken(this);
}
}

View file

@ -74,7 +74,7 @@ public final class CardUtil {
public static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
private static final List<String> costWords = Arrays.asList(
"put", "return", "exile", "discard", "sacrifice", "remove", "tap", "reveal", "pay"
"put", "return", "exile", "discard", "sacrifice", "remove", "tap", "reveal", "pay", "collect"
);
public static final int TESTS_SET_CODE_LOOKUP_LENGTH = 6; // search set code in commands like "set_code-card_name"
@ -1764,8 +1764,8 @@ public final class CardUtil {
* Warning, don't use self reference objects because it will raise StackOverflowError
*
* @param value
* @return
* @param <T>
* @return
*/
public static <T> T deepCopyObject(T value) {
if (isImmutableObject(value)) {