Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions Mage.Sets/src/mage/cards/k/KidLoki.java
Original file line number Diff line number Diff line change
@@ -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<Permanent> {
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<UUID, Set<MageObjectReference>> 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));
}
}
1 change: 1 addition & 0 deletions Mage.Sets/src/mage/sets/MarvelSuperHeroes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Loading