[FIC] Implement Locke, Treasure Hunter (#13695)

This commit is contained in:
Evan Kranzler 2025-05-31 13:42:39 -04:00 committed by GitHub
parent 3d45a24959
commit a7dcd6988f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 309 additions and 0 deletions

View file

@ -0,0 +1,205 @@
package mage.cards.l;
import mage.MageIdentifier;
import mage.MageInt;
import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesSourceEffect;
import mage.cards.*;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.TreasureToken;
import mage.players.Player;
import mage.util.CardUtil;
import mage.watchers.Watcher;
import java.util.*;
/**
* @author TheElk801
*/
public final class LockeTreasureHunter extends CardImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creatures with greater power");
static {
filter.add(LockeTreasureHunterPredicate.instance);
}
public LockeTreasureHunter(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.ROGUE);
this.power = new MageInt(2);
this.toughness = new MageInt(3);
// Locke can't be blocked by creatures with greater power.
this.addAbility(new SimpleStaticAbility(new CantBeBlockedByCreaturesSourceEffect(filter, Duration.WhileOnBattlefield)));
// Mug -- Whenever Locke attacks, each player mills a card. If a land card was milled this way, create a Treasure token. Until end of turn, you may cast a spell from among those cards.
this.addAbility(new AttacksTriggeredAbility(new LockeTreasureHunterEffect())
.withFlavorWord("Mug")
.setIdentifier(MageIdentifier.LockeTreasureHunterWatcher), new LockeTreasureHunterWatcher());
}
private LockeTreasureHunter(final LockeTreasureHunter card) {
super(card);
}
@Override
public LockeTreasureHunter copy() {
return new LockeTreasureHunter(this);
}
public static Ability makeTestAbility() {
Ability ability = new SimpleActivatedAbility(
new LockeTreasureHunterEffect(), new GenericManaCost(0)
).setIdentifier(MageIdentifier.LockeTreasureHunterWatcher);
ability.addWatcher(new LockeTreasureHunterWatcher());
return ability;
}
}
enum LockeTreasureHunterPredicate implements ObjectSourcePlayerPredicate<Permanent> {
instance;
@Override
public boolean apply(ObjectSourcePlayer<Permanent> input, Game game) {
return Optional
.ofNullable(input.getSource().getSourcePermanentIfItStillExists(game))
.map(MageObject::getPower)
.map(MageInt::getValue)
.map(x -> x < input.getObject().getPower().getValue())
.orElse(false);
}
}
class LockeTreasureHunterEffect extends OneShotEffect {
LockeTreasureHunterEffect() {
super(Outcome.Benefit);
staticText = "each player mills a card. If a land card was milled this way, " +
"create a Treasure token. Until end of turn, you may cast a spell from among those cards";
}
private LockeTreasureHunterEffect(final LockeTreasureHunterEffect effect) {
super(effect);
}
@Override
public LockeTreasureHunterEffect copy() {
return new LockeTreasureHunterEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Cards cards = new CardsImpl();
for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) {
Player player = game.getPlayer(playerId);
if (player != null) {
cards.addAllCards(player.millCards(1, source, game).getCards(game));
}
}
if (cards.isEmpty()) {
return false;
}
if (cards.count(StaticFilters.FILTER_CARD_LAND, game) > 0) {
new TreasureToken().putOntoBattlefield(1, game, source);
}
LockeTreasureHunterWatcher.saveCards(cards, game, source);
return true;
}
}
class LockeTreasureHunterWatcher extends Watcher {
private static class LockeTreasureHunterCondition implements Condition {
private final UUID permissionId;
LockeTreasureHunterCondition(UUID permissionId) {
this.permissionId = permissionId;
}
@Override
public boolean apply(Game game, Ability source) {
return game
.getState()
.getWatcher(LockeTreasureHunterWatcher.class)
.checkPermission(permissionId, source, game);
}
}
// Maps cards to the specific instance that gave them permission to be cast
private final Map<MageObjectReference, UUID> morMap = new HashMap<>();
// Maps permissions to the players who can use them
private static final Map<UUID, UUID> playerPermissionMap = new HashMap<>();
// Tracks permissions which have already been used
private final Set<UUID> usedSet = new HashSet<>();
LockeTreasureHunterWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() != GameEvent.EventType.SPELL_CAST
|| !event.hasApprovingIdentifier(MageIdentifier.LockeTreasureHunterWatcher)) {
return;
}
Optional.ofNullable(event)
.map(GameEvent::getTargetId)
.map(game::getSpell)
.map(spell -> new MageObjectReference(spell.getMainCard(), game, -1))
.map(mor -> morMap.getOrDefault(mor, null))
.ifPresent(usedSet::add);
}
@Override
public void reset() {
super.reset();
morMap.clear();
playerPermissionMap.clear();
usedSet.clear();
}
static void saveCards(Cards cards, Game game, Ability source) {
game.getState()
.getWatcher(LockeTreasureHunterWatcher.class)
.handleSaveCards(cards, game, source);
}
private void handleSaveCards(Cards cards, Game game, Ability source) {
UUID permissionId = UUID.randomUUID();
playerPermissionMap.put(permissionId, source.getControllerId());
Condition condition = new LockeTreasureHunterCondition(permissionId);
for (Card card : cards.getCards(game)) {
morMap.put(new MageObjectReference(card, game), permissionId);
CardUtil.makeCardPlayable(
game, source, card, true, Duration.EndOfTurn,
false, source.getControllerId(), condition
);
}
}
private boolean checkPermission(UUID permissionId, Ability source, Game game) {
return !usedSet.contains(permissionId)
&& source.isControlledBy(playerPermissionMap.getOrDefault(permissionId, null));
}
}

