mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
[FIC] Implement Locke, Treasure Hunter (#13695)
This commit is contained in:
parent
3d45a24959
commit
a7dcd6988f
4 changed files with 309 additions and 0 deletions
205
Mage.Sets/src/mage/cards/l/LockeTreasureHunter.java
Normal file
205
Mage.Sets/src/mage/cards/l/LockeTreasureHunter.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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("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("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("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", 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("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));
|
cards.add(new SetCardInfo("Lulu, Stern Guardian", 143, Rarity.RARE, mage.cards.l.LuluSternGuardian.class, NON_FULL_USE_VARIOUS));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ public enum MageIdentifier {
|
||||||
LaraCroftTombRaiderWatcher,
|
LaraCroftTombRaiderWatcher,
|
||||||
CoramTheUndertakerWatcher,
|
CoramTheUndertakerWatcher,
|
||||||
ThundermanDragonWatcher,
|
ThundermanDragonWatcher,
|
||||||
|
LockeTreasureHunterWatcher,
|
||||||
|
|
||||||
// ----------------------------//
|
// ----------------------------//
|
||||||
// alternate casts //
|
// alternate casts //
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue