diff --git a/Mage.Sets/src/mage/cards/k/KidLoki.java b/Mage.Sets/src/mage/cards/k/KidLoki.java new file mode 100644 index 000000000000..c86631c0c9ba --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KidLoki.java @@ -0,0 +1,170 @@ +package mage.cards.k; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import mage.MageObjectReference; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DrawNthCardTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.HexproofAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.Predicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.watchers.Watcher; +import mage.constants.WatcherScope; + +/** + * + * @author nandmp + */ +public final class KidLoki extends CardImpl { + + // Filter enforces the current board-state requirement; the watcher records "this turn" history + private static final FilterPermanent filter + = new FilterControlledCreaturePermanent("creature you control that you've put one or more +1/+1 counters on this turn"); + + static { + filter.add(KidLokiPredicate.instance); + } + + public KidLoki(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.GOD); + this.subtype.add(SubType.HERO); + this.subtype.add(SubType.VILLAIN); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Each creature you control that you've put one or more +1/+1 counters on this turn has hexproof. + Ability ability = new SimpleStaticAbility( + new GainAbilityControlledEffect( + HexproofAbility.getInstance(), Duration.WhileOnBattlefield, filter + ) + ); + this.addAbility(ability, new KidLokiP1P1CountersAddedByPlayerThisTurn()); + + // Whenever you draw your second card each turn, put a +1/+1 counter on Kid Loki. + this.addAbility(new DrawNthCardTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), false, 2 + )); + } + + private KidLoki(final KidLoki card) { + super(card); + } + + @Override + public KidLoki copy() { + return new KidLoki(this); + } +} + +enum KidLokiPredicate implements Predicate { + instance; + + @Override + public boolean apply(Permanent input, Game game) { + return KidLokiP1P1CountersAddedByPlayerThisTurn.checkPermanent(input, input.getControllerId(), game); + } +} + +class KidLokiP1P1CountersAddedByPlayerThisTurn extends Watcher { + + // For each player, remember permanents that received +1/+1 counters this turn. + // MageObjectReference is used so lookups stay correct across zone-change counters. + private final Map> countersAddedByPlayer = new HashMap<>(); + + KidLokiP1P1CountersAddedByPlayerThisTurn() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + UUID playerId; + Permanent permanent; + // If we observed the object while it was still entering, store with +1 offset + // so the later battlefield lookup can still match this object. + int offset = 0; + + if (event.getType() == GameEvent.EventType.COUNTER_ADDED) { + // Normal case: explicit +1/+1 counter add event. + if (!CounterType.P1P1.getName().equals(event.getData()) + || event.getPlayerId() == null) { + return; + } + playerId = event.getPlayerId(); + permanent = game.getPermanent(event.getTargetId()); + if (permanent == null) { + // Some counter events happen during ETB processing. + permanent = game.getPermanentEntering(event.getTargetId()); + offset = 1; + } + if (permanent == null || !permanent.isCreature(game)) { + return; + } + } else if (event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD) { + // ETB fallback: some creatures enter with counters but do not emit COUNTER_ADDED. + permanent = game.getPermanent(event.getTargetId()); + if (permanent == null) { + permanent = game.getPermanentEntering(event.getTargetId()); + offset = 1; + } + if (permanent == null + || !permanent.isCreature(game) + || !permanent.getCounters(game).containsKey(CounterType.P1P1)) { + return; + } + // For ETB events, controller is the player who put the counters as it entered. + playerId = permanent.getControllerId(); + } else { + return; + } + + if (playerId == null) { + return; + } + + countersAddedByPlayer + .computeIfAbsent(playerId, x -> new HashSet<>()) + .add(new MageObjectReference(permanent, game, offset)); + } + + @Override + public void reset() { + super.reset(); + // "This turn" memory must clear every turn. + countersAddedByPlayer.clear(); + } + + static boolean checkPermanent(Permanent permanent, UUID playerId, Game game) { + KidLokiP1P1CountersAddedByPlayerThisTurn watcher = game.getState().getWatcher(KidLokiP1P1CountersAddedByPlayerThisTurn.class); + if (watcher == null || permanent == null || playerId == null) { + return false; + } + + // Rebuild a MOR at current state and check whether this player marked it this turn. + return watcher.countersAddedByPlayer + .getOrDefault(playerId, Collections.emptySet()) + .contains(new MageObjectReference(permanent, game)); + } +} diff --git a/Mage.Sets/src/mage/sets/MarvelSuperHeroes.java b/Mage.Sets/src/mage/sets/MarvelSuperHeroes.java index 0c78fb74a146..247afb182002 100644 --- a/Mage.Sets/src/mage/sets/MarvelSuperHeroes.java +++ b/Mage.Sets/src/mage/sets/MarvelSuperHeroes.java @@ -191,6 +191,7 @@ private MarvelSuperHeroes() { cards.add(new SetCardInfo("Ka-Zar of the Savage Land", 174, Rarity.UNCOMMON, mage.cards.k.KaZarOfTheSavageLand.class)); cards.add(new SetCardInfo("Kang, Temporal Tyrant", 217, Rarity.UNCOMMON, mage.cards.k.KangTemporalTyrant.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Kang, Temporal Tyrant", 450, Rarity.UNCOMMON, mage.cards.k.KangTemporalTyrant.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Kid Loki", 63, Rarity.UNCOMMON, mage.cards.k.KidLoki.class)); cards.add(new SetCardInfo("Killmonger, Scourge of Wakanda", 218, Rarity.UNCOMMON, mage.cards.k.KillmongerScourgeOfWakanda.class)); cards.add(new SetCardInfo("King T'Challa", 219, Rarity.MYTHIC, mage.cards.k.KingTChalla.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("King T'Challa", 346, Rarity.MYTHIC, mage.cards.k.KingTChalla.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/msh/KidLokiTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/msh/KidLokiTest.java new file mode 100644 index 000000000000..668481e7d041 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/msh/KidLokiTest.java @@ -0,0 +1,139 @@ +package org.mage.test.cards.single.msh; + +import mage.abilities.keyword.HexproofAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author nandmp + */ +public class KidLokiTest extends CardTestPlayerBase { + + @Test + public void testCounterAddedBeforeKidLokiEntersStillGivesHexproof() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); + + addCard(Zone.HAND, playerA, "Battlegrowth", 2); + addCard(Zone.HAND, playerA, "Kid Loki", 1); + + // Turn 1: put a +1/+1 counter on Lion. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Battlegrowth", "Silvercoat Lion"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // Turn 3: put a +1/+1 counter on Bears first, then resolve Kid Loki in the same turn. + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Battlegrowth", "Grizzly Bears"); + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Kid Loki"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, "Grizzly Bears", CounterType.P1P1, 1); + assertCounterCount(playerA, "Silvercoat Lion", CounterType.P1P1, 1); + assertAbility(playerA, "Grizzly Bears", HexproofAbility.getInstance(), true); + assertAbility(playerA, "Silvercoat Lion", HexproofAbility.getInstance(), false); + } + + @Test + public void testOpponentAddsCounterNoHexproof() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); + + addCard(Zone.HAND, playerA, "Kid Loki", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Forest", 1); + addCard(Zone.HAND, playerB, "Battlegrowth", 1); + + // Opponent puts the +1/+1 counter on your creature + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Battlegrowth", "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // You cast Kid Loki + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Kid Loki"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, "Grizzly Bears", CounterType.P1P1, 1); + assertAbility(playerA, "Grizzly Bears", HexproofAbility.getInstance(), false); + } + + @Test + public void testAddCounterOnOpponentTurnKidLokiGivesHexproof() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Kid Loki", 1); + addCard(Zone.HAND, playerA, "Ancestral Recall", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1); + addCard(Zone.HAND, playerB, "Bloodchief's Thirst", 1); + + // Opponent tries to destroy creature + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Bloodchief's Thirst", "Kid Loki"); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Ancestral Recall"); + waitStackResolved(2, PhaseStep.PRECOMBAT_MAIN); + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, "Kid Loki", CounterType.P1P1, 1); + assertAbility(playerA, "Kid Loki", HexproofAbility.getInstance(), true); + } + + @Test + public void testEntersWithP1P1CounterGainsHexproof() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.HAND, playerA, "Kid Loki", 1); + addCard(Zone.HAND, playerA, "Stonecoil Serpent", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stonecoil Serpent"); + setChoice(playerA, "X=1"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Kid Loki"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, "Stonecoil Serpent", CounterType.P1P1, 1); + assertAbility(playerA, "Stonecoil Serpent", HexproofAbility.getInstance(), true); + } + + @Test + public void testEntersWithP1P1Counter2GainsHexproof() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Renata, Called to the Hunt", 1); + addCard(Zone.HAND, playerA, "Kid Loki", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Kid Loki"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, "Kid Loki", CounterType.P1P1, 1); + assertAbility(playerA, "Kid Loki", HexproofAbility.getInstance(), true); + } + + @Test + public void testGetsCounterFromTriggeredAbilityHexproof() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Good-Fortune Unicorn", 1); + addCard(Zone.HAND, playerA, "Kid Loki", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Kid Loki"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, "Kid Loki", CounterType.P1P1, 1); + assertAbility(playerA, "Kid Loki", HexproofAbility.getInstance(), true); + } + +}