View file

@ -222,6 +222,8 @@ public final class FinalFantasyCommander extends ExpansionSet {
cards.add(new SetCardInfo("Lethal Scheme", 277, Rarity.RARE, mage.cards.l.LethalScheme.class));
cards.add(new SetCardInfo("Lightning Greaves", 349, Rarity.UNCOMMON, mage.cards.l.LightningGreaves.class));
cards.add(new SetCardInfo("Lingering Souls", 245, Rarity.UNCOMMON, mage.cards.l.LingeringSouls.class));
cards.add(new SetCardInfo("Locke, Treasure Hunter", 177, Rarity.RARE, mage.cards.l.LockeTreasureHunter.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Locke, Treasure Hunter", 87, Rarity.RARE, mage.cards.l.LockeTreasureHunter.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Lord Jyscal Guado", 137, Rarity.RARE, mage.cards.l.LordJyscalGuado.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Lord Jyscal Guado", 23, Rarity.RARE, mage.cards.l.LordJyscalGuado.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Lulu, Stern Guardian", 143, Rarity.RARE, mage.cards.l.LuluSternGuardian.class, NON_FULL_USE_VARIOUS));

View file

@ -0,0 +1,101 @@
package org.mage.test.cards.single.fic;
import mage.cards.l.LockeTreasureHunter;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public class LockeTreasureHunterTest extends CardTestPlayerBase {
private static final String dwarvenGrunt = "Dwarven Grunt";
private static final String goblinMountaineer = "Goblin Mountaineer";
private static final String mountainGoat = "Mountain Goat";
private static final String zodiacGoat = "Zodiac Goat";
private void makeTester() {
addCustomCardWithAbility("tester", playerA, LockeTreasureHunter.makeTestAbility());
}
private void assertOptions(String... optionsToExpect) {
Set<String> options = playerA
.getPlayable(currentGame, false)
.stream()
.map(Objects::toString)
.collect(Collectors.toSet());
Set<String> failures = new HashSet<>();
for (String option : optionsToExpect) {
if (options.stream().noneMatch(s -> s.contains(option))) {
failures.add(option);
}
}
Assert.assertEquals(
"The following cards should be available to cast but aren't: " +
failures.stream().collect(Collectors.joining(", ")), 0, failures.size()
);
Assert.assertEquals(
"There should be " + (2 + optionsToExpect.length) + " available actions",
2 + optionsToExpect.length, options.size()
);
}
@Test
public void testCastSome() {
skipInitShuffling();
makeTester();
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.LIBRARY, playerA, dwarvenGrunt);
addCard(Zone.LIBRARY, playerA, goblinMountaineer);
addCard(Zone.LIBRARY, playerB, mountainGoat);
addCard(Zone.LIBRARY, playerB, zodiacGoat);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}");
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, dwarvenGrunt);
waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertOptions(goblinMountaineer, zodiacGoat);
assertPermanentCount(playerA, dwarvenGrunt, 1);
}
@Test
public void testCastAll() {
skipInitShuffling();
makeTester();
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.LIBRARY, playerA, dwarvenGrunt);
addCard(Zone.LIBRARY, playerA, goblinMountaineer);
addCard(Zone.LIBRARY, playerB, mountainGoat);
addCard(Zone.LIBRARY, playerB, zodiacGoat);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}");
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, dwarvenGrunt);
waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, zodiacGoat);
waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertOptions();
assertPermanentCount(playerA, dwarvenGrunt, 1);
assertPermanentCount(playerA, zodiacGoat, 1);
}
}

View file

@ -37,6 +37,7 @@ public enum MageIdentifier {
LaraCroftTombRaiderWatcher,
CoramTheUndertakerWatcher,
ThundermanDragonWatcher,
LockeTreasureHunterWatcher,
// ----------------------------//
// alternate casts //