Commit e0fd8125 authored by Michael Kamensky's avatar Michael Kamensky

Merge branch 'patch-teysa' into 'master'

TriggerHandler: Only real die can look back

Closes #797

See merge request core-developers/forge!1298
parents 2c2104df 9778f9e3
......@@ -1465,7 +1465,8 @@ public class Card extends GameEntity implements Comparable<Card> {
}
}
}
if (keyword.startsWith("CantBeCounteredBy")) {
if (keyword.startsWith("CantBeCounteredBy") || keyword.startsWith("Panharmonicon")
|| keyword.startsWith("Dieharmonicon")) {
final String[] p = keyword.split(":");
sbLong.append(p[2]).append("\r\n");
} else if (keyword.startsWith("etbCounter")) {
......
......@@ -11,6 +11,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Table;
import forge.game.Game;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
......@@ -45,4 +46,39 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
game.getTriggerHandler().runTrigger(TriggerType.ChangesZoneAll, runParams, false);
}
}
public CardCollection filterCards(Iterable<ZoneType> origin, ZoneType destination, String valid, Card host, SpellAbility sa) {
CardCollection allCards = new CardCollection();
if (destination != null) {
if (!containsColumn(destination)) {
return allCards;
}
}
if (origin != null) {
for (ZoneType z : origin) {
if (containsRow(z)) {
if (destination != null) {
allCards.addAll(row(z).get(destination));
} else {
for (CardCollection c : row(z).values()) {
allCards.addAll(c);
}
}
}
}
} else if (destination != null) {
for (CardCollection c : column(destination).values()) {
allCards.addAll(c);
}
} else {
for (CardCollection c : values()) {
allCards.addAll(c);
}
}
if (valid != null) {
allCards = CardLists.getValidCards(allCards, valid.split(","), host.getController(), host, sa);
}
return allCards;
}
}
......@@ -42,48 +42,19 @@ public class TriggerChangesZoneAll extends Trigger {
}
private CardCollection filterCards(CardZoneTable table) {
CardCollection allCards = new CardCollection();
ZoneType destination = null;
List<ZoneType> origin = null;
if (hasParam("Destination")) {
if (!getParam("Destination").equals("Any")) {
destination = ZoneType.valueOf(getParam("Destination"));
if (!table.containsColumn(destination)) {
return allCards;
}
}
if (hasParam("Destination") && !getParam("Destination").equals("Any")) {
destination = ZoneType.valueOf(getParam("Destination"));
}
if (hasParam("Origin") && !getParam("Origin").equals("Any")) {
if (getParam("Origin") == null) {
return allCards;
}
final List<ZoneType> origin = ZoneType.listValueOf(getParam("Origin"));
for (ZoneType z : origin) {
if (table.containsRow(z)) {
if (destination != null) {
allCards.addAll(table.row(z).get(destination));
} else {
for (CardCollection c : table.row(z).values()) {
allCards.addAll(c);
}
}
}
}
} else if (destination != null) {
for (CardCollection c : table.column(destination).values()) {
allCards.addAll(c);
}
} else {
for (CardCollection c : table.values()) {
allCards.addAll(c);
}
origin = ZoneType.listValueOf(getParam("Origin"));
}
if (hasParam("ValidCards")) {
allCards = CardLists.getValidCards(allCards, getParam("ValidCards").split(","),
getHostCard().getController(), getHostCard(), null);
}
return allCards;
final String valid = this.getParamOrDefault("ValidCards", null);
return table.filterCards(origin, destination, valid, getHostCard(), null);
}
}
......@@ -25,7 +25,10 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardUtil;
import forge.game.card.CardZoneTable;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.Ability;
......@@ -42,6 +45,7 @@ import io.sentry.event.BreadcrumbBuilder;
import java.util.*;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
......@@ -376,11 +380,7 @@ public class TriggerHandler {
// Static triggers
for (final Trigger t : Lists.newArrayList(activeTriggers)) {
if (t.isStatic() && canRunTrigger(t, mode, runParams)) {
int x = 1 + handlePanharmonicon(t, runParams);
for (int i = 0; i < x; ++i) {
runSingleTrigger(t, runParams);
}
runSingleTrigger(t, runParams);
checkStatics = true;
}
......@@ -448,7 +448,7 @@ public class TriggerHandler {
}
}
int x = 1 + handlePanharmonicon(t, runParams);;
int x = 1 + handlePanharmonicon(t, runParams, player);
for (int i = 0; i < x; ++i) {
runSingleTrigger(t, runParams);
......@@ -692,46 +692,81 @@ public class TriggerHandler {
}
}
private int handlePanharmonicon(final Trigger t, final Map<String, Object> runParams) {
// Need to get the last info from the trigger host
final Card host = game.getChangeZoneLKIInfo(t.getHostCard());
final Player p = host.getController();
private int handlePanharmonicon(final Trigger t, final Map<String, Object> runParams, final Player p) {
Card host = t.getHostCard();
// not a changesZone trigger
if (t.getMode() != TriggerType.ChangesZone) {
// not a changesZone trigger or changesZoneAll
if (t.getMode() != TriggerType.ChangesZone && t.getMode() != TriggerType.ChangesZoneAll) {
return 0;
}
// leave battlefield trigger, might be dying
// only real changeszone look back for this
if (t.getMode() == TriggerType.ChangesZone && "Battlefield".equals(t.getParam("Origin"))) {
// Need to get the last info from the trigger host
host = game.getChangeZoneLKIInfo(host);
}
// not a Permanent you control
if (!host.isPermanent() || !host.isInZone(ZoneType.Battlefield)) {
return 0;
}
int n = 0;
for (final String kw : p.getKeywords()) {
if (kw.startsWith("Panharmonicon")) {
// Enter the Battlefield Trigger
if (runParams.get("Destination") instanceof String) {
final String dest = (String) runParams.get("Destination");
if ("Battlefield".equals(dest) && runParams.get("Card") instanceof Card) {
final Card card = (Card) runParams.get("Card");
final String valid = kw.split(":")[1];
if (card.isValid(valid.split(","), p, host, null)) {
n++;
if (t.getMode() == TriggerType.ChangesZone) {
// iterate over all cards
final List<Card> lastCards = CardLists.filterControlledBy(p.getGame().getLastStateBattlefield(), p);
for (final Card ck : lastCards) {
for (final KeywordInterface ki : ck.getKeywords()) {
final String kw = ki.getOriginal();
if (kw.startsWith("Panharmonicon")) {
// Enter the Battlefield Trigger
if (runParams.get("Destination") instanceof String) {
final String dest = (String) runParams.get("Destination");
if ("Battlefield".equals(dest) && runParams.get("Card") instanceof Card) {
final Card card = (Card) runParams.get("Card");
final String valid = kw.split(":")[1];
if (card.isValid(valid.split(","), p, ck, null)) {
n++;
}
}
}
} else if (kw.startsWith("Dieharmonicon")) {
// 700.4. The term dies means “is put into a graveyard from the battlefield.”
if (runParams.get("Origin") instanceof String) {
final String origin = (String) runParams.get("Origin");
if ("Battlefield".equals(origin) && runParams.get("Destination") instanceof String) {
final String dest = (String) runParams.get("Destination");
if ("Graveyard".equals(dest) && runParams.get("Card") instanceof Card) {
final Card card = (Card) runParams.get("Card");
final String valid = kw.split(":")[1];
if (card.isValid(valid.split(","), p, ck, null)) {
n++;
}
}
}
}
}
}
} else if (kw.startsWith("Dieharmonicon")) {
// 700.4. The term dies means “is put into a graveyard from the battlefield.”
if (runParams.get("Origin") instanceof String) {
final String origin = (String) runParams.get("Origin");
if ("Battlefield".equals(origin) && runParams.get("Destination") instanceof String) {
final String dest = (String) runParams.get("Destination");
if ("Graveyard".equals(dest) && runParams.get("Card") instanceof Card) {
final Card card = (Card) runParams.get("Card");
if (card.isCreature()) {
n++;
}
}
} else if (t.getMode() == TriggerType.ChangesZoneAll) {
final CardZoneTable table = (CardZoneTable) runParams.get("Cards");
// iterate over all cards
for (final Card ck : p.getCardsIn(ZoneType.Battlefield)) {
for (final KeywordInterface ki : ck.getKeywords()) {
final String kw = ki.getOriginal();
if (kw.startsWith("Panharmonicon")) {
// currently there is no ChangesZoneAll that would trigger on etb
final String valid = kw.split(":")[1];
if (!table.filterCards(null, ZoneType.Battlefield, valid, ck, null).isEmpty()) {
n++;
}
} else if (kw.startsWith("Dieharmonicon")) {
// 700.4. The term dies means “is put into a graveyard from the battlefield.”
final String valid = kw.split(":")[1];
if (!table.filterCards(ImmutableList.of(ZoneType.Battlefield), ZoneType.Graveyard,
valid, ck, null).isEmpty()) {
n++;
}
}
}
......
......@@ -13,7 +13,7 @@ public class TriggerWaiting {
private Map<String, Object> params;
private List<Trigger> triggers = null;
public TriggerWaiting(TriggerType m, Map<String, Object> p) {
public TriggerWaiting(TriggerType m, Map<String, Object> p) {
mode = m;
params = p;
}
......@@ -25,7 +25,6 @@ public class TriggerWaiting {
public Map<String, Object> getParams() {
return params;
}
public List<Trigger> getTriggers() {
return triggers;
......@@ -35,7 +34,7 @@ public class TriggerWaiting {
this.triggers = triggers;
}
@Override
@Override
public String toString() {
return TextUtil.concatWithSpace("Waiting trigger:", mode.toString(),"with", params.toString());
}
......
......@@ -1532,4 +1532,168 @@ public class GameSimulatorTest extends SimulationTestCase {
int effects = simGoblin.getCounters(CounterType.P1P1) + simGoblin.getKeywordMagnitude(Keyword.HASTE);
assertTrue(effects == 2);
}
public void testTeysaKarlovXathridNecromancer() {
// Teysa Karlov and Xathrid Necromancer dying at the same time makes 4 token
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
addCard("Teysa Karlov", p);
addCard("Xathrid Necromancer", p);
for (int i = 0; i < 4; i++) {
addCardToZone("Plains", p, ZoneType.Battlefield);
}
Card wrathOfGod = addCardToZone("Wrath of God", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
SpellAbility wrathSA = wrathOfGod.getFirstSpellAbility();
assertNotNull(wrathSA);
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(wrathSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
int numZombies = countCardsWithName(simGame, "Zombie");
assertTrue(numZombies == 4);
}
public void testDoubleTeysaKarlovXathridNecromancer() {
// Teysa Karlov dieing because of Legendary rule will make Xathrid Necromancer trigger 3 times
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
addCard("Teysa Karlov", p);
addCard("Xathrid Necromancer", p);
for (int i = 0; i < 3; i++) {
addCard("Plains", p);
}
addCard("Swamp", p);
Card second = addCardToZone("Teysa Karlov", p, ZoneType.Hand);
SpellAbility secondSA = second.getFirstSpellAbility();
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(secondSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
int numZombies = countCardsWithName(simGame, "Zombie");
assertTrue(numZombies == 3);
}
public void testTeysaKarlovGitrogMonster() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
addCard("Teysa Karlov", p);
addCard("The Gitrog Monster", p);
addCard("Dryad Arbor", p);
for (int i = 0; i < 4; i++) {
addCard("Plains", p);
addCardToZone("Plains", p, ZoneType.Library);
}
Card armageddon = addCardToZone("Armageddon", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
SpellAbility armageddonSA = armageddon.getFirstSpellAbility();
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(armageddonSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
// Two cards drawn
assertTrue(simGame.getPlayers().get(0).getZone(ZoneType.Hand).size() == 2);
}
public void testTeysaKarlovGitrogMonsterGitrogDies() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
Card teysa = addCard("Teysa Karlov", p);
addCard("The Gitrog Monster", p);
addCard("Dryad Arbor", p);
String indestructibilityName = "Indestructibility";
Card indestructibility = addCard(indestructibilityName, p);
indestructibility.attachToEntity(teysa);
// update Indestructible state
game.getAction().checkStateEffects(true);
for (int i = 0; i < 4; i++) {
addCard("Plains", p);
addCardToZone("Plains", p, ZoneType.Library);
}
Card armageddon = addCardToZone("Wrath of God", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
SpellAbility armageddonSA = armageddon.getFirstSpellAbility();
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(armageddonSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
// One cards drawn
assertTrue(simGame.getPlayers().get(0).getZone(ZoneType.Hand).size() == 1);
}
public void testTeysaKarlovGitrogMonsterTeysaDies() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
addCard("Teysa Karlov", p);
Card gitrog = addCard("The Gitrog Monster", p);
addCard("Dryad Arbor", p);
String indestructibilityName = "Indestructibility";
Card indestructibility = addCard(indestructibilityName, p);
indestructibility.attachToEntity(gitrog);
// update Indestructible state
game.getAction().checkStateEffects(true);
for (int i = 0; i < 4; i++) {
addCard("Plains", p);
addCardToZone("Plains", p, ZoneType.Library);
}
Card armageddon = addCardToZone("Wrath of God", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
SpellAbility armageddonSA = armageddon.getFirstSpellAbility();
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(armageddonSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
// One cards drawn
assertTrue(simGame.getPlayers().get(0).getZone(ZoneType.Hand).size() == 1);
}
}
......@@ -2,7 +2,7 @@ Name:Naban, Dean of Iteration
ManaCost:1 U
Types:Legendary Creature Human Wizard
PT:2/1
S:Mode$ Continuous | Affected$ You | AddKeyword$ Panharmonicon:Wizard.YouCtrl | Description$ If a Wizard entering the battlefield under your control causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.
K:Panharmonicon:Wizard.YouCtrl:If a Wizard entering the battlefield under your control causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.
DeckHints:Type$Wizard
SVar:Picture:http://www.wizards.com/global/images/magic/general/naban_dean_of_iteration.jpg
Oracle:If a Wizard entering the battlefield under your control causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.
Name:Panharmonicon
ManaCost:4
Types:Artifact
S:Mode$ Continuous | Affected$ You | AddKeyword$ Panharmonicon:Creature,Artifact | Description$ If an artifact or creature entering the battlefield causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.
K:Panharmonicon:Creature,Artifact:If an artifact or creature entering the battlefield causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.
SVar:Picture:http://www.wizards.com/global/images/magic/general/panharmonicon.jpg
Oracle:If an artifact or creature entering the battlefield causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.
......@@ -2,7 +2,7 @@ Name:Teysa Karlov
ManaCost:2 W B
Types:Legendary Creature Human Advisor
PT:2/4
S:Mode$ Continuous | Affected$ You | AddKeyword$ Dieharmonicon | Description$ If a creature dying causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.
K:Dieharmonicon:Creature:If a creature dying causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.
S:Mode$ Continuous | Affected$ Creature.token+YouCtrl | AddKeyword$ Vigilance & Lifelink | Description$ Creature tokens you control have vigilance and lifelink.
DeckHints:Ability$Token
Oracle:If a creature dying causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.\nCreature tokens you control have vigilance and lifelink.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment