From 711b76ec49c3ca7550d70bea08b977c8d2d71152 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jun 2021 11:01:05 +0000 Subject: [PATCH 001/231] Bump ormlite-jdbc from 5.1 to 5.6 Bumps [ormlite-jdbc](https://github.com/j256/ormlite-jdbc) from 5.1 to 5.6. - [Release notes](https://github.com/j256/ormlite-jdbc/releases) - [Commits](https://github.com/j256/ormlite-jdbc/compare/ormlite-jdbc-5.1...ormlite-jdbc-5.6) --- updated-dependencies: - dependency-name: com.j256.ormlite:ormlite-jdbc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Mage/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage/pom.xml b/Mage/pom.xml index 1cd88f13793..7ce345309fe 100644 --- a/Mage/pom.xml +++ b/Mage/pom.xml @@ -38,7 +38,7 @@ com.j256.ormlite ormlite-jdbc - 5.1 + 5.6 junit From 582f160a695f6dcc250719d7abf97a85a50a7405 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 08:06:20 -0400 Subject: [PATCH 002/231] [MID] updated spoiler --- ...stIntruder.java => NebelgastIntruder.java} | 10 ++++----- .../src/mage/sets/InnistradMidnightHunt.java | 4 ++-- Utils/mtg-cards-data.txt | 22 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) rename Mage.Sets/src/mage/cards/n/{NeblegastIntruder.java => NebelgastIntruder.java} (82%) diff --git a/Mage.Sets/src/mage/cards/n/NeblegastIntruder.java b/Mage.Sets/src/mage/cards/n/NebelgastIntruder.java similarity index 82% rename from Mage.Sets/src/mage/cards/n/NeblegastIntruder.java rename to Mage.Sets/src/mage/cards/n/NebelgastIntruder.java index 9a871d1a0b1..178b28d563a 100644 --- a/Mage.Sets/src/mage/cards/n/NeblegastIntruder.java +++ b/Mage.Sets/src/mage/cards/n/NebelgastIntruder.java @@ -17,9 +17,9 @@ import java.util.UUID; /** * @author TheElk801 */ -public final class NeblegastIntruder extends CardImpl { +public final class NebelgastIntruder extends CardImpl { - public NeblegastIntruder(UUID ownerId, CardSetInfo setInfo) { + public NebelgastIntruder(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}"); this.subtype.add(SubType.SPIRIT); @@ -38,12 +38,12 @@ public final class NeblegastIntruder extends CardImpl { this.addAbility(ability); } - private NeblegastIntruder(final NeblegastIntruder card) { + private NebelgastIntruder(final NebelgastIntruder card) { super(card); } @Override - public NeblegastIntruder copy() { - return new NeblegastIntruder(this); + public NebelgastIntruder copy() { + return new NebelgastIntruder(this); } } diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 87425f69c4d..62b4e6af5e3 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -12,7 +12,7 @@ import java.util.List; */ public final class InnistradMidnightHunt extends ExpansionSet { - private static final List unfinished = Arrays.asList("Arlinn, the Pack's Hope", "Arlinn, the Moon's Fury", "Baithook Angler", "Hook-Haunt Drifter", "Baneblade Scoundrel", "Baneclaw Marauder", "Beloved Beggar", "Generous Soul", "Bird Admirer", "Wing Shredder", "Brimstone Vandal", "Brutal Cathar", "Moonrage Brute", "Burly Breaker", "Dire-Strain Demolisher", "Celestus Sanctifier", "Chaplain of Alms", "Chapel Shieldgeist", "Component Collector", "Covert Cutpurse", "Covetous Geist", "Covetous Castaway", "Ghostly Castigator", "Curse of Leeches", "Leeching Lurker", "Dennick, Pious Apprentice", "Dennick, Pious Apparition", "Devoted Grafkeeper", "Departed Soulkeeper", "Fangblade Brigand", "Fangblade Eviscerator", "Firmament Sage", "Galedrifter", "Waildrifter", "Gavony Dawnguard", "Graveyard Trespasser", "Graveyard Glutton", "Harvesttide Infiltrator", "Harvesttide Assailant", "Hound Tamer", "Untamed Pup", "Kessig Naturalist", "Lord of the Ulvenwald", "Lunarch Veteran", "Luminous Phantom", "Malevolent Hermit", "Benevolent Geist", "Mourning Patrol", "Morning Apparition", "Obsessive Astronomer", "Overwhelmed Archivist", "Archive Haunt", "Reckless Stormseeker", "Storm-Charged Slasher", "Shady Traveler", "Stalking Predator", "Shipwreck Sifters", "Spellrune Painter", "Spellrune Howler", "Sunrise Cavalier", "Sunstreak Phoenix", "Suspicious Stowaway", "Seafaring Werewolf", "Tavern Ruffian", "Tavern Smasher", "The Celestus", "Thraben Exorcism", "Tovolar, Dire Overlord", "Tovolar, the Midnight Scourge", "Tovolar's Huntmaster", "Tovolar's Packleader", "Unblinking Observer", "Vadrik, Astral Archmage", "Village Watch", "Village Reavers"); + private static final List unfinished = Arrays.asList("Arlinn, the Pack's Hope", "Arlinn, the Moon's Fury", "Baithook Angler", "Hook-Haunt Drifter", "Baneblade Scoundrel", "Baneclaw Marauder", "Beloved Beggar", "Generous Soul", "Bird Admirer", "Wing Shredder", "Brimstone Vandal", "Brutal Cathar", "Moonrage Brute", "Burly Breaker", "Dire-Strain Demolisher", "Celestus Sanctifier", "Chaplain of Alms", "Chapel Shieldgeist", "Component Collector", "Covert Cutpurse", "Covetous Geist", "Covetous Castaway", "Ghostly Castigator", "Curse of Leeches", "Leeching Lurker", "Dennick, Pious Apprentice", "Dennick, Pious Apparition", "Devoted Grafkeeper", "Departed Soulkeeper", "Fangblade Brigand", "Fangblade Eviscerator", "Firmament Sage", "Galedrifter", "Waildrifter", "Gavony Dawnguard", "Graveyard Trespasser", "Graveyard Glutton", "Harvesttide Infiltrator", "Harvesttide Assailant", "Hound Tamer", "Untamed Pup", "Kessig Naturalist", "Lord of the Ulvenwald", "Lunarch Veteran", "Luminous Phantom", "Malevolent Hermit", "Benevolent Geist", "Mourning Patrol", "Morning Apparition", "Obsessive Astronomer", "Outland Liberator", "Frenzied Trapbreaker", "Overwhelmed Archivist", "Archive Haunt", "Phantom Carriage", "Reckless Stormseeker", "Storm-Charged Slasher", "Shady Traveler", "Stalking Predator", "Shipwreck Sifters", "Spellrune Painter", "Spellrune Howler", "Sunrise Cavalier", "Sunstreak Phoenix", "Suspicious Stowaway", "Seafaring Werewolf", "Tavern Ruffian", "Tavern Smasher", "The Celestus", "Thraben Exorcism", "Tireless Hauler", "Dire-Strain Brawler", "Tovolar, Dire Overlord", "Tovolar, the Midnight Scourge", "Tovolar's Huntmaster", "Tovolar's Packleader", "Unblinking Observer", "Vadrik, Astral Archmage", "Village Watch", "Village Reavers"); private static final InnistradMidnightHunt instance = new InnistradMidnightHunt(); public static InnistradMidnightHunt getInstance() { @@ -156,7 +156,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Mountain", 274, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Mystic Monstrosity", 256, Rarity.UNCOMMON, mage.cards.m.MysticMonstrosity.class)); cards.add(new SetCardInfo("Mystic Skull", 256, Rarity.UNCOMMON, mage.cards.m.MysticSkull.class)); - cards.add(new SetCardInfo("Neblegast Intruder", 64, Rarity.UNCOMMON, mage.cards.n.NeblegastIntruder.class)); + cards.add(new SetCardInfo("Nebelgast Intruder", 64, Rarity.UNCOMMON, mage.cards.n.NebelgastIntruder.class)); cards.add(new SetCardInfo("Obsessive Astronomer", 152, Rarity.UNCOMMON, mage.cards.o.ObsessiveAstronomer.class)); cards.add(new SetCardInfo("Odric's Outrider", 29, Rarity.UNCOMMON, mage.cards.o.OdricsOutrider.class)); cards.add(new SetCardInfo("Organ Hoarder", 66, Rarity.COMMON, mage.cards.o.OrganHoarder.class)); diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index a86dcca9e88..81f23ba7c33 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -42176,7 +42176,7 @@ Ambitious Farmhand|Innistrad: Midnight Hunt|2|U|{1}{W}|Creature - Human Peasant| Seasoned Cathar|Innistrad: Midnight Hunt|2|U||Creature - Human Knight|3|3|Lifelink| Beloved Beggar|Innistrad: Midnight Hunt|3|U|{1}{W}|Creature - Human Peasant|0|4|Disturb {4}{W}{W}| Generous Soul|Innistrad: Midnight Hunt|3|U||Creature - Spirit|4|4|Flying, vigilance$If Generous Soul would be put into a graveyard from anywhere, exile it instead.| -Bereaved Survivor|Innistrad: Midnight Hunt|4|U|{2}{W}|Creature - Human Pesant|2|1|When another creature you control dies, transform Bereaved Survivor.| +Bereaved Survivor|Innistrad: Midnight Hunt|4|U|{2}{W}|Creature - Human Peasant|2|1|When another creature you control dies, transform Bereaved Survivor.| Dauntless Avenger|Innistrad: Midnight Hunt|4|U||Creature - Human Soldier|3|2|Whenever Dauntless Avenger attacks, return target creature card with mana value 2 or less from your graveyard to the battlefield tapped and attacking.| Blessed Defiance|Innistrad: Midnight Hunt|5|C|{W}|Instant|||Target creature you control gets +2/+0 and gains lifelink until end of turn. When that creature dies this turn, create a 1/1 white Spirit creature token with flying.| Borrowed Time|Innistrad: Midnight Hunt|6|U|{2}{W}|Enchantment|||When Borrowed Time enters the battlefield, exile target nonland permanent an opponent controls until Borrowed Time leaves the battlefield.| @@ -42234,7 +42234,7 @@ Dissipate|Innistrad: Midnight Hunt|49|U|{1}{U}{U}|Instant|||Counter target spell Drownyard Amalgam|Innistrad: Midnight Hunt|50|C|{4}{U}|Creature - Zombie Horror|3|6|When Drownyard Amalgam enters the battlefield, target player mills three cards.${2}{U}: Drownyard Amalgam can't be blocked this turn.| Fading Hope|Innistrad: Midnight Hunt|51|U|{U}|Instant|||Return target creature to its owner's hand. If its mana value was 3 or less, scry 1.| Falcon Abomination|Innistrad: Midnight Hunt|52|C|{2}{U}|Creature - Zombie Bird|2|2|Flying$When Falcon Abomination enters the battlefield, create a 2/2 black Zombie creature token with decayed.| -Firmament Sage|Innistrad: Midnight Hunt|53|U|{3}{U}|Creature - Human Wizard|2|3|If it's neither day or night, it becomes day as Firmament Sage enters the battlefield.$Whenever day becomes night or night becomes day, draw a card.| +Firmament Sage|Innistrad: Midnight Hunt|53|U|{3}{U}|Creature - Human Wizard|2|3|If it's neither day nor night, it becomes day as Firmament Sage enters the battlefield.$Whenever day becomes night or night becomes day, draw a card.| Flip the Switch|Innistrad: Midnight Hunt|54|C|{2}{U}|Instant|||Counter target spell unless its controller pays {4}. Create a 2/2 black Zombie creature token with decayed.| Galedrifter|Innistrad: Midnight Hunt|55|C|{3}{U}|Creature - Hippogriff|3|2|Flying$Disturb {4}{U}| Waildrifter|Innistrad: Midnight Hunt|55|C||Creature - Hippogriff Spirit|2|2|Flying$If Waildrifter would be put into a graveyard from anywhere, exile it instead.| @@ -42242,13 +42242,13 @@ Geistwave|Innistrad: Midnight Hunt|56|C|{1}{U}|Instant|||Return target nonland p Grafted Identity|Innistrad: Midnight Hunt|57|R|{2}{U}{U}|Enchantment - Aura|||As an additional cost to cast this spell, sacrifice a creature.$Enchant creature$You control enchanted creature.$Enchanted creature gets +1/+1.| Larder Zombie|Innistrad: Midnight Hunt|58|C|{U}|Creature - Zombie|1|3|Defender$Tap three untapped creatures you control: Look at the top card of your library. You may put it into your graveyard.| Lier, Disciple of the Drowned|Innistrad: Midnight Hunt|59|M|{3}{U}{U}|Legendary Creature - Human Wizard|3|4|Spells can't be countered.$Each instant and sorcery card in your graveyard has flashback. The flashback cost is equal to that card's mana cost.| -Locked in the Cemetary|Innistrad: Midnight Hunt|60|C|{1}{U}|Enchantment - Aura|||Enchant creature$When Locked in the Cemetary enters the battlefield, if there are five or more cards in your graveyard, tap enchanted creature.$Enchanted creature doesn't untap during its controller's untap step.| +Locked in the Cemetery|Innistrad: Midnight Hunt|60|C|{1}{U}|Enchantment - Aura|||Enchant creature$When Locked in the Cemetery enters the battlefield, if there are five or more cards in your graveyard, tap enchanted creature.$Enchanted creature doesn't untap during its controller's untap step.| Malevolent Hermit|Innistrad: Midnight Hunt|61|R|{1}{U}|Creature - Human Wizard|2|1|{U}, Sacrifice Malevolent Hermit: Counter target noncreature spell unless its controller pays {3}.$Disturb {2}{U}| Benevolent Geist|Innistrad: Midnight Hunt|61|R||Creature - Spirit Wizard|2|2|Flying$Noncreature spells you control can't be countered.$If Benevolent Geist would be put into a graveyard from anywhere, exile it instead.| Memory Deluge|Innistrad: Midnight Hunt|62|R|{2}{U}{U}|Instant|||Look at the top X cards of your library, where X is the amount of mana spent to cast this spell. Put two of them into your hand and the rest on the bottom of your library in a random order.$Flashback {5}{U}{U}| Mysterious Tome|Innistrad: Midnight Hunt|63|U|{2}{U}|Artifact|||{2}, {T}: Draw a card. Transform Mysterious Tome.| Chilling Chronicle|Innistrad: Midnight Hunt|63|U||Artifact|||{1}, {T}: Tap target nonland permanent. Transform Chilling Chronicle.| -Neblegast Intruder|Innistrad: Midnight Hunt|64|U|{2}{U}|Creature - Spirit|2|1|Flash$Flying$When Neblegast Intruder enters the battlefield, up to one target creature an opponent controls gets -2/-0 until end of turn.| +Nebelgast Intruder|Innistrad: Midnight Hunt|64|U|{2}{U}|Creature - Spirit|2|1|Flash$Flying$When Nebelgast Intruder enters the battlefield, up to one target creature an opponent controls gets -2/-0 until end of turn.| Ominous Roost|Innistrad: Midnight Hunt|65|U|{2}{U}|Enchantment|||When Ominous Roost enters the battlefield or whenever you cast a spell from your graveyard, create a 1/1 blue Bird creature token with flying and "This creature can block only creatures with flying."| Organ Hoarder|Innistrad: Midnight Hunt|66|C|{3}{U}|Creature - Zombie|3|2|When Organ Hoarder enters the battlefield, look at the top three cards of your library, then put one of them into your hand and the rest into you graveyard.| Otherworldly Gaze|Innistrad: Midnight Hunt|67|C|{U}|Instant|||Look at the top three cards of your library. Put any number of them into your graveyard and the rest back on top of your library in any order.$Flashback {1}{U}| @@ -42290,7 +42290,7 @@ Diregraf Horde|Innistrad: Midnight Hunt|96|C|{4}{B}|Creature - Zombie|3|4|When D Dreadhound|Innistrad: Midnight Hunt|97|U|{4}{B}{B}|Creature - Demon Dog|6|6|When Dreadhound enters the battlefield, mill three cards.$Whenever a creature dies or a creature card is put into a graveyard from a library, each opponent loses 1 life.| Duress|Innistrad: Midnight Hunt|98|C|{B}|Sorcery|||Target opponent reveals their hand. You choose a noncreature, nonland card from it. That player discards that card.| Eaten Alive|Innistrad: Midnight Hunt|99|C|{B}|Sorcery|||As an additional cost to cast this spell, sacrifice a creature or pay {3}{B}.$Exile target creature or planeswalker.| -Ecstatic Awakener|Innistrad: Midnight Hunt|100|C|{B}|Creature - Human Wizard|1|1|{2}{B}, Sacrifice another creature. Draw a card, then transform Ecstatic Awakener. Activate only once each turn.| +Ecstatic Awakener|Innistrad: Midnight Hunt|100|C|{B}|Creature - Human Wizard|1|1|{2}{B}, Sacrifice another creature: Draw a card, then transform Ecstatic Awakener. Activate only once each turn.| Awoken Demon|Innistrad: Midnight Hunt|100|C||Creature - Demon|4|4|| Foul Play|Innistrad: Midnight Hunt|101|U|{1}{B}|Sorcery|||Destroy target creature with power 2 or less. Investigate.| Ghoulish Procession|Innistrad: Midnight Hunt|102|U|{1}{B}|Enchantment|||Whenever one or more nontoken creatures die, create a 2/2 black Zombie creature token with decayed. This ability triggers only once each turn.| @@ -42347,12 +42347,12 @@ Immolation|Innistrad: Midnight Hunt|144|C|{R}|Enchantment - Aura|||Enchant creat Lambholt Harrier|Innistrad: Midnight Hunt|145|C|{1}{R}|Creature - Wolf|2|2|{3}{R}: Target creature can't block this turn.| Light Up the Night|Innistrad: Midnight Hunt|146|R|{X}{R}|Sorcery|||Light Up the Night deals X damage to any target. It deals X plus 1 damage instead if that target is a creature or planeswalker.$Flashback—{3}{R}, Remove X loyalty counters from among planeswalkers you control. If you cast this spell this way, X can't be 0.| Lunar Frenzy|Innistrad: Midnight Hunt|147|U|{X}{R}|Instant|||Target creature you control gets +X/+0 and gains first strike and trample until end of turn.| -Moonrager's Smash|Innistrad: Midnight Hunt|148|C|{2}{R}|Instant|||This spell costs {2} less to cast if it's night.$Moonrager's Smash deals 3 damage to any target.| +Moonrager's Slash|Innistrad: Midnight Hunt|148|C|{2}{R}|Instant|||This spell costs {2} less to cast if it's night.$Moonrager's Slash deals 3 damage to any target.| Moonveil Regent|Innistrad: Midnight Hunt|149|M|{3}{R}|Creature - Dragon|4|4|Flying$Whenever you cast a spell, you may discard your hand. If you do, draw a card for each of that spell's colors.$When Moonveil Regent dies, it deals X damage to any target, where X is the number of colors among permanents you control.| Mounted Dreadknight|Innistrad: Midnight Hunt|150|C|{4}{R}|Creature - Vampire Knight|5|4|Trample$Mounted Dreadknight enters the battlefield with a +1/+1 counter on it if an opponent lost life this turn.| Neonate's Rush|Innistrad: Midnight Hunt|151|C|{2}{R}|Instant|||This spell costs {1} less to cast if you control a Vampire.$Neonate's Rush deals 1 damage to target creature and 1 damage to its controller. Draw a card.| Obsessive Astronomer|Innistrad: Midnight Hunt|152|U|{1}{R}|Creature - Human Wizard|2|2|If it's neither day nor night, it becomes day as Obsessive Astronomer enters the battlefield.$Whenever day becomes night or night becomes day, discard up to two cards, then draw that many cards.| -Pack's Betrayal|Innistrad: Midnight Hunt|153|C|{2}{R}|Sorcery|||Gain control of target creature until end of turn. Untap that creature. It gains haste untl end of turn. If you control a Wolf or Werewolf, scry 2.| +Pack's Betrayal|Innistrad: Midnight Hunt|153|C|{2}{R}|Sorcery|||Gain control of target creature until end of turn. Untap that creature. It gains haste until end of turn. If you control a Wolf or Werewolf, scry 2.| Play with Fire|Innistrad: Midnight Hunt|154|U|{R}|Instant|||Play with Fire deals 2 damage to any target. If a player is dealt damage this way, scry 1.| Purifying Dragon|Innistrad: Midnight Hunt|155|U|{3}{R}{R}|Creature - Dragon|4|3|Flying$Whenever Purifying Dragon attacks, it deals 1 damage to target creature defending player controls. If that creature is a Zombie, Purifying Dragon deals 2 damage to that creature instead.| Raze the Effigy|Innistrad: Midnight Hunt|156|C|{R}|Instant|||Choose one—$• Destroy target artifact.$• Target attacking creature gets +2/+2 until end of turn.| @@ -42364,7 +42364,7 @@ Ashmouth Dragon|Innistrad: Midnight Hunt|159|R||Creature - Dragon|4|4|Flying$Whe Spellrune Painter|Innistrad: Midnight Hunt|160|U|{2}{R}|Creature - Human Shaman Werewolf|2|3|Whenever you cast an instant or sorcery spell, Spellrune Painter gets +1/+1 until end of turn.$Daybound| Spellrune Howler|Innistrad: Midnight Hunt|160|U||Creature - Werewolf|3|4|Whenever you cast an instant or sorcery spell, Spellrune Howler gets +2/+2 until end of turn.$Nightbound| Stolen Vitality|Innistrad: Midnight Hunt|161|C|{1}{R}|Instant|||Target creature gets +3/+1 until end of turn. If it's your turn, that creature gains trample until end of turn. Otherwise, it gains first strike until end of turn.| -Sunstreak Phoenix|Innistrad: Midnight Hunt|162|M|{2}{R}{R}|Creature - Phoenix|4|2|Flying$If it's nether day nor night, it becomes day when Sunstreak Phoenix enters the battlefield.$When day becomes night or night becomes day, you may pay {1}{R}. If you do, return Sunstreak Phoenix from your graveyard to the battlefield tapped.| +Sunstreak Phoenix|Innistrad: Midnight Hunt|162|M|{2}{R}{R}|Creature - Phoenix|4|2|Flying$If it's neither day nor night, it becomes day as Sunstreak Phoenix enters the battlefield.$When day becomes night or night becomes day, you may pay {1}{R}. If you do, return Sunstreak Phoenix from your graveyard to the battlefield tapped.| Tavern Ruffian|Innistrad: Midnight Hunt|163|C|{3}{R}|Creature - Human Warrior Werewolf|2|5|Daybound| Tavern Smasher|Innistrad: Midnight Hunt|163|C||Creature - Werewolf|6|5|Nightbound| Thermo-Alchemist|Innistrad: Midnight Hunt|164|U|{1}{R}|Creature - Human Shaman|0|3|Defender${T}: Thermo-Alchemist deals 1 damage to each opponent.$Whenever you cast an instant or sorcery spell, untap Thermo-Alchemist.| @@ -42393,13 +42393,13 @@ Defend the Celestus|Innistrad: Midnight Hunt|182|U|{2}{G}{G}|Instant|||Distribut Dryad's Revival|Innistrad: Midnight Hunt|183|U|{2}{G}|Sorcery|||Return target card from your graveyard to your hand.$Flashback {4}{G}| Duel for Dominance|Innistrad: Midnight Hunt|184|C|{1}{G}|Instant|||Coven — Choose target creature you control and target creature you don't control. If you control three or more creatures with different powers, put a +1/+1 counter on the chosen creature you control. Then the chosen creatures fight each other.| Eccentric Farmer|Innistrad: Midnight Hunt|185|C|{2}{G}|Creature - Human Peasant|2|3|When Eccentric Farmer enters the battlefield, mill three cards, then you may return a land card from your graveyard to your hand.| -Harvesttide Sentry|Innistrad: Midnight Hunt|186|C|{1}{G}|Creature - Human Warrior|3|1|Coven — At the beginning of combat on your turn, if. you control three or more creatures with different powers, Harvesttide Sentry can't be blocked by creatures with power 2 or less this turn.| +Harvesttide Sentry|Innistrad: Midnight Hunt|186|C|{1}{G}|Creature - Human Warrior|3|1|Coven — At the beginning of combat on your turn, if you control three or more creatures with different powers, Harvesttide Sentry can't be blocked by creatures with power 2 or less this turn.| Hound Tamer|Innistrad: Midnight Hunt|187|U|{2}{G}|Creature - Human Werewolf|3|3|Trample${3}{G}: Put a +1/+1 counter on target creature.$Daybound| Untamed Pup|Innistrad: Midnight Hunt|187|U||Creature - Werewolf|4|4|Trample$Other Wolves and Werewolves you control have trample.${3}{G}: Put a +1/+1 counter on target creature.$Nightbound| Howl of the Hunt|Innistrad: Midnight Hunt|188|C|{2}{G}|Enchantment - Aura|||Flash$Enchant creature$When Howl of the Hunt enters the battlefield, if enchanted creature is a Wolf or Werewolf, untap that creature.$Enchanted creature gets +2/+2 and has vigilance.| Might of the Old Ways|Innistrad: Midnight Hunt|189|C|{1}{G}|Instant|||Target creature gets +2/+2 until end of turn.$Coven — Then if you control three or more creatures with different powers, draw a card.| -Outland Liberator|Innistrad: Midnight Hunt|190|U|{1}{G}|Creature - Human Werwolf|2|2|{1}, Sacrifice Outland Liberator: Destroy target artifact or enchantment.$Daybound| -Frenzied Trapbreaker|Innistrad: Midnight Hunt|190|U||Creature - Werewolf|3|3|{1}, Sacrifice Frenzied Trapbreaker: Destroy target artifact or enchantment.$Whenenver "Frenzied Trapbreaker" attacks, destroy target artifact or enchantment defending player controls.$Nightbound| +Outland Liberator|Innistrad: Midnight Hunt|190|U|{1}{G}|Creature - Human Werewolf|2|2|{1}, Sacrifice Outland Liberator: Destroy target artifact or enchantment.$Daybound| +Frenzied Trapbreaker|Innistrad: Midnight Hunt|190|U||Creature - Werewolf|3|3|{1}, Sacrifice Frenzied Trapbreaker: Destroy target artifact or enchantment.$Whenever Frenzied Trapbreaker attacks, destroy target artifact or enchantment defending player controls.$Nightbound| Path to the Festival|Innistrad: Midnight Hunt|191|C|{2}{G}|Sorcery|||Search your library for a basic land card, put that card onto the battlefield tapped, then shuffle. Then if there are three or more basic land types among lands you control, scry 1.$Flashback {4}{G}| Pestilent Wolf|Innistrad: Midnight Hunt|192|C|{1}{G}|Creature - Wolf|2|2|{2}{G}: Pestilent Wolf gains deathtouch until end of turn.| Plummet|Innistrad: Midnight Hunt|193|C|{1}{G}|Instant|||Destroy target creature with flying.| From b0ad358e81a009809625a7389382ed9b263f3875 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 08:13:05 -0400 Subject: [PATCH 003/231] [MID] Implemented Component Collector --- .../src/mage/cards/c/ComponentCollector.java | 45 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 46 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/ComponentCollector.java diff --git a/Mage.Sets/src/mage/cards/c/ComponentCollector.java b/Mage.Sets/src/mage/cards/c/ComponentCollector.java new file mode 100644 index 00000000000..d744368d1ac --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ComponentCollector.java @@ -0,0 +1,45 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BecomeDayAsEntersAbility; +import mage.abilities.common.BecomesDayOrNightTriggeredAbility; +import mage.abilities.effects.common.MayTapOrUntapTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.target.common.TargetNonlandPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ComponentCollector extends CardImpl { + + public ComponentCollector(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}"); + + this.subtype.add(SubType.HOMUNCULUS); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + // If it's neither day nor night, it becomes day as Component Collector enters the battlefield. + this.addAbility(new BecomeDayAsEntersAbility()); + + // Whenever day becomes night or night becomes day, you may tap or untap target nonland permanent. + Ability ability = new BecomesDayOrNightTriggeredAbility(new MayTapOrUntapTargetEffect()); + ability.addTarget(new TargetNonlandPermanent()); + this.addAbility(ability); + } + + private ComponentCollector(final ComponentCollector card) { + super(card); + } + + @Override + public ComponentCollector copy() { + return new ComponentCollector(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 62b4e6af5e3..65036dd7c24 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -68,6 +68,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Chaplain of Alms", 13, Rarity.UNCOMMON, mage.cards.c.ChaplainOfAlms.class)); cards.add(new SetCardInfo("Clarion Cathars", 14, Rarity.COMMON, mage.cards.c.ClarionCathars.class)); cards.add(new SetCardInfo("Clear Shot", 176, Rarity.UNCOMMON, mage.cards.c.ClearShot.class)); + cards.add(new SetCardInfo("Component Collector", 43, Rarity.COMMON, mage.cards.c.ComponentCollector.class)); cards.add(new SetCardInfo("Consider", 44, Rarity.COMMON, mage.cards.c.Consider.class)); cards.add(new SetCardInfo("Contortionist Troupe", 178, Rarity.UNCOMMON, mage.cards.c.ContortionistTroupe.class)); cards.add(new SetCardInfo("Corpse Cobble", 214, Rarity.UNCOMMON, mage.cards.c.CorpseCobble.class)); From d6474296494fbd2a767268172a774cb35cfa3ce1 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 08:16:11 -0400 Subject: [PATCH 004/231] [MID] Implemented Crossroads Candleguid --- .../mage/cards/c/CrossroadsCandleguide.java | 46 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 47 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CrossroadsCandleguide.java diff --git a/Mage.Sets/src/mage/cards/c/CrossroadsCandleguide.java b/Mage.Sets/src/mage/cards/c/CrossroadsCandleguide.java new file mode 100644 index 00000000000..acc41ef4ced --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CrossroadsCandleguide.java @@ -0,0 +1,46 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.mana.AnyColorManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.target.common.TargetCardInGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CrossroadsCandleguide extends CardImpl { + + public CrossroadsCandleguide(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{4}"); + + this.subtype.add(SubType.SCARECROW); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // When Crossroads Candleguide enters the battlefield, exile up to one target card from a graveyard. + Ability ability = new EntersBattlefieldTriggeredAbility(new ExileTargetEffect()); + ability.addTarget(new TargetCardInGraveyard(0, 1)); + this.addAbility(ability); + + // {2}: Add one mana of any color. + this.addAbility(new AnyColorManaAbility(new GenericManaCost(2))); + } + + private CrossroadsCandleguide(final CrossroadsCandleguide card) { + super(card); + } + + @Override + public CrossroadsCandleguide copy() { + return new CrossroadsCandleguide(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 65036dd7c24..006de4c33ac 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -73,6 +73,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Contortionist Troupe", 178, Rarity.UNCOMMON, mage.cards.c.ContortionistTroupe.class)); cards.add(new SetCardInfo("Corpse Cobble", 214, Rarity.UNCOMMON, mage.cards.c.CorpseCobble.class)); cards.add(new SetCardInfo("Croaking Counterpart", 215, Rarity.RARE, mage.cards.c.CroakingCounterpart.class)); + cards.add(new SetCardInfo("Crossroads Candleguide", 253, Rarity.COMMON, mage.cards.c.CrossroadsCandleguide.class)); cards.add(new SetCardInfo("Curse of Shaken Faith", 134, Rarity.RARE, mage.cards.c.CurseOfShakenFaith.class)); cards.add(new SetCardInfo("Curse of Silence", 15, Rarity.RARE, mage.cards.c.CurseOfSilence.class)); cards.add(new SetCardInfo("Curse of Surveillance", 46, Rarity.RARE, mage.cards.c.CurseOfSurveillance.class)); From 042f60c8b6a7f5dd0f413d83f06adda0010cccae Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 08:21:50 -0400 Subject: [PATCH 005/231] [MID] Implemented Dawnhart Mentor --- .../src/mage/cards/d/DawnhartMentor.java | 59 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 60 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DawnhartMentor.java diff --git a/Mage.Sets/src/mage/cards/d/DawnhartMentor.java b/Mage.Sets/src/mage/cards/d/DawnhartMentor.java new file mode 100644 index 00000000000..6f3341802c6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DawnhartMentor.java @@ -0,0 +1,59 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.ActivateIfConditionActivatedAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.CovenCondition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.hint.common.CovenHint; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.permanent.token.HumanToken; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DawnhartMentor extends CardImpl { + + public DawnhartMentor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + + // When Dawnhart Mentor enters the battlefield, create a 1/1 white Human creature token. + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new HumanToken()))); + + // Coven — {5}{G}: Target creature you control gets +3/+3 and gains trample until end of turn. Activate only if you control three or more creatures with different powers. + Ability ability = new ActivateIfConditionActivatedAbility( + Zone.BATTLEFIELD, new BoostTargetEffect(3, 3) + .setText("target creature you control gets +3/+3"), + new ManaCostsImpl<>("{5}{G}"), CovenCondition.instance + ); + ability.addEffect(new GainAbilityTargetEffect( + TrampleAbility.getInstance(), Duration.EndOfTurn + ).setText("and gains trample until end of turn")); + ability.addTarget(new TargetControlledCreaturePermanent()); + this.addAbility(ability.addHint(CovenHint.instance).setAbilityWord(AbilityWord.COVEN)); + } + + private DawnhartMentor(final DawnhartMentor card) { + super(card); + } + + @Override + public DawnhartMentor copy() { + return new DawnhartMentor(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 006de4c33ac..9e951b8f93a 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -77,6 +77,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Curse of Shaken Faith", 134, Rarity.RARE, mage.cards.c.CurseOfShakenFaith.class)); cards.add(new SetCardInfo("Curse of Silence", 15, Rarity.RARE, mage.cards.c.CurseOfSilence.class)); cards.add(new SetCardInfo("Curse of Surveillance", 46, Rarity.RARE, mage.cards.c.CurseOfSurveillance.class)); + cards.add(new SetCardInfo("Dawnhart Mentor", 179, Rarity.UNCOMMON, mage.cards.d.DawnhartMentor.class)); cards.add(new SetCardInfo("Dawnhart Rejuvenator", 180, Rarity.COMMON, mage.cards.d.DawnhartRejuvenator.class)); cards.add(new SetCardInfo("Dawnhart Wardens", 216, Rarity.UNCOMMON, mage.cards.d.DawnhartWardens.class)); cards.add(new SetCardInfo("Defend the Celestus", 182, Rarity.UNCOMMON, mage.cards.d.DefendTheCelestus.class)); From df66345444c6414cde286618c04072a32a88cb56 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 08:25:18 -0400 Subject: [PATCH 006/231] [MID] Implemented Duelcraft Trainer --- .../src/mage/cards/d/DuelcraftTrainer.java | 55 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 56 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DuelcraftTrainer.java diff --git a/Mage.Sets/src/mage/cards/d/DuelcraftTrainer.java b/Mage.Sets/src/mage/cards/d/DuelcraftTrainer.java new file mode 100644 index 00000000000..4e145682161 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DuelcraftTrainer.java @@ -0,0 +1,55 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfCombatTriggeredAbility; +import mage.abilities.condition.common.CovenCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.hint.common.CovenHint; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DuelcraftTrainer extends CardImpl { + + public DuelcraftTrainer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // First strike + this.addAbility(FirstStrikeAbility.getInstance()); + + // Coven — At the beginning of combat on your turn, if you control three or more creatures with different powers, target creature you control gains double strike until end of turn. + Ability ability = new ConditionalInterveningIfTriggeredAbility( + new BeginningOfCombatTriggeredAbility(new GainAbilityTargetEffect( + DoubleStrikeAbility.getInstance(), Duration.EndOfTurn + ), TargetController.YOU, false), CovenCondition.instance, "At the beginning " + + "of combat on your turn, if you control three or more creatures with different powers, " + + "target creature you control gains double strike until end of turn." + ); + ability.addTarget(new TargetControlledCreaturePermanent()); + this.addAbility(ability.addHint(CovenHint.instance).setAbilityWord(AbilityWord.COVEN)); + } + + private DuelcraftTrainer(final DuelcraftTrainer card) { + super(card); + } + + @Override + public DuelcraftTrainer copy() { + return new DuelcraftTrainer(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 9e951b8f93a..b4b2d876c4b 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -95,6 +95,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Drownyard Amalgam", 50, Rarity.COMMON, mage.cards.d.DrownyardAmalgam.class)); cards.add(new SetCardInfo("Dryad's Revival", 183, Rarity.UNCOMMON, mage.cards.d.DryadsRevival.class)); cards.add(new SetCardInfo("Duel for Dominance", 184, Rarity.COMMON, mage.cards.d.DuelForDominance.class)); + cards.add(new SetCardInfo("Duelcraft Trainer", 16, Rarity.UNCOMMON, mage.cards.d.DuelcraftTrainer.class)); cards.add(new SetCardInfo("Duress", 98, Rarity.COMMON, mage.cards.d.Duress.class)); cards.add(new SetCardInfo("Eaten Alive", 99, Rarity.COMMON, mage.cards.e.EatenAlive.class)); cards.add(new SetCardInfo("Electric Revelation", 135, Rarity.COMMON, mage.cards.e.ElectricRevelation.class)); From 0a30beba22bb108fe4993ddb286dae7232cec67d Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 08:28:13 -0400 Subject: [PATCH 007/231] [MID] Implemented Galedrifter / Waildrifter --- Mage.Sets/src/mage/cards/g/Galedrifter.java | 43 ++++++++++++++++++ Mage.Sets/src/mage/cards/w/Waildrifter.java | 45 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 90 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/g/Galedrifter.java create mode 100644 Mage.Sets/src/mage/cards/w/Waildrifter.java diff --git a/Mage.Sets/src/mage/cards/g/Galedrifter.java b/Mage.Sets/src/mage/cards/g/Galedrifter.java new file mode 100644 index 00000000000..1b644d96c38 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/Galedrifter.java @@ -0,0 +1,43 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class Galedrifter extends CardImpl { + + public Galedrifter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}"); + + this.subtype.add(SubType.HIPPOGRIFF); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + this.transformable = true; + this.secondSideCardClazz = mage.cards.w.Waildrifter.class; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Disturb {4}{U} + this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{4}{U}"))); + } + + private Galedrifter(final Galedrifter card) { + super(card); + } + + @Override + public Galedrifter copy() { + return new Galedrifter(this); + } +} diff --git a/Mage.Sets/src/mage/cards/w/Waildrifter.java b/Mage.Sets/src/mage/cards/w/Waildrifter.java new file mode 100644 index 00000000000..14dde19604a --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/Waildrifter.java @@ -0,0 +1,45 @@ +package mage.cards.w; + +import mage.MageInt; +import mage.abilities.common.PutIntoGraveFromAnywhereSourceAbility; +import mage.abilities.effects.common.ExileSourceEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class Waildrifter extends CardImpl { + + public Waildrifter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.HIPPOGRIFF); + this.subtype.add(SubType.SPIRIT); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + this.color.setBlue(true); + this.transformable = true; + this.nightCard = true; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // If Waildrifter would be put into a graveyard from anywhere, exile it instead. + this.addAbility(new PutIntoGraveFromAnywhereSourceAbility(new ExileSourceEffect().setText("exile it instead"))); + } + + private Waildrifter(final Waildrifter card) { + super(card); + } + + @Override + public Waildrifter copy() { + return new Waildrifter(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index b4b2d876c4b..8598d1ec32b 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -117,6 +117,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Flip the Switch", 54, Rarity.COMMON, mage.cards.f.FlipTheSwitch.class)); cards.add(new SetCardInfo("Forest", 276, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Foul Play", 101, Rarity.UNCOMMON, mage.cards.f.FoulPlay.class)); + cards.add(new SetCardInfo("Galedrifter", 55, Rarity.COMMON, mage.cards.g.Galedrifter.class)); cards.add(new SetCardInfo("Galvanic Iteration", 224, Rarity.RARE, mage.cards.g.GalvanicIteration.class)); cards.add(new SetCardInfo("Gavony Dawnguard", 20, Rarity.UNCOMMON, mage.cards.g.GavonyDawnguard.class)); cards.add(new SetCardInfo("Gavony Silversmith", 21, Rarity.COMMON, mage.cards.g.GavonySilversmith.class)); @@ -227,6 +228,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Village Watch", 165, Rarity.UNCOMMON, mage.cards.v.VillageWatch.class)); cards.add(new SetCardInfo("Vivisection", 83, Rarity.UNCOMMON, mage.cards.v.Vivisection.class)); cards.add(new SetCardInfo("Voldaren Ambusher", 166, Rarity.UNCOMMON, mage.cards.v.VoldarenAmbusher.class)); + cards.add(new SetCardInfo("Waildrifter", 55, Rarity.COMMON, mage.cards.w.Waildrifter.class)); cards.add(new SetCardInfo("Willow Geist", 207, Rarity.RARE, mage.cards.w.WillowGeist.class)); cards.add(new SetCardInfo("Wing Shredder", 169, Rarity.COMMON, mage.cards.w.WingShredder.class)); cards.add(new SetCardInfo("Wrenn and Seven", 208, Rarity.MYTHIC, mage.cards.w.WrennAndSeven.class)); From 55ba5e8fa11f32a270a2ea6e6181de1be1dc69ee Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 08:31:38 -0400 Subject: [PATCH 008/231] [MID] Implemented No Way Out --- Mage.Sets/src/mage/cards/n/NoWayOut.java | 36 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 37 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/n/NoWayOut.java diff --git a/Mage.Sets/src/mage/cards/n/NoWayOut.java b/Mage.Sets/src/mage/cards/n/NoWayOut.java new file mode 100644 index 00000000000..9dad5c1f124 --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NoWayOut.java @@ -0,0 +1,36 @@ +package mage.cards.n; + +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.discard.DiscardTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.ZombieDecayedToken; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NoWayOut extends CardImpl { + + public NoWayOut(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}"); + + // Target opponent discards two cards. You create a 2/2 black Zombie creature token with decayed. + this.getSpellAbility().addEffect(new DiscardTargetEffect(2)); + this.getSpellAbility().addEffect(new CreateTokenEffect(new ZombieDecayedToken()).concatBy("You")); + this.getSpellAbility().addTarget(new TargetOpponent()); + } + + private NoWayOut(final NoWayOut card) { + super(card); + } + + @Override + public NoWayOut copy() { + return new NoWayOut(this); + } +} +// how does kevin costner keep getting work? diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 8598d1ec32b..25cc44eb06b 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -162,6 +162,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Mystic Monstrosity", 256, Rarity.UNCOMMON, mage.cards.m.MysticMonstrosity.class)); cards.add(new SetCardInfo("Mystic Skull", 256, Rarity.UNCOMMON, mage.cards.m.MysticSkull.class)); cards.add(new SetCardInfo("Nebelgast Intruder", 64, Rarity.UNCOMMON, mage.cards.n.NebelgastIntruder.class)); + cards.add(new SetCardInfo("No Way Out", 116, Rarity.COMMON, mage.cards.n.NoWayOut.class)); cards.add(new SetCardInfo("Obsessive Astronomer", 152, Rarity.UNCOMMON, mage.cards.o.ObsessiveAstronomer.class)); cards.add(new SetCardInfo("Odric's Outrider", 29, Rarity.UNCOMMON, mage.cards.o.OdricsOutrider.class)); cards.add(new SetCardInfo("Organ Hoarder", 66, Rarity.COMMON, mage.cards.o.OrganHoarder.class)); From dbbade61fa22ef64ccc23447ce84d54673c5a7d5 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 09:03:00 -0400 Subject: [PATCH 009/231] [MID] Implemented Morkrut Banshee --- .../src/mage/cards/m/MorkrutBehemoth.java | 49 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 50 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MorkrutBehemoth.java diff --git a/Mage.Sets/src/mage/cards/m/MorkrutBehemoth.java b/Mage.Sets/src/mage/cards/m/MorkrutBehemoth.java new file mode 100644 index 00000000000..4dd44b8fbbf --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MorkrutBehemoth.java @@ -0,0 +1,49 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.costs.OrCost; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.target.common.TargetControlledPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MorkrutBehemoth extends CardImpl { + + public MorkrutBehemoth(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{B}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.GIANT); + this.power = new MageInt(7); + this.toughness = new MageInt(6); + + // As an additional cost to cast this spell, sacrifice a creature or pay {1}{B}. + this.getSpellAbility().addCost(new OrCost( + new SacrificeTargetCost(new TargetControlledPermanent( + StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT + )), new ManaCostsImpl<>("{1}{B}"), "sacrifice a creature or pay {1}{B}" + )); + + // Menace + this.addAbility(new MenaceAbility()); + } + + private MorkrutBehemoth(final MorkrutBehemoth card) { + super(card); + } + + @Override + public MorkrutBehemoth copy() { + return new MorkrutBehemoth(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 25cc44eb06b..6dd55d9a685 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -158,6 +158,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Might of the Old Ways", 189, Rarity.COMMON, mage.cards.m.MightOfTheOldWays.class)); cards.add(new SetCardInfo("Moonsilver Key", 255, Rarity.UNCOMMON, mage.cards.m.MoonsilverKey.class)); cards.add(new SetCardInfo("Morbid Opportunist", 113, Rarity.UNCOMMON, mage.cards.m.MorbidOpportunist.class)); + cards.add(new SetCardInfo("Morkrut Behemoth", 114, Rarity.COMMON, mage.cards.m.MorkrutBehemoth.class)); cards.add(new SetCardInfo("Mountain", 274, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Mystic Monstrosity", 256, Rarity.UNCOMMON, mage.cards.m.MysticMonstrosity.class)); cards.add(new SetCardInfo("Mystic Skull", 256, Rarity.UNCOMMON, mage.cards.m.MysticSkull.class)); From d61923fb583e0de2fbbeb801e8f52ab2f6d461d9 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 09:07:05 -0400 Subject: [PATCH 010/231] [MID] Implemented Mounted Dreadknight --- .../src/mage/cards/m/MountedDreadknight.java | 49 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 50 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MountedDreadknight.java diff --git a/Mage.Sets/src/mage/cards/m/MountedDreadknight.java b/Mage.Sets/src/mage/cards/m/MountedDreadknight.java new file mode 100644 index 00000000000..2386f4f08a6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MountedDreadknight.java @@ -0,0 +1,49 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.condition.common.OpponentsLostLifeCondition; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.common.OpponentsLostLifeHint; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MountedDreadknight extends CardImpl { + + public MountedDreadknight(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}"); + + this.subtype.add(SubType.VAMPIRE); + this.subtype.add(SubType.KNIGHT); + this.power = new MageInt(5); + this.toughness = new MageInt(4); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Mounted Dreadknight enters the battlefield with a +1/+1 counter on it if an opponent lost life this turn. + this.addAbility(new EntersBattlefieldAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), + OpponentsLostLifeCondition.instance, null, + "with a +1/+1 counter on it if an opponent lost life this turn" + ).addHint(OpponentsLostLifeHint.instance)); + } + + private MountedDreadknight(final MountedDreadknight card) { + super(card); + } + + @Override + public MountedDreadknight copy() { + return new MountedDreadknight(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 6dd55d9a685..91901ba5259 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -160,6 +160,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Morbid Opportunist", 113, Rarity.UNCOMMON, mage.cards.m.MorbidOpportunist.class)); cards.add(new SetCardInfo("Morkrut Behemoth", 114, Rarity.COMMON, mage.cards.m.MorkrutBehemoth.class)); cards.add(new SetCardInfo("Mountain", 274, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Mounted Dreadknight", 150, Rarity.COMMON, mage.cards.m.MountedDreadknight.class)); cards.add(new SetCardInfo("Mystic Monstrosity", 256, Rarity.UNCOMMON, mage.cards.m.MysticMonstrosity.class)); cards.add(new SetCardInfo("Mystic Skull", 256, Rarity.UNCOMMON, mage.cards.m.MysticSkull.class)); cards.add(new SetCardInfo("Nebelgast Intruder", 64, Rarity.UNCOMMON, mage.cards.n.NebelgastIntruder.class)); From 835ee5514c49b651d9afb3607b47309b95dfddae Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 09:27:50 -0400 Subject: [PATCH 011/231] [MID] Implemented Malevolent Hermit / Benevolent Geist --- .../src/mage/cards/b/BenevolentGeist.java | 51 +++++++++++++++++ .../src/mage/cards/m/MalevolentHermit.java | 55 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + .../main/java/mage/filter/StaticFilters.java | 6 ++ 4 files changed, 114 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/b/BenevolentGeist.java create mode 100644 Mage.Sets/src/mage/cards/m/MalevolentHermit.java diff --git a/Mage.Sets/src/mage/cards/b/BenevolentGeist.java b/Mage.Sets/src/mage/cards/b/BenevolentGeist.java new file mode 100644 index 00000000000..64f85dc8644 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BenevolentGeist.java @@ -0,0 +1,51 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.abilities.common.PutIntoGraveFromAnywhereSourceAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.CantBeCounteredControlledEffect; +import mage.abilities.effects.common.ExileSourceEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BenevolentGeist extends CardImpl { + + public BenevolentGeist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Noncreature spells you control can't be countered. + this.addAbility(new SimpleStaticAbility(new CantBeCounteredControlledEffect( + StaticFilters.FILTER_SPELLS_NON_CREATURE, null, Duration.WhileOnBattlefield + ))); + + // If Benevolent Geist would be put into a graveyard from anywhere, exile it instead. + this.addAbility(new PutIntoGraveFromAnywhereSourceAbility(new ExileSourceEffect().setText("exile it instead"))); + } + + private BenevolentGeist(final BenevolentGeist card) { + super(card); + } + + @Override + public BenevolentGeist copy() { + return new BenevolentGeist(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MalevolentHermit.java b/Mage.Sets/src/mage/cards/m/MalevolentHermit.java new file mode 100644 index 00000000000..59e9a44d840 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MalevolentHermit.java @@ -0,0 +1,55 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CounterUnlessPaysEffect; +import mage.abilities.keyword.DisturbAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.target.TargetSpell; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MalevolentHermit extends CardImpl { + + public MalevolentHermit(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + this.transformable = true; + this.secondSideCardClazz = mage.cards.b.BenevolentGeist.class; + + // {U}, Sacrifice Malevolent Hermit: Counter target noncreature spell unless its controller pays {3}. + Ability ability = new SimpleActivatedAbility( + new CounterUnlessPaysEffect(new GenericManaCost(3)), new ManaCostsImpl<>("{U}") + ); + ability.addCost(new SacrificeSourceCost()); + ability.addTarget(new TargetSpell(StaticFilters.FILTER_SPELL_NON_CREATURE)); + this.addAbility(ability); + + // Disturb {2}{U} + this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{2}{U}"))); + } + + private MalevolentHermit(final MalevolentHermit card) { + super(card); + } + + @Override + public MalevolentHermit copy() { + return new MalevolentHermit(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 91901ba5259..8150cb2fdce 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -40,6 +40,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Augur of Autumn", 168, Rarity.RARE, mage.cards.a.AugurOfAutumn.class)); cards.add(new SetCardInfo("Baithook Angler", 42, Rarity.COMMON, mage.cards.b.BaithookAngler.class)); cards.add(new SetCardInfo("Bat Whisperer", 86, Rarity.COMMON, mage.cards.b.BatWhisperer.class)); + cards.add(new SetCardInfo("Benevolent Geist", 61, Rarity.RARE, mage.cards.b.BenevolentGeist.class)); cards.add(new SetCardInfo("Bird Admirer", 169, Rarity.COMMON, mage.cards.b.BirdAdmirer.class)); cards.add(new SetCardInfo("Bladebrand", 87, Rarity.COMMON, mage.cards.b.Bladebrand.class)); cards.add(new SetCardInfo("Bladestitched Skaab", 212, Rarity.UNCOMMON, mage.cards.b.BladestitchedSkaab.class)); @@ -154,6 +155,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Lord of the Ulvenwald", 231, Rarity.UNCOMMON, mage.cards.l.LordOfTheUlvenwald.class)); cards.add(new SetCardInfo("Loyal Gryff", 26, Rarity.UNCOMMON, mage.cards.l.LoyalGryff.class)); cards.add(new SetCardInfo("Lunar Frenzy", 147, Rarity.UNCOMMON, mage.cards.l.LunarFrenzy.class)); + cards.add(new SetCardInfo("Malevolent Hermit", 61, Rarity.RARE, mage.cards.m.MalevolentHermit.class)); cards.add(new SetCardInfo("Mask of Griselbrand", 111, Rarity.RARE, mage.cards.m.MaskOfGriselbrand.class)); cards.add(new SetCardInfo("Might of the Old Ways", 189, Rarity.COMMON, mage.cards.m.MightOfTheOldWays.class)); cards.add(new SetCardInfo("Moonsilver Key", 255, Rarity.UNCOMMON, mage.cards.m.MoonsilverKey.class)); diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index 18abc034416..26dd1191cef 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -613,6 +613,12 @@ public final class StaticFilters { FILTER_SPELL_NON_CREATURE.setLockedFilter(true); } + public static final FilterSpell FILTER_SPELLS_NON_CREATURE = (FilterSpell) new FilterSpell("noncreature spells").add(Predicates.not(CardType.CREATURE.getPredicate())); + + static { + FILTER_SPELLS_NON_CREATURE.setLockedFilter(true); + } + public static final FilterSpell FILTER_SPELL_A_NON_CREATURE = (FilterSpell) new FilterSpell("a noncreature spell").add(Predicates.not(CardType.CREATURE.getPredicate())); static { From 069c9a59ff4b3eac02d47af667fa2da3fbd9418d Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 09:28:11 -0400 Subject: [PATCH 012/231] [MID] Implemented Fangblade Brigand / Fangblade Eviscerator --- .../src/mage/cards/f/FangbladeBrigand.java | 55 +++++++++++++++++ .../mage/cards/f/FangbladeEviscerator.java | 61 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 118 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FangbladeBrigand.java create mode 100644 Mage.Sets/src/mage/cards/f/FangbladeEviscerator.java diff --git a/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java b/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java new file mode 100644 index 00000000000..53221949d4a --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java @@ -0,0 +1,55 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.DayboundAbility; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FangbladeBrigand extends CardImpl { + + public FangbladeBrigand(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WEREWOLF); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + this.transformable = true; + this.secondSideCardClazz = mage.cards.f.FangbladeEviscerator.class; + + // {1}{R}: Fangblade Brigand gets +1/+0 and gains first strike until end of turn. + Ability ability = new SimpleActivatedAbility(new BoostSourceEffect( + 1, 0, Duration.EndOfTurn + ).setText("{this} gets +1/+0"), new ManaCostsImpl<>("{1}{R}")); + ability.addEffect(new GainAbilitySourceEffect( + FirstStrikeAbility.getInstance(), Duration.EndOfTurn + ).setText("and gains first strike until end of turn")); + this.addAbility(ability); + + // Daybound + this.addAbility(DayboundAbility.getInstance()); + } + + private FangbladeBrigand(final FangbladeBrigand card) { + super(card); + } + + @Override + public FangbladeBrigand copy() { + return new FangbladeBrigand(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FangbladeEviscerator.java b/Mage.Sets/src/mage/cards/f/FangbladeEviscerator.java new file mode 100644 index 00000000000..2bf456d84b0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FangbladeEviscerator.java @@ -0,0 +1,61 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.NightboundAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FangbladeEviscerator extends CardImpl { + + public FangbladeEviscerator(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.WEREWOLF); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + this.color.setRed(true); + this.transformable = true; + this.nightCard = true; + + // {1}{R}: Fangblade Eviscerator gets +1/+0 and gains first strike until end of turn. + Ability ability = new SimpleActivatedAbility(new BoostSourceEffect( + 1, 0, Duration.EndOfTurn + ).setText("{this} gets +1/+0"), new ManaCostsImpl<>("{1}{R}")); + ability.addEffect(new GainAbilitySourceEffect( + FirstStrikeAbility.getInstance(), Duration.EndOfTurn + ).setText("and gains first strike until end of turn")); + this.addAbility(ability); + + // {4}{R}: Creatures you control get +2/+0 until end of turn. + this.addAbility(new SimpleActivatedAbility(new BoostControlledEffect( + 2, 0, Duration.EndOfTurn + ), new ManaCostsImpl<>("{4}{R}"))); + + // Nightbound + this.addAbility(NightboundAbility.getInstance()); + } + + private FangbladeEviscerator(final FangbladeEviscerator card) { + super(card); + } + + @Override + public FangbladeEviscerator copy() { + return new FangbladeEviscerator(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 8150cb2fdce..8d0d2f43fe2 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -108,6 +108,8 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Falkenrath Perforator", 136, Rarity.COMMON, mage.cards.f.FalkenrathPerforator.class)); cards.add(new SetCardInfo("Falkenrath Pit Fighter", 137, Rarity.RARE, mage.cards.f.FalkenrathPitFighter.class)); cards.add(new SetCardInfo("Famished Foragers", 138, Rarity.COMMON, mage.cards.f.FamishedForagers.class)); + cards.add(new SetCardInfo("Fangblade Brigand", 139, Rarity.UNCOMMON, mage.cards.f.FangbladeBrigand.class)); + cards.add(new SetCardInfo("Fangblade Eviscerator", 139, Rarity.UNCOMMON, mage.cards.f.FangbladeEviscerator.class)); cards.add(new SetCardInfo("Fateful Absence", 18, Rarity.RARE, mage.cards.f.FatefulAbsence.class)); cards.add(new SetCardInfo("Festival Crasher", 140, Rarity.COMMON, mage.cards.f.FestivalCrasher.class)); cards.add(new SetCardInfo("Field of Ruin", 262, Rarity.UNCOMMON, mage.cards.f.FieldOfRuin.class)); From 5bbcf03cb6d6207c2c7c34fae4fce1ac733291fb Mon Sep 17 00:00:00 2001 From: spjspj Date: Sat, 11 Sep 2021 23:32:34 +1000 Subject: [PATCH 013/231] EDH Power level update (based on Saltiest EDHREC survey). Update for Full Art Face art. --- .../CardViewEDHPowerLevelComparator.java | 323 +++++++++++------- .../org/mage/card/arcane/CardRenderer.java | 24 +- .../mage/card/arcane/ModernCardRenderer.java | 36 +- .../src/mage/deck/Commander.java | 218 ++++++++++-- 4 files changed, 433 insertions(+), 168 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/util/comparators/CardViewEDHPowerLevelComparator.java b/Mage.Client/src/main/java/mage/client/util/comparators/CardViewEDHPowerLevelComparator.java index 34105d8f145..06609df4ca8 100644 --- a/Mage.Client/src/main/java/mage/client/util/comparators/CardViewEDHPowerLevelComparator.java +++ b/Mage.Client/src/main/java/mage/client/util/comparators/CardViewEDHPowerLevelComparator.java @@ -315,203 +315,280 @@ public class CardViewEDHPowerLevelComparator implements CardViewComparator { } if (card.isPlanesWalker()) { - if (card.getName().toLowerCase(Locale.ENGLISH).equals("jace, the mind sculptor")) { - thisMaxPower = Math.max(thisMaxPower, 6); - } - if (card.getName().toLowerCase(Locale.ENGLISH).equals("ugin, the spirit dragon")) { - thisMaxPower = Math.max(thisMaxPower, 5); - } - thisMaxPower = Math.max(thisMaxPower, 4); + thisMaxPower = Math.max(thisMaxPower, 6); } String cn = card.getName().toLowerCase(Locale.ENGLISH); - if (cn.equals("ancient tomb") + if (cn.equals("acid rain") + || cn.equals("agent of treachery") || cn.equals("anafenza, the foremost") + || cn.equals("ancient tomb") + || cn.equals("animar, soul of element") + || cn.equals("animate artifact") + || cn.equals("apocalypse") + || cn.equals("archaeomancer") || cn.equals("arcum dagsson") || cn.equals("armageddon") + || cn.equals("ashnod's altar") + || cn.equals("atraxa, praetors' voice") + || cn.equals("aura flux") || cn.equals("aura shards") + || cn.equals("avacyn, angel of hope") || cn.equals("azami, lady of scrolls") || cn.equals("azusa, lost but seeking") || cn.equals("back to basics") || cn.equals("bane of progress") || cn.equals("basalt monolith") + || cn.equals("bend or break") || cn.equals("blightsteel collossus") + || cn.equals("blightsteel colossus") || cn.equals("blood moon") + || cn.equals("boil") + || cn.equals("boiling seas") + || cn.equals("brago, king eternal") || cn.equals("braids, cabal minion") + || cn.equals("bribery") + || cn.equals("burning sands") || cn.equals("cabal coffers") + || cn.equals("candelabra of tawnos") || cn.equals("captain sisay") + || cn.equals("card view") + || cn.equals("cataclysm") + || cn.equals("catastrophe") || cn.equals("celestial dawn") + || cn.equals("cephalid aristocrat") + || cn.equals("cephalid illusionist") + || cn.equals("changeling berserker") || cn.equals("child of alara") + || cn.equals("chulane, teller of tales") + || cn.equals("cinderhaze wretch") || cn.equals("coalition relic") + || cn.equals("confusion in the ranks") + || cn.equals("consecrated sphinx") + || cn.equals("contamination") || cn.equals("craterhoof behemoth") + || cn.equals("cryptic gateway") + || cn.equals("cyclonic rift") + || cn.equals("deadeye navigator") + || cn.equals("death cloud") + || cn.equals("decree of annihilation") + || cn.equals("decree of silence") || cn.equals("deepglow skate") + || cn.equals("demonic consultation") || cn.equals("derevi, empyrial tactician") + || cn.equals("devastation") || cn.equals("dig through time") + || cn.equals("divine intervention") + || cn.equals("dockside extortionist") + || cn.equals("doomsday") + || cn.equals("doubling season") + || cn.equals("drannith magistrate") + || cn.equals("dross scorpion") + || cn.equals("earthcraft") || cn.equals("edric, spymaster of trest") || cn.equals("elesh norn, grand cenobite") + || cn.equals("embargo") + || cn.equals("emrakul, the promised end") + || cn.equals("enter the infinite") || cn.equals("entomb") - || cn.equals("force of will") + || cn.equals("epicenter") + || cn.equals("erratic portal") + || cn.equals("expropriate") + || cn.equals("exquisite blood") + || cn.equals("fall of the thran") + || cn.equals("fierce guardianship") || cn.equals("food chain") + || cn.equals("force of negation") + || cn.equals("force of will") + || cn.equals("future sight") || cn.equals("gaddock teeg") || cn.equals("gaea's cradle") + || cn.equals("genesis chamber") + || cn.equals("ghave, guru of spores") + || cn.equals("gilded drake") + || cn.equals("glenn, the voice of calm") + || cn.equals("global ruin") + || cn.equals("golos, tireless pilgrim") || cn.equals("grand arbiter augustin iv") + || cn.equals("grave pact") + || cn.equals("grave titan") + || cn.equals("great whale") || cn.equals("grim monolith") + || cn.equals("grip of chaos") + || cn.equals("gush") + || cn.equals("hellkite charger") || cn.equals("hermit druid") || cn.equals("hokori, dust drinker") || cn.equals("humility") + || cn.equals("impending disaster") || cn.equals("imperial seal") + || cn.equals("intruder alarm") + || cn.equals("invoke prejudice") || cn.equals("iona, shield of emeria") || cn.equals("jin-gitaxias, core augur") + || cn.equals("jokulhaups") + || cn.equals("kaalia of the vast") || cn.equals("karador, ghost chieftain") || cn.equals("karakas") + || cn.equals("karn, silver golem") || cn.equals("kataki, war's wage") + || cn.equals("keldon firebombers") + || cn.equals("kiki-jiki, mirror breaker") + || cn.equals("kinnan, bonder prodigy") || cn.equals("knowledge pool") + || cn.equals("kozilek, butcher of truth") + || cn.equals("krark-clan ironworks") + || cn.equals("krenko, mob boss") + || cn.equals("krosan restorer") + || cn.equals("laboratory maniac") + || cn.equals("land equilibrium") + || cn.equals("leonin relic-warder") + || cn.equals("leovold, emissary of trest") + || cn.equals("leyline of the void") || cn.equals("linvala, keeper of silence") || cn.equals("living death") || cn.equals("llawan, cephalid empress") || cn.equals("loyal retainers") || cn.equals("maelstrom wanderer") + || cn.equals("magister sphinx") || cn.equals("malfegor") - || cn.equals("master of cruelties") + || cn.equals("mana breach") || cn.equals("mana crypt") || cn.equals("mana drain") || cn.equals("mana vault") + || cn.equals("mana vortex") + || cn.equals("master of cruelties") + || cn.equals("memnarch") + || cn.equals("meren of clan nel toth") || cn.equals("michiko konda, truth seeker") + || cn.equals("mikaeus the unhallowed") + || cn.equals("mikaeus, the unhallowed") + || cn.equals("mindcrank") + || cn.equals("mindslaver") + || cn.equals("minion reflector") + || cn.equals("mycosynth lattice") + || cn.equals("myr turbine") + || cn.equals("narset, enlightened master") + || cn.equals("narset, parter of veils") || cn.equals("nath of the gilt-leaf") || cn.equals("natural order") || cn.equals("necrotic ooze") + || cn.equals("negan, the cold-blooded") + || cn.equals("nekusar, the mindrazer") + || cn.equals("nether void") + || cn.equals("nexus of fate") || cn.equals("nicol bolas") + || cn.equals("norin the wary") + || cn.equals("notion thief") || cn.equals("numot, the devastator") || cn.equals("oath of druids") + || cn.equals("obliterate") + || cn.equals("oko, thief of crowns") + || cn.equals("oloro, ageless ascetic") + || cn.equals("omniscience") + || cn.equals("opalescence") + || cn.equals("opposition agent") + || cn.equals("oppression") + || cn.equals("ornithopter") + || cn.equals("overwhelming splendor") + || cn.equals("palinchron") + || cn.equals("paradox engine") || cn.equals("pattern of rebirth") + || cn.equals("peregrine drake") + || cn.equals("planar portal") + || cn.equals("possessed portal") + || cn.equals("power artifact") + || cn.equals("price of glory") + || cn.equals("prossh, skyraider of kher") || cn.equals("protean hulk") || cn.equals("purphoros, god of the forge") || cn.equals("ravages of war") || cn.equals("reclamation sage") + || cn.equals("rhystic study") + || cn.equals("rick, steadfast leader") + || cn.equals("rings of brighthearth") + || cn.equals("rising waters") + || cn.equals("rite of replication") + || cn.equals("ruination") + || cn.equals("sanguine bond") + || cn.equals("scrambleverse") + || cn.equals("seedborn muse") || cn.equals("sen triplets") + || cn.equals("sensei's divining top") || cn.equals("serra's sanctum") || cn.equals("sheoldred, whispering one") + || cn.equals("sire of insanity") + || cn.equals("skithiryx, the blight dragon") + || cn.equals("smokestack") + || cn.equals("smothering tithe") || cn.equals("sol ring") + || cn.equals("sorin markov") + || cn.equals("splinter twin") || cn.equals("spore frog") || cn.equals("stasis") + || cn.equals("static orb") + || cn.equals("stony silence") + || cn.equals("storage matrix") + || cn.equals("storm cauldron") || cn.equals("strip mine") - || cn.equals("the tabernacle at pendrell vale") - || cn.equals("tinker") - || cn.equals("treasure cruise") - || cn.equals("urabrask the hidden") - || cn.equals("vorinclex, voice of hunger") - || cn.equals("winter orb") - || cn.equals("zur the enchanter")) { - thisMaxPower = Math.max(thisMaxPower, 5); - } - - // Parts of infinite combos - if (cn.equals("animate artifact") || cn.equals("animar, soul of element") - || cn.equals("archaeomancer") - || cn.equals("ashnod's altar") || cn.equals("azami, lady of scrolls") - || cn.equals("aura flux") - || cn.equals("basalt monolith") || cn.equals("brago, king eternal") - || cn.equals("candelabra of tawnos") || cn.equals("cephalid aristocrat") - || cn.equals("cephalid illusionist") || cn.equals("changeling berserker") - || cn.equals("consecrated sphinx") - || cn.equals("cyclonic rift") - || cn.equals("the chain veil") - || cn.equals("cinderhaze wretch") || cn.equals("cryptic gateway") - || cn.equals("deadeye navigator") || cn.equals("derevi, empyrial tactician") - || cn.equals("doubling season") || cn.equals("dross scorpion") - || cn.equals("earthcraft") || cn.equals("erratic portal") - || cn.equals("enter the infinite") || cn.equals("omniscience") - || cn.equals("exquisite blood") || cn.equals("future sight") - || cn.equals("genesis chamber") - || cn.equals("ghave, guru of spores") - || cn.equals("grave pact") - || cn.equals("grave titan") || cn.equals("great whale") - || cn.equals("grim monolith") || cn.equals("gush") - || cn.equals("hellkite charger") || cn.equals("intruder alarm") - || cn.equals("hermit druid") - || cn.equals("humility") - || cn.equals("iona, shield of emeria") - || cn.equals("karn, silver golem") || cn.equals("kiki-jiki, mirror breaker") - || cn.equals("krark-clan ironworks") || cn.equals("krenko, mob boss") - || cn.equals("krosan restorer") || cn.equals("laboratory maniac") - || cn.equals("leovold, emissary of trest") - || cn.equals("leonin relic-warder") || cn.equals("leyline of the void") - || cn.equals("memnarch") - || cn.equals("meren of clan nel toth") || cn.equals("mikaeus, the unhallowed") - || cn.equals("mindcrank") || cn.equals("mindslaver") - || cn.equals("minion reflector") || cn.equals("mycosynth lattice") - || cn.equals("myr turbine") || cn.equals("narset, enlightened master") - || cn.equals("nekusar, the mindrazer") || cn.equals("norin the wary") - || cn.equals("notion thief") - || cn.equals("opalescence") || cn.equals("ornithopter") - || cn.equals("paradox engine") - || cn.equals("purphoros, god of the forge") - || cn.equals("peregrine drake") || cn.equals("palinchron") - || cn.equals("planar portal") || cn.equals("power artifact") - || cn.equals("rings of brighthearth") || cn.equals("rite of replication") - || cn.equals("sanguine bond") || cn.equals("sensei's divining top") - || cn.equals("splinter twin") || cn.equals("stony silence") || cn.equals("sunder") - || cn.equals("storm cauldron") || cn.equals("teferi's puzzle box") + || cn.equals("survival of the fittest") + || cn.equals("table view") + || cn.equals("tainted aether") || cn.equals("tangle wire") + || cn.equals("tectonic break") + || cn.equals("teferi's protection") + || cn.equals("teferi's puzzle box") || cn.equals("teferi, mage of zhalfir") - || cn.equals("tezzeret the seeker") || cn.equals("time stretch") - || cn.equals("time warp") || cn.equals("training grounds") - || cn.equals("triskelavus") || cn.equals("triskelion") - || cn.equals("turnabout") || cn.equals("umbral mantle") - || cn.equals("uyo, silent prophet") || cn.equals("voltaic key") - || cn.equals("workhorse") || cn.equals("worldgorger dragon") - || cn.equals("worthy cause") || cn.equals("yawgmoth's will") - || cn.equals("zealous conscripts")) { - thisMaxPower = Math.max(thisMaxPower, 12); - } - - if (cn.equals("animar, soul of element") - || cn.equals("azami, lady of scrolls") - || cn.equals("braids, cabal minion") - || cn.equals("child of alara") - || cn.equals("derevi, empyrial tactician") - || cn.equals("edric, spymaster of trest") - || cn.equals("gaddock teeg") - || cn.equals("grand arbiter augustin iv") - || cn.equals("hokori, dust drinker") - || cn.equals("iona, shield of emeria") - || cn.equals("jin-gitaxias, core augur") - || cn.equals("kaalia of the vast") - || cn.equals("karador, ghost chieftain") - || cn.equals("leovold, emissary of trest") - || cn.equals("linvala, keeper of silence") - || cn.equals("llawan, cephalid empress") - || cn.equals("memnarch") - || cn.equals("meren of clan nel toth") - || cn.equals("michiko konda, truth seeker") - || cn.equals("narset, enlightened master") - || cn.equals("nekusar, the mindrazer") - || cn.equals("norin the wary") - || cn.equals("numot, the devastator") - || cn.equals("sheoldred, whispering one") - || cn.equals("teferi, mage of zhalfir") - || cn.equals("zur the enchanter")) { - thisMaxPower = Math.max(thisMaxPower, 12); - } - - if (cn.equals("anafenza, the foremost") - || cn.equals("arcum dagsson") - || cn.equals("azusa, lost but seeking") - || cn.equals("brago, king eternal") - || cn.equals("captain sisay") - || cn.equals("elesh norn, grand cenobite") - || cn.equals("malfegor") - || cn.equals("maelstrom wanderer") - || cn.equals("mikaeus the unhallowed") - || cn.equals("nath of the gilt-leaf") - || cn.equals("prossh, skyraider of kher") - || cn.equals("purphoros, god of the forge") - || cn.equals("sen triplets") + || cn.equals("teferi, master of time") + || cn.equals("teferi, time raveler") + || cn.equals("temporal manipulation") + || cn.equals("tergrid, god of fright") + || cn.equals("text view") + || cn.equals("tezzeret the seeker") + || cn.equals("thassa's oracle") + || cn.equals("the chain veil") + || cn.equals("the tabernacle at pendrell vale") + || cn.equals("thieves' auction") + || cn.equals("thoughts of ruin") + || cn.equals("thrasios, triton hero") + || cn.equals("time stretch") + || cn.equals("time warp") + || cn.equals("tinker") + || cn.equals("tooth and nail") + || cn.equals("torment of hailfire") + || cn.equals("torpor orb") + || cn.equals("training grounds") + || cn.equals("treasure cruise") + || cn.equals("triskelavus") + || cn.equals("triskelion") + || cn.equals("triumph of the hordes") + || cn.equals("turnabout") + || cn.equals("ugin, the spirit dragon") + || cn.equals("ulamog, the ceaseless hunger") + || cn.equals("ulamog, the infinite gyre") + || cn.equals("umbral mantle") || cn.equals("urabrask the hidden") - || cn.equals("vorinclex, voice of hunger")) { - thisMaxPower = Math.max(thisMaxPower, 10); - } + || cn.equals("urza, lord high artificer") + || cn.equals("uyo, silent prophet") + || cn.equals("void winnower") + || cn.equals("voltaic key") + || cn.equals("vorinclex, voice of hunger") + || cn.equals("wake of destruction") + || cn.equals("warp world") + || cn.equals("winter orb") + || cn.equals("workhorse") + || cn.equals("worldgorger dragon") + || cn.equals("worthy cause") + || cn.equals("xanathar, guild kingpin") + || cn.equals("yawgmoth's will") + || cn.equals("zealous conscripts") + || cn.equals("zur the enchanter")) { + thisMaxPower = Math.max(thisMaxPower, 12); + } return thisMaxPower; } + } + + diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardRenderer.java index 5a2e2870d1d..92f9d625ce4 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardRenderer.java @@ -225,8 +225,9 @@ public abstract class CardRenderer { // Call the template methods drawBorder(g); drawBackground(g); + lessOpaqueRulesTextBox = false; drawArt(g); - drawFrame(g, attribs, image); + drawFrame(g, attribs, image, lessOpaqueRulesTextBox); if (!cardView.isAbility()) { drawOverlays(g); drawCounters(g); @@ -241,7 +242,7 @@ public abstract class CardRenderer { protected abstract void drawArt(Graphics2D g); - protected abstract void drawFrame(Graphics2D g, CardPanelAttributes attribs, BufferedImage image); + protected abstract void drawFrame(Graphics2D g, CardPanelAttributes attribs, BufferedImage image, boolean lessOpaqueRulesTextBox); // Template methods that are possible to override, but unlikely to be // overridden. @@ -318,7 +319,8 @@ public abstract class CardRenderer { } } - protected void drawFaceArtIntoRect(Graphics2D g, int x, int y, int w, int h, Rectangle2D artRect, boolean shouldPreserveAspect) { + private boolean lessOpaqueRulesTextBox = false; + protected void drawFaceArtIntoRect(Graphics2D g, int x, int y, int w, int h, int alternate_h, Rectangle2D artRect, boolean shouldPreserveAspect) { // Perform a process to make sure that the art is scaled uniformly to fill the frame, cutting // off the minimum amount necessary to make it completely fill the frame without "squashing" it. double fullCardImgWidth = faceArtImage.getWidth(); @@ -346,10 +348,18 @@ public abstract class CardRenderer { RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g.setRenderingHints(rh); - g.drawImage(faceArtImage, - x, y, - (int) targetWidth, (int) targetHeight, - null); + if (fullCardImgWidth > fullCardImgHeight) { + g.drawImage(faceArtImage, + x, y, + (int) targetWidth, (int) targetHeight, + null); + } else { + g.drawImage(faceArtImage, + x, y, + (int) targetWidth, alternate_h, // alernate_h is roughly (targetWidth / 0.74) + null); + lessOpaqueRulesTextBox = true; + } } catch (RasterFormatException e) { // At very small card sizes we may encounter a problem with rounding error making the rect not fit System.out.println(e); diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java index 7d72a9d4688..f39cc96b467 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java @@ -450,9 +450,11 @@ public class ModernCardRenderer extends CardRenderer { // Normal drawing of art from a source part of the card frame into the rect if (useFaceArt) { + int alternate_height = cardHeight - boxHeight * 2 - totalContentInset; drawFaceArtIntoRect(g, totalContentInset + 1, totalContentInset + boxHeight, contentWidth - 2, typeLineY - totalContentInset - boxHeight, + alternate_height, sourceRect, shouldPreserveAspect); } else if (!isZendikarFullArtLand()) { drawArtIntoRect(g, @@ -464,14 +466,14 @@ public class ModernCardRenderer extends CardRenderer { } @Override - protected void drawFrame(Graphics2D g, CardPanelAttributes attribs, BufferedImage image) { + protected void drawFrame(Graphics2D g, CardPanelAttributes attribs, BufferedImage image, boolean lessOpaqueRulesTextBox) { // Get the card colors to base the frame on ObjectColor frameColors = getFrameObjectColor(); // Get the border paint Color boxColor = getBoxColor(frameColors, cardView.getCardTypes(), attribs.isTransformed); Color additionalBoxColor = getAdditionalBoxColor(frameColors, cardView.getCardTypes(), attribs.isTransformed); - Paint textboxPaint = getTextboxPaint(frameColors, cardView.getCardTypes(), cardWidth); + Paint textboxPaint = getTextboxPaint(frameColors, cardView.getCardTypes(), cardWidth, lessOpaqueRulesTextBox); Paint borderPaint = getBorderPaint(frameColors, cardView.getCardTypes(), cardWidth); // Special colors @@ -1765,21 +1767,29 @@ public class ModernCardRenderer extends CardRenderer { } } + private static Color getLessOpaqueColor(Color color, boolean lessOpaqueRulesTextBox) { + if (lessOpaqueRulesTextBox) { + Color lessOpaque = new Color (color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() - 50); + return lessOpaque; + } + return color; + } + // Determine the border paint to use, based on an ObjectColors - protected static Paint getTextboxPaint(ObjectColor colors, Collection types, int width) { + protected static Paint getTextboxPaint(ObjectColor colors, Collection types, int width, boolean lessOpaqueRulesTextBox) { if (colors.isMulticolored()) { if (colors.getColorCount() == 2) { List twoColors = colors.getColors(); Color[] translatedColors; if (types.contains(CardType.LAND)) { translatedColors = new Color[]{ - getLandTextboxColor(twoColors.get(0)), - getLandTextboxColor(twoColors.get(1)) + getLessOpaqueColor(getLandTextboxColor(twoColors.get(0)), lessOpaqueRulesTextBox), + getLessOpaqueColor(getLandTextboxColor(twoColors.get(1)), lessOpaqueRulesTextBox) }; } else { translatedColors = new Color[]{ - getTextboxColor(twoColors.get(0)), - getTextboxColor(twoColors.get(1)) + getLessOpaqueColor(getTextboxColor(twoColors.get(0)), lessOpaqueRulesTextBox), + getLessOpaqueColor(getTextboxColor(twoColors.get(1)), lessOpaqueRulesTextBox) }; } @@ -1789,20 +1799,20 @@ public class ModernCardRenderer extends CardRenderer { new float[]{0.4f, 0.6f}, translatedColors); } else if (types.contains(CardType.LAND)) { - return LAND_TEXTBOX_GOLD; + return getLessOpaqueColor(LAND_TEXTBOX_GOLD, lessOpaqueRulesTextBox); } else { - return TEXTBOX_GOLD; + return getLessOpaqueColor(TEXTBOX_GOLD, lessOpaqueRulesTextBox); } } else if (colors.isColorless()) { if (types.contains(CardType.LAND)) { - return TEXTBOX_LAND; + return getLessOpaqueColor(TEXTBOX_LAND, lessOpaqueRulesTextBox); } else { - return TEXTBOX_COLORLESS; + return getLessOpaqueColor(TEXTBOX_COLORLESS, lessOpaqueRulesTextBox); } } else if (types.contains(CardType.LAND)) { - return getLandTextboxColor(colors); + return getLessOpaqueColor(getLandTextboxColor(colors), lessOpaqueRulesTextBox); } else { - return getTextboxColor(colors); + return getLessOpaqueColor(getTextboxColor(colors), lessOpaqueRulesTextBox); } } } diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java index b732e7faac5..6a81bbc6b07 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java @@ -586,13 +586,7 @@ public class Commander extends Constructed { } if (card.isPlaneswalker()) { - if (card.getName().toLowerCase(Locale.ENGLISH).equals("jace, the mind sculptor")) { - thisMaxPower = Math.max(thisMaxPower, 6); - } - if (card.getName().toLowerCase(Locale.ENGLISH).equals("ugin, the spirit dragon")) { - thisMaxPower = Math.max(thisMaxPower, 5); - } - thisMaxPower = Math.max(thisMaxPower, 4); + thisMaxPower = Math.max(thisMaxPower, 6); } String cn = card.getName().toLowerCase(Locale.ENGLISH); @@ -673,7 +667,7 @@ public class Commander extends Constructed { || cn.equals("vorinclex, voice of hunger") || cn.equals("winter orb") || cn.equals("zur the enchanter")) { - thisMaxPower = Math.max(thisMaxPower, 5); + thisMaxPower = Math.max(thisMaxPower, 12); } // Parts of infinite combos @@ -734,9 +728,151 @@ public class Commander extends Constructed { || cn.equals("workhorse") || cn.equals("worldgorger dragon") || cn.equals("worthy cause") || cn.equals("yawgmoth's will") || cn.equals("zealous conscripts")) { - thisMaxPower = Math.max(thisMaxPower, 12); + thisMaxPower = Math.max(thisMaxPower, 15); numberInfinitePieces++; } + + // Saltiest cards (edhrec) + if (cn.equals("acid rain") + || cn.equals("agent of treachery") + || cn.equals("apocalypse") + || cn.equals("armageddon") + || cn.equals("atraxa, praetors' voice") + || cn.equals("aura shards") + || cn.equals("avacyn, angel of hope") + || cn.equals("back to basics") + || cn.equals("bend or break") + || cn.equals("blightsteel colossus") + || cn.equals("blood moon") + || cn.equals("boil") + || cn.equals("boiling seas") + || cn.equals("bribery") + || cn.equals("burning sands") + || cn.equals("card view") + || cn.equals("cataclysm") + || cn.equals("catastrophe") + || cn.equals("chulane, teller of tales") + || cn.equals("confusion in the ranks") + || cn.equals("consecrated sphinx") + || cn.equals("contamination") + || cn.equals("craterhoof behemoth") + || cn.equals("cyclonic rift") + || cn.equals("death cloud") + || cn.equals("decree of annihilation") + || cn.equals("decree of silence") + || cn.equals("demonic consultation") + || cn.equals("derevi, empyrial tactician") + || cn.equals("devastation") + || cn.equals("divine intervention") + || cn.equals("dockside extortionist") + || cn.equals("doomsday") + || cn.equals("doubling season") + || cn.equals("drannith magistrate") + || cn.equals("elesh norn, grand cenobite") + || cn.equals("embargo") + || cn.equals("emrakul, the promised end") + || cn.equals("epicenter") + || cn.equals("expropriate") + || cn.equals("fall of the thran") + || cn.equals("fierce guardianship") + || cn.equals("food chain") + || cn.equals("force of negation") + || cn.equals("force of will") + || cn.equals("gaddock teeg") + || cn.equals("gaea's cradle") + || cn.equals("gilded drake") + || cn.equals("glenn, the voice of calm") + || cn.equals("global ruin") + || cn.equals("golos, tireless pilgrim") + || cn.equals("grand arbiter augustin iv") + || cn.equals("grip of chaos") + || cn.equals("hokori, dust drinker") + || cn.equals("humility") + || cn.equals("impending disaster") + || cn.equals("invoke prejudice") + || cn.equals("iona, shield of emeria") + || cn.equals("jin-gitaxias, core augur") + || cn.equals("jokulhaups") + || cn.equals("keldon firebombers") + || cn.equals("kinnan, bonder prodigy") + || cn.equals("kozilek, butcher of truth") + || cn.equals("land equilibrium") + || cn.equals("linvala, keeper of silence") + || cn.equals("magister sphinx") + || cn.equals("mana breach") + || cn.equals("mana crypt") + || cn.equals("mana drain") + || cn.equals("mana vortex") + || cn.equals("mindslaver") + || cn.equals("narset, enlightened master") + || cn.equals("narset, parter of veils") + || cn.equals("negan, the cold-blooded") + || cn.equals("nether void") + || cn.equals("nexus of fate") + || cn.equals("notion thief") + || cn.equals("obliterate") + || cn.equals("oko, thief of crowns") + || cn.equals("oloro, ageless ascetic") + || cn.equals("omniscience") + || cn.equals("opposition agent") + || cn.equals("oppression") + || cn.equals("overwhelming splendor") + || cn.equals("palinchron") + || cn.equals("paradox engine") + || cn.equals("possessed portal") + || cn.equals("price of glory") + || cn.equals("protean hulk") + || cn.equals("ravages of war") + || cn.equals("rhystic study") + || cn.equals("rick, steadfast leader") + || cn.equals("rising waters") + || cn.equals("ruination") + || cn.equals("scrambleverse") + || cn.equals("seedborn muse") + || cn.equals("sen triplets") + || cn.equals("sire of insanity") + || cn.equals("skithiryx, the blight dragon") + || cn.equals("smokestack") + || cn.equals("smothering tithe") + || cn.equals("sorin markov") + || cn.equals("stasis") + || cn.equals("static orb") + || cn.equals("storage matrix") + || cn.equals("sunder") + || cn.equals("survival of the fittest") + || cn.equals("table view") + || cn.equals("tainted aether") + || cn.equals("tectonic break") + || cn.equals("teferi's protection") + || cn.equals("teferi, master of time") + || cn.equals("teferi, time raveler") + || cn.equals("temporal manipulation") + || cn.equals("tergrid, god of fright") + || cn.equals("text view") + || cn.equals("thassa's oracle") + || cn.equals("the tabernacle at pendrell vale") + || cn.equals("thieves' auction") + || cn.equals("thoughts of ruin") + || cn.equals("thrasios, triton hero") + || cn.equals("time stretch") + || cn.equals("time warp") + || cn.equals("tooth and nail") + || cn.equals("torment of hailfire") + || cn.equals("torpor orb") + || cn.equals("triumph of the hordes") + || cn.equals("ugin, the spirit dragon") + || cn.equals("ulamog, the ceaseless hunger") + || cn.equals("ulamog, the infinite gyre") + || cn.equals("urza, lord high artificer") + || cn.equals("void winnower") + || cn.equals("vorinclex, voice of hunger") + || cn.equals("wake of destruction") + || cn.equals("warp world") + || cn.equals("winter orb") + || cn.equals("xanathar, guild kingpin") + || cn.equals("zur the enchanter")) { + thisMaxPower = Math.max(thisMaxPower, 15); + } edhPowerLevel += thisMaxPower; } @@ -769,11 +905,17 @@ public class Commander extends Constructed { // Least fun commanders if (cn.equals("animar, soul of element") + || cn.equals("anafenza, the foremost") + || cn.equals("arcum dagsson") || cn.equals("azami, lady of scrolls") + || cn.equals("azusa, lost but seeking") + || cn.equals("brago, king eternal") || cn.equals("braids, cabal minion") + || cn.equals("captain sisay") || cn.equals("child of alara") || cn.equals("derevi, empyrial tactician") || cn.equals("edric, spymaster of trest") + || cn.equals("elesh norn, grand cenobite") || cn.equals("gaddock teeg") || cn.equals("grand arbiter augustin iv") || cn.equals("hokori, dust drinker") @@ -784,41 +926,67 @@ public class Commander extends Constructed { || cn.equals("leovold, emissary of trest") || cn.equals("linvala, keeper of silence") || cn.equals("llawan, cephalid empress") + || cn.equals("maelstrom wanderer") + || cn.equals("malfegor") || cn.equals("memnarch") || cn.equals("meren of clan nel toth") || cn.equals("michiko konda, truth seeker") + || cn.equals("mikaeus the unhallowed") || cn.equals("narset, enlightened master") + || cn.equals("nath of the gilt-leaf") || cn.equals("nekusar, the mindrazer") || cn.equals("norin the wary") || cn.equals("numot, the devastator") + || cn.equals("prossh, skyraider of kher") + || cn.equals("purphoros, god of the forge") + || cn.equals("sen triplets") || cn.equals("sheoldred, whispering one") || cn.equals("teferi, mage of zhalfir") + || cn.equals("urabrask the hidden") + || cn.equals("vorinclex, voice of hunger") || cn.equals("zur the enchanter")) { thisMaxPower = Math.max(thisMaxPower, 25); } - // Next least fun commanders - if (cn.equals("anafenza, the foremost") - || cn.equals("arcum dagsson") - || cn.equals("azusa, lost but seeking") - || cn.equals("brago, king eternal") - || cn.equals("captain sisay") + // Saltiest commanders + if (cn.equals("atraxa, praetors' voice") + || cn.equals("avacyn, angel of hope") + || cn.equals("chulane, teller of tales") + || cn.equals("derevi, empyrial tactician") || cn.equals("elesh norn, grand cenobite") - || cn.equals("malfegor") - || cn.equals("maelstrom wanderer") - || cn.equals("mikaeus the unhallowed") - || cn.equals("nath of the gilt-leaf") - || cn.equals("prossh, skyraider of kher") - || cn.equals("purphoros, god of the forge") + || cn.equals("emrakul, the promised end") + || cn.equals("gaddock teeg") + || cn.equals("glenn, the voice of calm") + || cn.equals("golos, tireless pilgrim") + || cn.equals("grand arbiter augustin iv") + || cn.equals("hokori, dust drinker") + || cn.equals("iona, shield of emeria") + || cn.equals("jin-gitaxias, core augur") + || cn.equals("kinnan, bonder prodigy") + || cn.equals("kozilek, butcher of truth") + || cn.equals("linvala, keeper of silence") + || cn.equals("narset, enlightened master") + || cn.equals("negan, the cold-blooded") + || cn.equals("oko, thief of crowns") + || cn.equals("oloro, ageless ascetic") + || cn.equals("rick, steadfast leader") || cn.equals("sen triplets") - || cn.equals("urabrask the hidden") - || cn.equals("vorinclex, voice of hunger")) { - thisMaxPower = Math.max(thisMaxPower, 15); + || cn.equals("skithiryx, the blight dragon") + || cn.equals("teferi, master of time") + || cn.equals("teferi, time raveler") + || cn.equals("thrasios, triton hero") + || cn.equals("ulamog, the ceaseless hunger") + || cn.equals("ulamog, the infinite gyre") + || cn.equals("urza, lord high artificer") + || cn.equals("vorinclex, voice of hunger") + || cn.equals("xanathar, guild kingpin") + || cn.equals("zur the enchanter")) { + thisMaxPower = Math.max(thisMaxPower, 20); } edhPowerLevel += thisMaxPower; } - edhPowerLevel += numberInfinitePieces * 12; + edhPowerLevel += numberInfinitePieces * 18; edhPowerLevel = Math.round(edhPowerLevel / 10); if (edhPowerLevel >= 100) { edhPowerLevel = 99; From 33870c7a4b69a5b02496437ece90e9a710431dd3 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 09:42:06 -0400 Subject: [PATCH 014/231] [MID] Implemented Mysterious Tome / Chilling Chronicle --- .../src/mage/cards/c/ChillingChronicle.java | 43 ++++++++++++++++++ .../src/mage/cards/m/MysteriousTome.java | 45 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 90 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/ChillingChronicle.java create mode 100644 Mage.Sets/src/mage/cards/m/MysteriousTome.java diff --git a/Mage.Sets/src/mage/cards/c/ChillingChronicle.java b/Mage.Sets/src/mage/cards/c/ChillingChronicle.java new file mode 100644 index 00000000000..324529c58e8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ChillingChronicle.java @@ -0,0 +1,43 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.TapTargetEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.common.TargetNonlandPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ChillingChronicle extends CardImpl { + + public ChillingChronicle(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, ""); + + this.transformable = true; + this.nightCard = true; + + // {1}, {T}: Tap target nonland permanent. Transform Chilling Chronicle. + Ability ability = new SimpleActivatedAbility(new TapTargetEffect(), new GenericManaCost(1)); + ability.addCost(new TapSourceCost()); + ability.addEffect(new TransformSourceEffect(false)); + ability.addTarget(new TargetNonlandPermanent()); + this.addAbility(ability); + } + + private ChillingChronicle(final ChillingChronicle card) { + super(card); + } + + @Override + public ChillingChronicle copy() { + return new ChillingChronicle(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MysteriousTome.java b/Mage.Sets/src/mage/cards/m/MysteriousTome.java new file mode 100644 index 00000000000..3458823bb07 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MysteriousTome.java @@ -0,0 +1,45 @@ +package mage.cards.m; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MysteriousTome extends CardImpl { + + public MysteriousTome(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{U}"); + + this.transformable = true; + this.secondSideCardClazz = mage.cards.c.ChillingChronicle.class; + + // {2}, {T}: Draw a card. Transform Mysterious Tome. + this.addAbility(new TransformAbility()); + Ability ability = new SimpleActivatedAbility( + new DrawCardSourceControllerEffect(1), new GenericManaCost(2) + ); + ability.addCost(new TapSourceCost()); + ability.addEffect(new TransformSourceEffect(true)); + this.addAbility(ability); + } + + private MysteriousTome(final MysteriousTome card) { + super(card); + } + + @Override + public MysteriousTome copy() { + return new MysteriousTome(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 8d0d2f43fe2..d3d7c389bcd 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -67,6 +67,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Champion of the Perished", 91, Rarity.RARE, mage.cards.c.ChampionOfThePerished.class)); cards.add(new SetCardInfo("Chapel Shieldgeist", 13, Rarity.UNCOMMON, mage.cards.c.ChapelShieldgeist.class)); cards.add(new SetCardInfo("Chaplain of Alms", 13, Rarity.UNCOMMON, mage.cards.c.ChaplainOfAlms.class)); + cards.add(new SetCardInfo("Chilling Chronicle", 63, Rarity.UNCOMMON, mage.cards.c.ChillingChronicle.class)); cards.add(new SetCardInfo("Clarion Cathars", 14, Rarity.COMMON, mage.cards.c.ClarionCathars.class)); cards.add(new SetCardInfo("Clear Shot", 176, Rarity.UNCOMMON, mage.cards.c.ClearShot.class)); cards.add(new SetCardInfo("Component Collector", 43, Rarity.COMMON, mage.cards.c.ComponentCollector.class)); @@ -165,6 +166,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Morkrut Behemoth", 114, Rarity.COMMON, mage.cards.m.MorkrutBehemoth.class)); cards.add(new SetCardInfo("Mountain", 274, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Mounted Dreadknight", 150, Rarity.COMMON, mage.cards.m.MountedDreadknight.class)); + cards.add(new SetCardInfo("Mysterious Tome", 63, Rarity.UNCOMMON, mage.cards.m.MysteriousTome.class)); cards.add(new SetCardInfo("Mystic Monstrosity", 256, Rarity.UNCOMMON, mage.cards.m.MysticMonstrosity.class)); cards.add(new SetCardInfo("Mystic Skull", 256, Rarity.UNCOMMON, mage.cards.m.MysticSkull.class)); cards.add(new SetCardInfo("Nebelgast Intruder", 64, Rarity.UNCOMMON, mage.cards.n.NebelgastIntruder.class)); From 857a1bc9b24c7805bc8d44051ce88cf146afba62 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 09:54:46 -0400 Subject: [PATCH 015/231] [MID] fixed a few dfcs --- Mage.Sets/src/mage/cards/b/BaithookAngler.java | 2 ++ Mage.Sets/src/mage/cards/b/BenevolentGeist.java | 3 +++ Mage.Sets/src/mage/cards/c/ChaplainOfAlms.java | 2 ++ Mage.Sets/src/mage/cards/m/MalevolentHermit.java | 2 ++ Mage.Sets/src/mage/cards/o/OverwhelmedArchivist.java | 2 ++ Mage.Sets/src/mage/cards/s/SpellrunePainter.java | 2 ++ .../src/test/java/mage/verify/VerifyCardDataTest.java | 9 +++++++++ 7 files changed, 22 insertions(+) diff --git a/Mage.Sets/src/mage/cards/b/BaithookAngler.java b/Mage.Sets/src/mage/cards/b/BaithookAngler.java index 2549bd9dd13..200ccb38c7a 100644 --- a/Mage.Sets/src/mage/cards/b/BaithookAngler.java +++ b/Mage.Sets/src/mage/cards/b/BaithookAngler.java @@ -3,6 +3,7 @@ package mage.cards.b; import mage.MageInt; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -26,6 +27,7 @@ public final class BaithookAngler extends CardImpl { this.secondSideCardClazz = mage.cards.h.HookHauntDrifter.class; // Disturb {1}{U} + this.addAbility(new TransformAbility()); this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{1}{U}"))); } diff --git a/Mage.Sets/src/mage/cards/b/BenevolentGeist.java b/Mage.Sets/src/mage/cards/b/BenevolentGeist.java index 64f85dc8644..f54295208ea 100644 --- a/Mage.Sets/src/mage/cards/b/BenevolentGeist.java +++ b/Mage.Sets/src/mage/cards/b/BenevolentGeist.java @@ -27,6 +27,9 @@ public final class BenevolentGeist extends CardImpl { this.subtype.add(SubType.WIZARD); this.power = new MageInt(2); this.toughness = new MageInt(2); + this.color.setBlue(true); + this.transformable = true; + this.nightCard = true; // Flying this.addAbility(FlyingAbility.getInstance()); diff --git a/Mage.Sets/src/mage/cards/c/ChaplainOfAlms.java b/Mage.Sets/src/mage/cards/c/ChaplainOfAlms.java index 371ec588518..73105c7bf2f 100644 --- a/Mage.Sets/src/mage/cards/c/ChaplainOfAlms.java +++ b/Mage.Sets/src/mage/cards/c/ChaplainOfAlms.java @@ -4,6 +4,7 @@ import mage.MageInt; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.keyword.DisturbAbility; import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.TransformAbility; import mage.abilities.keyword.WardAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -34,6 +35,7 @@ public final class ChaplainOfAlms extends CardImpl { this.addAbility(new WardAbility(new ManaCostsImpl<>("{1}"))); // Disturb {3}{W} + this.addAbility(new TransformAbility()); this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{3}{W}"))); } diff --git a/Mage.Sets/src/mage/cards/m/MalevolentHermit.java b/Mage.Sets/src/mage/cards/m/MalevolentHermit.java index 59e9a44d840..743e168f114 100644 --- a/Mage.Sets/src/mage/cards/m/MalevolentHermit.java +++ b/Mage.Sets/src/mage/cards/m/MalevolentHermit.java @@ -8,6 +8,7 @@ import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.CounterUnlessPaysEffect; import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -41,6 +42,7 @@ public final class MalevolentHermit extends CardImpl { this.addAbility(ability); // Disturb {2}{U} + this.addAbility(new TransformAbility()); this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{2}{U}"))); } diff --git a/Mage.Sets/src/mage/cards/o/OverwhelmedArchivist.java b/Mage.Sets/src/mage/cards/o/OverwhelmedArchivist.java index 11d1cc90be4..724a434b1f5 100644 --- a/Mage.Sets/src/mage/cards/o/OverwhelmedArchivist.java +++ b/Mage.Sets/src/mage/cards/o/OverwhelmedArchivist.java @@ -5,6 +5,7 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.DrawDiscardControllerEffect; import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -31,6 +32,7 @@ public final class OverwhelmedArchivist extends CardImpl { this.addAbility(new EntersBattlefieldTriggeredAbility(new DrawDiscardControllerEffect(1, 1))); // Disturb {3}{U} + this.addAbility(new TransformAbility()); this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{3}{U}"))); } diff --git a/Mage.Sets/src/mage/cards/s/SpellrunePainter.java b/Mage.Sets/src/mage/cards/s/SpellrunePainter.java index 1a6c4c568f8..3e46dc3ef0b 100644 --- a/Mage.Sets/src/mage/cards/s/SpellrunePainter.java +++ b/Mage.Sets/src/mage/cards/s/SpellrunePainter.java @@ -4,6 +4,7 @@ import mage.MageInt; import mage.abilities.common.SpellCastControllerTriggeredAbility; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.keyword.DayboundAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -36,6 +37,7 @@ public final class SpellrunePainter extends CardImpl { )); // Daybound + this.addAbility(new TransformAbility()); this.addAbility(DayboundAbility.getInstance()); } diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index b6570834104..d74434efa3f 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -9,6 +9,7 @@ import mage.abilities.common.WerewolfFrontTriggeredAbility; import mage.abilities.effects.keyword.ScryEffect; import mage.abilities.keyword.MenaceAbility; import mage.abilities.keyword.MultikickerAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.*; import mage.cards.decks.DeckCardLists; import mage.cards.decks.importer.DeckImporter; @@ -1381,6 +1382,14 @@ public class VerifyCardDataTest { fail(card, "abilities", "card is a front face werewolf with a back face ability"); } + if (card.getSecondCardFace() != null && !card.isNightCard() && !card.getAbilities().containsClass(TransformAbility.class)) { + fail(card, "abilities", "double-faced cards should have transform ability on the front"); + } + + if (card.getSecondCardFace() != null && card.isNightCard() && card.getAbilities().containsClass(TransformAbility.class)) { + fail(card, "abilities", "double-faced cards should not have transform ability on the back"); + } + // special check: missing or wrong ability/effect hints Map hints = new HashMap<>(); hints.put(MenaceAbility.class, "can't be blocked except by two or more"); From 90ad64749d2c0bc7ae2013f8c35e03235d452585 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 10:01:11 -0400 Subject: [PATCH 016/231] [MID] Implemented Soul-Guide Gryff --- .../src/mage/cards/s/SoulGuideGryff.java | 46 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 47 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SoulGuideGryff.java diff --git a/Mage.Sets/src/mage/cards/s/SoulGuideGryff.java b/Mage.Sets/src/mage/cards/s/SoulGuideGryff.java new file mode 100644 index 00000000000..0ca2513aadf --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SoulGuideGryff.java @@ -0,0 +1,46 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.target.common.TargetCardInGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SoulGuideGryff extends CardImpl { + + public SoulGuideGryff(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{W}"); + + this.subtype.add(SubType.HIPPOGRIFF); + this.subtype.add(SubType.SPIRIT); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When Soul-Guide Gryff enters the battlefield, exile up to one target card from a graveyard. + Ability ability = new EntersBattlefieldTriggeredAbility(new ExileTargetEffect()); + ability.addTarget(new TargetCardInGraveyard(0, 1)); + this.addAbility(ability); + } + + private SoulGuideGryff(final SoulGuideGryff card) { + super(card); + } + + @Override + public SoulGuideGryff copy() { + return new SoulGuideGryff(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index d3d7c389bcd..295d231eb9b 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -204,6 +204,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Sigarda, Champion of Light", 240, Rarity.MYTHIC, mage.cards.s.SigardaChampionOfLight.class)); cards.add(new SetCardInfo("Slaughter Specialist", 122, Rarity.RARE, mage.cards.s.SlaughterSpecialist.class)); cards.add(new SetCardInfo("Snarling Wolf", 199, Rarity.COMMON, mage.cards.s.SnarlingWolf.class)); + cards.add(new SetCardInfo("Soul-Guide Gryff", 35, Rarity.COMMON, mage.cards.s.SoulGuideGryff.class)); cards.add(new SetCardInfo("Spectral Adversary", 77, Rarity.MYTHIC, mage.cards.s.SpectralAdversary.class)); cards.add(new SetCardInfo("Spellrune Howler", 160, Rarity.UNCOMMON, mage.cards.s.SpellruneHowler.class)); cards.add(new SetCardInfo("Spellrune Painter", 160, Rarity.UNCOMMON, mage.cards.s.SpellrunePainter.class)); From e799765518676ecd51ee7e29bc00048879f026df Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 10:05:46 -0400 Subject: [PATCH 017/231] [MID] Implemented Geistwave --- Mage.Sets/src/mage/cards/g/Geistwave.java | 71 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 72 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/g/Geistwave.java diff --git a/Mage.Sets/src/mage/cards/g/Geistwave.java b/Mage.Sets/src/mage/cards/g/Geistwave.java new file mode 100644 index 00000000000..b21a901bc79 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/Geistwave.java @@ -0,0 +1,71 @@ +package mage.cards.g; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetNonlandPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class Geistwave extends CardImpl { + + public Geistwave(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{U}"); + + // Return target nonland permanent to its owner's hand. If you controlled that permanent, draw a card. + this.getSpellAbility().addEffect(new GeistwaveEffect()); + this.getSpellAbility().addTarget(new TargetNonlandPermanent()); + } + + private Geistwave(final Geistwave card) { + super(card); + } + + @Override + public Geistwave copy() { + return new Geistwave(this); + } +} + +class GeistwaveEffect extends OneShotEffect { + + GeistwaveEffect() { + super(Outcome.Benefit); + staticText = "return target nonland permanent to its owner's hand. " + + "If you controlled that permanent, draw a card"; + } + + private GeistwaveEffect(final GeistwaveEffect effect) { + super(effect); + } + + @Override + public GeistwaveEffect copy() { + return new GeistwaveEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = game.getPermanent(source.getControllerId()); + if (player == null || permanent == null) { + return false; + } + boolean flag = permanent.isControlledBy(source.getControllerId()); + player.moveCards(permanent, Zone.HAND, source, game); + if (flag) { + player.drawCards(1, source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 295d231eb9b..affdfa95ac9 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -127,6 +127,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Gavony Silversmith", 21, Rarity.COMMON, mage.cards.g.GavonySilversmith.class)); cards.add(new SetCardInfo("Gavony Trapper", 22, Rarity.COMMON, mage.cards.g.GavonyTrapper.class)); cards.add(new SetCardInfo("Geistflame Reservoir", 142, Rarity.RARE, mage.cards.g.GeistflameReservoir.class)); + cards.add(new SetCardInfo("Geistwave", 56, Rarity.COMMON, mage.cards.g.Geistwave.class)); cards.add(new SetCardInfo("Ghoulcaller's Harvest", 225, Rarity.RARE, mage.cards.g.GhoulcallersHarvest.class)); cards.add(new SetCardInfo("Grafted Identity", 57, Rarity.RARE, mage.cards.g.GraftedIdentity.class)); cards.add(new SetCardInfo("Graveyard Glutton", 104, Rarity.RARE, mage.cards.g.GraveyardGlutton.class)); From a3ec6803b7c13dce884b291ad1b2f3c42a639be6 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 10:13:05 -0400 Subject: [PATCH 018/231] [MID] Implemented Harvesttide Sentry --- .../src/mage/cards/h/HarvesttideSentry.java | 55 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 56 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HarvesttideSentry.java diff --git a/Mage.Sets/src/mage/cards/h/HarvesttideSentry.java b/Mage.Sets/src/mage/cards/h/HarvesttideSentry.java new file mode 100644 index 00000000000..38c8683bfdb --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HarvesttideSentry.java @@ -0,0 +1,55 @@ +package mage.cards.h; + +import mage.MageInt; +import mage.abilities.common.BeginningOfCombatTriggeredAbility; +import mage.abilities.condition.common.CovenCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesSourceEffect; +import mage.abilities.hint.common.CovenHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HarvesttideSentry extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(); + + static { + filter.add(new PowerPredicate(ComparisonType.FEWER_THAN, 3)); + } + + public HarvesttideSentry(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // Coven — At the beginning of combat on your turn, if you control three or more creatures with different powers, Harvesttide Sentry can't be blocked by creatures with power 2 or less this turn. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new BeginningOfCombatTriggeredAbility( + new CantBeBlockedByCreaturesSourceEffect(filter, Duration.EndOfTurn), + TargetController.YOU, false + ), CovenCondition.instance, "At the beginning of combat on your turn, " + + "if you control three or more creatures with different powers, " + + "{this} can't be blocked by creatures with power 2 or less this turn." + ).addHint(CovenHint.instance).setAbilityWord(AbilityWord.COVEN)); + } + + private HarvesttideSentry(final HarvesttideSentry card) { + super(card); + } + + @Override + public HarvesttideSentry copy() { + return new HarvesttideSentry(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index affdfa95ac9..62a8c59f90d 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -135,6 +135,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Grizzly Ghoul", 226, Rarity.UNCOMMON, mage.cards.g.GrizzlyGhoul.class)); cards.add(new SetCardInfo("Harvesttide Assailant", 143, Rarity.COMMON, mage.cards.h.HarvesttideAssailant.class)); cards.add(new SetCardInfo("Harvesttide Infiltrator", 143, Rarity.COMMON, mage.cards.h.HarvesttideInfiltrator.class)); + cards.add(new SetCardInfo("Harvesttide Sentry", 186, Rarity.COMMON, mage.cards.h.HarvesttideSentry.class)); cards.add(new SetCardInfo("Haunted Ridge", 263, Rarity.RARE, mage.cards.h.HauntedRidge.class)); cards.add(new SetCardInfo("Hedgewitch's Mask", 23, Rarity.COMMON, mage.cards.h.HedgewitchsMask.class)); cards.add(new SetCardInfo("Heirloom Mirror", 105, Rarity.UNCOMMON, mage.cards.h.HeirloomMirror.class)); From 67c07ecefcd5ea7bf43edabc1c8a7c6da978f6a1 Mon Sep 17 00:00:00 2001 From: spjspj Date: Sun, 12 Sep 2021 00:49:00 +1000 Subject: [PATCH 019/231] Update for Full Art Face art. --- .../java/org/mage/card/arcane/ModernSplitCardRenderer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java index 03f29d8fe52..1d56ecde616 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java @@ -223,7 +223,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { protected void drawSplitHalfFrame(Graphics2D g, CardPanelAttributes attribs, HalfCardProps half, int typeLineY) { // Get the border paint Color boxColor = getBoxColor(half.color, cardView.getCardTypes(), attribs.isTransformed); - Paint textboxPaint = getTextboxPaint(half.color, cardView.getCardTypes(), cardWidth); + Paint textboxPaint = getTextboxPaint(half.color, cardView.getCardTypes(), cardWidth, false); Paint borderPaint = getBorderPaint(half.color, cardView.getCardTypes(), cardWidth); // Draw main frame @@ -299,7 +299,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { } @Override - protected void drawFrame(Graphics2D g, CardPanelAttributes attribs, BufferedImage image) { + protected void drawFrame(Graphics2D g, CardPanelAttributes attribs, BufferedImage image, boolean lessOpaqueRulesTextBox) { if (isAftermath()) { drawSplitHalfFrame(getUnmodifiedHalfContext(g), attribs, leftHalf, (int) (leftHalf.ch * TYPE_LINE_Y_FRAC)); drawSplitHalfFrame(getAftermathHalfContext(g), attribs, rightHalf, (rightHalf.ch - boxHeight) / 2); @@ -309,7 +309,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { if (isFuse()) { Graphics2D g2 = getRightHalfContext(g); int totalFuseBoxWidth = rightHalf.cw * 2 + 2 * borderWidth + dividerSize; - Paint boxColor = getTextboxPaint(cardView.getColor(), ONLY_LAND_TYPE, totalFuseBoxWidth); + Paint boxColor = getTextboxPaint(cardView.getColor(), ONLY_LAND_TYPE, totalFuseBoxWidth, false); Paint borderPaint = getBorderPaint(cardView.getColor(), ONLY_LAND_TYPE, totalFuseBoxWidth); CardRendererUtils.drawRoundedBox(g2, -borderWidth, rightHalf.ch, From 2b2f30a88a16566a69f882c9e1e84b0cc8dcda35 Mon Sep 17 00:00:00 2001 From: Phred Date: Sat, 11 Sep 2021 10:03:10 -0500 Subject: [PATCH 020/231] corrected typo: prefered -> preferred ag -l prefered | xargs sed -i 's/refered/referred/g' --- .../mage/client/dialog/PreferencesDialog.form | 12 +++---- .../mage/client/dialog/PreferencesDialog.java | 32 +++++++++---------- .../java/mage/server/ChatManagerImpl.java | 4 +-- .../decks/importer/TxtDeckImporterTest.java | 4 +-- .../java/mage/verify/VerifyCardDataTest.java | 2 +- Mage/src/main/java/mage/cards/Sets.java | 6 ++-- .../mage/cards/decks/importer/CardLookup.java | 2 +- .../cards/decks/importer/DckDeckImporter.java | 2 +- .../cards/decks/importer/DekDeckImporter.java | 2 +- .../cards/decks/importer/TxtDeckImporter.java | 2 +- .../mage/cards/repository/CardRepository.java | 18 +++++------ .../main/java/mage/game/draft/DraftCube.java | 2 +- 12 files changed, 44 insertions(+), 44 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form index c2f5942521f..ebea76ef656 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form @@ -4378,11 +4378,11 @@ - + - + @@ -4416,8 +4416,8 @@ - - + + @@ -4453,7 +4453,7 @@ - + @@ -4469,7 +4469,7 @@ - + diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java index 6efba3c286f..cfd05aa198c 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java @@ -81,7 +81,7 @@ public class PreferencesDialog extends javax.swing.JDialog { public static final String KEY_CARD_IMAGES_THREADS = "cardImagesThreads"; public static final String KEY_CARD_IMAGES_THREADS_DEFAULT = "3"; public static final String KEY_CARD_IMAGES_SAVE_TO_ZIP = "cardImagesSaveToZip"; - public static final String KEY_CARD_IMAGES_PREF_LANGUAGE = "cardImagesPreferedImageLaguage"; + public static final String KEY_CARD_IMAGES_PREF_LANGUAGE = "cardImagesPreferredImageLaguage"; public static final String KEY_CARD_RENDERING_FALLBACK = "cardRenderingFallback"; public static final String KEY_CARD_RENDERING_ICONS_FOR_ABILITIES = "cardRenderingIconsForAbilities"; @@ -400,7 +400,7 @@ public class PreferencesDialog extends javax.swing.JDialog { cbTheme.setModel(new DefaultComboBoxModel<>(ThemeType.values())); addAvatars(); - cbPreferedImageLanguage.setModel(new DefaultComboBoxModel<>(CardLanguage.toList())); + cbPreferredImageLanguage.setModel(new DefaultComboBoxModel<>(CardLanguage.toList())); cbNumberOfDownloadThreads.setModel(new DefaultComboBoxModel<>(new String[]{"10", "9", "8", "7", "6", "5", "4", "3", "2", "1"})); } @@ -509,8 +509,8 @@ public class PreferencesDialog extends javax.swing.JDialog { txtImageFolderPath = new javax.swing.JTextField(); btnBrowseImageLocation = new javax.swing.JButton(); cbSaveToZipFiles = new javax.swing.JCheckBox(); - cbPreferedImageLanguage = new javax.swing.JComboBox<>(); - labelPreferedImageLanguage = new javax.swing.JLabel(); + cbPreferredImageLanguage = new javax.swing.JComboBox<>(); + labelPreferredImageLanguage = new javax.swing.JLabel(); labelNumberOfDownloadThreads = new javax.swing.JLabel(); cbNumberOfDownloadThreads = new javax.swing.JComboBox(); labelHint1 = new javax.swing.JLabel(); @@ -1658,11 +1658,11 @@ public class PreferencesDialog extends javax.swing.JDialog { } }); - cbPreferedImageLanguage.setMaximumRowCount(20); - cbPreferedImageLanguage.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); + cbPreferredImageLanguage.setMaximumRowCount(20); + cbPreferredImageLanguage.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); - labelPreferedImageLanguage.setText("Default images language:"); - labelPreferedImageLanguage.setFocusable(false); + labelPreferredImageLanguage.setText("Default images language:"); + labelPreferredImageLanguage.setFocusable(false); labelNumberOfDownloadThreads.setText("Default download threads:"); @@ -1689,10 +1689,10 @@ public class PreferencesDialog extends javax.swing.JDialog { .add(panelCardImagesLayout.createSequentialGroup() .add(panelCardImagesLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) .add(labelNumberOfDownloadThreads) - .add(labelPreferedImageLanguage)) + .add(labelPreferredImageLanguage)) .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) .add(panelCardImagesLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) - .add(cbPreferedImageLanguage, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 153, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE) + .add(cbPreferredImageLanguage, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 153, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE) .add(panelCardImagesLayout.createSequentialGroup() .add(cbNumberOfDownloadThreads, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 153, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE) .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED) @@ -1716,8 +1716,8 @@ public class PreferencesDialog extends javax.swing.JDialog { .add(labelHint1)) .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED) .add(panelCardImagesLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE) - .add(labelPreferedImageLanguage) - .add(cbPreferedImageLanguage, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))) + .add(labelPreferredImageLanguage) + .add(cbPreferredImageLanguage, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))) ); panelCardStyles.setBorder(javax.swing.BorderFactory.createTitledBorder(javax.swing.BorderFactory.createEtchedBorder(), "Card styles (restart xmage to apply new settings)")); @@ -2945,7 +2945,7 @@ public class PreferencesDialog extends javax.swing.JDialog { saveImagesPath(prefs); save(prefs, dialog.cbSaveToZipFiles, KEY_CARD_IMAGES_SAVE_TO_ZIP, "true", "false", UPDATE_CACHE_POLICY); save(prefs, dialog.cbNumberOfDownloadThreads, KEY_CARD_IMAGES_THREADS); - save(prefs, dialog.cbPreferedImageLanguage, KEY_CARD_IMAGES_PREF_LANGUAGE); + save(prefs, dialog.cbPreferredImageLanguage, KEY_CARD_IMAGES_PREF_LANGUAGE); save(prefs, dialog.cbUseDefaultBackground, KEY_BACKGROUND_IMAGE_DEFAULT, "true", "false", UPDATE_CACHE_POLICY); save(prefs, dialog.cbUseDefaultBattleImage, KEY_BATTLEFIELD_IMAGE_DEFAULT, "true", "false", UPDATE_CACHE_POLICY); @@ -3518,7 +3518,7 @@ public class PreferencesDialog extends javax.swing.JDialog { } load(prefs, dialog.cbSaveToZipFiles, KEY_CARD_IMAGES_SAVE_TO_ZIP, "true"); dialog.cbNumberOfDownloadThreads.setSelectedItem(MageFrame.getPreferences().get(KEY_CARD_IMAGES_THREADS, KEY_CARD_IMAGES_THREADS_DEFAULT)); - dialog.cbPreferedImageLanguage.setSelectedItem(MageFrame.getPreferences().get(KEY_CARD_IMAGES_PREF_LANGUAGE, CardLanguage.ENGLISH.getCode())); + dialog.cbPreferredImageLanguage.setSelectedItem(MageFrame.getPreferences().get(KEY_CARD_IMAGES_PREF_LANGUAGE, CardLanguage.ENGLISH.getCode())); // rendering settings load(prefs, dialog.cbCardRenderImageFallback, KEY_CARD_RENDERING_FALLBACK, "true", "false"); @@ -4073,7 +4073,7 @@ public class PreferencesDialog extends javax.swing.JDialog { private javax.swing.JComboBox cbNumberOfDownloadThreads; private javax.swing.JCheckBox cbPassPriorityActivation; private javax.swing.JCheckBox cbPassPriorityCast; - private javax.swing.JComboBox cbPreferedImageLanguage; + private javax.swing.JComboBox cbPreferredImageLanguage; private javax.swing.JComboBox cbProxyType; private javax.swing.JCheckBox cbSaveToZipFiles; private javax.swing.JCheckBox cbShowStormCounter; @@ -4176,7 +4176,7 @@ public class PreferencesDialog extends javax.swing.JDialog { private javax.swing.JLabel labelMainStep; private javax.swing.JLabel labelNextTurn; private javax.swing.JLabel labelNumberOfDownloadThreads; - private javax.swing.JLabel labelPreferedImageLanguage; + private javax.swing.JLabel labelPreferredImageLanguage; private javax.swing.JLabel labelPriorEnd; private javax.swing.JLabel labelSkipStep; private javax.swing.JLabel labelStackWidth; diff --git a/Mage.Server/src/main/java/mage/server/ChatManagerImpl.java b/Mage.Server/src/main/java/mage/server/ChatManagerImpl.java index 1bd283e03eb..dbdbb00dd5f 100644 --- a/Mage.Server/src/main/java/mage/server/ChatManagerImpl.java +++ b/Mage.Server/src/main/java/mage/server/ChatManagerImpl.java @@ -128,7 +128,7 @@ public class ChatManagerImpl implements ChatManager { Matcher matchPattern = cardNamePattern.matcher(message); while (matchPattern.find()) { String cardName = matchPattern.group(1); - CardInfo cardInfo = CardRepository.instance.findPreferedCoreExpansionCard(cardName, true); + CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName, true); if (cardInfo != null) { String colour = "silver"; if (cardInfo.getCard().getColor(null).isMulticolored()) { @@ -270,7 +270,7 @@ public class ChatManagerImpl implements ChatManager { Matcher matchPattern = getCardTextPattern.matcher(message.toLowerCase(Locale.ENGLISH)); if (matchPattern.find()) { String cardName = matchPattern.group(1); - CardInfo cardInfo = CardRepository.instance.findPreferedCoreExpansionCard(cardName, true); + CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName, true); if (cardInfo != null) { cardInfo.getRules(); message = "" + cardInfo.getName() + ": Cost:" + cardInfo.getManaCosts(CardInfo.ManaCostSide.ALL).toString() + ", Types:" + cardInfo.getTypes().toString() + ", "; diff --git a/Mage.Tests/src/test/java/org/mage/test/decks/importer/TxtDeckImporterTest.java b/Mage.Tests/src/test/java/org/mage/test/decks/importer/TxtDeckImporterTest.java index 50180ee3d34..4043e013be3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/decks/importer/TxtDeckImporterTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/decks/importer/TxtDeckImporterTest.java @@ -21,13 +21,13 @@ public class TxtDeckImporterTest { String[] sideboard = {"Swamp", "Mountain"}; for (String c : cards) { - card = CardRepository.instance.findPreferedCoreExpansionCard(c, true); + card = CardRepository.instance.findPreferredCoreExpansionCard(c, true); assert card != null; deck.getCards().add(new DeckCardInfo(card.getName(), card.getCardNumber(), card.getSetCode())); } for (String s : sideboard) { - card = CardRepository.instance.findPreferedCoreExpansionCard(s, true); + card = CardRepository.instance.findPreferredCoreExpansionCard(s, true); assert card != null; deck.getSideboard().add(new DeckCardInfo(card.getName(), card.getCardNumber(), card.getSetCode())); } diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index b6570834104..62b7cf39ee5 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -1849,7 +1849,7 @@ public class VerifyCardDataTest { if (!cardId.getExtension().isEmpty()) { cardInfo = CardRepository.instance.findCardWPreferredSet(cardId.getName(), cardId.getExtension(), false); } else { - cardInfo = CardRepository.instance.findPreferedCoreExpansionCard(cardId.getName(), false); + cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardId.getName(), false); } if (cardInfo == null) { errorsList.add("Error: broken cube, can't find card: " + cube.getClass().getCanonicalName() + " - " + cardId.getName()); diff --git a/Mage/src/main/java/mage/cards/Sets.java b/Mage/src/main/java/mage/cards/Sets.java index 18a94102ece..1435da7c90a 100644 --- a/Mage/src/main/java/mage/cards/Sets.java +++ b/Mage/src/main/java/mage/cards/Sets.java @@ -199,10 +199,10 @@ public class Sets extends HashMap { return null; } - public static ExpansionSet.SetCardInfo findCardByClass(Class clazz, String preferedSetCode) { + public static ExpansionSet.SetCardInfo findCardByClass(Class clazz, String preferredSetCode) { ExpansionSet.SetCardInfo info = null; - if (instance.containsKey(preferedSetCode)) { - info = instance.get(preferedSetCode).findCardInfoByClass(clazz).stream().findFirst().orElse(null); + if (instance.containsKey(preferredSetCode)) { + info = instance.get(preferredSetCode).findCardInfoByClass(clazz).stream().findFirst().orElse(null); } if (info == null) { diff --git a/Mage/src/main/java/mage/cards/decks/importer/CardLookup.java b/Mage/src/main/java/mage/cards/decks/importer/CardLookup.java index 73d07878e9d..2998803e369 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/CardLookup.java +++ b/Mage/src/main/java/mage/cards/decks/importer/CardLookup.java @@ -12,7 +12,7 @@ public class CardLookup { public static final CardLookup instance = new CardLookup(); public Optional lookupCardInfo(String name) { - return Optional.ofNullable(CardRepository.instance.findPreferedCoreExpansionCard(name, true)); + return Optional.ofNullable(CardRepository.instance.findPreferredCoreExpansionCard(name, true)); } public List lookupCardInfo(CardCriteria criteria) { diff --git a/Mage/src/main/java/mage/cards/decks/importer/DckDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/DckDeckImporter.java index 834aaf0ef8b..0b9119d5661 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/DckDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/DckDeckImporter.java @@ -92,7 +92,7 @@ public class DckDeckImporter extends PlainTextDeckImporter { } if (!cardName.equals("")) { - foundedCard = CardRepository.instance.findPreferedCoreExpansionCard(cardName, false, setCode); + foundedCard = CardRepository.instance.findPreferredCoreExpansionCard(cardName, false, setCode); } if (foundedCard != null) { diff --git a/Mage/src/main/java/mage/cards/decks/importer/DekDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/DekDeckImporter.java index 2bb529ce896..af3c4841564 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/DekDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/DekDeckImporter.java @@ -21,7 +21,7 @@ public class DekDeckImporter extends PlainTextDeckImporter { Integer cardCount = Integer.parseInt(extractAttribute(line, "Quantity")); String cardName = extractAttribute(line, "Name"); boolean isSideboard = "true".equals(extractAttribute(line, "Sideboard")); - CardInfo cardInfo = CardRepository.instance.findPreferedCoreExpansionCard(cardName, true); + CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName, true); if (cardInfo == null) { sbMessage.append("Could not find card: '").append(cardName).append("' at line ").append(lineCount).append('\n'); } else { diff --git a/Mage/src/main/java/mage/cards/decks/importer/TxtDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/TxtDeckImporter.java index 002c7711a84..8b940b39ed3 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/TxtDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/TxtDeckImporter.java @@ -126,7 +126,7 @@ public class TxtDeckImporter extends PlainTextDeckImporter { wasCardLines = true; - CardInfo cardInfo = CardRepository.instance.findPreferedCoreExpansionCard(lineName, true); + CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(lineName, true); if (cardInfo == null) { sbMessage.append("Could not find card: '").append(lineName).append("' at line ").append(lineCount).append('\n'); } else { diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java index 1a83934c3bf..fcfc1ce01a4 100644 --- a/Mage/src/main/java/mage/cards/repository/CardRepository.java +++ b/Mage/src/main/java/mage/cards/repository/CardRepository.java @@ -381,26 +381,26 @@ public enum CardRepository { return null; } - public CardInfo findPreferedCoreExpansionCard(String name, boolean caseInsensitive) { - return findPreferedCoreExpansionCard(name, caseInsensitive, null); + public CardInfo findPreferredCoreExpansionCard(String name, boolean caseInsensitive) { + return findPreferredCoreExpansionCard(name, caseInsensitive, null); } - public CardInfo findPreferedCoreExpansionCard(String name, boolean caseInsensitive, String preferedSetCode) { + public CardInfo findPreferredCoreExpansionCard(String name, boolean caseInsensitive, String preferredSetCode) { List cards; if (caseInsensitive) { cards = findCardsCaseInsensitive(name); } else { cards = findCards(name); } - return findPreferedOrLatestCard(cards, preferedSetCode); + return findPreferredOrLatestCard(cards, preferredSetCode); } - public CardInfo findPreferedCoreExpansionCardByClassName(String canonicalClassName, String preferedSetCode) { + public CardInfo findPreferredCoreExpansionCardByClassName(String canonicalClassName, String preferredSetCode) { List cards = findCardsByClass(canonicalClassName); - return findPreferedOrLatestCard(cards, preferedSetCode); + return findPreferredOrLatestCard(cards, preferredSetCode); } - private CardInfo findPreferedOrLatestCard(List cards, String preferedSetCode) { + private CardInfo findPreferredOrLatestCard(List cards, String preferredSetCode) { if (!cards.isEmpty()) { Date lastReleaseDate = null; Date lastExpansionDate = null; @@ -409,7 +409,7 @@ public enum CardRepository { ExpansionInfo set = ExpansionRepository.instance.getSetByCode(cardinfo.getSetCode()); if (set != null) { - if ((preferedSetCode != null) && (preferedSetCode.equals(set.getCode()))) { + if ((preferredSetCode != null) && (preferredSetCode.equals(set.getCode()))) { return cardinfo; } @@ -443,7 +443,7 @@ public enum CardRepository { } } } - return findPreferedCoreExpansionCard(name, true); + return findPreferredCoreExpansionCard(name, true); } public List findCards(String name) { diff --git a/Mage/src/main/java/mage/game/draft/DraftCube.java b/Mage/src/main/java/mage/game/draft/DraftCube.java index 5c4e63cc99a..70b96132ec5 100644 --- a/Mage/src/main/java/mage/game/draft/DraftCube.java +++ b/Mage/src/main/java/mage/game/draft/DraftCube.java @@ -86,7 +86,7 @@ public abstract class DraftCube { if (!cardId.getExtension().isEmpty()) { cardInfo = CardRepository.instance.findCardWPreferredSet(cardId.getName(), cardId.getExtension(), false); } else { - cardInfo = CardRepository.instance.findPreferedCoreExpansionCard(cardId.getName(), false); + cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardId.getName(), false); } if (cardInfo != null) { From 79c85efe1b0ff9334a4353913f65d3caee3dae92 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 12:48:21 -0400 Subject: [PATCH 021/231] [MID] Implemented Ambitious Farmhand / Seasoned Cathar --- .../src/mage/cards/a/AmbitiousFarmhand.java | 63 +++++++++++++++++++ .../src/mage/cards/s/SeasonedCathar.java | 40 ++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 105 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/AmbitiousFarmhand.java create mode 100644 Mage.Sets/src/mage/cards/s/SeasonedCathar.java diff --git a/Mage.Sets/src/mage/cards/a/AmbitiousFarmhand.java b/Mage.Sets/src/mage/cards/a/AmbitiousFarmhand.java new file mode 100644 index 00000000000..9070dbea35b --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AmbitiousFarmhand.java @@ -0,0 +1,63 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.common.ActivateIfConditionActivatedAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.CovenCondition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; +import mage.abilities.hint.common.CovenHint; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.target.common.TargetCardInLibrary; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AmbitiousFarmhand extends CardImpl { + + private static final FilterCard filter = new FilterCard("basic Plains card"); + + static { + filter.add(SuperType.BASIC.getPredicate()); + filter.add(SubType.PLAINS.getPredicate()); + } + + public AmbitiousFarmhand(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.PEASANT); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + this.transformable = true; + this.secondSideCardClazz = mage.cards.s.SeasonedCathar.class; + + // When Ambitious Farmhand enters the battlefield, you may search your library for a basic Plains card, reveal it, put it into your hand, then shuffle. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new SearchLibraryPutInHandEffect(new TargetCardInLibrary(filter)), true + )); + + // Coven—{1}{W}{W}: Transform Ambitious Farmhand. Activate only if you control three or more creatures with different powers. + this.addAbility(new TransformAbility()); + this.addAbility(new ActivateIfConditionActivatedAbility( + Zone.BATTLEFIELD, new TransformSourceEffect(true), + new ManaCostsImpl<>("{1}{W}{W}"), CovenCondition.instance + ).setAbilityWord(AbilityWord.COVEN).addHint(CovenHint.instance)); + } + + private AmbitiousFarmhand(final AmbitiousFarmhand card) { + super(card); + } + + @Override + public AmbitiousFarmhand copy() { + return new AmbitiousFarmhand(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SeasonedCathar.java b/Mage.Sets/src/mage/cards/s/SeasonedCathar.java new file mode 100644 index 00000000000..85d9b7e5876 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SeasonedCathar.java @@ -0,0 +1,40 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SeasonedCathar extends CardImpl { + + public SeasonedCathar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.KNIGHT); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + this.color.setWhite(true); + this.transformable = true; + this.nightCard = true; + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + } + + private SeasonedCathar(final SeasonedCathar card) { + super(card); + } + + @Override + public SeasonedCathar copy() { + return new SeasonedCathar(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 62a8c59f90d..558863fcbc4 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -32,6 +32,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { this.numBoosterDoubleFaced = 1; cards.add(new SetCardInfo("Abandon the Post", 127, Rarity.COMMON, mage.cards.a.AbandonThePost.class)); + cards.add(new SetCardInfo("Ambitious Farmhand", 2, Rarity.UNCOMMON, mage.cards.a.AmbitiousFarmhand.class)); cards.add(new SetCardInfo("Angelfire Ignition", 209, Rarity.RARE, mage.cards.a.AngelfireIgnition.class)); cards.add(new SetCardInfo("Arcane Infusion", 210, Rarity.UNCOMMON, mage.cards.a.ArcaneInfusion.class)); cards.add(new SetCardInfo("Archive Haunt", 68, Rarity.UNCOMMON, mage.cards.a.ArchiveHaunt.class)); @@ -199,6 +200,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Sacred Fire", 239, Rarity.UNCOMMON, mage.cards.s.SacredFire.class)); cards.add(new SetCardInfo("Saryth, the Viper's Fang", 197, Rarity.RARE, mage.cards.s.SarythTheVipersFang.class)); cards.add(new SetCardInfo("Seafaring Werewolf", 80, Rarity.RARE, mage.cards.s.SeafaringWerewolf.class)); + cards.add(new SetCardInfo("Seasoned Cathar", 2, Rarity.UNCOMMON, mage.cards.s.SeasonedCathar.class)); cards.add(new SetCardInfo("Secrets of the Key", 73, Rarity.COMMON, mage.cards.s.SecretsOfTheKey.class)); cards.add(new SetCardInfo("Shadowbeast Sighting", 198, Rarity.COMMON, mage.cards.s.ShadowbeastSighting.class)); cards.add(new SetCardInfo("Shady Traveler", 120, Rarity.COMMON, mage.cards.s.ShadyTraveler.class)); From cc2a72fb69267e90426d417a0ab7f2afef858e08 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 12:54:46 -0400 Subject: [PATCH 022/231] [MID] Implemented Eccentric Farmer --- .../src/mage/cards/e/EccentricFarmer.java | 81 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 82 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/e/EccentricFarmer.java diff --git a/Mage.Sets/src/mage/cards/e/EccentricFarmer.java b/Mage.Sets/src/mage/cards/e/EccentricFarmer.java new file mode 100644 index 00000000000..018ce65be2b --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EccentricFarmer.java @@ -0,0 +1,81 @@ +package mage.cards.e; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EccentricFarmer extends CardImpl { + + public EccentricFarmer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.PEASANT); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // When Eccentric Farmer enters the battlefield, mill three cards, then you may return a land card from your graveyard to your hand. + this.addAbility(new EntersBattlefieldTriggeredAbility(new EccentricFarmerEffect())); + } + + private EccentricFarmer(final EccentricFarmer card) { + super(card); + } + + @Override + public EccentricFarmer copy() { + return new EccentricFarmer(this); + } +} + +class EccentricFarmerEffect extends OneShotEffect { + + EccentricFarmerEffect() { + super(Outcome.Benefit); + staticText = "mill three cards, then you may return a land card from your graveyard to your hand"; + } + + private EccentricFarmerEffect(final EccentricFarmerEffect effect) { + super(effect); + } + + @Override + public EccentricFarmerEffect copy() { + return new EccentricFarmerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + player.millCards(3, source, game); + if (player.getGraveyard().count(StaticFilters.FILTER_CARD_LAND, game) < 1) { + return true; + } + TargetCard target = new TargetCardInYourGraveyard( + 0, 1, StaticFilters.FILTER_CARD_LAND, true + ); + player.choose(outcome, target, source.getSourceId(), game); + player.moveCards(game.getCard(target.getFirstTarget()), Zone.HAND, source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 558863fcbc4..46729c5431d 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -101,6 +101,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Duelcraft Trainer", 16, Rarity.UNCOMMON, mage.cards.d.DuelcraftTrainer.class)); cards.add(new SetCardInfo("Duress", 98, Rarity.COMMON, mage.cards.d.Duress.class)); cards.add(new SetCardInfo("Eaten Alive", 99, Rarity.COMMON, mage.cards.e.EatenAlive.class)); + cards.add(new SetCardInfo("Eccentric Farmer", 185, Rarity.COMMON, mage.cards.e.EccentricFarmer.class)); cards.add(new SetCardInfo("Electric Revelation", 135, Rarity.COMMON, mage.cards.e.ElectricRevelation.class)); cards.add(new SetCardInfo("Embodiment of Flame", 141, Rarity.UNCOMMON, mage.cards.e.EmbodimentOfFlame.class)); cards.add(new SetCardInfo("Evolving Wilds", 261, Rarity.COMMON, mage.cards.e.EvolvingWilds.class)); From 68b7e7ed4d049092f55523cbca6e81a2d6b58b03 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 12:58:59 -0400 Subject: [PATCH 023/231] [MID] Implemented Ecstatic Awakener / Awoken Demon --- Mage.Sets/src/mage/cards/a/AwokenDemon.java | 34 ++++++++++++ .../src/mage/cards/e/EcstaticAwakener.java | 54 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 90 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/AwokenDemon.java create mode 100644 Mage.Sets/src/mage/cards/e/EcstaticAwakener.java diff --git a/Mage.Sets/src/mage/cards/a/AwokenDemon.java b/Mage.Sets/src/mage/cards/a/AwokenDemon.java new file mode 100644 index 00000000000..98d64bbed1f --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AwokenDemon.java @@ -0,0 +1,34 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AwokenDemon extends CardImpl { + + public AwokenDemon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.DEMON); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + this.transformable = true; + this.nightCard = true; + } + + private AwokenDemon(final AwokenDemon card) { + super(card); + } + + @Override + public AwokenDemon copy() { + return new AwokenDemon(this); + } +} diff --git a/Mage.Sets/src/mage/cards/e/EcstaticAwakener.java b/Mage.Sets/src/mage/cards/e/EcstaticAwakener.java new file mode 100644 index 00000000000..50f4541680b --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EcstaticAwakener.java @@ -0,0 +1,54 @@ +package mage.cards.e; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.LimitedTimesPerTurnActivatedAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.target.common.TargetControlledPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EcstaticAwakener extends CardImpl { + + public EcstaticAwakener(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + this.transformable = true; + this.secondSideCardClazz = mage.cards.a.AwokenDemon.class; + + // {2}{B}, Sacrifice another creature: Draw a card, then transform Ecstatic Awakener. Activate only once each turn. + this.addAbility(new TransformAbility()); + Ability ability = new LimitedTimesPerTurnActivatedAbility( + Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), new ManaCostsImpl<>("{2}{B}") + ); + ability.addEffect(new TransformSourceEffect(true).concatBy(", then")); + ability.addCost(new SacrificeTargetCost(new TargetControlledPermanent(StaticFilters.FILTER_CONTROLLED_ANOTHER_CREATURE))); + this.addAbility(ability); + } + + private EcstaticAwakener(final EcstaticAwakener card) { + super(card); + } + + @Override + public EcstaticAwakener copy() { + return new EcstaticAwakener(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 46729c5431d..30a33cb5c65 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -39,6 +39,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Ardent Elementalist", 128, Rarity.COMMON, mage.cards.a.ArdentElementalist.class)); cards.add(new SetCardInfo("Arrogant Outlaw", 84, Rarity.COMMON, mage.cards.a.ArrogantOutlaw.class)); cards.add(new SetCardInfo("Augur of Autumn", 168, Rarity.RARE, mage.cards.a.AugurOfAutumn.class)); + cards.add(new SetCardInfo("Awoken Demon", 100, Rarity.COMMON, mage.cards.a.AwokenDemon.class)); cards.add(new SetCardInfo("Baithook Angler", 42, Rarity.COMMON, mage.cards.b.BaithookAngler.class)); cards.add(new SetCardInfo("Bat Whisperer", 86, Rarity.COMMON, mage.cards.b.BatWhisperer.class)); cards.add(new SetCardInfo("Benevolent Geist", 61, Rarity.RARE, mage.cards.b.BenevolentGeist.class)); @@ -102,6 +103,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Duress", 98, Rarity.COMMON, mage.cards.d.Duress.class)); cards.add(new SetCardInfo("Eaten Alive", 99, Rarity.COMMON, mage.cards.e.EatenAlive.class)); cards.add(new SetCardInfo("Eccentric Farmer", 185, Rarity.COMMON, mage.cards.e.EccentricFarmer.class)); + cards.add(new SetCardInfo("Ecstatic Awakener", 100, Rarity.COMMON, mage.cards.e.EcstaticAwakener.class)); cards.add(new SetCardInfo("Electric Revelation", 135, Rarity.COMMON, mage.cards.e.ElectricRevelation.class)); cards.add(new SetCardInfo("Embodiment of Flame", 141, Rarity.UNCOMMON, mage.cards.e.EmbodimentOfFlame.class)); cards.add(new SetCardInfo("Evolving Wilds", 261, Rarity.COMMON, mage.cards.e.EvolvingWilds.class)); From 2a1762e4207bd25bb175cc26c8ed946aa43b6091 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 13:04:14 -0400 Subject: [PATCH 024/231] [MID] Implemented Mourning Patrol / Morning Apparition --- Mage.Sets/src/mage/cards/a/AwokenDemon.java | 1 + .../src/mage/cards/m/MorningApparition.java | 49 +++++++++++++++++++ .../src/mage/cards/m/MourningPatrol.java | 44 +++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 4 files changed, 96 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MorningApparition.java create mode 100644 Mage.Sets/src/mage/cards/m/MourningPatrol.java diff --git a/Mage.Sets/src/mage/cards/a/AwokenDemon.java b/Mage.Sets/src/mage/cards/a/AwokenDemon.java index 98d64bbed1f..d7b9e1c6edc 100644 --- a/Mage.Sets/src/mage/cards/a/AwokenDemon.java +++ b/Mage.Sets/src/mage/cards/a/AwokenDemon.java @@ -19,6 +19,7 @@ public final class AwokenDemon extends CardImpl { this.subtype.add(SubType.DEMON); this.power = new MageInt(4); this.toughness = new MageInt(4); + this.color.setBlack(true); this.transformable = true; this.nightCard = true; } diff --git a/Mage.Sets/src/mage/cards/m/MorningApparition.java b/Mage.Sets/src/mage/cards/m/MorningApparition.java new file mode 100644 index 00000000000..2ab7a6c2582 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MorningApparition.java @@ -0,0 +1,49 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.common.PutIntoGraveFromAnywhereSourceAbility; +import mage.abilities.effects.common.ExileSourceEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MorningApparition extends CardImpl { + + public MorningApparition(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + this.color.setWhite(true); + this.transformable = true; + this.nightCard = true; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // If Morning Apparition would be put into a graveyard from anywhere, exile it instead. + this.addAbility(new PutIntoGraveFromAnywhereSourceAbility(new ExileSourceEffect().setText("exile it instead"))); + } + + private MorningApparition(final MorningApparition card) { + super(card); + } + + @Override + public MorningApparition copy() { + return new MorningApparition(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MourningPatrol.java b/Mage.Sets/src/mage/cards/m/MourningPatrol.java new file mode 100644 index 00000000000..70448be2db0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MourningPatrol.java @@ -0,0 +1,44 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MourningPatrol extends CardImpl { + + public MourningPatrol(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + this.transformable = true; + this.secondSideCardClazz = mage.cards.m.MorningApparition.class; + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Disturb {3}{W} + this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{3}{W}"))); + } + + private MourningPatrol(final MourningPatrol card) { + super(card); + } + + @Override + public MourningPatrol copy() { + return new MourningPatrol(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 30a33cb5c65..725901df5ac 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -170,8 +170,10 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Moonsilver Key", 255, Rarity.UNCOMMON, mage.cards.m.MoonsilverKey.class)); cards.add(new SetCardInfo("Morbid Opportunist", 113, Rarity.UNCOMMON, mage.cards.m.MorbidOpportunist.class)); cards.add(new SetCardInfo("Morkrut Behemoth", 114, Rarity.COMMON, mage.cards.m.MorkrutBehemoth.class)); + cards.add(new SetCardInfo("Morning Apparition", 28, Rarity.COMMON, mage.cards.m.MorningApparition.class)); cards.add(new SetCardInfo("Mountain", 274, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Mounted Dreadknight", 150, Rarity.COMMON, mage.cards.m.MountedDreadknight.class)); + cards.add(new SetCardInfo("Mourning Patrol", 28, Rarity.COMMON, mage.cards.m.MourningPatrol.class)); cards.add(new SetCardInfo("Mysterious Tome", 63, Rarity.UNCOMMON, mage.cards.m.MysteriousTome.class)); cards.add(new SetCardInfo("Mystic Monstrosity", 256, Rarity.UNCOMMON, mage.cards.m.MysticMonstrosity.class)); cards.add(new SetCardInfo("Mystic Skull", 256, Rarity.UNCOMMON, mage.cards.m.MysticSkull.class)); From 8a310c2ae26d4a6a9fcbfb85fd38782033c3cc91 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 13:09:28 -0400 Subject: [PATCH 025/231] [MID] Implemented Outland Liberator / Frenzied Trapbreaker --- .../src/mage/cards/f/FrenziedTrapbreaker.java | 66 +++++++++++++++++++ .../src/mage/cards/o/OutlandLiberator.java | 51 ++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 119 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FrenziedTrapbreaker.java create mode 100644 Mage.Sets/src/mage/cards/o/OutlandLiberator.java diff --git a/Mage.Sets/src/mage/cards/f/FrenziedTrapbreaker.java b/Mage.Sets/src/mage/cards/f/FrenziedTrapbreaker.java new file mode 100644 index 00000000000..9dbe001be71 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FrenziedTrapbreaker.java @@ -0,0 +1,66 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.keyword.NightboundAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterArtifactOrEnchantmentPermanent; +import mage.filter.predicate.permanent.DefendingPlayerControlsPredicate; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FrenziedTrapbreaker extends CardImpl { + + private static final FilterPermanent filter + = new FilterArtifactOrEnchantmentPermanent("artifact or enchantment defending player controls"); + + static { + filter.add(DefendingPlayerControlsPredicate.instance); + } + + public FrenziedTrapbreaker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.WEREWOLF); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + this.color.setGreen(true); + this.transformable = true; + this.nightCard = true; + + // {1}, Sacrifice Frenzied Trapbreaker: Destroy target artifact or enchantment. + Ability ability = new SimpleActivatedAbility(new DestroyTargetEffect(), new GenericManaCost(1)); + ability.addCost(new SacrificeSourceCost()); + this.addAbility(ability); + + // Whenever Frenzied Trapbreaker attacks, destroy target artifact or enchantment defending player controls. + ability = new AttacksTriggeredAbility(new DestroyTargetEffect()); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + + // Nightbound + this.addAbility(NightboundAbility.getInstance()); + } + + private FrenziedTrapbreaker(final FrenziedTrapbreaker card) { + super(card); + } + + @Override + public FrenziedTrapbreaker copy() { + return new FrenziedTrapbreaker(this); + } +} diff --git a/Mage.Sets/src/mage/cards/o/OutlandLiberator.java b/Mage.Sets/src/mage/cards/o/OutlandLiberator.java new file mode 100644 index 00000000000..b4a2e9affc0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OutlandLiberator.java @@ -0,0 +1,51 @@ +package mage.cards.o; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.keyword.DayboundAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class OutlandLiberator extends CardImpl { + + public OutlandLiberator(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WEREWOLF); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + this.transformable = true; + this.secondSideCardClazz = mage.cards.f.FrenziedTrapbreaker.class; + + // {1}, Sacrifice Outland Liberator: Destroy target artifact or enchantment. + Ability ability = new SimpleActivatedAbility(new DestroyTargetEffect(), new GenericManaCost(1)); + ability.addCost(new SacrificeSourceCost()); + this.addAbility(ability); + + // Daybound + this.addAbility(new TransformAbility()); + this.addAbility(DayboundAbility.getInstance()); + } + + private OutlandLiberator(final OutlandLiberator card) { + super(card); + } + + @Override + public OutlandLiberator copy() { + return new OutlandLiberator(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 725901df5ac..c60cb980979 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -125,6 +125,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Flip the Switch", 54, Rarity.COMMON, mage.cards.f.FlipTheSwitch.class)); cards.add(new SetCardInfo("Forest", 276, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Foul Play", 101, Rarity.UNCOMMON, mage.cards.f.FoulPlay.class)); + cards.add(new SetCardInfo("Frenzied Trapbreaker", 190, Rarity.UNCOMMON, mage.cards.f.FrenziedTrapbreaker.class)); cards.add(new SetCardInfo("Galedrifter", 55, Rarity.COMMON, mage.cards.g.Galedrifter.class)); cards.add(new SetCardInfo("Galvanic Iteration", 224, Rarity.RARE, mage.cards.g.GalvanicIteration.class)); cards.add(new SetCardInfo("Gavony Dawnguard", 20, Rarity.UNCOMMON, mage.cards.g.GavonyDawnguard.class)); @@ -182,6 +183,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Obsessive Astronomer", 152, Rarity.UNCOMMON, mage.cards.o.ObsessiveAstronomer.class)); cards.add(new SetCardInfo("Odric's Outrider", 29, Rarity.UNCOMMON, mage.cards.o.OdricsOutrider.class)); cards.add(new SetCardInfo("Organ Hoarder", 66, Rarity.COMMON, mage.cards.o.OrganHoarder.class)); + cards.add(new SetCardInfo("Outland Liberator", 190, Rarity.UNCOMMON, mage.cards.o.OutlandLiberator.class)); cards.add(new SetCardInfo("Overgrown Farmland", 265, Rarity.RARE, mage.cards.o.OvergrownFarmland.class)); cards.add(new SetCardInfo("Overwhelmed Archivist", 68, Rarity.UNCOMMON, mage.cards.o.OverwhelmedArchivist.class)); cards.add(new SetCardInfo("Patrician Geist", 69, Rarity.RARE, mage.cards.p.PatricianGeist.class)); From 0be8b6b4c0a0cf893e1d42b96b10fd42a409c958 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 13:23:09 -0400 Subject: [PATCH 026/231] [MID] Implemented Novice Occultist --- .../src/mage/cards/n/NoviceOccultist.java | 44 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 45 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/n/NoviceOccultist.java diff --git a/Mage.Sets/src/mage/cards/n/NoviceOccultist.java b/Mage.Sets/src/mage/cards/n/NoviceOccultist.java new file mode 100644 index 00000000000..67ee4ae8510 --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NoviceOccultist.java @@ -0,0 +1,44 @@ +package mage.cards.n; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NoviceOccultist extends CardImpl { + + public NoviceOccultist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(1); + this.toughness = new MageInt(2); + + // When Novice Occultist dies, you draw a card and you lose 1 life. + Ability ability = new DiesSourceTriggeredAbility( + new DrawCardSourceControllerEffect(1).setText("you draw a card") + ); + ability.addEffect(new LoseLifeSourceControllerEffect(1).concatBy("and")); + this.addAbility(ability); + } + + private NoviceOccultist(final NoviceOccultist card) { + super(card); + } + + @Override + public NoviceOccultist copy() { + return new NoviceOccultist(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index c60cb980979..0621bf9adc9 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -180,6 +180,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Mystic Skull", 256, Rarity.UNCOMMON, mage.cards.m.MysticSkull.class)); cards.add(new SetCardInfo("Nebelgast Intruder", 64, Rarity.UNCOMMON, mage.cards.n.NebelgastIntruder.class)); cards.add(new SetCardInfo("No Way Out", 116, Rarity.COMMON, mage.cards.n.NoWayOut.class)); + cards.add(new SetCardInfo("Novice Occultist", 117, Rarity.COMMON, mage.cards.n.NoviceOccultist.class)); cards.add(new SetCardInfo("Obsessive Astronomer", 152, Rarity.UNCOMMON, mage.cards.o.ObsessiveAstronomer.class)); cards.add(new SetCardInfo("Odric's Outrider", 29, Rarity.UNCOMMON, mage.cards.o.OdricsOutrider.class)); cards.add(new SetCardInfo("Organ Hoarder", 66, Rarity.COMMON, mage.cards.o.OrganHoarder.class)); From 531cc11a832b372414bdb001286da27a9ecf772c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 13:33:13 -0400 Subject: [PATCH 027/231] [MID] Implemented Neonate's Rush --- Mage.Sets/src/mage/cards/n/NeonatesRush.java | 87 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 88 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/n/NeonatesRush.java diff --git a/Mage.Sets/src/mage/cards/n/NeonatesRush.java b/Mage.Sets/src/mage/cards/n/NeonatesRush.java new file mode 100644 index 00000000000..578aa28814e --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NeonatesRush.java @@ -0,0 +1,87 @@ +package mage.cards.n; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NeonatesRush extends CardImpl { + + private static final Condition condition = new PermanentsOnTheBattlefieldCondition( + new FilterPermanent(SubType.VAMPIRE, "you control a Vampire") + ); + private static final Hint hint = new ConditionHint(condition, "You control a Vampire"); + + public NeonatesRush(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{R}"); + + // This spell costs {1} less to cast if you control a Vampire. + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new SpellCostReductionSourceEffect(1, condition) + ).addHint(hint).setRuleAtTheTop(true)); + + // Neonate's Rush deals 1 damage to target creature and 1 damage to its controller. Draw a card. + this.getSpellAbility().addEffect(new NeonatesRushEffect()); + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); + } + + private NeonatesRush(final NeonatesRush card) { + super(card); + } + + @Override + public NeonatesRush copy() { + return new NeonatesRush(this); + } +} + +class NeonatesRushEffect extends OneShotEffect { + + NeonatesRushEffect() { + super(Outcome.Benefit); + staticText = "{this} deals 1 damage to target creature and 1 damage to its controller."; + } + + private NeonatesRushEffect(final NeonatesRushEffect effect) { + super(effect); + } + + @Override + public NeonatesRushEffect copy() { + return new NeonatesRushEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent == null) { + return false; + } + permanent.damage(1, source.getSourceId(), source, game); + Player player = game.getPlayer(permanent.getControllerId()); + if (player != null) { + player.damage(1, source.getSourceId(), source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 0621bf9adc9..95a0c638cba 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -179,6 +179,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Mystic Monstrosity", 256, Rarity.UNCOMMON, mage.cards.m.MysticMonstrosity.class)); cards.add(new SetCardInfo("Mystic Skull", 256, Rarity.UNCOMMON, mage.cards.m.MysticSkull.class)); cards.add(new SetCardInfo("Nebelgast Intruder", 64, Rarity.UNCOMMON, mage.cards.n.NebelgastIntruder.class)); + cards.add(new SetCardInfo("Neonate's Rush", 151, Rarity.COMMON, mage.cards.n.NeonatesRush.class)); cards.add(new SetCardInfo("No Way Out", 116, Rarity.COMMON, mage.cards.n.NoWayOut.class)); cards.add(new SetCardInfo("Novice Occultist", 117, Rarity.COMMON, mage.cards.n.NoviceOccultist.class)); cards.add(new SetCardInfo("Obsessive Astronomer", 152, Rarity.UNCOMMON, mage.cards.o.ObsessiveAstronomer.class)); From 9e68649c3250bbd02afb00a2876d81b4f1c5192d Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 13:53:25 -0400 Subject: [PATCH 028/231] [MID] Implemented Siege Zombie --- Mage.Sets/src/mage/cards/s/SiegeZombie.java | 45 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../main/java/mage/filter/StaticFilters.java | 8 ++++ 3 files changed, 54 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SiegeZombie.java diff --git a/Mage.Sets/src/mage/cards/s/SiegeZombie.java b/Mage.Sets/src/mage/cards/s/SiegeZombie.java new file mode 100644 index 00000000000..44df52957a4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SiegeZombie.java @@ -0,0 +1,45 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapTargetCost; +import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.target.common.TargetControlledPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SiegeZombie extends CardImpl { + + public SiegeZombie(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.subtype.add(SubType.ZOMBIE); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Tap three untapped creatures you control: Each opponent loses 1 life. + this.addAbility(new SimpleActivatedAbility( + new LoseLifeOpponentsEffect(1), + new TapTargetCost(new TargetControlledPermanent( + 3, StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURES + )) + )); + } + + private SiegeZombie(final SiegeZombie card) { + super(card); + } + + @Override + public SiegeZombie copy() { + return new SiegeZombie(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 95a0c638cba..b6a53ee37e4 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -214,6 +214,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Shadowbeast Sighting", 198, Rarity.COMMON, mage.cards.s.ShadowbeastSighting.class)); cards.add(new SetCardInfo("Shady Traveler", 120, Rarity.COMMON, mage.cards.s.ShadyTraveler.class)); cards.add(new SetCardInfo("Shipwreck Marsh", 267, Rarity.RARE, mage.cards.s.ShipwreckMarsh.class)); + cards.add(new SetCardInfo("Siege Zombie", 121, Rarity.COMMON, mage.cards.s.SiegeZombie.class)); cards.add(new SetCardInfo("Sigarda, Champion of Light", 240, Rarity.MYTHIC, mage.cards.s.SigardaChampionOfLight.class)); cards.add(new SetCardInfo("Slaughter Specialist", 122, Rarity.RARE, mage.cards.s.SlaughterSpecialist.class)); cards.add(new SetCardInfo("Snarling Wolf", 199, Rarity.COMMON, mage.cards.s.SnarlingWolf.class)); diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index 26dd1191cef..2ee42a1a61a 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -10,6 +10,7 @@ import mage.filter.predicate.mageobject.AnotherPredicate; import mage.filter.predicate.mageobject.KickedSpellPredicate; import mage.filter.predicate.mageobject.MulticoloredPredicate; import mage.filter.predicate.permanent.AttackingPredicate; +import mage.filter.predicate.permanent.TappedPredicate; import mage.filter.predicate.permanent.TokenPredicate; /** @@ -440,6 +441,13 @@ public final class StaticFilters { FILTER_CONTROLLED_ANOTHER_CREATURE.setLockedFilter(true); } + public static final FilterControlledCreaturePermanent FILTER_CONTROLLED_UNTAPPED_CREATURES = new FilterControlledCreaturePermanent("untapped creature you control"); + + static { + FILTER_CONTROLLED_UNTAPPED_CREATURES.add(TappedPredicate.UNTAPPED); + FILTER_CONTROLLED_UNTAPPED_CREATURES.setLockedFilter(true); + } + public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_NON_LAND = new FilterControlledPermanent("nonland permanent"); static { From 0d9376ac7cd664ae31e9c23500fe385cd218e266 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 13:55:57 -0400 Subject: [PATCH 029/231] [MID] Implemented Skaab Wrangler --- Mage.Sets/src/mage/cards/s/SkaabWrangler.java | 50 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 51 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SkaabWrangler.java diff --git a/Mage.Sets/src/mage/cards/s/SkaabWrangler.java b/Mage.Sets/src/mage/cards/s/SkaabWrangler.java new file mode 100644 index 00000000000..51523ee24d2 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SkaabWrangler.java @@ -0,0 +1,50 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapTargetCost; +import mage.abilities.effects.common.TapTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.target.common.TargetControlledPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SkaabWrangler extends CardImpl { + + public SkaabWrangler(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Tap three untapped creatures you control: Tap target creature. + Ability ability = new SimpleActivatedAbility( + new TapTargetEffect(), + new TapTargetCost(new TargetControlledPermanent( + 3, StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURES + )) + ); + ability.addTarget(new TargetCreaturePermanent()); + this.addAbility(ability); + } + + private SkaabWrangler(final SkaabWrangler card) { + super(card); + } + + @Override + public SkaabWrangler copy() { + return new SkaabWrangler(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index b6a53ee37e4..5af3b9b5157 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -216,6 +216,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Shipwreck Marsh", 267, Rarity.RARE, mage.cards.s.ShipwreckMarsh.class)); cards.add(new SetCardInfo("Siege Zombie", 121, Rarity.COMMON, mage.cards.s.SiegeZombie.class)); cards.add(new SetCardInfo("Sigarda, Champion of Light", 240, Rarity.MYTHIC, mage.cards.s.SigardaChampionOfLight.class)); + cards.add(new SetCardInfo("Skaab Wrangler", 75, Rarity.UNCOMMON, mage.cards.s.SkaabWrangler.class)); cards.add(new SetCardInfo("Slaughter Specialist", 122, Rarity.RARE, mage.cards.s.SlaughterSpecialist.class)); cards.add(new SetCardInfo("Snarling Wolf", 199, Rarity.COMMON, mage.cards.s.SnarlingWolf.class)); cards.add(new SetCardInfo("Soul-Guide Gryff", 35, Rarity.COMMON, mage.cards.s.SoulGuideGryff.class)); From c7c57289981be0356f240ceb2f1f195df3be7543 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 13:59:25 -0400 Subject: [PATCH 030/231] [MID] Implemented Larder Zombie --- Mage.Sets/src/mage/cards/l/LarderZombie.java | 86 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 87 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LarderZombie.java diff --git a/Mage.Sets/src/mage/cards/l/LarderZombie.java b/Mage.Sets/src/mage/cards/l/LarderZombie.java new file mode 100644 index 00000000000..4f591aed0cf --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LarderZombie.java @@ -0,0 +1,86 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapTargetCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.DefenderAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetControlledPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LarderZombie extends CardImpl { + + public LarderZombie(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}"); + + this.subtype.add(SubType.ZOMBIE); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + + // Defender + this.addAbility(DefenderAbility.getInstance()); + + // Tap three untapped creatures you control: Look at the top card of your library. You may put it into your graveyard. + this.addAbility(new SimpleActivatedAbility( + new LarderZombieEffect(), + new TapTargetCost(new TargetControlledPermanent( + 3, StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURES + )) + )); + } + + private LarderZombie(final LarderZombie card) { + super(card); + } + + @Override + public LarderZombie copy() { + return new LarderZombie(this); + } +} + +class LarderZombieEffect extends OneShotEffect { + + LarderZombieEffect() { + super(Outcome.Benefit); + staticText = "look at the top card of your library. You may put it into your graveyard"; + } + + private LarderZombieEffect(final LarderZombieEffect effect) { + super(effect); + } + + @Override + public LarderZombieEffect copy() { + return new LarderZombieEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Card card = player.getLibrary().getFromTop(game); + player.lookAtCards("Top card of your library", card, game); + if (player.chooseUse(Outcome.AIDontUseIt, "Put the top card of your library into your graveyard?", source, game)) { + player.moveCards(card, Zone.GRAVEYARD, source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 5af3b9b5157..649104d58a3 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -161,6 +161,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Join the Dance", 229, Rarity.UNCOMMON, mage.cards.j.JoinTheDance.class)); cards.add(new SetCardInfo("Kessig Naturalist", 231, Rarity.UNCOMMON, mage.cards.k.KessigNaturalist.class)); cards.add(new SetCardInfo("Lambholt Harrier", 145, Rarity.COMMON, mage.cards.l.LambholtHarrier.class)); + cards.add(new SetCardInfo("Larder Zombie", 58, Rarity.COMMON, mage.cards.l.LarderZombie.class)); cards.add(new SetCardInfo("Lier, Disciple of the Drowned", 59, Rarity.MYTHIC, mage.cards.l.LierDiscipleOfTheDrowned.class)); cards.add(new SetCardInfo("Lord of the Ulvenwald", 231, Rarity.UNCOMMON, mage.cards.l.LordOfTheUlvenwald.class)); cards.add(new SetCardInfo("Loyal Gryff", 26, Rarity.UNCOMMON, mage.cards.l.LoyalGryff.class)); From 0740863fe8f08848fe0c21be43ac4f1818a30cf6 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 14:27:52 -0400 Subject: [PATCH 031/231] [MID] Implemented Pack's Betrayal --- Mage.Sets/src/mage/cards/p/PacksBetrayal.java | 66 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 67 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/p/PacksBetrayal.java diff --git a/Mage.Sets/src/mage/cards/p/PacksBetrayal.java b/Mage.Sets/src/mage/cards/p/PacksBetrayal.java new file mode 100644 index 00000000000..f71fa6184db --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PacksBetrayal.java @@ -0,0 +1,66 @@ +package mage.cards.p; + +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.UntapTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.common.continuous.GainControlTargetEffect; +import mage.abilities.effects.keyword.ScryEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class PacksBetrayal extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent(); + + static { + filter.add(Predicates.or( + SubType.WOLF.getPredicate(), + SubType.WEREWOLF.getPredicate() + )); + } + + private static final Condition condition = new PermanentsOnTheBattlefieldCondition(filter); + private static final Hint hint = new ConditionHint(condition, "You control a Wolf or Werewolf"); + + public PacksBetrayal(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{R}"); + + // Gain control of target creature until end of turn. Untap that creature. It gains haste until end of turn. If you control a Wolf or Werewolf, scry 2. + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.getSpellAbility().addEffect(new GainControlTargetEffect(Duration.EndOfTurn)); + this.getSpellAbility().addEffect(new UntapTargetEffect().setText("Untap that creature")); + this.getSpellAbility().addEffect(new GainAbilityTargetEffect( + HasteAbility.getInstance(), Duration.EndOfTurn + ).setText("It gains haste until end of turn.")); + this.getSpellAbility().addEffect(new ConditionalOneShotEffect( + new ScryEffect(2), condition, + "If you control a Wolf or Werewolf, scry 2" + )); + this.getSpellAbility().addHint(hint); + } + + private PacksBetrayal(final PacksBetrayal card) { + super(card); + } + + @Override + public PacksBetrayal copy() { + return new PacksBetrayal(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 649104d58a3..91306062a43 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -189,6 +189,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Outland Liberator", 190, Rarity.UNCOMMON, mage.cards.o.OutlandLiberator.class)); cards.add(new SetCardInfo("Overgrown Farmland", 265, Rarity.RARE, mage.cards.o.OvergrownFarmland.class)); cards.add(new SetCardInfo("Overwhelmed Archivist", 68, Rarity.UNCOMMON, mage.cards.o.OverwhelmedArchivist.class)); + cards.add(new SetCardInfo("Pack's Betrayal", 153, Rarity.COMMON, mage.cards.p.PacksBetrayal.class)); cards.add(new SetCardInfo("Patrician Geist", 69, Rarity.RARE, mage.cards.p.PatricianGeist.class)); cards.add(new SetCardInfo("Pestilent Wolf", 192, Rarity.COMMON, mage.cards.p.PestilentWolf.class)); cards.add(new SetCardInfo("Phantom Carriage", 70, Rarity.UNCOMMON, mage.cards.p.PhantomCarriage.class)); From 8642f8c460331ad6a33d463a239b6fc3145e5de8 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 14:31:43 -0400 Subject: [PATCH 032/231] [MID] Implemented Purifying Dragon --- .../src/mage/cards/p/PurifyingDragon.java | 88 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 89 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/p/PurifyingDragon.java diff --git a/Mage.Sets/src/mage/cards/p/PurifyingDragon.java b/Mage.Sets/src/mage/cards/p/PurifyingDragon.java new file mode 100644 index 00000000000..036cb0aa637 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PurifyingDragon.java @@ -0,0 +1,88 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.DefendingPlayerControlsPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class PurifyingDragon extends CardImpl { + + private static final FilterPermanent filter + = new FilterCreaturePermanent("creature defending player controls"); + + static { + filter.add(DefendingPlayerControlsPredicate.instance); + } + + public PurifyingDragon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}{R}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever Purifying Dragon attacks, it deals 1 damage to target creature defending player controls. If that creature is a Zombie, Purifying Dragon deals 2 damage to that creature instead. + Ability ability = new AttacksTriggeredAbility(new PurifyingDragonEffect()); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private PurifyingDragon(final PurifyingDragon card) { + super(card); + } + + @Override + public PurifyingDragon copy() { + return new PurifyingDragon(this); + } +} + +class PurifyingDragonEffect extends OneShotEffect { + + PurifyingDragonEffect() { + super(Outcome.Benefit); + staticText = "it deals 1 damage to target creature defending player controls. " + + "If that creature is a Zombie, {this} deals 2 damage to that creature instead"; + } + + private PurifyingDragonEffect(final PurifyingDragonEffect effect) { + super(effect); + } + + @Override + public PurifyingDragonEffect copy() { + return new PurifyingDragonEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent == null) { + return false; + } + return permanent.damage( + permanent.hasSubtype(SubType.ZOMBIE, game) ? 2 : 1, + source.getSourceId(), source, game + ) > 0; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 91306062a43..863bc1ac3b5 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -200,6 +200,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Poppet Factory", 71, Rarity.MYTHIC, mage.cards.p.PoppetFactory.class)); cards.add(new SetCardInfo("Poppet Stitcher", 71, Rarity.MYTHIC, mage.cards.p.PoppetStitcher.class)); cards.add(new SetCardInfo("Primal Adversary", 194, Rarity.MYTHIC, mage.cards.p.PrimalAdversary.class)); + cards.add(new SetCardInfo("Purifying Dragon", 155, Rarity.UNCOMMON, mage.cards.p.PurifyingDragon.class)); cards.add(new SetCardInfo("Raze the Effigy", 156, Rarity.COMMON, mage.cards.r.RazeTheEffigy.class)); cards.add(new SetCardInfo("Reckless Stormseeker", 157, Rarity.RARE, mage.cards.r.RecklessStormseeker.class)); cards.add(new SetCardInfo("Return to Nature", 195, Rarity.COMMON, mage.cards.r.ReturnToNature.class)); From 87e2efd0332ce351d09a708f32c95f6a7bdc75e5 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Sat, 11 Sep 2021 15:05:00 -0500 Subject: [PATCH 033/231] [MID] Implemented Adeline, Resplendent Cathar --- .../cards/a/AdelineResplendentCathar.java | 79 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 80 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/AdelineResplendentCathar.java diff --git a/Mage.Sets/src/mage/cards/a/AdelineResplendentCathar.java b/Mage.Sets/src/mage/cards/a/AdelineResplendentCathar.java new file mode 100644 index 00000000000..7af6c796092 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AdelineResplendentCathar.java @@ -0,0 +1,79 @@ +package mage.cards.a; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksWithCreaturesTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.CreaturesYouControlCount; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.SetPowerSourceEffect; +import mage.abilities.hint.common.CreaturesYouControlHint; +import mage.constants.*; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.game.Game; +import mage.game.permanent.token.HumanToken; + +/** + * + * @author weirddan455 + */ +public final class AdelineResplendentCathar extends CardImpl { + + public AdelineResplendentCathar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}{W}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.KNIGHT); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Adeline, Resplendent Cathar's power is equal to the number of creatures you control. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetPowerSourceEffect( + CreaturesYouControlCount.instance, Duration.EndOfGame)).addHint(CreaturesYouControlHint.instance) + ); + + // Whenever you attack, for each opponent, create a 1/1 white Human creature token that's tapped and attacking that player or a planeswalker they control. + this.addAbility(new AttacksWithCreaturesTriggeredAbility(new AdelineResplendentCatharEffect(), 1)); + } + + private AdelineResplendentCathar(final AdelineResplendentCathar card) { + super(card); + } + + @Override + public AdelineResplendentCathar copy() { + return new AdelineResplendentCathar(this); + } +} + +class AdelineResplendentCatharEffect extends OneShotEffect { + + public AdelineResplendentCatharEffect() { + super(Outcome.Benefit); + staticText = "for each opponent, create a 1/1 white Human creature token that's tapped and attacking that player or a planeswalker they control"; + } + + private AdelineResplendentCatharEffect(final AdelineResplendentCatharEffect effect) { + super(effect); + } + + @Override + public AdelineResplendentCatharEffect copy() { + return new AdelineResplendentCatharEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (UUID opponentId : game.getOpponents(source.getControllerId())) { + new HumanToken().putOntoBattlefield(1, game, source, source.getControllerId(), true, true, opponentId); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 863bc1ac3b5..693b0f2542d 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -32,6 +32,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { this.numBoosterDoubleFaced = 1; cards.add(new SetCardInfo("Abandon the Post", 127, Rarity.COMMON, mage.cards.a.AbandonThePost.class)); + cards.add(new SetCardInfo("Adeline, Resplendent Cathar", 1, Rarity.RARE, mage.cards.a.AdelineResplendentCathar.class)); cards.add(new SetCardInfo("Ambitious Farmhand", 2, Rarity.UNCOMMON, mage.cards.a.AmbitiousFarmhand.class)); cards.add(new SetCardInfo("Angelfire Ignition", 209, Rarity.RARE, mage.cards.a.AngelfireIgnition.class)); cards.add(new SetCardInfo("Arcane Infusion", 210, Rarity.UNCOMMON, mage.cards.a.ArcaneInfusion.class)); From 996a1b42f258cb6fe5e39c360b53c0c323c430b3 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Sat, 11 Sep 2021 15:33:01 -0500 Subject: [PATCH 034/231] [MID] Implemented Crawl from the Cellar --- .../src/mage/cards/c/CrawlFromTheCellar.java | 51 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 52 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CrawlFromTheCellar.java diff --git a/Mage.Sets/src/mage/cards/c/CrawlFromTheCellar.java b/Mage.Sets/src/mage/cards/c/CrawlFromTheCellar.java new file mode 100644 index 00000000000..804b31d1c9c --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CrawlFromTheCellar.java @@ -0,0 +1,51 @@ +package mage.cards.c; + +import java.util.UUID; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TimingRule; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.targetpointer.SecondTargetPointer; + +/** + * + * @author weirddan455 + */ +public final class CrawlFromTheCellar extends CardImpl { + + private static final FilterControlledCreaturePermanent filter + = new FilterControlledCreaturePermanent(SubType.ZOMBIE); + + public CrawlFromTheCellar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{B}"); + + // Return target creature card from your graveyard to your hand. Put a +1/+1 counter on up to one target Zombie you control. + this.getSpellAbility().addEffect(new ReturnFromGraveyardToHandTargetEffect()); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); + + this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance()).setTargetPointer(SecondTargetPointer.getInstance())); + this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent(0, 1, filter, false)); + + // Flashback {3}{B} + this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{B}"), TimingRule.SORCERY)); + } + + private CrawlFromTheCellar(final CrawlFromTheCellar card) { + super(card); + } + + @Override + public CrawlFromTheCellar copy() { + return new CrawlFromTheCellar(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 693b0f2542d..275b92f94bc 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -77,6 +77,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Consider", 44, Rarity.COMMON, mage.cards.c.Consider.class)); cards.add(new SetCardInfo("Contortionist Troupe", 178, Rarity.UNCOMMON, mage.cards.c.ContortionistTroupe.class)); cards.add(new SetCardInfo("Corpse Cobble", 214, Rarity.UNCOMMON, mage.cards.c.CorpseCobble.class)); + cards.add(new SetCardInfo("Crawl from the Cellar", 93, Rarity.COMMON, mage.cards.c.CrawlFromTheCellar.class)); cards.add(new SetCardInfo("Croaking Counterpart", 215, Rarity.RARE, mage.cards.c.CroakingCounterpart.class)); cards.add(new SetCardInfo("Crossroads Candleguide", 253, Rarity.COMMON, mage.cards.c.CrossroadsCandleguide.class)); cards.add(new SetCardInfo("Curse of Shaken Faith", 134, Rarity.RARE, mage.cards.c.CurseOfShakenFaith.class)); From 70a9f44bda57abb2ee26a410f2828721f9dd6dbb Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Sat, 11 Sep 2021 15:57:48 -0500 Subject: [PATCH 035/231] [MID] Implemented Ghoulish Procession --- .../src/mage/cards/g/GhoulishProcession.java | 42 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 43 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/g/GhoulishProcession.java diff --git a/Mage.Sets/src/mage/cards/g/GhoulishProcession.java b/Mage.Sets/src/mage/cards/g/GhoulishProcession.java new file mode 100644 index 00000000000..f52c54c7461 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GhoulishProcession.java @@ -0,0 +1,42 @@ +package mage.cards.g; + +import java.util.UUID; + +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.game.permanent.token.ZombieDecayedToken; + +/** + * + * @author weirddan455 + */ +public final class GhoulishProcession extends CardImpl { + + private static final FilterCreaturePermanent filter + = new FilterCreaturePermanent("one or more nontoken creatures"); + + static { + filter.add(TokenPredicate.FALSE); + } + + public GhoulishProcession(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}"); + + // Whenever one or more nontoken creatures die, create a 2/2 black Zombie creature token with decayed. This ability triggers only once each turn. + this.addAbility(new DiesCreatureTriggeredAbility(new CreateTokenEffect(new ZombieDecayedToken()), false, filter).setTriggersOnce(true)); + } + + private GhoulishProcession(final GhoulishProcession card) { + super(card); + } + + @Override + public GhoulishProcession copy() { + return new GhoulishProcession(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 275b92f94bc..f8153536038 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -136,6 +136,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Geistflame Reservoir", 142, Rarity.RARE, mage.cards.g.GeistflameReservoir.class)); cards.add(new SetCardInfo("Geistwave", 56, Rarity.COMMON, mage.cards.g.Geistwave.class)); cards.add(new SetCardInfo("Ghoulcaller's Harvest", 225, Rarity.RARE, mage.cards.g.GhoulcallersHarvest.class)); + cards.add(new SetCardInfo("Ghoulish Procession", 102, Rarity.UNCOMMON, mage.cards.g.GhoulishProcession.class)); cards.add(new SetCardInfo("Grafted Identity", 57, Rarity.RARE, mage.cards.g.GraftedIdentity.class)); cards.add(new SetCardInfo("Graveyard Glutton", 104, Rarity.RARE, mage.cards.g.GraveyardGlutton.class)); cards.add(new SetCardInfo("Graveyard Trespasser", 104, Rarity.RARE, mage.cards.g.GraveyardTrespasser.class)); From 3332d41e2aa964e5b69a881e7aa0fc8a7c2fa26e Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Sat, 11 Sep 2021 16:00:58 -0500 Subject: [PATCH 036/231] [AFR] Fixed wrong cost increase on Paladin Class (fixes #8244) --- Mage.Sets/src/mage/cards/p/PaladinClass.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/p/PaladinClass.java b/Mage.Sets/src/mage/cards/p/PaladinClass.java index 15c0b70713b..3f09a67d05e 100644 --- a/Mage.Sets/src/mage/cards/p/PaladinClass.java +++ b/Mage.Sets/src/mage/cards/p/PaladinClass.java @@ -48,7 +48,7 @@ public final class PaladinClass extends CardImpl { // Spells your opponents cast during your turn cost {1} more to cast. this.addAbility(new SimpleStaticAbility(new ConditionalCostModificationEffect( new SpellsCostIncreasingAllEffect( - 2, StaticFilters.FILTER_CARD, TargetController.OPPONENT + 1, StaticFilters.FILTER_CARD, TargetController.OPPONENT ), MyTurnCondition.instance, "spells your opponents cast during your turn cost {1} more to cast" ))); From 1c1ad8f073dcfb6fc0115b81b07b6c85cd4ec893 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 15:33:56 -0400 Subject: [PATCH 037/231] [MID] Implemented Burn the Accursed --- .../src/mage/cards/b/BurnTheAccursed.java | 70 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 71 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/b/BurnTheAccursed.java diff --git a/Mage.Sets/src/mage/cards/b/BurnTheAccursed.java b/Mage.Sets/src/mage/cards/b/BurnTheAccursed.java new file mode 100644 index 00000000000..f8a73c59a94 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BurnTheAccursed.java @@ -0,0 +1,70 @@ +package mage.cards.b; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetIfDiesEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BurnTheAccursed extends CardImpl { + + public BurnTheAccursed(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{4}{R}"); + + // Burn the Accused deals 5 damage to target creature and 2 damage to that creature's controller. If that creature would die this turn, exile it instead. + this.getSpellAbility().addEffect(new BurnTheAccursedEffect()); + this.getSpellAbility().addEffect(new ExileTargetIfDiesEffect()); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + } + + private BurnTheAccursed(final BurnTheAccursed card) { + super(card); + } + + @Override + public BurnTheAccursed copy() { + return new BurnTheAccursed(this); + } +} + +class BurnTheAccursedEffect extends OneShotEffect { + + BurnTheAccursedEffect() { + super(Outcome.Benefit); + staticText = "{this} deals 5 damage to target creature and 2 damage to that creature's controller."; + } + + private BurnTheAccursedEffect(final BurnTheAccursedEffect effect) { + super(effect); + } + + @Override + public BurnTheAccursedEffect copy() { + return new BurnTheAccursedEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent == null) { + return false; + } + permanent.damage(5, source.getSourceId(), source, game); + Player player = game.getPlayer(permanent.getControllerId()); + if (player != null) { + player.damage(2, source.getSourceId(), source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index f8153536038..e46a2e84d32 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -60,6 +60,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Brood Weaver", 173, Rarity.UNCOMMON, mage.cards.b.BroodWeaver.class)); cards.add(new SetCardInfo("Burly Breaker", 174, Rarity.UNCOMMON, mage.cards.b.BurlyBreaker.class)); cards.add(new SetCardInfo("Burn Down the House", 131, Rarity.RARE, mage.cards.b.BurnDownTheHouse.class)); + cards.add(new SetCardInfo("Burn the Accursed", 132, Rarity.COMMON, mage.cards.b.BurnTheAccursed.class)); cards.add(new SetCardInfo("Can't Stay Away", 213, Rarity.RARE, mage.cards.c.CantStayAway.class)); cards.add(new SetCardInfo("Candlegrove Witch", 8, Rarity.COMMON, mage.cards.c.CandlegroveWitch.class)); cards.add(new SetCardInfo("Candlelit Cavalry", 175, Rarity.COMMON, mage.cards.c.CandlelitCavalry.class)); From 8622493b98bc8d266702beb8c71190d4a31d3479 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 15:37:05 -0400 Subject: [PATCH 038/231] [MID] Implemented Covetous Castaway / Ghostly Castigator --- .../src/mage/cards/c/CovetousCastaway.java | 44 ++++++++++++++++ .../src/mage/cards/g/GhostlyCastigator.java | 52 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 5 +- 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/CovetousCastaway.java create mode 100644 Mage.Sets/src/mage/cards/g/GhostlyCastigator.java diff --git a/Mage.Sets/src/mage/cards/c/CovetousCastaway.java b/Mage.Sets/src/mage/cards/c/CovetousCastaway.java new file mode 100644 index 00000000000..75f609c2df1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CovetousCastaway.java @@ -0,0 +1,44 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.keyword.DisturbAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CovetousCastaway extends CardImpl { + + public CovetousCastaway(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.HUMAN); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + this.transformable = true; + this.secondSideCardClazz = mage.cards.g.GhostlyCastigator.class; + + // When Covetous Castaway dies, mill three cards. + this.addAbility(new DiesSourceTriggeredAbility(new MillCardsControllerEffect(3))); + + // Disturb {3}{U}{U} + this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{3}{U}{U}"))); + } + + private CovetousCastaway(final CovetousCastaway card) { + super(card); + } + + @Override + public CovetousCastaway copy() { + return new CovetousCastaway(this); + } +} diff --git a/Mage.Sets/src/mage/cards/g/GhostlyCastigator.java b/Mage.Sets/src/mage/cards/g/GhostlyCastigator.java new file mode 100644 index 00000000000..5e882234492 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GhostlyCastigator.java @@ -0,0 +1,52 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.PutIntoGraveFromAnywhereSourceAbility; +import mage.abilities.effects.common.ExileSourceEffect; +import mage.abilities.effects.common.ShuffleIntoLibraryTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GhostlyCastigator extends CardImpl { + + public GhostlyCastigator(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.SPIRIT); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + this.transformable = true; + this.nightCard = true; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When Ghostly Castigator enters the battlefield, shuffle up to three target cards from your graveyard into your library. + Ability ability = new EntersBattlefieldTriggeredAbility(new ShuffleIntoLibraryTargetEffect()); + ability.addTarget(new TargetCardInYourGraveyard(0, 3)); + this.addAbility(ability); + + // If Ghostly Castigator would be put into a graveyard from anywhere, exile it instead. + this.addAbility(new PutIntoGraveFromAnywhereSourceAbility(new ExileSourceEffect().setText("exile it instead"))); + } + + private GhostlyCastigator(final GhostlyCastigator card) { + super(card); + } + + @Override + public GhostlyCastigator copy() { + return new GhostlyCastigator(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index e46a2e84d32..972483a1638 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -32,7 +32,6 @@ public final class InnistradMidnightHunt extends ExpansionSet { this.numBoosterDoubleFaced = 1; cards.add(new SetCardInfo("Abandon the Post", 127, Rarity.COMMON, mage.cards.a.AbandonThePost.class)); - cards.add(new SetCardInfo("Adeline, Resplendent Cathar", 1, Rarity.RARE, mage.cards.a.AdelineResplendentCathar.class)); cards.add(new SetCardInfo("Ambitious Farmhand", 2, Rarity.UNCOMMON, mage.cards.a.AmbitiousFarmhand.class)); cards.add(new SetCardInfo("Angelfire Ignition", 209, Rarity.RARE, mage.cards.a.AngelfireIgnition.class)); cards.add(new SetCardInfo("Arcane Infusion", 210, Rarity.UNCOMMON, mage.cards.a.ArcaneInfusion.class)); @@ -78,7 +77,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Consider", 44, Rarity.COMMON, mage.cards.c.Consider.class)); cards.add(new SetCardInfo("Contortionist Troupe", 178, Rarity.UNCOMMON, mage.cards.c.ContortionistTroupe.class)); cards.add(new SetCardInfo("Corpse Cobble", 214, Rarity.UNCOMMON, mage.cards.c.CorpseCobble.class)); - cards.add(new SetCardInfo("Crawl from the Cellar", 93, Rarity.COMMON, mage.cards.c.CrawlFromTheCellar.class)); + cards.add(new SetCardInfo("Covetous Castaway", 45, Rarity.UNCOMMON, mage.cards.c.CovetousCastaway.class)); cards.add(new SetCardInfo("Croaking Counterpart", 215, Rarity.RARE, mage.cards.c.CroakingCounterpart.class)); cards.add(new SetCardInfo("Crossroads Candleguide", 253, Rarity.COMMON, mage.cards.c.CrossroadsCandleguide.class)); cards.add(new SetCardInfo("Curse of Shaken Faith", 134, Rarity.RARE, mage.cards.c.CurseOfShakenFaith.class)); @@ -136,8 +135,8 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Gavony Trapper", 22, Rarity.COMMON, mage.cards.g.GavonyTrapper.class)); cards.add(new SetCardInfo("Geistflame Reservoir", 142, Rarity.RARE, mage.cards.g.GeistflameReservoir.class)); cards.add(new SetCardInfo("Geistwave", 56, Rarity.COMMON, mage.cards.g.Geistwave.class)); + cards.add(new SetCardInfo("Ghostly Castigator", 45, Rarity.UNCOMMON, mage.cards.g.GhostlyCastigator.class)); cards.add(new SetCardInfo("Ghoulcaller's Harvest", 225, Rarity.RARE, mage.cards.g.GhoulcallersHarvest.class)); - cards.add(new SetCardInfo("Ghoulish Procession", 102, Rarity.UNCOMMON, mage.cards.g.GhoulishProcession.class)); cards.add(new SetCardInfo("Grafted Identity", 57, Rarity.RARE, mage.cards.g.GraftedIdentity.class)); cards.add(new SetCardInfo("Graveyard Glutton", 104, Rarity.RARE, mage.cards.g.GraveyardGlutton.class)); cards.add(new SetCardInfo("Graveyard Trespasser", 104, Rarity.RARE, mage.cards.g.GraveyardTrespasser.class)); From 0408e08306d1dbb07938b7b1d8a1d0b557230e6a Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 15:40:54 -0400 Subject: [PATCH 039/231] [MID] Implemented Covert Cutpurse / Covetous Geist --- .../src/mage/cards/c/CovertCutpurse.java | 63 +++++++++++++++++++ .../src/mage/cards/c/CovetousCastaway.java | 2 + Mage.Sets/src/mage/cards/c/CovetousGeist.java | 48 ++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 4 files changed, 115 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CovertCutpurse.java create mode 100644 Mage.Sets/src/mage/cards/c/CovetousGeist.java diff --git a/Mage.Sets/src/mage/cards/c/CovertCutpurse.java b/Mage.Sets/src/mage/cards/c/CovertCutpurse.java new file mode 100644 index 00000000000..4659de478d1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CovertCutpurse.java @@ -0,0 +1,63 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.WasDealtDamageThisTurnPredicate; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CovertCutpurse extends CardImpl { + + private static final FilterPermanent filter + = new FilterCreaturePermanent("creature you don't control that was dealt damage this turn"); + + static { + filter.add(TargetController.OPPONENT.getControllerPredicate()); + filter.add(WasDealtDamageThisTurnPredicate.instance); + } + + public CovertCutpurse(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + this.transformable = true; + this.secondSideCardClazz = mage.cards.c.CovetousGeist.class; + + // When Covert Cutpurse enters the battlefield, destroy target creature you don't control that was dealt damage this turn. + Ability ability = new EntersBattlefieldTriggeredAbility(new DestroyTargetEffect()); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + + // Disturb {4}{B} + this.addAbility(new TransformAbility()); + this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{4}{B}"))); + } + + private CovertCutpurse(final CovertCutpurse card) { + super(card); + } + + @Override + public CovertCutpurse copy() { + return new CovertCutpurse(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/CovetousCastaway.java b/Mage.Sets/src/mage/cards/c/CovetousCastaway.java index 75f609c2df1..9f1a2180606 100644 --- a/Mage.Sets/src/mage/cards/c/CovetousCastaway.java +++ b/Mage.Sets/src/mage/cards/c/CovetousCastaway.java @@ -5,6 +5,7 @@ import mage.abilities.common.DiesSourceTriggeredAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.MillCardsControllerEffect; import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -30,6 +31,7 @@ public final class CovetousCastaway extends CardImpl { this.addAbility(new DiesSourceTriggeredAbility(new MillCardsControllerEffect(3))); // Disturb {3}{U}{U} + this.addAbility(new TransformAbility()); this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{3}{U}{U}"))); } diff --git a/Mage.Sets/src/mage/cards/c/CovetousGeist.java b/Mage.Sets/src/mage/cards/c/CovetousGeist.java new file mode 100644 index 00000000000..f72b20fd483 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CovetousGeist.java @@ -0,0 +1,48 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.common.PutIntoGraveFromAnywhereSourceAbility; +import mage.abilities.effects.common.ExileSourceEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CovetousGeist extends CardImpl { + + public CovetousGeist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + this.transformable = true; + this.nightCard = true; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // If Covetous Geist would be put into a graveyard from anywhere, exile it instead. + this.addAbility(new PutIntoGraveFromAnywhereSourceAbility(new ExileSourceEffect().setText("exile it instead"))); + } + + private CovetousGeist(final CovetousGeist card) { + super(card); + } + + @Override + public CovetousGeist copy() { + return new CovetousGeist(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 972483a1638..0f85c663765 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -77,7 +77,9 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Consider", 44, Rarity.COMMON, mage.cards.c.Consider.class)); cards.add(new SetCardInfo("Contortionist Troupe", 178, Rarity.UNCOMMON, mage.cards.c.ContortionistTroupe.class)); cards.add(new SetCardInfo("Corpse Cobble", 214, Rarity.UNCOMMON, mage.cards.c.CorpseCobble.class)); + cards.add(new SetCardInfo("Covert Cutpurse", 92, Rarity.UNCOMMON, mage.cards.c.CovertCutpurse.class)); cards.add(new SetCardInfo("Covetous Castaway", 45, Rarity.UNCOMMON, mage.cards.c.CovetousCastaway.class)); + cards.add(new SetCardInfo("Covetous Geist", 92, Rarity.UNCOMMON, mage.cards.c.CovetousGeist.class)); cards.add(new SetCardInfo("Croaking Counterpart", 215, Rarity.RARE, mage.cards.c.CroakingCounterpart.class)); cards.add(new SetCardInfo("Crossroads Candleguide", 253, Rarity.COMMON, mage.cards.c.CrossroadsCandleguide.class)); cards.add(new SetCardInfo("Curse of Shaken Faith", 134, Rarity.RARE, mage.cards.c.CurseOfShakenFaith.class)); From 37f71d0df4c6336028423ec96813d8b908fe5d79 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 15:52:42 -0400 Subject: [PATCH 040/231] [MID] Implemented Deathbonnet Sprout / Deathbonnet Hulk --- .../src/mage/cards/d/DeathbonnetHulk.java | 93 +++++++++++++++++++ .../src/mage/cards/d/DeathbonnetSprout.java | 62 +++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 157 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DeathbonnetHulk.java create mode 100644 Mage.Sets/src/mage/cards/d/DeathbonnetSprout.java diff --git a/Mage.Sets/src/mage/cards/d/DeathbonnetHulk.java b/Mage.Sets/src/mage/cards/d/DeathbonnetHulk.java new file mode 100644 index 00000000000..99b151931c9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DeathbonnetHulk.java @@ -0,0 +1,93 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DeathbonnetHulk extends CardImpl { + + public DeathbonnetHulk(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.FUNGUS); + this.subtype.add(SubType.HORROR); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + this.color.setGreen(true); + this.transformable = true; + this.nightCard = true; + + // At the beginning of your upkeep, you may exile a card from a graveyard. If a creature card was exiled this way, put a +1/+1 counter on Deathbonnet Hulk. + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new DeathbonnetHulkEffect(), TargetController.YOU, false + )); + } + + private DeathbonnetHulk(final DeathbonnetHulk card) { + super(card); + } + + @Override + public DeathbonnetHulk copy() { + return new DeathbonnetHulk(this); + } +} + +class DeathbonnetHulkEffect extends OneShotEffect { + + DeathbonnetHulkEffect() { + super(Outcome.Benefit); + staticText = "you may exile a card from a graveyard. " + + "If a creature card was exiled this way, put a +1/+1 counter on {this}"; + } + + private DeathbonnetHulkEffect(final DeathbonnetHulkEffect effect) { + super(effect); + } + + @Override + public DeathbonnetHulkEffect copy() { + return new DeathbonnetHulkEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetCard target = new TargetCardInGraveyard(0, 1); + target.setNotTarget(true); + player.choose(outcome, target, source.getSourceId(), game); + Card card = game.getCard(target.getFirstTarget()); + if (card == null) { + return false; + } + boolean flag = card.isCreature(game); + player.moveCards(card, Zone.EXILED, source, game); + if (!flag) { + return true; + } + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent != null) { + permanent.addCounters(CounterType.P1P1.createInstance(), source.getControllerId(), source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/d/DeathbonnetSprout.java b/Mage.Sets/src/mage/cards/d/DeathbonnetSprout.java new file mode 100644 index 00000000000..1dca8bc1abc --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DeathbonnetSprout.java @@ -0,0 +1,62 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.CardsInControllerGraveyardCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DeathbonnetSprout extends CardImpl { + + private static final Condition condition + = new CardsInControllerGraveyardCondition(3, StaticFilters.FILTER_CARD_CREATURE); + private static final Hint hint = new ValueHint( + "Creature cards in your graveyard", + new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURE) + ); + + public DeathbonnetSprout(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}"); + + this.subtype.add(SubType.FUNGUS); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + this.transformable = true; + this.secondSideCardClazz = mage.cards.d.DeathbonnetHulk.class; + + // At the beginning of your upkeep, mill a card. Then if there are three or more creature cards in your graveyard, transform Deathbonnet Sprout. + this.addAbility(new TransformAbility()); + Ability ability = new EntersBattlefieldTriggeredAbility(new MillCardsControllerEffect(1)); + ability.addEffect(new ConditionalOneShotEffect( + new TransformSourceEffect(true), condition, + "Then if there are three or more creature cards in your graveyard, transform {this}" + )); + this.addAbility(ability.addHint(hint)); + } + + private DeathbonnetSprout(final DeathbonnetSprout card) { + super(card); + } + + @Override + public DeathbonnetSprout copy() { + return new DeathbonnetSprout(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 0f85c663765..4518ddeeeff 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -88,6 +88,8 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Dawnhart Mentor", 179, Rarity.UNCOMMON, mage.cards.d.DawnhartMentor.class)); cards.add(new SetCardInfo("Dawnhart Rejuvenator", 180, Rarity.COMMON, mage.cards.d.DawnhartRejuvenator.class)); cards.add(new SetCardInfo("Dawnhart Wardens", 216, Rarity.UNCOMMON, mage.cards.d.DawnhartWardens.class)); + cards.add(new SetCardInfo("Deathbonnet Hulk", 181, Rarity.UNCOMMON, mage.cards.d.DeathbonnetHulk.class)); + cards.add(new SetCardInfo("Deathbonnet Sprout", 181, Rarity.UNCOMMON, mage.cards.d.DeathbonnetSprout.class)); cards.add(new SetCardInfo("Defend the Celestus", 182, Rarity.UNCOMMON, mage.cards.d.DefendTheCelestus.class)); cards.add(new SetCardInfo("Defenestrate", 95, Rarity.COMMON, mage.cards.d.Defenestrate.class)); cards.add(new SetCardInfo("Delver of Secrets", 47, Rarity.UNCOMMON, mage.cards.d.DelverOfSecrets.class)); From e1c3b4c337468c8466bb382a380117776e9afe31 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 15:58:02 -0400 Subject: [PATCH 041/231] [MID] Implemented Cathar's Call --- Mage.Sets/src/mage/cards/c/CatharsCall.java | 56 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 57 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CatharsCall.java diff --git a/Mage.Sets/src/mage/cards/c/CatharsCall.java b/Mage.Sets/src/mage/cards/c/CatharsCall.java new file mode 100644 index 00000000000..0dcbf8b25df --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CatharsCall.java @@ -0,0 +1,56 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.permanent.token.HumanToken; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CatharsCall extends CardImpl { + + public CatharsCall(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}"); + + this.subtype.add(SubType.AURA); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + Ability ability = new EnchantAbility(auraTarget.getTargetName()); + this.addAbility(ability); + + // Enchanted creature has vigilance and "At the beginning of your end step, create a 1/1 white Human creature token." + ability = new SimpleStaticAbility(new GainAbilityAttachedEffect(VigilanceAbility.getInstance(), AttachmentType.AURA)); + ability.addEffect(new GainAbilityAttachedEffect( + new BeginningOfEndStepTriggeredAbility( + new CreateTokenEffect(new HumanToken()), + TargetController.YOU, false + ), AttachmentType.AURA + ).setText("and \"At the beginning of your end step, create a 1/1 white Human creature token.\"")); + this.addAbility(ability); + } + + private CatharsCall(final CatharsCall card) { + super(card); + } + + @Override + public CatharsCall copy() { + return new CatharsCall(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 4518ddeeeff..292a15fbd96 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -65,6 +65,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Candlelit Cavalry", 175, Rarity.COMMON, mage.cards.c.CandlelitCavalry.class)); cards.add(new SetCardInfo("Candletrap", 9, Rarity.COMMON, mage.cards.c.Candletrap.class)); cards.add(new SetCardInfo("Cathar Commando", 10, Rarity.COMMON, mage.cards.c.CatharCommando.class)); + cards.add(new SetCardInfo("Cathar's Call", 11, Rarity.UNCOMMON, mage.cards.c.CatharsCall.class)); cards.add(new SetCardInfo("Cathartic Pyre", 133, Rarity.UNCOMMON, mage.cards.c.CatharticPyre.class)); cards.add(new SetCardInfo("Celestus Sanctifier", 12, Rarity.COMMON, mage.cards.c.CelestusSanctifier.class)); cards.add(new SetCardInfo("Champion of the Perished", 91, Rarity.RARE, mage.cards.c.ChampionOfThePerished.class)); From 090aa66839d22906ed49c79b9c2891a96a1af747 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 16:00:07 -0400 Subject: [PATCH 042/231] [MID] Implemented Voldaren Stinger --- .../src/mage/cards/v/VoldarenStinger.java | 53 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 54 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/v/VoldarenStinger.java diff --git a/Mage.Sets/src/mage/cards/v/VoldarenStinger.java b/Mage.Sets/src/mage/cards/v/VoldarenStinger.java new file mode 100644 index 00000000000..e118ad8d8fc --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VoldarenStinger.java @@ -0,0 +1,53 @@ +package mage.cards.v; + +import mage.MageInt; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.SourceAttackingCondition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VoldarenStinger extends CardImpl { + + public VoldarenStinger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}"); + + this.subtype.add(SubType.VAMPIRE); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Voldaren Stinger has first strike as long as it's attacking. + this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect( + new GainAbilitySourceEffect(FirstStrikeAbility.getInstance(), Duration.WhileOnBattlefield), + SourceAttackingCondition.instance, "{this} has first strike as long as it's attacking" + ))); + + // {2}{R}: Voldaren Stinger gets +2/+0 until end of turn. + this.addAbility(new SimpleActivatedAbility(new BoostSourceEffect( + 2, 0, Duration.EndOfTurn + ), new ManaCostsImpl<>("{2}{R}"))); + } + + private VoldarenStinger(final VoldarenStinger card) { + super(card); + } + + @Override + public VoldarenStinger copy() { + return new VoldarenStinger(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 292a15fbd96..76fe433d80d 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -264,6 +264,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Village Watch", 165, Rarity.UNCOMMON, mage.cards.v.VillageWatch.class)); cards.add(new SetCardInfo("Vivisection", 83, Rarity.UNCOMMON, mage.cards.v.Vivisection.class)); cards.add(new SetCardInfo("Voldaren Ambusher", 166, Rarity.UNCOMMON, mage.cards.v.VoldarenAmbusher.class)); + cards.add(new SetCardInfo("Voldaren Stinger", 167, Rarity.COMMON, mage.cards.v.VoldarenStinger.class)); cards.add(new SetCardInfo("Waildrifter", 55, Rarity.COMMON, mage.cards.w.Waildrifter.class)); cards.add(new SetCardInfo("Willow Geist", 207, Rarity.RARE, mage.cards.w.WillowGeist.class)); cards.add(new SetCardInfo("Wing Shredder", 169, Rarity.COMMON, mage.cards.w.WingShredder.class)); From 008c1b133445e95620c199f9f8ff8489157fd9e4 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 16:07:20 -0400 Subject: [PATCH 043/231] [MID] Implemented Turn the Earth --- Mage.Sets/src/mage/cards/t/TurnTheEarth.java | 42 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 43 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TurnTheEarth.java diff --git a/Mage.Sets/src/mage/cards/t/TurnTheEarth.java b/Mage.Sets/src/mage/cards/t/TurnTheEarth.java new file mode 100644 index 00000000000..4298dca55f5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TurnTheEarth.java @@ -0,0 +1,42 @@ +package mage.cards.t; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.ShuffleIntoLibraryTargetEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TimingRule; +import mage.target.common.TargetCardInGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TurnTheEarth extends CardImpl { + + public TurnTheEarth(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{G}"); + + // Choose up to three target cards in graveyards. The owners of those cards shuffle them into their libraries. You gain 2 life. + this.getSpellAbility().addEffect(new ShuffleIntoLibraryTargetEffect() + .setText("choose up to three target cards in graveyards. " + + "The owners of those cards shuffle them into their libraries.")); + this.getSpellAbility().addEffect(new GainLifeEffect(2)); + this.getSpellAbility().addTarget(new TargetCardInGraveyard(0, 3)); + + // Flashback {1}{G} + this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{1}{G}"), TimingRule.INSTANT)); + } + + private TurnTheEarth(final TurnTheEarth card) { + super(card); + } + + @Override + public TurnTheEarth copy() { + return new TurnTheEarth(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 76fe433d80d..11313670dc2 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -253,6 +253,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Timberland Guide", 202, Rarity.COMMON, mage.cards.t.TimberlandGuide.class)); cards.add(new SetCardInfo("Tireless Hauler", 203, Rarity.COMMON, mage.cards.t.TirelessHauler.class)); cards.add(new SetCardInfo("Triskaidekaphile", 81, Rarity.RARE, mage.cards.t.Triskaidekaphile.class)); + cards.add(new SetCardInfo("Turn the Earth", 205, Rarity.UNCOMMON, mage.cards.t.TurnTheEarth.class)); cards.add(new SetCardInfo("Unnatural Growth", 206, Rarity.RARE, mage.cards.u.UnnaturalGrowth.class)); cards.add(new SetCardInfo("Unruly Mob", 40, Rarity.COMMON, mage.cards.u.UnrulyMob.class)); cards.add(new SetCardInfo("Untamed Pup", 187, Rarity.UNCOMMON, mage.cards.u.UntamedPup.class)); From 58926ce4c1bb0a019ddab71f36813f06d242557e Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 16:19:26 -0400 Subject: [PATCH 044/231] [MID] Implemented Tovolar's Huntmaster / Tovolar's Packleader --- .../src/mage/cards/t/TovolarsHuntmaster.java | 47 ++++++++++++ .../src/mage/cards/t/TovolarsPackleader.java | 75 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 124 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TovolarsHuntmaster.java create mode 100644 Mage.Sets/src/mage/cards/t/TovolarsPackleader.java diff --git a/Mage.Sets/src/mage/cards/t/TovolarsHuntmaster.java b/Mage.Sets/src/mage/cards/t/TovolarsHuntmaster.java new file mode 100644 index 00000000000..40ffabbe628 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TovolarsHuntmaster.java @@ -0,0 +1,47 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.DayboundAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.game.permanent.token.WolfToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TovolarsHuntmaster extends CardImpl { + + public TovolarsHuntmaster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}{G}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WEREWOLF); + this.power = new MageInt(6); + this.toughness = new MageInt(6); + this.transformable = true; + this.secondSideCardClazz = mage.cards.t.TovolarsPackleader.class; + + // Whenever Tovolar's Huntmaster enters the battlefield, create two 2/2 green Wolf creature tokens. + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new WolfToken(), 2))); + + // Daybound + this.addAbility(new TransformAbility()); + this.addAbility(DayboundAbility.getInstance()); + } + + private TovolarsHuntmaster(final TovolarsHuntmaster card) { + super(card); + } + + @Override + public TovolarsHuntmaster copy() { + return new TovolarsHuntmaster(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TovolarsPackleader.java b/Mage.Sets/src/mage/cards/t/TovolarsPackleader.java new file mode 100644 index 00000000000..b167aff49c2 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TovolarsPackleader.java @@ -0,0 +1,75 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.FightTargetsEffect; +import mage.abilities.keyword.NightboundAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.permanent.token.WolfToken; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TovolarsPackleader extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledPermanent("another Wolf or Werewolf you control"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(Predicates.or( + SubType.WOLF.getPredicate(), + SubType.WEREWOLF.getPredicate() + )); + } + + public TovolarsPackleader(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.WEREWOLF); + this.power = new MageInt(7); + this.toughness = new MageInt(7); + this.color.setGreen(true); + this.transformable = true; + this.nightCard = true; + + // Whenever Tovolar's Packleader enters the battlefield or attacks, create two 2/2 green Wolf creature tokens. + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility( + new CreateTokenEffect(new WolfToken(), 2) + )); + + // {2}{G}{G}: Another target Wolf or Werewolf you control fights target creature you don't control. + Ability ability = new SimpleActivatedAbility(new FightTargetsEffect( + "another target Wolf or Werewolf you control fights target creature you don't control" + ), new ManaCostsImpl<>("{2}{G}{G}")); + ability.addTarget(new TargetPermanent(filter)); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL)); + + // Nightbound + this.addAbility(NightboundAbility.getInstance()); + } + + private TovolarsPackleader(final TovolarsPackleader card) { + super(card); + } + + @Override + public TovolarsPackleader copy() { + return new TovolarsPackleader(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 11313670dc2..5cbbca6712f 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -252,6 +252,8 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Thraben Exorcism", 39, Rarity.COMMON, mage.cards.t.ThrabenExorcism.class)); cards.add(new SetCardInfo("Timberland Guide", 202, Rarity.COMMON, mage.cards.t.TimberlandGuide.class)); cards.add(new SetCardInfo("Tireless Hauler", 203, Rarity.COMMON, mage.cards.t.TirelessHauler.class)); + cards.add(new SetCardInfo("Tovolar's Huntmaster", 204, Rarity.RARE, mage.cards.t.TovolarsHuntmaster.class)); + cards.add(new SetCardInfo("Tovolar's Packleader", 204, Rarity.RARE, mage.cards.t.TovolarsPackleader.class)); cards.add(new SetCardInfo("Triskaidekaphile", 81, Rarity.RARE, mage.cards.t.Triskaidekaphile.class)); cards.add(new SetCardInfo("Turn the Earth", 205, Rarity.UNCOMMON, mage.cards.t.TurnTheEarth.class)); cards.add(new SetCardInfo("Unnatural Growth", 206, Rarity.RARE, mage.cards.u.UnnaturalGrowth.class)); From 25bc5d65896523f710f7358df3344470ef32b2f5 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 17:11:56 -0400 Subject: [PATCH 045/231] resolved rebase conflicts --- Mage.Sets/src/mage/sets/InnistradMidnightHunt.java | 3 +++ Utils/mtg-cards-data.txt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 5cbbca6712f..b747df49673 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -32,6 +32,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { this.numBoosterDoubleFaced = 1; cards.add(new SetCardInfo("Abandon the Post", 127, Rarity.COMMON, mage.cards.a.AbandonThePost.class)); + cards.add(new SetCardInfo("Adeline, Resplendent Cathar", 1, Rarity.RARE, mage.cards.a.AdelineResplendentCathar.class)); cards.add(new SetCardInfo("Ambitious Farmhand", 2, Rarity.UNCOMMON, mage.cards.a.AmbitiousFarmhand.class)); cards.add(new SetCardInfo("Angelfire Ignition", 209, Rarity.RARE, mage.cards.a.AngelfireIgnition.class)); cards.add(new SetCardInfo("Arcane Infusion", 210, Rarity.UNCOMMON, mage.cards.a.ArcaneInfusion.class)); @@ -81,6 +82,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Covert Cutpurse", 92, Rarity.UNCOMMON, mage.cards.c.CovertCutpurse.class)); cards.add(new SetCardInfo("Covetous Castaway", 45, Rarity.UNCOMMON, mage.cards.c.CovetousCastaway.class)); cards.add(new SetCardInfo("Covetous Geist", 92, Rarity.UNCOMMON, mage.cards.c.CovetousGeist.class)); + cards.add(new SetCardInfo("Crawl from the Cellar", 93, Rarity.COMMON, mage.cards.c.CrawlFromTheCellar.class)); cards.add(new SetCardInfo("Croaking Counterpart", 215, Rarity.RARE, mage.cards.c.CroakingCounterpart.class)); cards.add(new SetCardInfo("Crossroads Candleguide", 253, Rarity.COMMON, mage.cards.c.CrossroadsCandleguide.class)); cards.add(new SetCardInfo("Curse of Shaken Faith", 134, Rarity.RARE, mage.cards.c.CurseOfShakenFaith.class)); @@ -142,6 +144,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Geistwave", 56, Rarity.COMMON, mage.cards.g.Geistwave.class)); cards.add(new SetCardInfo("Ghostly Castigator", 45, Rarity.UNCOMMON, mage.cards.g.GhostlyCastigator.class)); cards.add(new SetCardInfo("Ghoulcaller's Harvest", 225, Rarity.RARE, mage.cards.g.GhoulcallersHarvest.class)); + cards.add(new SetCardInfo("Ghoulish Procession", 102, Rarity.UNCOMMON, mage.cards.g.GhoulishProcession.class)); cards.add(new SetCardInfo("Grafted Identity", 57, Rarity.RARE, mage.cards.g.GraftedIdentity.class)); cards.add(new SetCardInfo("Graveyard Glutton", 104, Rarity.RARE, mage.cards.g.GraveyardGlutton.class)); cards.add(new SetCardInfo("Graveyard Trespasser", 104, Rarity.RARE, mage.cards.g.GraveyardTrespasser.class)); diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 81f23ba7c33..3d9ddf2684c 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -42336,7 +42336,7 @@ Falkenrath Perforator|Innistrad: Midnight Hunt|136|C|{1}{R}|Creature - Vampire|2 Falkenrath Pit Fighter|Innistrad: Midnight Hunt|137|R|{R}|Creature - Vampire Warrior|2|1|{1}{R}, Discard a card, Sacrifice a Vampire: Draw two cards. Activate only if an opponent lost life this turn.| Famished Foragers|Innistrad: Midnight Hunt|138|C|{3}{R}|Creature - Vampire|4|3|When Famished Foragers enters the battlefield, if an opponent lost life this turn, add {R}{R}{R}.${2}{R}, Discard a card: Draw a card.| Fangblade Brigand|Innistrad: Midnight Hunt|139|U|{3}{R}|Creature - Human Werewolf|3|4|{1}{R}: Fangblade Brigand gets +1/+0 and gains first strike until end of turn.$Daybound| -Fangblade Eviscerator|Innistrad: Midnight Hunt|139|U||Creature - Werewolf|4|5|{1}{R}: "Blade-Fang Eviscerator" gets +1/+0 and gains first strike until end of turn.${4}{R}: Creatures you control get +2/+0 until end of turn.$Nightbound| +Fangblade Eviscerator|Innistrad: Midnight Hunt|139|U||Creature - Werewolf|4|5|{1}{R}: Fangblade Eviscerator gets +1/+0 and gains first strike until end of turn.${4}{R}: Creatures you control get +2/+0 until end of turn.$Nightbound| Festival Crasher|Innistrad: Midnight Hunt|140|C|{1}{R}|Creature - Devil|1|3|Whenever you cast an instant or sorcery spell, Festival Crasher gets +2/+0 until end of turn.| Flame Channeler|Innistrad: Midnight Hunt|141|U|{1}{R}|Creature - Human Wizard|2|2|When a spell you control deals damage, transform Flame Channeler.| Embodiment of Flame|Innistrad: Midnight Hunt|141|U||Creature - Elemental Wizard|3|3|Whenever a spell you control deals damage, put a flame counter on Embodiment of Flame.${1}, Remove a flame counter from Embodiment of Flame: Exile the top card of your library. You may play that card this turn.| From 82c370d79fef54b14c8e7cf382d80a4e5dd4744a Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 17:25:37 -0400 Subject: [PATCH 046/231] [MID] Implemented Locked in the Cemetery --- .../src/mage/cards/l/LockedInTheCemetery.java | 61 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 62 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LockedInTheCemetery.java diff --git a/Mage.Sets/src/mage/cards/l/LockedInTheCemetery.java b/Mage.Sets/src/mage/cards/l/LockedInTheCemetery.java new file mode 100644 index 00000000000..a292808439a --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LockedInTheCemetery.java @@ -0,0 +1,61 @@ +package mage.cards.l; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.CardsInControllerGraveyardCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.DontUntapInControllersUntapStepEnchantedEffect; +import mage.abilities.effects.common.TapEnchantedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LockedInTheCemetery extends CardImpl { + + private static final Condition condition = new CardsInControllerGraveyardCondition(5); + + public LockedInTheCemetery(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}"); + + this.subtype.add(SubType.AURA); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + Ability ability = new EnchantAbility(auraTarget.getTargetName()); + this.addAbility(ability); + + // When Locked in the Cemetery enters the battlefield, if there are five or more cards in your graveyard, tap enchanted creature. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new EntersBattlefieldTriggeredAbility(new TapEnchantedEffect()), + condition, "When {this} enters the battlefield, if there are " + + "five or more cards in your graveyard, tap enchanted creature." + )); + + // Enchanted creature doesn't untap during its controller's untap step. + this.addAbility(new SimpleStaticAbility(new DontUntapInControllersUntapStepEnchantedEffect())); + } + + private LockedInTheCemetery(final LockedInTheCemetery card) { + super(card); + } + + @Override + public LockedInTheCemetery copy() { + return new LockedInTheCemetery(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index b747df49673..398fb8f46b8 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -174,6 +174,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Lambholt Harrier", 145, Rarity.COMMON, mage.cards.l.LambholtHarrier.class)); cards.add(new SetCardInfo("Larder Zombie", 58, Rarity.COMMON, mage.cards.l.LarderZombie.class)); cards.add(new SetCardInfo("Lier, Disciple of the Drowned", 59, Rarity.MYTHIC, mage.cards.l.LierDiscipleOfTheDrowned.class)); + cards.add(new SetCardInfo("Locked in the Cemetery", 60, Rarity.COMMON, mage.cards.l.LockedInTheCemetery.class)); cards.add(new SetCardInfo("Lord of the Ulvenwald", 231, Rarity.UNCOMMON, mage.cards.l.LordOfTheUlvenwald.class)); cards.add(new SetCardInfo("Loyal Gryff", 26, Rarity.UNCOMMON, mage.cards.l.LoyalGryff.class)); cards.add(new SetCardInfo("Lunar Frenzy", 147, Rarity.UNCOMMON, mage.cards.l.LunarFrenzy.class)); From 0d49a91a6f12acc0a7d2e532e654cdfc49ffbdcb Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 17:31:28 -0400 Subject: [PATCH 047/231] [MID] Implemented Stolen Vitality --- .../src/mage/cards/s/StolenVitality.java | 44 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 45 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StolenVitality.java diff --git a/Mage.Sets/src/mage/cards/s/StolenVitality.java b/Mage.Sets/src/mage/cards/s/StolenVitality.java new file mode 100644 index 00000000000..b4b47c4b04b --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StolenVitality.java @@ -0,0 +1,44 @@ +package mage.cards.s; + +import mage.abilities.condition.common.MyTurnCondition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StolenVitality extends CardImpl { + + public StolenVitality(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{R}"); + + // Target creature gets +3/+1 until end of turn. If it's your turn, that creature gains trample until end of turn. Otherwise, it gains first strike until end of turn. + this.getSpellAbility().addEffect(new BoostTargetEffect(3, 1)); + this.getSpellAbility().addEffect(new ConditionalContinuousEffect( + new GainAbilityTargetEffect(TrampleAbility.getInstance(), Duration.EndOfTurn), + new GainAbilityTargetEffect(FirstStrikeAbility.getInstance(), Duration.EndOfTurn), + MyTurnCondition.instance, "If it's your turn, that creature gains trample " + + "until end of turn. Otherwise, it gains first strike until end of turn" + )); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + } + + private StolenVitality(final StolenVitality card) { + super(card); + } + + @Override + public StolenVitality copy() { + return new StolenVitality(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 398fb8f46b8..08c2536197e 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -240,6 +240,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Spellrune Painter", 160, Rarity.UNCOMMON, mage.cards.s.SpellrunePainter.class)); cards.add(new SetCardInfo("Stalking Predator", 120, Rarity.COMMON, mage.cards.s.StalkingPredator.class)); cards.add(new SetCardInfo("Startle", 78, Rarity.COMMON, mage.cards.s.Startle.class)); + cards.add(new SetCardInfo("Stolen Vitality", 161, Rarity.COMMON, mage.cards.s.StolenVitality.class)); cards.add(new SetCardInfo("Storm-Charged Slasher", 157, Rarity.RARE, mage.cards.s.StormChargedSlasher.class)); cards.add(new SetCardInfo("Stormrider Spirit", 79, Rarity.COMMON, mage.cards.s.StormriderSpirit.class)); cards.add(new SetCardInfo("Stromkirk Bloodthief", 123, Rarity.UNCOMMON, mage.cards.s.StromkirkBloodthief.class)); From 05e80e3a02399ed8bee9c2a8a27b305852754577 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 17:44:32 -0400 Subject: [PATCH 048/231] [MID] Implemented Path to the Festival --- .../src/mage/cards/p/PathToTheFestival.java | 63 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 64 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/p/PathToTheFestival.java diff --git a/Mage.Sets/src/mage/cards/p/PathToTheFestival.java b/Mage.Sets/src/mage/cards/p/PathToTheFestival.java new file mode 100644 index 00000000000..2b9bb72719c --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PathToTheFestival.java @@ -0,0 +1,63 @@ +package mage.cards.p; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.DomainValue; +import mage.abilities.effects.common.search.SearchLibraryPutInPlayEffect; +import mage.abilities.effects.keyword.ScryEffect; +import mage.abilities.hint.common.DomainHint; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TimingRule; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.target.common.TargetCardInLibrary; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class PathToTheFestival extends CardImpl { + + public PathToTheFestival(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{G}"); + + // Search your library for a basic land card, put that card onto the battlefield tapped, then shuffle. Then if there are three or more basic land types among lands you control, scry 1. + this.getSpellAbility().addEffect(new SearchLibraryPutInPlayEffect( + new TargetCardInLibrary(StaticFilters.FILTER_CARD_BASIC_LAND), true + )); + this.getSpellAbility().addEffect(new ConditionalOneShotEffect( + new ScryEffect(1), PathToTheFestivalCondition.instance, + "Then if there are three or more basic land types among lands you control, scry 1" + )); + this.getSpellAbility().addHint(DomainHint.instance); + + // Flashback {4}{G} + this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{4}{G}"), TimingRule.SORCERY)); + } + + private PathToTheFestival(final PathToTheFestival card) { + super(card); + } + + @Override + public PathToTheFestival copy() { + return new PathToTheFestival(this); + } +} + +enum PathToTheFestivalCondition implements Condition { + instance; + private static final DynamicValue xValue = new DomainValue(); + + @Override + public boolean apply(Game game, Ability source) { + return xValue.calculate(game, source, null) >= 3; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 08c2536197e..1b8b9c90b2b 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -202,6 +202,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Overgrown Farmland", 265, Rarity.RARE, mage.cards.o.OvergrownFarmland.class)); cards.add(new SetCardInfo("Overwhelmed Archivist", 68, Rarity.UNCOMMON, mage.cards.o.OverwhelmedArchivist.class)); cards.add(new SetCardInfo("Pack's Betrayal", 153, Rarity.COMMON, mage.cards.p.PacksBetrayal.class)); + cards.add(new SetCardInfo("Path to the Festival", 191, Rarity.COMMON, mage.cards.p.PathToTheFestival.class)); cards.add(new SetCardInfo("Patrician Geist", 69, Rarity.RARE, mage.cards.p.PatricianGeist.class)); cards.add(new SetCardInfo("Pestilent Wolf", 192, Rarity.COMMON, mage.cards.p.PestilentWolf.class)); cards.add(new SetCardInfo("Phantom Carriage", 70, Rarity.UNCOMMON, mage.cards.p.PhantomCarriage.class)); From d93475a5bb8b1f5438634196acad6bab1f1833f4 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 18:08:35 -0400 Subject: [PATCH 049/231] refactored flashback to remove unnecessary timing rule argument --- .../src/mage/cards/a/AbandonThePost.java | 2 +- Mage.Sets/src/mage/cards/a/AcornHarvest.java | 2 +- .../src/mage/cards/a/AncestralTribute.java | 2 +- Mage.Sets/src/mage/cards/a/AncientGrudge.java | 2 +- .../src/mage/cards/a/AngelfireIgnition.java | 2 +- .../src/mage/cards/a/ArcaneInfusion.java | 2 +- .../src/mage/cards/a/ArmyOfTheDamned.java | 2 +- Mage.Sets/src/mage/cards/a/ArtfulDodge.java | 2 +- .../src/mage/cards/b/BackdraftHellkite.java | 10 +---- Mage.Sets/src/mage/cards/b/BashToBits.java | 2 +- Mage.Sets/src/mage/cards/b/BattleScreech.java | 2 +- Mage.Sets/src/mage/cards/b/BeastAttack.java | 2 +- .../src/mage/cards/b/BlastFromThePast.java | 2 +- .../src/mage/cards/b/BumpInTheNight.java | 2 +- Mage.Sets/src/mage/cards/b/BurningOil.java | 2 +- Mage.Sets/src/mage/cards/c/CabalTherapy.java | 7 ++-- .../src/mage/cards/c/CacklingCounterpart.java | 2 +- .../src/mage/cards/c/CalibratedBlast.java | 2 +- Mage.Sets/src/mage/cards/c/CallOfTheHerd.java | 2 +- Mage.Sets/src/mage/cards/c/CanopyClaws.java | 2 +- Mage.Sets/src/mage/cards/c/CantStayAway.java | 2 +- Mage.Sets/src/mage/cards/c/ChainersEdict.java | 2 +- .../mage/cards/c/ChatterOfTheSquirrel.java | 2 +- .../src/mage/cards/c/ChillOfForeboding.java | 2 +- Mage.Sets/src/mage/cards/c/CoffinPurge.java | 2 +- Mage.Sets/src/mage/cards/c/Conflagrate.java | 2 +- Mage.Sets/src/mage/cards/c/CorpseCobble.java | 2 +- .../src/mage/cards/c/CrawlFromTheCellar.java | 2 +- .../src/mage/cards/c/CreepingRenaissance.java | 2 +- .../src/mage/cards/c/CripplingFatigue.java | 2 +- .../src/mage/cards/c/CroakingCounterpart.java | 2 +- Mage.Sets/src/mage/cards/c/CrushOfWurms.java | 2 +- Mage.Sets/src/mage/cards/d/DeadlyAllure.java | 2 +- Mage.Sets/src/mage/cards/d/DeepAnalysis.java | 2 +- .../src/mage/cards/d/DeepReconnaissance.java | 2 +- Mage.Sets/src/mage/cards/d/DefyGravity.java | 2 +- Mage.Sets/src/mage/cards/d/Dematerialize.java | 2 +- .../src/mage/cards/d/DesperateRavings.java | 2 +- Mage.Sets/src/mage/cards/d/DevilsPlay.java | 2 +- .../src/mage/cards/d/DireStrainRampage.java | 2 +- .../src/mage/cards/d/DiregrafRebirth.java | 2 +- .../src/mage/cards/d/DivineReckoning.java | 2 +- .../src/mage/cards/d/DralnuLichLord.java | 29 ++++++-------- Mage.Sets/src/mage/cards/d/DreadReturn.java | 2 +- Mage.Sets/src/mage/cards/d/DreamTwist.java | 2 +- Mage.Sets/src/mage/cards/d/DryadsRevival.java | 2 +- Mage.Sets/src/mage/cards/e/EarthRift.java | 2 +- Mage.Sets/src/mage/cards/e/EchoOfEons.java | 2 +- .../src/mage/cards/e/ElectricRevelation.java | 2 +- .../src/mage/cards/e/ElephantAmbush.java | 2 +- Mage.Sets/src/mage/cards/e/Embolden.java | 2 +- .../src/mage/cards/e/EngulfingFlames.java | 2 +- .../src/mage/cards/f/FaithfulMending.java | 2 +- .../src/mage/cards/f/FaithlessLooting.java | 2 +- .../src/mage/cards/f/FeelingOfDread.java | 2 +- Mage.Sets/src/mage/cards/f/FerventDenial.java | 2 +- Mage.Sets/src/mage/cards/f/Firebolt.java | 2 +- Mage.Sets/src/mage/cards/f/FirecatBlitz.java | 2 +- .../src/mage/cards/f/FiresOfUndeath.java | 2 +- Mage.Sets/src/mage/cards/f/FlaringPain.java | 2 +- .../src/mage/cards/f/FlashOfDefiance.java | 2 +- .../src/mage/cards/f/FlashOfInsight.java | 2 +- Mage.Sets/src/mage/cards/f/FolkMedicine.java | 2 +- .../src/mage/cards/f/ForbiddenAlchemy.java | 2 +- .../src/mage/cards/g/GalvanicIteration.java | 2 +- Mage.Sets/src/mage/cards/g/GazeOfJustice.java | 2 +- Mage.Sets/src/mage/cards/g/Geistflame.java | 2 +- .../src/mage/cards/g/GhoulcallersHarvest.java | 2 +- Mage.Sets/src/mage/cards/g/GnawToTheBone.java | 2 +- .../src/mage/cards/g/GraspOfPhantoms.java | 2 +- Mage.Sets/src/mage/cards/g/GrizzlyFate.java | 2 +- .../src/mage/cards/h/HomesteadCourage.java | 2 +- Mage.Sets/src/mage/cards/h/HowlingGale.java | 2 +- Mage.Sets/src/mage/cards/h/HungryForMore.java | 2 +- .../src/mage/cards/i/IgniteTheFuture.java | 2 +- .../src/mage/cards/i/IncreasingAmbition.java | 2 +- .../src/mage/cards/i/IncreasingConfusion.java | 2 +- .../src/mage/cards/i/IncreasingDevotion.java | 2 +- .../src/mage/cards/i/IncreasingSavagery.java | 2 +- .../src/mage/cards/i/IncreasingVengeance.java | 2 +- Mage.Sets/src/mage/cards/j/JoinTheDance.java | 2 +- Mage.Sets/src/mage/cards/k/Kaleidoscorch.java | 2 +- .../src/mage/cards/k/KrosanReclamation.java | 2 +- Mage.Sets/src/mage/cards/l/LavaDart.java | 2 +- .../cards/l/LierDiscipleOfTheDrowned.java | 4 +- .../src/mage/cards/l/LightningSurge.java | 2 +- .../src/mage/cards/l/LingeringSouls.java | 2 +- Mage.Sets/src/mage/cards/m/MarshalingCry.java | 2 +- Mage.Sets/src/mage/cards/m/MassDiminish.java | 2 +- .../src/mage/cards/m/MemorysJourney.java | 2 +- .../src/mage/cards/m/MoanOfTheUnhallowed.java | 2 +- .../src/mage/cards/m/MomentaryBlink.java | 2 +- Mage.Sets/src/mage/cards/m/MomentsPeace.java | 2 +- Mage.Sets/src/mage/cards/m/MorbidHunger.java | 2 +- Mage.Sets/src/mage/cards/m/MorgueTheft.java | 2 +- .../src/mage/cards/m/MysticRetrieval.java | 2 +- .../src/mage/cards/m/MysticalTeachings.java | 2 +- .../src/mage/cards/n/NightbirdsClutches.java | 2 +- .../src/mage/cards/p/ParallelEvolution.java | 2 +- Mage.Sets/src/mage/cards/p/PastInFlames.java | 36 +++++++---------- .../src/mage/cards/p/PathToTheFestival.java | 2 +- .../src/mage/cards/p/PrismaticStrands.java | 2 +- .../src/mage/cards/p/PurifyTheGrave.java | 2 +- .../src/mage/cards/r/RallyThePeasants.java | 2 +- .../src/mage/cards/r/RayOfDistortion.java | 2 +- .../src/mage/cards/r/RayOfRevelation.java | 2 +- .../src/mage/cards/r/ReapTheSeagraf.java | 2 +- .../src/mage/cards/r/RecklessCharge.java | 2 +- Mage.Sets/src/mage/cards/r/Recoup.java | 40 ++++++++----------- Mage.Sets/src/mage/cards/r/RiteOfHarmony.java | 2 +- Mage.Sets/src/mage/cards/r/RoarOfTheWurm.java | 2 +- .../src/mage/cards/r/RollingTemblor.java | 2 +- Mage.Sets/src/mage/cards/r/RottenReunion.java | 2 +- Mage.Sets/src/mage/cards/s/SacredFire.java | 2 +- Mage.Sets/src/mage/cards/s/SavingGrasp.java | 2 +- .../src/mage/cards/s/ScorchingMissile.java | 2 +- .../mage/cards/s/ScourAllPossibilities.java | 2 +- .../src/mage/cards/s/SecretsOfTheKey.java | 2 +- Mage.Sets/src/mage/cards/s/SeizeTheDay.java | 2 +- .../src/mage/cards/s/SeverTheBloodline.java | 2 +- .../src/mage/cards/s/SevinnesReclamation.java | 2 +- .../src/mage/cards/s/ShadowbeastSighting.java | 2 +- .../src/mage/cards/s/ShatteredPerception.java | 2 +- .../src/mage/cards/s/SilentDeparture.java | 2 +- Mage.Sets/src/mage/cards/s/SkullFracture.java | 2 +- Mage.Sets/src/mage/cards/s/SmitingHelix.java | 2 +- .../src/mage/cards/s/SnapcasterMage.java | 22 +++------- .../src/mage/cards/s/SpiderSpawning.java | 2 +- Mage.Sets/src/mage/cards/s/SpiritFlare.java | 2 +- .../src/mage/cards/s/StranglingSoot.java | 2 +- Mage.Sets/src/mage/cards/s/StrikeItRich.java | 2 +- Mage.Sets/src/mage/cards/s/SylvanMight.java | 2 +- Mage.Sets/src/mage/cards/t/ThinkTwice.java | 2 +- .../src/mage/cards/t/ThrillOfTheHunt.java | 2 +- .../src/mage/cards/t/TrackersInstincts.java | 2 +- .../src/mage/cards/t/TraitorsClutch.java | 2 +- .../src/mage/cards/t/TravelPreparations.java | 2 +- Mage.Sets/src/mage/cards/t/TurnTheEarth.java | 2 +- Mage.Sets/src/mage/cards/u/UnburialRites.java | 2 +- Mage.Sets/src/mage/cards/v/VolcanicSpray.java | 2 +- .../src/mage/cards/v/VolleyOfBoulders.java | 2 +- Mage.Sets/src/mage/cards/w/WildHunger.java | 2 +- .../abilities/keyword/FlashbackAbility.java | 4 +- Mage/src/main/java/mage/cards/CardImpl.java | 2 +- Utils/keywords.txt | 2 +- 145 files changed, 192 insertions(+), 234 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AbandonThePost.java b/Mage.Sets/src/mage/cards/a/AbandonThePost.java index 267cc544add..f32e342fc9e 100644 --- a/Mage.Sets/src/mage/cards/a/AbandonThePost.java +++ b/Mage.Sets/src/mage/cards/a/AbandonThePost.java @@ -25,7 +25,7 @@ public final class AbandonThePost extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, 2)); // Flashback {3}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{R}"))); } private AbandonThePost(final AbandonThePost card) { diff --git a/Mage.Sets/src/mage/cards/a/AcornHarvest.java b/Mage.Sets/src/mage/cards/a/AcornHarvest.java index c52ef3374c4..ccd4e55a212 100644 --- a/Mage.Sets/src/mage/cards/a/AcornHarvest.java +++ b/Mage.Sets/src/mage/cards/a/AcornHarvest.java @@ -26,7 +26,7 @@ public final class AcornHarvest extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new SquirrelToken(), 2)); // Flashback-{1}{G} - Pay 3 life. - FlashbackAbility ability = new FlashbackAbility(new ManaCostsImpl("{1}{G}"), TimingRule.SORCERY); + FlashbackAbility ability = new FlashbackAbility(this, new ManaCostsImpl("{1}{G}")); ability.addCost(new PayLifeCost(3)); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/a/AncestralTribute.java b/Mage.Sets/src/mage/cards/a/AncestralTribute.java index 28bde5e75a6..ba9b95b70cb 100644 --- a/Mage.Sets/src/mage/cards/a/AncestralTribute.java +++ b/Mage.Sets/src/mage/cards/a/AncestralTribute.java @@ -27,7 +27,7 @@ public final class AncestralTribute extends CardImpl { this.getSpellAbility().addEffect(new GainLifeEffect((new CardsInControllerGraveyardCount(new FilterCard(), 2)))); // Flashback {9}{W}{W}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{9}{W}{W}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{9}{W}{W}{W}"))); } diff --git a/Mage.Sets/src/mage/cards/a/AncientGrudge.java b/Mage.Sets/src/mage/cards/a/AncientGrudge.java index 5503f190d37..733954463f0 100644 --- a/Mage.Sets/src/mage/cards/a/AncientGrudge.java +++ b/Mage.Sets/src/mage/cards/a/AncientGrudge.java @@ -23,7 +23,7 @@ public final class AncientGrudge extends CardImpl { this.getSpellAbility().addTarget(new TargetArtifactPermanent()); // Flashback {G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{G}"))); } private AncientGrudge(final AncientGrudge card) { diff --git a/Mage.Sets/src/mage/cards/a/AngelfireIgnition.java b/Mage.Sets/src/mage/cards/a/AngelfireIgnition.java index b7c091c891a..84d0f7c284b 100644 --- a/Mage.Sets/src/mage/cards/a/AngelfireIgnition.java +++ b/Mage.Sets/src/mage/cards/a/AngelfireIgnition.java @@ -42,7 +42,7 @@ public final class AngelfireIgnition extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {2}{R}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{2}{R}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{2}{R}{W}"))); } private AngelfireIgnition(final AngelfireIgnition card) { diff --git a/Mage.Sets/src/mage/cards/a/ArcaneInfusion.java b/Mage.Sets/src/mage/cards/a/ArcaneInfusion.java index 5bdc4825d76..33cb5d8ca39 100644 --- a/Mage.Sets/src/mage/cards/a/ArcaneInfusion.java +++ b/Mage.Sets/src/mage/cards/a/ArcaneInfusion.java @@ -32,7 +32,7 @@ public final class ArcaneInfusion extends CardImpl { "Put the rest on the bottom of your library in a random order.")); // Flashback {3}{U}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{U}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{U}{R}"))); } private ArcaneInfusion(final ArcaneInfusion card) { diff --git a/Mage.Sets/src/mage/cards/a/ArmyOfTheDamned.java b/Mage.Sets/src/mage/cards/a/ArmyOfTheDamned.java index 08285b4742f..a220f66c4e4 100644 --- a/Mage.Sets/src/mage/cards/a/ArmyOfTheDamned.java +++ b/Mage.Sets/src/mage/cards/a/ArmyOfTheDamned.java @@ -23,7 +23,7 @@ public final class ArmyOfTheDamned extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new ZombieToken(), 13, true, false)); // Flashback {7}{B}{B}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{7}{B}{B}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{7}{B}{B}{B}"))); } private ArmyOfTheDamned(final ArmyOfTheDamned card) { diff --git a/Mage.Sets/src/mage/cards/a/ArtfulDodge.java b/Mage.Sets/src/mage/cards/a/ArtfulDodge.java index 7f31e547ad5..c2d89265238 100644 --- a/Mage.Sets/src/mage/cards/a/ArtfulDodge.java +++ b/Mage.Sets/src/mage/cards/a/ArtfulDodge.java @@ -25,7 +25,7 @@ public final class ArtfulDodge extends CardImpl { this.getSpellAbility().addEffect(new CantBeBlockedTargetEffect()); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{U}"))); } private ArtfulDodge(final ArtfulDodge card) { diff --git a/Mage.Sets/src/mage/cards/b/BackdraftHellkite.java b/Mage.Sets/src/mage/cards/b/BackdraftHellkite.java index e500f89c212..712d08a1044 100644 --- a/Mage.Sets/src/mage/cards/b/BackdraftHellkite.java +++ b/Mage.Sets/src/mage/cards/b/BackdraftHellkite.java @@ -93,15 +93,7 @@ class BackdraftHellkiteEffect extends ContinuousEffectImpl { if (card == null) { return; } - FlashbackAbility ability = null; - if (card.isInstant(game)) { - ability = new FlashbackAbility(card.getManaCost(), TimingRule.INSTANT); - } else if (card.isSorcery(game)) { - ability = new FlashbackAbility(card.getManaCost(), TimingRule.SORCERY); - } - if (ability == null) { - return; - } + FlashbackAbility ability = new FlashbackAbility(card, card.getManaCost()); ability.setSourceId(cardId); ability.setControllerId(card.getOwnerId()); game.getState().addOtherAbility(card, ability); diff --git a/Mage.Sets/src/mage/cards/b/BashToBits.java b/Mage.Sets/src/mage/cards/b/BashToBits.java index 07a23d0c605..5f1db4474e4 100644 --- a/Mage.Sets/src/mage/cards/b/BashToBits.java +++ b/Mage.Sets/src/mage/cards/b/BashToBits.java @@ -27,7 +27,7 @@ public final class BashToBits extends CardImpl { Target target = new TargetArtifactPermanent(); this.getSpellAbility().addTarget(target); // Flashback {4}{R}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{R}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{R}{R}"))); } private BashToBits(final BashToBits card) { diff --git a/Mage.Sets/src/mage/cards/b/BattleScreech.java b/Mage.Sets/src/mage/cards/b/BattleScreech.java index 3922c90a5c1..feb673697ce 100644 --- a/Mage.Sets/src/mage/cards/b/BattleScreech.java +++ b/Mage.Sets/src/mage/cards/b/BattleScreech.java @@ -37,7 +37,7 @@ public final class BattleScreech extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new BirdToken(), 2)); // Flashback-Tap three untapped white creatures you control. - this.addAbility(new FlashbackAbility(new TapTargetCost(new TargetControlledCreaturePermanent(3,3, filter, true)), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new TapTargetCost(new TargetControlledCreaturePermanent(3,3, filter, true)))); } private BattleScreech(final BattleScreech card) { diff --git a/Mage.Sets/src/mage/cards/b/BeastAttack.java b/Mage.Sets/src/mage/cards/b/BeastAttack.java index b33e287feed..3f90496f984 100644 --- a/Mage.Sets/src/mage/cards/b/BeastAttack.java +++ b/Mage.Sets/src/mage/cards/b/BeastAttack.java @@ -24,7 +24,7 @@ public final class BeastAttack extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new BeastToken2())); // Flashback {2}{G}{G}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{G}{G}{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{G}{G}{G}"))); } private BeastAttack(final BeastAttack card) { diff --git a/Mage.Sets/src/mage/cards/b/BlastFromThePast.java b/Mage.Sets/src/mage/cards/b/BlastFromThePast.java index 72af5d013e7..97bdd6bc46a 100644 --- a/Mage.Sets/src/mage/cards/b/BlastFromThePast.java +++ b/Mage.Sets/src/mage/cards/b/BlastFromThePast.java @@ -35,7 +35,7 @@ public final class BlastFromThePast extends CardImpl { // Kicker {2}{R} this.addAbility(new KickerAbility("{2}{R}")); // Flashback {3}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{R}"))); // Buyback {4}{R} this.addAbility(new BuybackAbility("{4}{R}")); diff --git a/Mage.Sets/src/mage/cards/b/BumpInTheNight.java b/Mage.Sets/src/mage/cards/b/BumpInTheNight.java index 70072d56b65..4bb42bf6392 100644 --- a/Mage.Sets/src/mage/cards/b/BumpInTheNight.java +++ b/Mage.Sets/src/mage/cards/b/BumpInTheNight.java @@ -24,7 +24,7 @@ public final class BumpInTheNight extends CardImpl { this.getSpellAbility().addTarget(new TargetOpponent()); // Flashback {5}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{R}"))); } private BumpInTheNight(final BumpInTheNight card) { diff --git a/Mage.Sets/src/mage/cards/b/BurningOil.java b/Mage.Sets/src/mage/cards/b/BurningOil.java index 0eeaa8c0cd0..2106bcf3a7b 100644 --- a/Mage.Sets/src/mage/cards/b/BurningOil.java +++ b/Mage.Sets/src/mage/cards/b/BurningOil.java @@ -25,7 +25,7 @@ public final class BurningOil extends CardImpl { this.getSpellAbility().addTarget(new TargetAttackingOrBlockingCreature()); this.getSpellAbility().addEffect(new DamageTargetEffect(3)); // Flashback {3}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{W}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{W}"))); } private BurningOil(final BurningOil card) { diff --git a/Mage.Sets/src/mage/cards/c/CabalTherapy.java b/Mage.Sets/src/mage/cards/c/CabalTherapy.java index 597a6da161d..59cd97eef5f 100644 --- a/Mage.Sets/src/mage/cards/c/CabalTherapy.java +++ b/Mage.Sets/src/mage/cards/c/CabalTherapy.java @@ -12,7 +12,6 @@ import mage.cards.CardSetInfo; import mage.cards.Cards; import mage.constants.CardType; import mage.constants.Outcome; -import mage.constants.TimingRule; import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; @@ -36,9 +35,9 @@ public final class CabalTherapy extends CardImpl { this.getSpellAbility().addEffect(new CabalTherapyEffect()); // Flashback-Sacrifice a creature. - this.addAbility(new FlashbackAbility( - new SacrificeTargetCost(new TargetControlledCreaturePermanent(1, 1, StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT, true)), - TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new SacrificeTargetCost( + new TargetControlledCreaturePermanent(StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT) + ))); } private CabalTherapy(final CabalTherapy card) { diff --git a/Mage.Sets/src/mage/cards/c/CacklingCounterpart.java b/Mage.Sets/src/mage/cards/c/CacklingCounterpart.java index 92121d3c46f..290d6ae92c4 100644 --- a/Mage.Sets/src/mage/cards/c/CacklingCounterpart.java +++ b/Mage.Sets/src/mage/cards/c/CacklingCounterpart.java @@ -24,7 +24,7 @@ public final class CacklingCounterpart extends CardImpl { this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); // Flashback {5}{U}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{U}{U}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{U}{U}"))); } private CacklingCounterpart(final CacklingCounterpart card) { diff --git a/Mage.Sets/src/mage/cards/c/CalibratedBlast.java b/Mage.Sets/src/mage/cards/c/CalibratedBlast.java index 320cb6f805a..adf7d89564f 100644 --- a/Mage.Sets/src/mage/cards/c/CalibratedBlast.java +++ b/Mage.Sets/src/mage/cards/c/CalibratedBlast.java @@ -28,7 +28,7 @@ public final class CalibratedBlast extends CardImpl { this.getSpellAbility().addEffect(new CalibratedBlastEffect()); // Flashback {3}{R}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{R}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{R}{R}"))); } private CalibratedBlast(final CalibratedBlast card) { diff --git a/Mage.Sets/src/mage/cards/c/CallOfTheHerd.java b/Mage.Sets/src/mage/cards/c/CallOfTheHerd.java index 7a3b3cb7398..f5973d64a60 100644 --- a/Mage.Sets/src/mage/cards/c/CallOfTheHerd.java +++ b/Mage.Sets/src/mage/cards/c/CallOfTheHerd.java @@ -24,7 +24,7 @@ public final class CallOfTheHerd extends CardImpl { // Create a 3/3 green Elephant creature token. this.getSpellAbility().addEffect(new CreateTokenEffect(new ElephantToken())); // Flashback {3}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{G}"))); } private CallOfTheHerd(final CallOfTheHerd card) { diff --git a/Mage.Sets/src/mage/cards/c/CanopyClaws.java b/Mage.Sets/src/mage/cards/c/CanopyClaws.java index 1fb66de4464..5fe1dae8e80 100644 --- a/Mage.Sets/src/mage/cards/c/CanopyClaws.java +++ b/Mage.Sets/src/mage/cards/c/CanopyClaws.java @@ -26,7 +26,7 @@ public final class CanopyClaws extends CardImpl { this.getSpellAbility().addEffect(new LoseAbilityTargetEffect(FlyingAbility.getInstance(), Duration.EndOfTurn)); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{G}"))); } private CanopyClaws(final CanopyClaws card) { diff --git a/Mage.Sets/src/mage/cards/c/CantStayAway.java b/Mage.Sets/src/mage/cards/c/CantStayAway.java index 0d72ab474c0..813896ec60c 100644 --- a/Mage.Sets/src/mage/cards/c/CantStayAway.java +++ b/Mage.Sets/src/mage/cards/c/CantStayAway.java @@ -44,7 +44,7 @@ public final class CantStayAway extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(filter)); // Flashback {3}{W}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{W}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{W}{B}"))); } private CantStayAway(final CantStayAway card) { diff --git a/Mage.Sets/src/mage/cards/c/ChainersEdict.java b/Mage.Sets/src/mage/cards/c/ChainersEdict.java index b4956b0b7d8..c9b34236619 100644 --- a/Mage.Sets/src/mage/cards/c/ChainersEdict.java +++ b/Mage.Sets/src/mage/cards/c/ChainersEdict.java @@ -26,7 +26,7 @@ public final class ChainersEdict extends CardImpl { this.getSpellAbility().addTarget(new TargetPlayer()); // Flashback {5}{B}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{B}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{B}{B}"))); } private ChainersEdict(final ChainersEdict card) { diff --git a/Mage.Sets/src/mage/cards/c/ChatterOfTheSquirrel.java b/Mage.Sets/src/mage/cards/c/ChatterOfTheSquirrel.java index 0357a1e387f..f0ab9129de7 100644 --- a/Mage.Sets/src/mage/cards/c/ChatterOfTheSquirrel.java +++ b/Mage.Sets/src/mage/cards/c/ChatterOfTheSquirrel.java @@ -25,7 +25,7 @@ public final class ChatterOfTheSquirrel extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new SquirrelToken())); // Flashback {1}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{G}"))); } private ChatterOfTheSquirrel(final ChatterOfTheSquirrel card) { diff --git a/Mage.Sets/src/mage/cards/c/ChillOfForeboding.java b/Mage.Sets/src/mage/cards/c/ChillOfForeboding.java index aa52573d3a2..ed7c2f47ebc 100644 --- a/Mage.Sets/src/mage/cards/c/ChillOfForeboding.java +++ b/Mage.Sets/src/mage/cards/c/ChillOfForeboding.java @@ -23,7 +23,7 @@ public final class ChillOfForeboding extends CardImpl { this.getSpellAbility().addEffect(new MillCardsEachPlayerEffect(5, TargetController.ANY)); // Flashback {7}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{7}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{7}{U}"))); } private ChillOfForeboding(final ChillOfForeboding card) { diff --git a/Mage.Sets/src/mage/cards/c/CoffinPurge.java b/Mage.Sets/src/mage/cards/c/CoffinPurge.java index 8bd9edf701b..60cd3162de0 100644 --- a/Mage.Sets/src/mage/cards/c/CoffinPurge.java +++ b/Mage.Sets/src/mage/cards/c/CoffinPurge.java @@ -26,7 +26,7 @@ public final class CoffinPurge extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInGraveyard()); // Flashback {B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{B}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{B}"))); } private CoffinPurge(final CoffinPurge card) { diff --git a/Mage.Sets/src/mage/cards/c/Conflagrate.java b/Mage.Sets/src/mage/cards/c/Conflagrate.java index 0c9ed5d6fb7..f988a7b82d4 100644 --- a/Mage.Sets/src/mage/cards/c/Conflagrate.java +++ b/Mage.Sets/src/mage/cards/c/Conflagrate.java @@ -33,7 +33,7 @@ public final class Conflagrate extends CardImpl { this.getSpellAbility().addTarget(new TargetAnyTargetAmount(xValue)); // Flashback-{R}{R}, Discard X cards. - Ability ability = new FlashbackAbility(new ManaCostsImpl("{R}{R}"), TimingRule.SORCERY); + Ability ability = new FlashbackAbility(this, new ManaCostsImpl("{R}{R}")); ability.addCost(new DiscardXTargetCost(new FilterCard("cards"))); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/c/CorpseCobble.java b/Mage.Sets/src/mage/cards/c/CorpseCobble.java index 8cd490110b9..91d695c606d 100644 --- a/Mage.Sets/src/mage/cards/c/CorpseCobble.java +++ b/Mage.Sets/src/mage/cards/c/CorpseCobble.java @@ -38,7 +38,7 @@ public final class CorpseCobble extends CardImpl { this.getSpellAbility().addEffect(new CorpseCobbleEffect()); // Flashback {3}{U}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{U}{B}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{U}{B}"))); } private CorpseCobble(final CorpseCobble card) { diff --git a/Mage.Sets/src/mage/cards/c/CrawlFromTheCellar.java b/Mage.Sets/src/mage/cards/c/CrawlFromTheCellar.java index 804b31d1c9c..84d2f9f38d8 100644 --- a/Mage.Sets/src/mage/cards/c/CrawlFromTheCellar.java +++ b/Mage.Sets/src/mage/cards/c/CrawlFromTheCellar.java @@ -37,7 +37,7 @@ public final class CrawlFromTheCellar extends CardImpl { this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent(0, 1, filter, false)); // Flashback {3}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{B}"))); } private CrawlFromTheCellar(final CrawlFromTheCellar card) { diff --git a/Mage.Sets/src/mage/cards/c/CreepingRenaissance.java b/Mage.Sets/src/mage/cards/c/CreepingRenaissance.java index 7edb1ee3f15..10f09e1f1d3 100644 --- a/Mage.Sets/src/mage/cards/c/CreepingRenaissance.java +++ b/Mage.Sets/src/mage/cards/c/CreepingRenaissance.java @@ -33,7 +33,7 @@ public final class CreepingRenaissance extends CardImpl { this.getSpellAbility().addEffect(new CreepingRenaissanceEffect()); // Flashback {5}{G}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{G}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{G}{G}"))); } private CreepingRenaissance(final CreepingRenaissance card) { diff --git a/Mage.Sets/src/mage/cards/c/CripplingFatigue.java b/Mage.Sets/src/mage/cards/c/CripplingFatigue.java index ac4a7e05f6a..5a8a8e0b1ca 100644 --- a/Mage.Sets/src/mage/cards/c/CripplingFatigue.java +++ b/Mage.Sets/src/mage/cards/c/CripplingFatigue.java @@ -28,7 +28,7 @@ public final class CripplingFatigue extends CardImpl { this.getSpellAbility().addEffect(new BoostTargetEffect(-2, -2, Duration.EndOfTurn)); // Flashback-{1}{B}, Pay 3 life - Ability ability = new FlashbackAbility(new ManaCostsImpl("{1}{B}"), TimingRule.SORCERY); + Ability ability = new FlashbackAbility(this, new ManaCostsImpl("{1}{B}")); ability.addCost(new PayLifeCost(3)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/c/CroakingCounterpart.java b/Mage.Sets/src/mage/cards/c/CroakingCounterpart.java index 5fb85942c33..a7172e58211 100644 --- a/Mage.Sets/src/mage/cards/c/CroakingCounterpart.java +++ b/Mage.Sets/src/mage/cards/c/CroakingCounterpart.java @@ -42,7 +42,7 @@ public final class CroakingCounterpart extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent(filter)); // Flashback {3}{G}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{G}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{G}{U}"))); } private CroakingCounterpart(final CroakingCounterpart card) { diff --git a/Mage.Sets/src/mage/cards/c/CrushOfWurms.java b/Mage.Sets/src/mage/cards/c/CrushOfWurms.java index 208402f61b7..68b48a02f94 100644 --- a/Mage.Sets/src/mage/cards/c/CrushOfWurms.java +++ b/Mage.Sets/src/mage/cards/c/CrushOfWurms.java @@ -23,7 +23,7 @@ public final class CrushOfWurms extends CardImpl { // Put three 6/6 green Wurm creature tokens onto the battlefield. this.getSpellAbility().addEffect(new CreateTokenEffect(new WurmToken(), 3)); // Flashback {9}{G}{G}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{9}{G}{G}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{9}{G}{G}{G}"))); } private CrushOfWurms(final CrushOfWurms card) { diff --git a/Mage.Sets/src/mage/cards/d/DeadlyAllure.java b/Mage.Sets/src/mage/cards/d/DeadlyAllure.java index 3beea7c54c2..a7550779d91 100644 --- a/Mage.Sets/src/mage/cards/d/DeadlyAllure.java +++ b/Mage.Sets/src/mage/cards/d/DeadlyAllure.java @@ -33,7 +33,7 @@ public final class DeadlyAllure extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{G}"))); } diff --git a/Mage.Sets/src/mage/cards/d/DeepAnalysis.java b/Mage.Sets/src/mage/cards/d/DeepAnalysis.java index 8b9341d4ace..b9b51f2bd22 100644 --- a/Mage.Sets/src/mage/cards/d/DeepAnalysis.java +++ b/Mage.Sets/src/mage/cards/d/DeepAnalysis.java @@ -26,7 +26,7 @@ public final class DeepAnalysis extends CardImpl { this.getSpellAbility().addTarget(new TargetPlayer()); // Flashback-{1}{U}, Pay 3 life. - FlashbackAbility ability = new FlashbackAbility(new ManaCostsImpl("{1}{U}"), TimingRule.SORCERY); + FlashbackAbility ability = new FlashbackAbility(this, new ManaCostsImpl("{1}{U}")); ability.addCost(new PayLifeCost(3)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/d/DeepReconnaissance.java b/Mage.Sets/src/mage/cards/d/DeepReconnaissance.java index 651eed5e88f..14c9c7c6ce5 100644 --- a/Mage.Sets/src/mage/cards/d/DeepReconnaissance.java +++ b/Mage.Sets/src/mage/cards/d/DeepReconnaissance.java @@ -26,7 +26,7 @@ public final class DeepReconnaissance extends CardImpl { // Search your library for a basic land card and put that card onto the battlefield tapped. Then shuffle your library. this.getSpellAbility().addEffect(new SearchLibraryPutInPlayEffect(new TargetCardInLibrary(StaticFilters.FILTER_CARD_BASIC_LAND), true, true)); // Flashback {4}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{G}"))); } private DeepReconnaissance(final DeepReconnaissance card) { diff --git a/Mage.Sets/src/mage/cards/d/DefyGravity.java b/Mage.Sets/src/mage/cards/d/DefyGravity.java index 468aa3f8293..7c94505a3d0 100644 --- a/Mage.Sets/src/mage/cards/d/DefyGravity.java +++ b/Mage.Sets/src/mage/cards/d/DefyGravity.java @@ -26,7 +26,7 @@ public final class DefyGravity extends CardImpl { this.getSpellAbility().addEffect(new GainAbilityTargetEffect(FlyingAbility.getInstance(), Duration.EndOfTurn)); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{U}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{U}"))); } private DefyGravity(final DefyGravity card) { diff --git a/Mage.Sets/src/mage/cards/d/Dematerialize.java b/Mage.Sets/src/mage/cards/d/Dematerialize.java index 289d9aacf16..8a42a17c84c 100644 --- a/Mage.Sets/src/mage/cards/d/Dematerialize.java +++ b/Mage.Sets/src/mage/cards/d/Dematerialize.java @@ -25,7 +25,7 @@ public final class Dematerialize extends CardImpl { this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); this.getSpellAbility().addTarget(new TargetPermanent()); // Flashback {5}{U}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{U}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{U}{U}"))); } private Dematerialize(final Dematerialize card) { diff --git a/Mage.Sets/src/mage/cards/d/DesperateRavings.java b/Mage.Sets/src/mage/cards/d/DesperateRavings.java index b36ff25a4d9..868562aad8c 100644 --- a/Mage.Sets/src/mage/cards/d/DesperateRavings.java +++ b/Mage.Sets/src/mage/cards/d/DesperateRavings.java @@ -29,7 +29,7 @@ public final class DesperateRavings extends CardImpl { // Draw two cards, then discard a card at random. this.getSpellAbility().addEffect(new DesperateRavingsEffect()); // Flashback {2}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{U}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{U}"))); } private DesperateRavings(final DesperateRavings card) { diff --git a/Mage.Sets/src/mage/cards/d/DevilsPlay.java b/Mage.Sets/src/mage/cards/d/DevilsPlay.java index a6b959ccbd9..15b3150a291 100644 --- a/Mage.Sets/src/mage/cards/d/DevilsPlay.java +++ b/Mage.Sets/src/mage/cards/d/DevilsPlay.java @@ -26,7 +26,7 @@ public final class DevilsPlay extends CardImpl { this.getSpellAbility().addEffect(new DamageTargetEffect(ManacostVariableValue.REGULAR)); this.getSpellAbility().addTarget(new TargetAnyTarget()); // Flashback {X}{R}{R}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{X}{R}{R}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{X}{R}{R}{R}"))); } private DevilsPlay(final DevilsPlay card) { diff --git a/Mage.Sets/src/mage/cards/d/DireStrainRampage.java b/Mage.Sets/src/mage/cards/d/DireStrainRampage.java index 5b4e90df050..1b9f3feaeab 100644 --- a/Mage.Sets/src/mage/cards/d/DireStrainRampage.java +++ b/Mage.Sets/src/mage/cards/d/DireStrainRampage.java @@ -50,7 +50,7 @@ public final class DireStrainRampage extends CardImpl { this.getSpellAbility().addTarget(new TargetPermanent(filter)); // Flashback {3}{R}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{R}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{R}{G}"))); } private DireStrainRampage(final DireStrainRampage card) { diff --git a/Mage.Sets/src/mage/cards/d/DiregrafRebirth.java b/Mage.Sets/src/mage/cards/d/DiregrafRebirth.java index 3634044f72d..6a7d1186acf 100644 --- a/Mage.Sets/src/mage/cards/d/DiregrafRebirth.java +++ b/Mage.Sets/src/mage/cards/d/DiregrafRebirth.java @@ -37,7 +37,7 @@ public final class DiregrafRebirth extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); // Flashback {5}{B}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{5}{B}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{5}{B}{G}"))); } private DiregrafRebirth(final DiregrafRebirth card) { diff --git a/Mage.Sets/src/mage/cards/d/DivineReckoning.java b/Mage.Sets/src/mage/cards/d/DivineReckoning.java index bf59ff23b4c..946a25e9803 100644 --- a/Mage.Sets/src/mage/cards/d/DivineReckoning.java +++ b/Mage.Sets/src/mage/cards/d/DivineReckoning.java @@ -35,7 +35,7 @@ public final class DivineReckoning extends CardImpl { this.getSpellAbility().addEffect(new DivineReckoningEffect()); // Flashback {5}{W}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{W}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{W}{W}"))); } private DivineReckoning(final DivineReckoning card) { diff --git a/Mage.Sets/src/mage/cards/d/DralnuLichLord.java b/Mage.Sets/src/mage/cards/d/DralnuLichLord.java index 1470d9d36d8..fc97de46680 100644 --- a/Mage.Sets/src/mage/cards/d/DralnuLichLord.java +++ b/Mage.Sets/src/mage/cards/d/DralnuLichLord.java @@ -1,7 +1,5 @@ - package mage.cards.d; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; @@ -23,21 +21,24 @@ import mage.game.events.DamageEvent; import mage.game.events.GameEvent; import mage.target.common.TargetCardInYourGraveyard; +import java.util.UUID; + /** - * * @author emerald000 */ public final class DralnuLichLord extends CardImpl { - + private static final FilterCard filter = new FilterCard("instant or sorcery card in your graveyard"); + static { filter.add(Predicates.or( CardType.INSTANT.getPredicate(), - CardType.SORCERY.getPredicate())); + CardType.SORCERY.getPredicate() + )); } public DralnuLichLord(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{U}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{B}"); addSuperType(SuperType.LEGENDARY); this.subtype.add(SubType.ZOMBIE); this.subtype.add(SubType.WIZARD); @@ -46,10 +47,10 @@ public final class DralnuLichLord extends CardImpl { this.toughness = new MageInt(3); // If damage would be dealt to Dralnu, Lich Lord, sacrifice that many permanents instead. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new DralnuLichLordReplacementEffect())); - + this.addAbility(new SimpleStaticAbility(new DralnuLichLordReplacementEffect())); + // {tap}: Target instant or sorcery card in your graveyard gains flashback until end of turn. The flashback cost is equal to its mana cost. - Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DralnuLichLordFlashbackEffect(), new TapSourceCost()); + Ability ability = new SimpleActivatedAbility(new DralnuLichLordFlashbackEffect(), new TapSourceCost()); ability.addTarget(new TargetCardInYourGraveyard(filter)); this.addAbility(ability); } @@ -85,7 +86,7 @@ class DralnuLichLordReplacementEffect extends ReplacementEffectImpl { public boolean checksEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGE_PERMANENT; } - + @Override public boolean applies(GameEvent event, Ability source, Game game) { return event.getTargetId().equals(source.getSourceId()); @@ -117,13 +118,7 @@ class DralnuLichLordFlashbackEffect extends ContinuousEffectImpl { public boolean apply(Game game, Ability source) { Card card = game.getCard(targetPointer.getFirst(game, source)); if (card != null) { - FlashbackAbility ability; - if (card.isInstant(game)) { - ability = new FlashbackAbility(card.getManaCost(), TimingRule.INSTANT); - } - else { - ability = new FlashbackAbility(card.getManaCost(), TimingRule.SORCERY); - } + FlashbackAbility ability = new FlashbackAbility(card, card.getManaCost()); ability.setSourceId(card.getId()); ability.setControllerId(card.getOwnerId()); game.getState().addOtherAbility(card, ability); diff --git a/Mage.Sets/src/mage/cards/d/DreadReturn.java b/Mage.Sets/src/mage/cards/d/DreadReturn.java index f68946f58a0..76d8a35e5cd 100644 --- a/Mage.Sets/src/mage/cards/d/DreadReturn.java +++ b/Mage.Sets/src/mage/cards/d/DreadReturn.java @@ -30,7 +30,7 @@ public final class DreadReturn extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); // Flashback-Sacrifice three creatures. - this.addAbility(new FlashbackAbility(new SacrificeTargetCost(new TargetControlledPermanent(3, filter)), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new SacrificeTargetCost(new TargetControlledPermanent(3, filter)))); } private DreadReturn(final DreadReturn card) { diff --git a/Mage.Sets/src/mage/cards/d/DreamTwist.java b/Mage.Sets/src/mage/cards/d/DreamTwist.java index 96eca41bc01..5aadffe592c 100644 --- a/Mage.Sets/src/mage/cards/d/DreamTwist.java +++ b/Mage.Sets/src/mage/cards/d/DreamTwist.java @@ -25,7 +25,7 @@ public final class DreamTwist extends CardImpl { this.getSpellAbility().addEffect(new PutLibraryIntoGraveTargetEffect(3)); // Flashback {1}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{U}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{U}"))); } private DreamTwist(final DreamTwist card) { diff --git a/Mage.Sets/src/mage/cards/d/DryadsRevival.java b/Mage.Sets/src/mage/cards/d/DryadsRevival.java index e49405fc4b2..cdecd89d77c 100644 --- a/Mage.Sets/src/mage/cards/d/DryadsRevival.java +++ b/Mage.Sets/src/mage/cards/d/DryadsRevival.java @@ -24,7 +24,7 @@ public final class DryadsRevival extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInYourGraveyard()); // Flashback {4}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{4}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{4}{G}"))); } private DryadsRevival(final DryadsRevival card) { diff --git a/Mage.Sets/src/mage/cards/e/EarthRift.java b/Mage.Sets/src/mage/cards/e/EarthRift.java index 3a8b6ff66b6..812dab060f3 100644 --- a/Mage.Sets/src/mage/cards/e/EarthRift.java +++ b/Mage.Sets/src/mage/cards/e/EarthRift.java @@ -25,7 +25,7 @@ public final class EarthRift extends CardImpl { this.getSpellAbility().addEffect(new DestroyTargetEffect()); this.getSpellAbility().addTarget(new TargetLandPermanent()); // Flashback {5}{R}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{R}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{R}{R}"))); } private EarthRift(final EarthRift card) { diff --git a/Mage.Sets/src/mage/cards/e/EchoOfEons.java b/Mage.Sets/src/mage/cards/e/EchoOfEons.java index c831c4b65f4..742b095f6e8 100644 --- a/Mage.Sets/src/mage/cards/e/EchoOfEons.java +++ b/Mage.Sets/src/mage/cards/e/EchoOfEons.java @@ -27,7 +27,7 @@ public final class EchoOfEons extends CardImpl { this.getSpellAbility().addEffect(effect); // Flashback {2}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{U}"))); } private EchoOfEons(final EchoOfEons card) { diff --git a/Mage.Sets/src/mage/cards/e/ElectricRevelation.java b/Mage.Sets/src/mage/cards/e/ElectricRevelation.java index 363f68c5968..7396bf67331 100644 --- a/Mage.Sets/src/mage/cards/e/ElectricRevelation.java +++ b/Mage.Sets/src/mage/cards/e/ElectricRevelation.java @@ -26,7 +26,7 @@ public final class ElectricRevelation extends CardImpl { this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(2)); // Flashback {3}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{R}"))); } private ElectricRevelation(final ElectricRevelation card) { diff --git a/Mage.Sets/src/mage/cards/e/ElephantAmbush.java b/Mage.Sets/src/mage/cards/e/ElephantAmbush.java index 454ea235572..493271104f8 100644 --- a/Mage.Sets/src/mage/cards/e/ElephantAmbush.java +++ b/Mage.Sets/src/mage/cards/e/ElephantAmbush.java @@ -24,7 +24,7 @@ public final class ElephantAmbush extends CardImpl { // Create a 3/3 green Elephant creature token. this.getSpellAbility().addEffect(new CreateTokenEffect(new ElephantToken())); // Flashback {6}{G}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{6}{G}{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{6}{G}{G}"))); } private ElephantAmbush(final ElephantAmbush card) { diff --git a/Mage.Sets/src/mage/cards/e/Embolden.java b/Mage.Sets/src/mage/cards/e/Embolden.java index bc28d573110..ab93ab68df4 100644 --- a/Mage.Sets/src/mage/cards/e/Embolden.java +++ b/Mage.Sets/src/mage/cards/e/Embolden.java @@ -26,7 +26,7 @@ public final class Embolden extends CardImpl { this.getSpellAbility().addTarget(new TargetAnyTargetAmount(4)); // Flashback {1}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{W}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{W}"))); } diff --git a/Mage.Sets/src/mage/cards/e/EngulfingFlames.java b/Mage.Sets/src/mage/cards/e/EngulfingFlames.java index f837f06ad3d..7f81b63295d 100644 --- a/Mage.Sets/src/mage/cards/e/EngulfingFlames.java +++ b/Mage.Sets/src/mage/cards/e/EngulfingFlames.java @@ -28,7 +28,7 @@ public final class EngulfingFlames extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent()); this.getSpellAbility().addEffect(new CantRegenerateTargetEffect(Duration.EndOfTurn, "It")); // Flashback {3}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{R}"))); } private EngulfingFlames(final EngulfingFlames card) { diff --git a/Mage.Sets/src/mage/cards/f/FaithfulMending.java b/Mage.Sets/src/mage/cards/f/FaithfulMending.java index 7025bd611c7..ee079d3841e 100644 --- a/Mage.Sets/src/mage/cards/f/FaithfulMending.java +++ b/Mage.Sets/src/mage/cards/f/FaithfulMending.java @@ -26,7 +26,7 @@ public final class FaithfulMending extends CardImpl { this.getSpellAbility().addEffect(new DiscardControllerEffect(2).concatBy(", then")); // Flashback {1}{W}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{1}{W}{U}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{W}{U}"))); } private FaithfulMending(final FaithfulMending card) { diff --git a/Mage.Sets/src/mage/cards/f/FaithlessLooting.java b/Mage.Sets/src/mage/cards/f/FaithlessLooting.java index 754a3b292a8..c2c63fd555c 100644 --- a/Mage.Sets/src/mage/cards/f/FaithlessLooting.java +++ b/Mage.Sets/src/mage/cards/f/FaithlessLooting.java @@ -23,7 +23,7 @@ public final class FaithlessLooting extends CardImpl { // Draw two cards, then discard two cards. this.getSpellAbility().addEffect(new DrawDiscardControllerEffect(2,2)); // Flashback {2}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{R}"))); } private FaithlessLooting(final FaithlessLooting card) { diff --git a/Mage.Sets/src/mage/cards/f/FeelingOfDread.java b/Mage.Sets/src/mage/cards/f/FeelingOfDread.java index 71933458621..a47b8bf95b5 100644 --- a/Mage.Sets/src/mage/cards/f/FeelingOfDread.java +++ b/Mage.Sets/src/mage/cards/f/FeelingOfDread.java @@ -26,7 +26,7 @@ public final class FeelingOfDread extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, 2)); // Flashback {1}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{U}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{U}"))); } private FeelingOfDread(final FeelingOfDread card) { diff --git a/Mage.Sets/src/mage/cards/f/FerventDenial.java b/Mage.Sets/src/mage/cards/f/FerventDenial.java index 7c20d75425d..d7c4fc8f218 100644 --- a/Mage.Sets/src/mage/cards/f/FerventDenial.java +++ b/Mage.Sets/src/mage/cards/f/FerventDenial.java @@ -25,7 +25,7 @@ public final class FerventDenial extends CardImpl { this.getSpellAbility().addEffect(new CounterTargetEffect()); this.getSpellAbility().addTarget(new TargetSpell()); // Flashback {5}{U}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{U}{U}"),TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{U}{U}"))); } private FerventDenial(final FerventDenial card) { diff --git a/Mage.Sets/src/mage/cards/f/Firebolt.java b/Mage.Sets/src/mage/cards/f/Firebolt.java index 13c0f663759..a012d1cd264 100644 --- a/Mage.Sets/src/mage/cards/f/Firebolt.java +++ b/Mage.Sets/src/mage/cards/f/Firebolt.java @@ -25,7 +25,7 @@ public final class Firebolt extends CardImpl { this.getSpellAbility().addEffect(new DamageTargetEffect(2)); this.getSpellAbility().addTarget(new TargetAnyTarget()); // Flashback {4}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{R}"))); } private Firebolt(final Firebolt card) { diff --git a/Mage.Sets/src/mage/cards/f/FirecatBlitz.java b/Mage.Sets/src/mage/cards/f/FirecatBlitz.java index ea733f26ed0..c6527f4225f 100644 --- a/Mage.Sets/src/mage/cards/f/FirecatBlitz.java +++ b/Mage.Sets/src/mage/cards/f/FirecatBlitz.java @@ -41,7 +41,7 @@ public final class FirecatBlitz extends CardImpl { this.getSpellAbility().addEffect(new FirecatBlitzEffect()); // Flashback-{R}{R}, Sacrifice X Mountains. - Ability ability = new FlashbackAbility(new SacrificeXTargetCost(filter), TimingRule.SORCERY); + Ability ability = new FlashbackAbility(this, new SacrificeXTargetCost(filter)); ability.addManaCost(new ManaCostsImpl("{R}{R}")); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/f/FiresOfUndeath.java b/Mage.Sets/src/mage/cards/f/FiresOfUndeath.java index 987c9253440..7d8ceb162ab 100644 --- a/Mage.Sets/src/mage/cards/f/FiresOfUndeath.java +++ b/Mage.Sets/src/mage/cards/f/FiresOfUndeath.java @@ -25,7 +25,7 @@ public final class FiresOfUndeath extends CardImpl { this.getSpellAbility().addTarget(new TargetAnyTarget()); this.getSpellAbility().addEffect(new DamageTargetEffect(2)); // Flashback {5}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{B}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{B}"))); } private FiresOfUndeath(final FiresOfUndeath card) { diff --git a/Mage.Sets/src/mage/cards/f/FlaringPain.java b/Mage.Sets/src/mage/cards/f/FlaringPain.java index dc37fbd2593..4c80330baa0 100644 --- a/Mage.Sets/src/mage/cards/f/FlaringPain.java +++ b/Mage.Sets/src/mage/cards/f/FlaringPain.java @@ -24,7 +24,7 @@ public final class FlaringPain extends CardImpl { // Damage can't be prevented this turn. this.getSpellAbility().addEffect(new DamageCantBePreventedEffect(Duration.EndOfTurn, "Damage can't be prevented this turn", false, false)); // Flashback {R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{R}"))); } private FlaringPain(final FlaringPain card) { diff --git a/Mage.Sets/src/mage/cards/f/FlashOfDefiance.java b/Mage.Sets/src/mage/cards/f/FlashOfDefiance.java index b63c718e0e9..8635f90a1dc 100644 --- a/Mage.Sets/src/mage/cards/f/FlashOfDefiance.java +++ b/Mage.Sets/src/mage/cards/f/FlashOfDefiance.java @@ -38,7 +38,7 @@ public final class FlashOfDefiance extends CardImpl { this.getSpellAbility().addEffect(new CantBlockAllEffect(filter, Duration.EndOfTurn)); // Flashback-{1}{R}, Pay 3 life. - Ability ability = new FlashbackAbility(new ManaCostsImpl("{1}{R}"), TimingRule.SORCERY); + Ability ability = new FlashbackAbility(this, new ManaCostsImpl("{1}{R}")); ability.addCost(new PayLifeCost(3)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/f/FlashOfInsight.java b/Mage.Sets/src/mage/cards/f/FlashOfInsight.java index 6e1a3c210ab..c0eee07e074 100644 --- a/Mage.Sets/src/mage/cards/f/FlashOfInsight.java +++ b/Mage.Sets/src/mage/cards/f/FlashOfInsight.java @@ -37,7 +37,7 @@ public final class FlashOfInsight extends CardImpl { this.getSpellAbility().addEffect(new FlashOfInsightEffect()); // Flashback-{1}{U}, Exile X blue cards from your graveyard. - Ability ability = new FlashbackAbility(new ManaCostsImpl("{1}{U}"), TimingRule.INSTANT); + Ability ability = new FlashbackAbility(this, new ManaCostsImpl("{1}{U}")); FilterCard filter = new FilterCard("blue cards from your graveyard"); filter.add(new ColorPredicate(ObjectColor.BLUE)); filter.add(Predicates.not(new CardIdPredicate(getId()))); diff --git a/Mage.Sets/src/mage/cards/f/FolkMedicine.java b/Mage.Sets/src/mage/cards/f/FolkMedicine.java index 84634147aa0..aead1d97917 100644 --- a/Mage.Sets/src/mage/cards/f/FolkMedicine.java +++ b/Mage.Sets/src/mage/cards/f/FolkMedicine.java @@ -25,7 +25,7 @@ public final class FolkMedicine extends CardImpl { DynamicValue amount = new PermanentsOnBattlefieldCount(StaticFilters.FILTER_CONTROLLED_CREATURE); this.getSpellAbility().addEffect(new GainLifeEffect(amount)); // Flashback {1}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{W}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{W}"))); } private FolkMedicine(final FolkMedicine card) { diff --git a/Mage.Sets/src/mage/cards/f/ForbiddenAlchemy.java b/Mage.Sets/src/mage/cards/f/ForbiddenAlchemy.java index 0c7335a50e4..cdc2e92ae55 100644 --- a/Mage.Sets/src/mage/cards/f/ForbiddenAlchemy.java +++ b/Mage.Sets/src/mage/cards/f/ForbiddenAlchemy.java @@ -26,7 +26,7 @@ public final class ForbiddenAlchemy extends CardImpl { StaticFilters.FILTER_CARD, Zone.GRAVEYARD, false, false, false, Zone.HAND, false)); // Flashback {6}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{6}{B}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{6}{B}"))); } private ForbiddenAlchemy(final ForbiddenAlchemy card) { diff --git a/Mage.Sets/src/mage/cards/g/GalvanicIteration.java b/Mage.Sets/src/mage/cards/g/GalvanicIteration.java index f2756748012..892dbebc041 100644 --- a/Mage.Sets/src/mage/cards/g/GalvanicIteration.java +++ b/Mage.Sets/src/mage/cards/g/GalvanicIteration.java @@ -29,7 +29,7 @@ public final class GalvanicIteration extends CardImpl { this.getSpellAbility().addEffect(new CreateDelayedTriggeredAbilityEffect(new GalvanicIterationAbility())); // Flashback {1}{U}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{1}{U}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{U}{R}"))); } private GalvanicIteration(final GalvanicIteration card) { diff --git a/Mage.Sets/src/mage/cards/g/GazeOfJustice.java b/Mage.Sets/src/mage/cards/g/GazeOfJustice.java index d7b02929bdf..12647541ddf 100644 --- a/Mage.Sets/src/mage/cards/g/GazeOfJustice.java +++ b/Mage.Sets/src/mage/cards/g/GazeOfJustice.java @@ -41,7 +41,7 @@ public final class GazeOfJustice extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {5}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{W}"))); } private GazeOfJustice(final GazeOfJustice card) { diff --git a/Mage.Sets/src/mage/cards/g/Geistflame.java b/Mage.Sets/src/mage/cards/g/Geistflame.java index e783c6ba0a3..7d8c774cd65 100644 --- a/Mage.Sets/src/mage/cards/g/Geistflame.java +++ b/Mage.Sets/src/mage/cards/g/Geistflame.java @@ -25,7 +25,7 @@ public final class Geistflame extends CardImpl { this.getSpellAbility().addTarget(new TargetAnyTarget()); // Flashback {3}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{R}"))); } private Geistflame(final Geistflame card) { diff --git a/Mage.Sets/src/mage/cards/g/GhoulcallersHarvest.java b/Mage.Sets/src/mage/cards/g/GhoulcallersHarvest.java index f33a3f72459..e4173903f24 100644 --- a/Mage.Sets/src/mage/cards/g/GhoulcallersHarvest.java +++ b/Mage.Sets/src/mage/cards/g/GhoulcallersHarvest.java @@ -39,7 +39,7 @@ public final class GhoulcallersHarvest extends CardImpl { this.getSpellAbility().addHint(hint); // Flashback {3}{B}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{B}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{B}{G}"))); } private GhoulcallersHarvest(final GhoulcallersHarvest card) { diff --git a/Mage.Sets/src/mage/cards/g/GnawToTheBone.java b/Mage.Sets/src/mage/cards/g/GnawToTheBone.java index 288092df271..26949fb457d 100644 --- a/Mage.Sets/src/mage/cards/g/GnawToTheBone.java +++ b/Mage.Sets/src/mage/cards/g/GnawToTheBone.java @@ -26,7 +26,7 @@ public final class GnawToTheBone extends CardImpl { this.getSpellAbility().addEffect(new GainLifeEffect(value)); // Flashback {2}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{G}"))); } private GnawToTheBone(final GnawToTheBone card) { diff --git a/Mage.Sets/src/mage/cards/g/GraspOfPhantoms.java b/Mage.Sets/src/mage/cards/g/GraspOfPhantoms.java index da3ca2980b0..31fb236694e 100644 --- a/Mage.Sets/src/mage/cards/g/GraspOfPhantoms.java +++ b/Mage.Sets/src/mage/cards/g/GraspOfPhantoms.java @@ -26,7 +26,7 @@ public final class GraspOfPhantoms extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {7}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{7}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{7}{U}"))); } private GraspOfPhantoms(final GraspOfPhantoms card) { diff --git a/Mage.Sets/src/mage/cards/g/GrizzlyFate.java b/Mage.Sets/src/mage/cards/g/GrizzlyFate.java index 7a79d495b12..fe9b432feb8 100644 --- a/Mage.Sets/src/mage/cards/g/GrizzlyFate.java +++ b/Mage.Sets/src/mage/cards/g/GrizzlyFate.java @@ -33,7 +33,7 @@ public final class GrizzlyFate extends CardImpl { this.getSpellAbility().addEffect(effect); // Flashback {5}{G}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{G}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{G}{G}"))); } private GrizzlyFate(final GrizzlyFate card) { diff --git a/Mage.Sets/src/mage/cards/h/HomesteadCourage.java b/Mage.Sets/src/mage/cards/h/HomesteadCourage.java index 6e1986628ec..2b011596836 100644 --- a/Mage.Sets/src/mage/cards/h/HomesteadCourage.java +++ b/Mage.Sets/src/mage/cards/h/HomesteadCourage.java @@ -31,7 +31,7 @@ public final class HomesteadCourage extends CardImpl { this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); // Flashback {W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{W}"))); } private HomesteadCourage(final HomesteadCourage card) { diff --git a/Mage.Sets/src/mage/cards/h/HowlingGale.java b/Mage.Sets/src/mage/cards/h/HowlingGale.java index 1ec9b6e4a58..7c8ae9fae78 100644 --- a/Mage.Sets/src/mage/cards/h/HowlingGale.java +++ b/Mage.Sets/src/mage/cards/h/HowlingGale.java @@ -36,7 +36,7 @@ public final class HowlingGale extends CardImpl { effect.setText("and each player"); this.getSpellAbility().addEffect(effect); // Flashback {1}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{G}"))); } private HowlingGale(final HowlingGale card) { diff --git a/Mage.Sets/src/mage/cards/h/HungryForMore.java b/Mage.Sets/src/mage/cards/h/HungryForMore.java index 3e527743025..2c91750e367 100644 --- a/Mage.Sets/src/mage/cards/h/HungryForMore.java +++ b/Mage.Sets/src/mage/cards/h/HungryForMore.java @@ -31,7 +31,7 @@ public final class HungryForMore extends CardImpl { this.getSpellAbility().addEffect(new HungryForMoreEffect()); // Flashback {1}{B}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{1}{B}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{B}{R}"))); } private HungryForMore(final HungryForMore card) { diff --git a/Mage.Sets/src/mage/cards/i/IgniteTheFuture.java b/Mage.Sets/src/mage/cards/i/IgniteTheFuture.java index 9fdf7a6aaa2..4883b16f657 100644 --- a/Mage.Sets/src/mage/cards/i/IgniteTheFuture.java +++ b/Mage.Sets/src/mage/cards/i/IgniteTheFuture.java @@ -28,7 +28,7 @@ public final class IgniteTheFuture extends CardImpl { this.getSpellAbility().addEffect(new IgniteTheFutureEffect()); // Flashback {7}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{7}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{7}{R}"))); } private IgniteTheFuture(final IgniteTheFuture card) { diff --git a/Mage.Sets/src/mage/cards/i/IncreasingAmbition.java b/Mage.Sets/src/mage/cards/i/IncreasingAmbition.java index eae6ce33edc..f32b6cfb445 100644 --- a/Mage.Sets/src/mage/cards/i/IncreasingAmbition.java +++ b/Mage.Sets/src/mage/cards/i/IncreasingAmbition.java @@ -37,7 +37,7 @@ public final class IncreasingAmbition extends CardImpl { )); // Flashback {7}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{7}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{7}{B}"))); } private IncreasingAmbition(final IncreasingAmbition card) { diff --git a/Mage.Sets/src/mage/cards/i/IncreasingConfusion.java b/Mage.Sets/src/mage/cards/i/IncreasingConfusion.java index 410c76d2e72..7e6f63dd965 100644 --- a/Mage.Sets/src/mage/cards/i/IncreasingConfusion.java +++ b/Mage.Sets/src/mage/cards/i/IncreasingConfusion.java @@ -29,7 +29,7 @@ public final class IncreasingConfusion extends CardImpl { this.getSpellAbility().addTarget(new TargetPlayer()); // Flashback {X}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{X}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{X}{U}"))); } private IncreasingConfusion(final IncreasingConfusion card) { diff --git a/Mage.Sets/src/mage/cards/i/IncreasingDevotion.java b/Mage.Sets/src/mage/cards/i/IncreasingDevotion.java index 7c78a06360c..fa318c4bddd 100644 --- a/Mage.Sets/src/mage/cards/i/IncreasingDevotion.java +++ b/Mage.Sets/src/mage/cards/i/IncreasingDevotion.java @@ -29,7 +29,7 @@ public final class IncreasingDevotion extends CardImpl { this.getSpellAbility().addEffect(new IncreasingDevotionEffect()); // Flashback {7}{W}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{7}{W}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{7}{W}{W}"))); } private IncreasingDevotion(final IncreasingDevotion card) { diff --git a/Mage.Sets/src/mage/cards/i/IncreasingSavagery.java b/Mage.Sets/src/mage/cards/i/IncreasingSavagery.java index 096e5b95f1b..5eddaae8427 100644 --- a/Mage.Sets/src/mage/cards/i/IncreasingSavagery.java +++ b/Mage.Sets/src/mage/cards/i/IncreasingSavagery.java @@ -30,7 +30,7 @@ public final class IncreasingSavagery extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {5}{G}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{G}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{G}{G}"))); } private IncreasingSavagery(final IncreasingSavagery card) { diff --git a/Mage.Sets/src/mage/cards/i/IncreasingVengeance.java b/Mage.Sets/src/mage/cards/i/IncreasingVengeance.java index 166fef9d023..78b4b1d479a 100644 --- a/Mage.Sets/src/mage/cards/i/IncreasingVengeance.java +++ b/Mage.Sets/src/mage/cards/i/IncreasingVengeance.java @@ -41,7 +41,7 @@ public final class IncreasingVengeance extends CardImpl { this.getSpellAbility().addTarget(target); // Flashback {3}{R}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{R}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{R}{R}"))); } private IncreasingVengeance(final IncreasingVengeance card) { diff --git a/Mage.Sets/src/mage/cards/j/JoinTheDance.java b/Mage.Sets/src/mage/cards/j/JoinTheDance.java index 78f62ccdd88..a62b20cd8dd 100644 --- a/Mage.Sets/src/mage/cards/j/JoinTheDance.java +++ b/Mage.Sets/src/mage/cards/j/JoinTheDance.java @@ -23,7 +23,7 @@ public final class JoinTheDance extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new HumanToken(), 2)); // Flashback {3}{G}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{G}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{G}{W}"))); } private JoinTheDance(final JoinTheDance card) { diff --git a/Mage.Sets/src/mage/cards/k/Kaleidoscorch.java b/Mage.Sets/src/mage/cards/k/Kaleidoscorch.java index 8eaf49672ea..8bee5084d56 100644 --- a/Mage.Sets/src/mage/cards/k/Kaleidoscorch.java +++ b/Mage.Sets/src/mage/cards/k/Kaleidoscorch.java @@ -28,7 +28,7 @@ public final class Kaleidoscorch extends CardImpl { this.getSpellAbility().setAbilityWord(AbilityWord.CONVERGE); // Flashback {4}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{4}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{4}{R}"))); } private Kaleidoscorch(final Kaleidoscorch card) { diff --git a/Mage.Sets/src/mage/cards/k/KrosanReclamation.java b/Mage.Sets/src/mage/cards/k/KrosanReclamation.java index fb95f86803c..56730558301 100644 --- a/Mage.Sets/src/mage/cards/k/KrosanReclamation.java +++ b/Mage.Sets/src/mage/cards/k/KrosanReclamation.java @@ -26,7 +26,7 @@ public final class KrosanReclamation extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInTargetPlayersGraveyard(2)); // Flashback {1}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{G}"))); } private KrosanReclamation(final KrosanReclamation card) { diff --git a/Mage.Sets/src/mage/cards/l/LavaDart.java b/Mage.Sets/src/mage/cards/l/LavaDart.java index b6228d7b6db..8842cc5176a 100644 --- a/Mage.Sets/src/mage/cards/l/LavaDart.java +++ b/Mage.Sets/src/mage/cards/l/LavaDart.java @@ -34,7 +34,7 @@ public final class LavaDart extends CardImpl { this.getSpellAbility().addTarget(new TargetAnyTarget()); // Flashback-Sacrifice a Mountain. - this.addAbility(new FlashbackAbility(new SacrificeTargetCost(new TargetControlledPermanent(filter)), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new SacrificeTargetCost(new TargetControlledPermanent(filter)))); } private LavaDart(final LavaDart card) { diff --git a/Mage.Sets/src/mage/cards/l/LierDiscipleOfTheDrowned.java b/Mage.Sets/src/mage/cards/l/LierDiscipleOfTheDrowned.java index 87af1a11a5f..2eb3748b1a3 100644 --- a/Mage.Sets/src/mage/cards/l/LierDiscipleOfTheDrowned.java +++ b/Mage.Sets/src/mage/cards/l/LierDiscipleOfTheDrowned.java @@ -104,9 +104,7 @@ class LierDiscipleOfTheDrownedFlashbackEffect extends ContinuousEffectImpl { return false; } for (Card card : player.getGraveyard().getCards(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY, game)) { - Ability ability = new FlashbackAbility( - card.getManaCost(), card.isInstant(game) ? TimingRule.INSTANT : TimingRule.SORCERY - ); + Ability ability = new FlashbackAbility(card, card.getManaCost()); ability.setSourceId(card.getId()); ability.setControllerId(card.getOwnerId()); game.getState().addOtherAbility(card, ability); diff --git a/Mage.Sets/src/mage/cards/l/LightningSurge.java b/Mage.Sets/src/mage/cards/l/LightningSurge.java index 3d2aec4148a..0c2bdf6f2e5 100644 --- a/Mage.Sets/src/mage/cards/l/LightningSurge.java +++ b/Mage.Sets/src/mage/cards/l/LightningSurge.java @@ -33,7 +33,7 @@ public final class LightningSurge extends CardImpl { this.getSpellAbility().addEffect(effect); // Flashback {5}{R}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{R}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{R}{R}"))); } private LightningSurge(final LightningSurge card) { diff --git a/Mage.Sets/src/mage/cards/l/LingeringSouls.java b/Mage.Sets/src/mage/cards/l/LingeringSouls.java index 51d53922b21..109d50883ae 100644 --- a/Mage.Sets/src/mage/cards/l/LingeringSouls.java +++ b/Mage.Sets/src/mage/cards/l/LingeringSouls.java @@ -23,7 +23,7 @@ public final class LingeringSouls extends CardImpl { // Create two 1/1 white Spirit creature tokens with flying. this.getSpellAbility().addEffect(new CreateTokenEffect(new SpiritWhiteToken(), 2)); // Flashback {1}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{B}"))); } private LingeringSouls(final LingeringSouls card) { diff --git a/Mage.Sets/src/mage/cards/m/MarshalingCry.java b/Mage.Sets/src/mage/cards/m/MarshalingCry.java index e02d033bcd3..7a2535de656 100644 --- a/Mage.Sets/src/mage/cards/m/MarshalingCry.java +++ b/Mage.Sets/src/mage/cards/m/MarshalingCry.java @@ -36,7 +36,7 @@ public final class MarshalingCry extends CardImpl { this.addAbility(new CyclingAbility(new ManaCostsImpl("{2}"))); // Flashback {3}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{W}"))); } private MarshalingCry(final MarshalingCry card) { diff --git a/Mage.Sets/src/mage/cards/m/MassDiminish.java b/Mage.Sets/src/mage/cards/m/MassDiminish.java index d11f660cbee..4cc90e77d00 100644 --- a/Mage.Sets/src/mage/cards/m/MassDiminish.java +++ b/Mage.Sets/src/mage/cards/m/MassDiminish.java @@ -31,7 +31,7 @@ public final class MassDiminish extends CardImpl { this.getSpellAbility().addTarget(new TargetPlayer()); // Flashback {3}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{U}"))); } private MassDiminish(final MassDiminish card) { diff --git a/Mage.Sets/src/mage/cards/m/MemorysJourney.java b/Mage.Sets/src/mage/cards/m/MemorysJourney.java index cd3dba5db43..9ed78043b07 100644 --- a/Mage.Sets/src/mage/cards/m/MemorysJourney.java +++ b/Mage.Sets/src/mage/cards/m/MemorysJourney.java @@ -26,7 +26,7 @@ public final class MemorysJourney extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInTargetPlayersGraveyard(3)); // Flashback {G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{G}"))); } private MemorysJourney(final MemorysJourney card) { diff --git a/Mage.Sets/src/mage/cards/m/MoanOfTheUnhallowed.java b/Mage.Sets/src/mage/cards/m/MoanOfTheUnhallowed.java index 3e59ada135b..4b3ccb090c7 100644 --- a/Mage.Sets/src/mage/cards/m/MoanOfTheUnhallowed.java +++ b/Mage.Sets/src/mage/cards/m/MoanOfTheUnhallowed.java @@ -24,7 +24,7 @@ public final class MoanOfTheUnhallowed extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new ZombieToken(), 2)); // Flashback {5}{B}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{B}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{B}{B}"))); } private MoanOfTheUnhallowed(final MoanOfTheUnhallowed card) { diff --git a/Mage.Sets/src/mage/cards/m/MomentaryBlink.java b/Mage.Sets/src/mage/cards/m/MomentaryBlink.java index b1e127743e1..16da3515398 100644 --- a/Mage.Sets/src/mage/cards/m/MomentaryBlink.java +++ b/Mage.Sets/src/mage/cards/m/MomentaryBlink.java @@ -26,7 +26,7 @@ public final class MomentaryBlink extends CardImpl { this.getSpellAbility().addEffect(new ReturnToBattlefieldUnderOwnerControlTargetEffect(false, false)); // Flashback {3}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{U}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{U}"))); } private MomentaryBlink(final MomentaryBlink card) { diff --git a/Mage.Sets/src/mage/cards/m/MomentsPeace.java b/Mage.Sets/src/mage/cards/m/MomentsPeace.java index ffc81e051a8..6fa6f598d54 100644 --- a/Mage.Sets/src/mage/cards/m/MomentsPeace.java +++ b/Mage.Sets/src/mage/cards/m/MomentsPeace.java @@ -24,7 +24,7 @@ public final class MomentsPeace extends CardImpl { this.getSpellAbility().addEffect(new PreventAllDamageByAllPermanentsEffect(Duration.EndOfTurn, true)); // Flashback {2}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{G}"))); } private MomentsPeace(final MomentsPeace card) { diff --git a/Mage.Sets/src/mage/cards/m/MorbidHunger.java b/Mage.Sets/src/mage/cards/m/MorbidHunger.java index ef554322be9..60453a85eee 100644 --- a/Mage.Sets/src/mage/cards/m/MorbidHunger.java +++ b/Mage.Sets/src/mage/cards/m/MorbidHunger.java @@ -27,7 +27,7 @@ public final class MorbidHunger extends CardImpl { this.getSpellAbility().addTarget(new TargetAnyTarget()); this.getSpellAbility().addEffect(new GainLifeEffect(3)); // Flashback {7}{B}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{7}{B}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{7}{B}{B}"))); } diff --git a/Mage.Sets/src/mage/cards/m/MorgueTheft.java b/Mage.Sets/src/mage/cards/m/MorgueTheft.java index 2063f2697ad..c2046bc60ca 100644 --- a/Mage.Sets/src/mage/cards/m/MorgueTheft.java +++ b/Mage.Sets/src/mage/cards/m/MorgueTheft.java @@ -24,7 +24,7 @@ public final class MorgueTheft extends CardImpl { this.getSpellAbility().addEffect(new ReturnFromGraveyardToHandTargetEffect()); this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE)); // Flashback {4}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{B}"))); } diff --git a/Mage.Sets/src/mage/cards/m/MysticRetrieval.java b/Mage.Sets/src/mage/cards/m/MysticRetrieval.java index 0bdf0f18601..36fd978faf8 100644 --- a/Mage.Sets/src/mage/cards/m/MysticRetrieval.java +++ b/Mage.Sets/src/mage/cards/m/MysticRetrieval.java @@ -34,7 +34,7 @@ public final class MysticRetrieval extends CardImpl { this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(filter)); // Flashback {2}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{R}"))); } private MysticRetrieval(final MysticRetrieval card) { diff --git a/Mage.Sets/src/mage/cards/m/MysticalTeachings.java b/Mage.Sets/src/mage/cards/m/MysticalTeachings.java index 5f2075c478f..3678e99a796 100644 --- a/Mage.Sets/src/mage/cards/m/MysticalTeachings.java +++ b/Mage.Sets/src/mage/cards/m/MysticalTeachings.java @@ -37,7 +37,7 @@ public final class MysticalTeachings extends CardImpl { // Search your library for an instant card or a card with flash, reveal it, and put it into your hand. Then shuffle your library. this.getSpellAbility().addEffect(new SearchLibraryPutInHandEffect(new TargetCardInLibrary(filter), true, true)); // Flashback {5}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{B}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{B}"))); } private MysticalTeachings(final MysticalTeachings card) { diff --git a/Mage.Sets/src/mage/cards/n/NightbirdsClutches.java b/Mage.Sets/src/mage/cards/n/NightbirdsClutches.java index 64cf3b93a3c..d5a6ade10ee 100644 --- a/Mage.Sets/src/mage/cards/n/NightbirdsClutches.java +++ b/Mage.Sets/src/mage/cards/n/NightbirdsClutches.java @@ -26,7 +26,7 @@ public final class NightbirdsClutches extends CardImpl { this.getSpellAbility().addEffect(new CantBlockTargetEffect(Duration.EndOfTurn)); this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, 2)); // Flashback {3}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{R}"))); } private NightbirdsClutches(final NightbirdsClutches card) { diff --git a/Mage.Sets/src/mage/cards/p/ParallelEvolution.java b/Mage.Sets/src/mage/cards/p/ParallelEvolution.java index 764fb263858..46ce42596c3 100644 --- a/Mage.Sets/src/mage/cards/p/ParallelEvolution.java +++ b/Mage.Sets/src/mage/cards/p/ParallelEvolution.java @@ -31,7 +31,7 @@ public final class ParallelEvolution extends CardImpl { this.getSpellAbility().addEffect(new ParallelEvolutionEffect()); // Flashback {4}{G}{G}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{G}{G}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{G}{G}{G}"))); } private ParallelEvolution(final ParallelEvolution card) { diff --git a/Mage.Sets/src/mage/cards/p/PastInFlames.java b/Mage.Sets/src/mage/cards/p/PastInFlames.java index 509c98ec848..4477908a722 100644 --- a/Mage.Sets/src/mage/cards/p/PastInFlames.java +++ b/Mage.Sets/src/mage/cards/p/PastInFlames.java @@ -22,12 +22,11 @@ public final class PastInFlames extends CardImpl { public PastInFlames(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{R}"); - // Each instant and sorcery card in your graveyard gains flashback until end of turn. The flashback cost is equal to its mana cost. this.getSpellAbility().addEffect(new PastInFlamesEffect()); // Flashback {4}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{4}{R}"))); } @@ -73,25 +72,18 @@ class PastInFlamesEffect extends ContinuousEffectImpl { @Override public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); - if (player != null) { - player.getGraveyard().stream().filter((cardId) -> (affectedObjectList.contains(new MageObjectReference(cardId, game)))).forEachOrdered((cardId) -> { - Card card = game.getCard(cardId); - if (card != null) { - FlashbackAbility ability = null; - if (card.isInstant(game)) { - ability = new FlashbackAbility(card.getManaCost(), TimingRule.INSTANT); - } else if (card.isSorcery(game)) { - ability = new FlashbackAbility(card.getManaCost(), TimingRule.SORCERY); - } - if (ability != null) { - ability.setSourceId(cardId); - ability.setControllerId(card.getOwnerId()); - game.getState().addOtherAbility(card, ability); - } - } - }); - return true; + if (player == null) { + return false; } - return false; + player.getGraveyard().stream().filter((cardId) -> (affectedObjectList.contains(new MageObjectReference(cardId, game)))).forEachOrdered((cardId) -> { + Card card = game.getCard(cardId); + if (card != null) { + FlashbackAbility ability = new FlashbackAbility(card, card.getManaCost()); + ability.setSourceId(cardId); + ability.setControllerId(card.getOwnerId()); + game.getState().addOtherAbility(card, ability); + } + }); + return true; } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/p/PathToTheFestival.java b/Mage.Sets/src/mage/cards/p/PathToTheFestival.java index 2b9bb72719c..3964b46d1ec 100644 --- a/Mage.Sets/src/mage/cards/p/PathToTheFestival.java +++ b/Mage.Sets/src/mage/cards/p/PathToTheFestival.java @@ -39,7 +39,7 @@ public final class PathToTheFestival extends CardImpl { this.getSpellAbility().addHint(DomainHint.instance); // Flashback {4}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{4}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{4}{G}"))); } private PathToTheFestival(final PathToTheFestival card) { diff --git a/Mage.Sets/src/mage/cards/p/PrismaticStrands.java b/Mage.Sets/src/mage/cards/p/PrismaticStrands.java index ac8e713ca25..803605e2a27 100644 --- a/Mage.Sets/src/mage/cards/p/PrismaticStrands.java +++ b/Mage.Sets/src/mage/cards/p/PrismaticStrands.java @@ -44,7 +44,7 @@ public final class PrismaticStrands extends CardImpl { this.getSpellAbility().addEffect(new PrismaticStrandsEffect()); // Flashback-Tap an untapped white creature you control. - this.addAbility(new FlashbackAbility(new TapTargetCost(new TargetControlledCreaturePermanent(1, 1, filter, false)), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new TapTargetCost(new TargetControlledCreaturePermanent(1, 1, filter, false)))); } private PrismaticStrands(final PrismaticStrands card) { diff --git a/Mage.Sets/src/mage/cards/p/PurifyTheGrave.java b/Mage.Sets/src/mage/cards/p/PurifyTheGrave.java index 4c75313121c..cca1e6de826 100644 --- a/Mage.Sets/src/mage/cards/p/PurifyTheGrave.java +++ b/Mage.Sets/src/mage/cards/p/PurifyTheGrave.java @@ -26,7 +26,7 @@ public final class PurifyTheGrave extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInGraveyard()); // Flashback {W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{W}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{W}"))); } private PurifyTheGrave(final PurifyTheGrave card) { diff --git a/Mage.Sets/src/mage/cards/r/RallyThePeasants.java b/Mage.Sets/src/mage/cards/r/RallyThePeasants.java index eadf0365257..5f75b77de9c 100644 --- a/Mage.Sets/src/mage/cards/r/RallyThePeasants.java +++ b/Mage.Sets/src/mage/cards/r/RallyThePeasants.java @@ -24,7 +24,7 @@ public final class RallyThePeasants extends CardImpl { this.getSpellAbility().addEffect(new BoostControlledEffect(2, 0, Duration.EndOfTurn)); // Flashback {2}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{R}"))); } private RallyThePeasants(final RallyThePeasants card) { diff --git a/Mage.Sets/src/mage/cards/r/RayOfDistortion.java b/Mage.Sets/src/mage/cards/r/RayOfDistortion.java index fabf4c432cb..05d26a65545 100644 --- a/Mage.Sets/src/mage/cards/r/RayOfDistortion.java +++ b/Mage.Sets/src/mage/cards/r/RayOfDistortion.java @@ -26,7 +26,7 @@ public final class RayOfDistortion extends CardImpl { this.getSpellAbility().addEffect(new DestroyTargetEffect()); this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT)); // Flashback {4}{W}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{W}{W}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{W}{W}"))); } private RayOfDistortion(final RayOfDistortion card) { diff --git a/Mage.Sets/src/mage/cards/r/RayOfRevelation.java b/Mage.Sets/src/mage/cards/r/RayOfRevelation.java index 701114b54df..c6747afdf4b 100644 --- a/Mage.Sets/src/mage/cards/r/RayOfRevelation.java +++ b/Mage.Sets/src/mage/cards/r/RayOfRevelation.java @@ -25,7 +25,7 @@ public final class RayOfRevelation extends CardImpl { this.getSpellAbility().addTarget(new TargetEnchantmentPermanent()); this.getSpellAbility().addEffect(new DestroyTargetEffect()); // Flashback {G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{G}"))); } private RayOfRevelation(final RayOfRevelation card) { diff --git a/Mage.Sets/src/mage/cards/r/ReapTheSeagraf.java b/Mage.Sets/src/mage/cards/r/ReapTheSeagraf.java index 8562bc22417..8cfa7c141a6 100644 --- a/Mage.Sets/src/mage/cards/r/ReapTheSeagraf.java +++ b/Mage.Sets/src/mage/cards/r/ReapTheSeagraf.java @@ -19,7 +19,7 @@ public final class ReapTheSeagraf extends CardImpl { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{2}{B}"); this.getSpellAbility().addEffect(new CreateTokenEffect(new ZombieToken())); - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{U}"))); } private ReapTheSeagraf(final ReapTheSeagraf card) { diff --git a/Mage.Sets/src/mage/cards/r/RecklessCharge.java b/Mage.Sets/src/mage/cards/r/RecklessCharge.java index 0208a3d353a..4b1fcb8c420 100644 --- a/Mage.Sets/src/mage/cards/r/RecklessCharge.java +++ b/Mage.Sets/src/mage/cards/r/RecklessCharge.java @@ -31,7 +31,7 @@ public final class RecklessCharge extends CardImpl { ).setText("and gains haste until end of turn")); // Flashback {2}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{2}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{2}{R}"))); } private RecklessCharge(final RecklessCharge card) { diff --git a/Mage.Sets/src/mage/cards/r/Recoup.java b/Mage.Sets/src/mage/cards/r/Recoup.java index bb144aa1117..d6aaf7223fe 100644 --- a/Mage.Sets/src/mage/cards/r/Recoup.java +++ b/Mage.Sets/src/mage/cards/r/Recoup.java @@ -1,7 +1,5 @@ - package mage.cards.r; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.ContinuousEffectImpl; @@ -9,38 +7,34 @@ import mage.abilities.keyword.FlashbackAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; -import mage.constants.TimingRule; +import mage.constants.*; import mage.filter.FilterCard; import mage.game.Game; import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; +import java.util.UUID; + /** - * * @author cbt33, BetaSteward (PastInFlames) */ public final class Recoup extends CardImpl { - + private static final FilterCard filter = new FilterCard("sorcery card"); - - static{ + + static { filter.add(CardType.SORCERY.getPredicate()); } public Recoup(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{1}{R}"); - + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{R}"); // Target sorcery card in your graveyard gains flashback until end of turn. The flashback cost is equal to its mana cost. this.getSpellAbility().addEffect(new RecoupEffect()); this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(filter)); + // Flashback {3}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{R}"))); } private Recoup(final Recoup card) { @@ -69,20 +63,20 @@ class RecoupEffect extends ContinuousEffectImpl { return new RecoupEffect(this); } - @Override + @Override public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); - if (player != null) { - Card card = game.getCard(targetPointer.getFirst(game, source)); - if (card != null) { - FlashbackAbility ability = new FlashbackAbility(card.getManaCost(), TimingRule.SORCERY); + if (player == null) { + return false; + } + Card card = game.getCard(targetPointer.getFirst(game, source)); + if (card != null) { + FlashbackAbility ability = new FlashbackAbility(card, card.getManaCost()); ability.setSourceId(card.getId()); ability.setControllerId(card.getOwnerId()); game.getState().addOtherAbility(card, ability); return true; - } } return false; + } } - -} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RiteOfHarmony.java b/Mage.Sets/src/mage/cards/r/RiteOfHarmony.java index 1a16039ac87..d5e5db6d7c5 100644 --- a/Mage.Sets/src/mage/cards/r/RiteOfHarmony.java +++ b/Mage.Sets/src/mage/cards/r/RiteOfHarmony.java @@ -32,7 +32,7 @@ public final class RiteOfHarmony extends CardImpl { getSpellAbility().addEffect(new CreateDelayedTriggeredAbilityEffect(new RiteOfHarmonyTriggeredAbility())); // Flashback {2}{G}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{G}{W}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{G}{W}"))); } diff --git a/Mage.Sets/src/mage/cards/r/RoarOfTheWurm.java b/Mage.Sets/src/mage/cards/r/RoarOfTheWurm.java index 09bb68a1e28..6138c755928 100644 --- a/Mage.Sets/src/mage/cards/r/RoarOfTheWurm.java +++ b/Mage.Sets/src/mage/cards/r/RoarOfTheWurm.java @@ -25,7 +25,7 @@ public final class RoarOfTheWurm extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new WurmToken())); // Flashback {3}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{G}"))); } private RoarOfTheWurm(final RoarOfTheWurm card) { diff --git a/Mage.Sets/src/mage/cards/r/RollingTemblor.java b/Mage.Sets/src/mage/cards/r/RollingTemblor.java index d6da7bb290f..ac06b6b7f29 100644 --- a/Mage.Sets/src/mage/cards/r/RollingTemblor.java +++ b/Mage.Sets/src/mage/cards/r/RollingTemblor.java @@ -33,7 +33,7 @@ public final class RollingTemblor extends CardImpl { // Rolling Temblor deals 2 damage to each creature without flying. this.getSpellAbility().addEffect(new DamageAllEffect(2, filter)); // Flashback {4}{R}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{R}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{R}{R}"))); } private RollingTemblor(final RollingTemblor card) { diff --git a/Mage.Sets/src/mage/cards/r/RottenReunion.java b/Mage.Sets/src/mage/cards/r/RottenReunion.java index 535fa76a6a8..d720c4f4804 100644 --- a/Mage.Sets/src/mage/cards/r/RottenReunion.java +++ b/Mage.Sets/src/mage/cards/r/RottenReunion.java @@ -27,7 +27,7 @@ public final class RottenReunion extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInGraveyard(0, 1)); // Flashback {1}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{1}{B}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{B}"))); } private RottenReunion(final RottenReunion card) { diff --git a/Mage.Sets/src/mage/cards/s/SacredFire.java b/Mage.Sets/src/mage/cards/s/SacredFire.java index 4d0a65072de..27efc4e4c86 100644 --- a/Mage.Sets/src/mage/cards/s/SacredFire.java +++ b/Mage.Sets/src/mage/cards/s/SacredFire.java @@ -26,7 +26,7 @@ public final class SacredFire extends CardImpl { this.getSpellAbility().addTarget(new TargetAnyTarget()); // Flashback {4}{R}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{4}{R}{W}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{4}{R}{W}"))); } private SacredFire(final SacredFire card) { diff --git a/Mage.Sets/src/mage/cards/s/SavingGrasp.java b/Mage.Sets/src/mage/cards/s/SavingGrasp.java index 588d35a8255..775056361de 100644 --- a/Mage.Sets/src/mage/cards/s/SavingGrasp.java +++ b/Mage.Sets/src/mage/cards/s/SavingGrasp.java @@ -33,7 +33,7 @@ public final class SavingGrasp extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent(filter)); this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); // Flashback {W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{W}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{W}"))); } private SavingGrasp(final SavingGrasp card) { diff --git a/Mage.Sets/src/mage/cards/s/ScorchingMissile.java b/Mage.Sets/src/mage/cards/s/ScorchingMissile.java index dec85edfaae..8a217d2ff8f 100644 --- a/Mage.Sets/src/mage/cards/s/ScorchingMissile.java +++ b/Mage.Sets/src/mage/cards/s/ScorchingMissile.java @@ -25,7 +25,7 @@ public final class ScorchingMissile extends CardImpl { this.getSpellAbility().addTarget(new TargetPlayerOrPlaneswalker()); // Flashback {9}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{9}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{9}{R}"))); } diff --git a/Mage.Sets/src/mage/cards/s/ScourAllPossibilities.java b/Mage.Sets/src/mage/cards/s/ScourAllPossibilities.java index 6ce1b3d0f25..9018a5534a3 100644 --- a/Mage.Sets/src/mage/cards/s/ScourAllPossibilities.java +++ b/Mage.Sets/src/mage/cards/s/ScourAllPossibilities.java @@ -24,7 +24,7 @@ public final class ScourAllPossibilities extends CardImpl { this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1).concatBy(", then")); // Flashback {4}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{U}"))); } diff --git a/Mage.Sets/src/mage/cards/s/SecretsOfTheKey.java b/Mage.Sets/src/mage/cards/s/SecretsOfTheKey.java index 52e01b162a5..4e0874441e0 100644 --- a/Mage.Sets/src/mage/cards/s/SecretsOfTheKey.java +++ b/Mage.Sets/src/mage/cards/s/SecretsOfTheKey.java @@ -31,7 +31,7 @@ public final class SecretsOfTheKey extends CardImpl { )); // Flashback {3}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{3}{U}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{3}{U}"))); } private SecretsOfTheKey(final SecretsOfTheKey card) { diff --git a/Mage.Sets/src/mage/cards/s/SeizeTheDay.java b/Mage.Sets/src/mage/cards/s/SeizeTheDay.java index 0ef6c503e99..8c9e698c07d 100644 --- a/Mage.Sets/src/mage/cards/s/SeizeTheDay.java +++ b/Mage.Sets/src/mage/cards/s/SeizeTheDay.java @@ -27,7 +27,7 @@ public final class SeizeTheDay extends CardImpl { this.getSpellAbility().addEffect(new AddCombatAndMainPhaseEffect()); // Flashback {2}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{R}"))); } private SeizeTheDay(final SeizeTheDay card) { diff --git a/Mage.Sets/src/mage/cards/s/SeverTheBloodline.java b/Mage.Sets/src/mage/cards/s/SeverTheBloodline.java index c470cdb91c5..4c94dc9d2e3 100644 --- a/Mage.Sets/src/mage/cards/s/SeverTheBloodline.java +++ b/Mage.Sets/src/mage/cards/s/SeverTheBloodline.java @@ -34,7 +34,7 @@ public final class SeverTheBloodline extends CardImpl { this.getSpellAbility().addEffect(new SeverTheBloodlineEffect()); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {5}{B}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{B}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{B}{B}"))); } private SeverTheBloodline(final SeverTheBloodline card) { diff --git a/Mage.Sets/src/mage/cards/s/SevinnesReclamation.java b/Mage.Sets/src/mage/cards/s/SevinnesReclamation.java index e60c3d23784..0cf6945e4b1 100644 --- a/Mage.Sets/src/mage/cards/s/SevinnesReclamation.java +++ b/Mage.Sets/src/mage/cards/s/SevinnesReclamation.java @@ -40,7 +40,7 @@ public final class SevinnesReclamation extends CardImpl { this.getSpellAbility().addEffect(new SevinnesReclamationEffect()); // Flashback {4}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{W}"))); } private SevinnesReclamation(final SevinnesReclamation card) { diff --git a/Mage.Sets/src/mage/cards/s/ShadowbeastSighting.java b/Mage.Sets/src/mage/cards/s/ShadowbeastSighting.java index f51827fc8e3..6fdbb21cf82 100644 --- a/Mage.Sets/src/mage/cards/s/ShadowbeastSighting.java +++ b/Mage.Sets/src/mage/cards/s/ShadowbeastSighting.java @@ -23,7 +23,7 @@ public final class ShadowbeastSighting extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new BeastToken2())); // Flashback {6}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{6}{G}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{6}{G}"))); } private ShadowbeastSighting(final ShadowbeastSighting card) { diff --git a/Mage.Sets/src/mage/cards/s/ShatteredPerception.java b/Mage.Sets/src/mage/cards/s/ShatteredPerception.java index f5383b5fb6f..f6780909757 100644 --- a/Mage.Sets/src/mage/cards/s/ShatteredPerception.java +++ b/Mage.Sets/src/mage/cards/s/ShatteredPerception.java @@ -22,7 +22,7 @@ public final class ShatteredPerception extends CardImpl { // Discard all the cards in your hand, then draw that many cards. this.getSpellAbility().addEffect(new DiscardHandDrawSameNumberSourceEffect()); // Flashback {5}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{R}"))); } private ShatteredPerception(final ShatteredPerception card) { diff --git a/Mage.Sets/src/mage/cards/s/SilentDeparture.java b/Mage.Sets/src/mage/cards/s/SilentDeparture.java index c3770e0e07c..d0e8e7cb8a4 100644 --- a/Mage.Sets/src/mage/cards/s/SilentDeparture.java +++ b/Mage.Sets/src/mage/cards/s/SilentDeparture.java @@ -25,7 +25,7 @@ public final class SilentDeparture extends CardImpl { this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); // Flashback {4}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{4}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{4}{U}"))); } private SilentDeparture(final SilentDeparture card) { diff --git a/Mage.Sets/src/mage/cards/s/SkullFracture.java b/Mage.Sets/src/mage/cards/s/SkullFracture.java index 9ae9ca46462..a4186e6dd26 100644 --- a/Mage.Sets/src/mage/cards/s/SkullFracture.java +++ b/Mage.Sets/src/mage/cards/s/SkullFracture.java @@ -26,7 +26,7 @@ public final class SkullFracture extends CardImpl { this.getSpellAbility().addEffect(new DiscardTargetEffect(1)); this.getSpellAbility().addTarget(new TargetPlayer()); // Flashback {3}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{B}"))); } private SkullFracture(final SkullFracture card) { diff --git a/Mage.Sets/src/mage/cards/s/SmitingHelix.java b/Mage.Sets/src/mage/cards/s/SmitingHelix.java index a2c6ec0951c..7dba0e9b9bc 100644 --- a/Mage.Sets/src/mage/cards/s/SmitingHelix.java +++ b/Mage.Sets/src/mage/cards/s/SmitingHelix.java @@ -26,7 +26,7 @@ public final class SmitingHelix extends CardImpl { this.getSpellAbility().addTarget(new TargetAnyTarget()); // Flashback {R}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{R}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{R}{W}"))); } private SmitingHelix(final SmitingHelix card) { diff --git a/Mage.Sets/src/mage/cards/s/SnapcasterMage.java b/Mage.Sets/src/mage/cards/s/SnapcasterMage.java index 75abfda093c..58a03c9c026 100644 --- a/Mage.Sets/src/mage/cards/s/SnapcasterMage.java +++ b/Mage.Sets/src/mage/cards/s/SnapcasterMage.java @@ -1,7 +1,6 @@ package mage.cards.s; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; @@ -11,20 +10,15 @@ import mage.abilities.keyword.FlashbackAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; -import mage.constants.TimingRule; +import mage.constants.*; import mage.filter.FilterCard; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.target.common.TargetCardInYourGraveyard; +import java.util.UUID; + /** - * * @author BetaSteward */ public final class SnapcasterMage extends CardImpl { @@ -38,7 +32,7 @@ public final class SnapcasterMage extends CardImpl { } public SnapcasterMage(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{1}{U}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.WIZARD); @@ -84,13 +78,7 @@ class SnapcasterMageEffect extends ContinuousEffectImpl { public boolean apply(Game game, Ability source) { Card card = game.getCard(targetPointer.getFirst(game, source)); if (card != null) { - FlashbackAbility ability; - if (card.isInstant(game)) { - ability = new FlashbackAbility(card.getManaCost(), TimingRule.INSTANT); - } - else { - ability = new FlashbackAbility(card.getManaCost(), TimingRule.SORCERY); - } + FlashbackAbility ability = new FlashbackAbility(card, card.getManaCost()); ability.setSourceId(card.getId()); ability.setControllerId(card.getOwnerId()); game.getState().addOtherAbility(card, ability); diff --git a/Mage.Sets/src/mage/cards/s/SpiderSpawning.java b/Mage.Sets/src/mage/cards/s/SpiderSpawning.java index 5a0a669947b..41bf4309977 100644 --- a/Mage.Sets/src/mage/cards/s/SpiderSpawning.java +++ b/Mage.Sets/src/mage/cards/s/SpiderSpawning.java @@ -24,7 +24,7 @@ public final class SpiderSpawning extends CardImpl { // Create a 1/2 green Spider creature token with reach for each creature card in your graveyard. this.getSpellAbility().addEffect(new CreateTokenEffect(new SpiderToken(), new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURE))); // Flashback {6}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{6}{B}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{6}{B}"))); } private SpiderSpawning(final SpiderSpawning card) { diff --git a/Mage.Sets/src/mage/cards/s/SpiritFlare.java b/Mage.Sets/src/mage/cards/s/SpiritFlare.java index a45cf540d5e..e1190693adc 100644 --- a/Mage.Sets/src/mage/cards/s/SpiritFlare.java +++ b/Mage.Sets/src/mage/cards/s/SpiritFlare.java @@ -45,7 +45,7 @@ public final class SpiritFlare extends CardImpl { this.getSpellAbility().addTarget(new TargetPermanent(filter2)); // Flashback-{1}{W}, Pay 3 life. - FlashbackAbility ability = new FlashbackAbility(new ManaCostsImpl("{1}{W}"), TimingRule.INSTANT); + FlashbackAbility ability = new FlashbackAbility(this, new ManaCostsImpl<>("{1}{W}")); ability.addCost(new PayLifeCost(3)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/s/StranglingSoot.java b/Mage.Sets/src/mage/cards/s/StranglingSoot.java index 854599d26ce..b5061c16ab2 100644 --- a/Mage.Sets/src/mage/cards/s/StranglingSoot.java +++ b/Mage.Sets/src/mage/cards/s/StranglingSoot.java @@ -32,7 +32,7 @@ public final class StranglingSoot extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent(filter)); this.getSpellAbility().addEffect(new DestroyTargetEffect()); // Flashback {5}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{5}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{5}{R}"))); } private StranglingSoot(final StranglingSoot card) { diff --git a/Mage.Sets/src/mage/cards/s/StrikeItRich.java b/Mage.Sets/src/mage/cards/s/StrikeItRich.java index 27e974ad193..5e3f94dae08 100644 --- a/Mage.Sets/src/mage/cards/s/StrikeItRich.java +++ b/Mage.Sets/src/mage/cards/s/StrikeItRich.java @@ -23,7 +23,7 @@ public final class StrikeItRich extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new TreasureToken())); // Flashback {2}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{2}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{2}{R}"))); } private StrikeItRich(final StrikeItRich card) { diff --git a/Mage.Sets/src/mage/cards/s/SylvanMight.java b/Mage.Sets/src/mage/cards/s/SylvanMight.java index 8452ea5ce7e..8345379ada5 100644 --- a/Mage.Sets/src/mage/cards/s/SylvanMight.java +++ b/Mage.Sets/src/mage/cards/s/SylvanMight.java @@ -33,7 +33,7 @@ public final class SylvanMight extends CardImpl { this.getSpellAbility().addEffect(effect); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {2}{G}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{G}{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{G}{G}"))); } private SylvanMight(final SylvanMight card) { diff --git a/Mage.Sets/src/mage/cards/t/ThinkTwice.java b/Mage.Sets/src/mage/cards/t/ThinkTwice.java index 9f5268db679..093263187e7 100644 --- a/Mage.Sets/src/mage/cards/t/ThinkTwice.java +++ b/Mage.Sets/src/mage/cards/t/ThinkTwice.java @@ -24,7 +24,7 @@ public final class ThinkTwice extends CardImpl { this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); // Flashback {2}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{U}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{U}"))); } private ThinkTwice(final ThinkTwice card) { diff --git a/Mage.Sets/src/mage/cards/t/ThrillOfTheHunt.java b/Mage.Sets/src/mage/cards/t/ThrillOfTheHunt.java index a411ec865bb..edae0965579 100644 --- a/Mage.Sets/src/mage/cards/t/ThrillOfTheHunt.java +++ b/Mage.Sets/src/mage/cards/t/ThrillOfTheHunt.java @@ -25,7 +25,7 @@ public final class ThrillOfTheHunt extends CardImpl { this.getSpellAbility().addEffect(new BoostTargetEffect(1, 2, Duration.EndOfTurn)); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{W}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{W}"))); } private ThrillOfTheHunt(final ThrillOfTheHunt card) { diff --git a/Mage.Sets/src/mage/cards/t/TrackersInstincts.java b/Mage.Sets/src/mage/cards/t/TrackersInstincts.java index cf4ed011de2..a224dec3fec 100644 --- a/Mage.Sets/src/mage/cards/t/TrackersInstincts.java +++ b/Mage.Sets/src/mage/cards/t/TrackersInstincts.java @@ -33,7 +33,7 @@ public final class TrackersInstincts extends CardImpl { // Reveal the top four cards of your library. Put a creature card from among them into your hand and the rest into your graveyard. this.getSpellAbility().addEffect(new TrackersInstinctsEffect()); // Flashback {2}{U} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{2}{U}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{2}{U}"))); } private TrackersInstincts(final TrackersInstincts card) { diff --git a/Mage.Sets/src/mage/cards/t/TraitorsClutch.java b/Mage.Sets/src/mage/cards/t/TraitorsClutch.java index 54601a12cc6..50b5d830ef7 100644 --- a/Mage.Sets/src/mage/cards/t/TraitorsClutch.java +++ b/Mage.Sets/src/mage/cards/t/TraitorsClutch.java @@ -38,7 +38,7 @@ public final class TraitorsClutch extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Flashback {1}{B} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{B}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{B}"))); } private TraitorsClutch(final TraitorsClutch card) { diff --git a/Mage.Sets/src/mage/cards/t/TravelPreparations.java b/Mage.Sets/src/mage/cards/t/TravelPreparations.java index ab9c5767b9e..ae2d1e296ce 100644 --- a/Mage.Sets/src/mage/cards/t/TravelPreparations.java +++ b/Mage.Sets/src/mage/cards/t/TravelPreparations.java @@ -29,7 +29,7 @@ public final class TravelPreparations extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, 2)); // Flashback {1}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{W}"))); } private TravelPreparations(final TravelPreparations card) { diff --git a/Mage.Sets/src/mage/cards/t/TurnTheEarth.java b/Mage.Sets/src/mage/cards/t/TurnTheEarth.java index 4298dca55f5..a7c2fc214bd 100644 --- a/Mage.Sets/src/mage/cards/t/TurnTheEarth.java +++ b/Mage.Sets/src/mage/cards/t/TurnTheEarth.java @@ -28,7 +28,7 @@ public final class TurnTheEarth extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInGraveyard(0, 3)); // Flashback {1}{G} - this.addAbility(new FlashbackAbility(new ManaCostsImpl<>("{1}{G}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{G}"))); } private TurnTheEarth(final TurnTheEarth card) { diff --git a/Mage.Sets/src/mage/cards/u/UnburialRites.java b/Mage.Sets/src/mage/cards/u/UnburialRites.java index 222d344b74b..269246ac265 100644 --- a/Mage.Sets/src/mage/cards/u/UnburialRites.java +++ b/Mage.Sets/src/mage/cards/u/UnburialRites.java @@ -25,7 +25,7 @@ public final class UnburialRites extends CardImpl { this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); // Flashback {3}{W} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{W}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{W}"))); } private UnburialRites(final UnburialRites card) { diff --git a/Mage.Sets/src/mage/cards/v/VolcanicSpray.java b/Mage.Sets/src/mage/cards/v/VolcanicSpray.java index 0d7ccbe3480..86995eb086e 100644 --- a/Mage.Sets/src/mage/cards/v/VolcanicSpray.java +++ b/Mage.Sets/src/mage/cards/v/VolcanicSpray.java @@ -32,7 +32,7 @@ public final class VolcanicSpray extends CardImpl { // Volcanic Spray deals 1 damage to each creature without flying and each player. this.getSpellAbility().addEffect(new DamageEverythingEffect(1, filter)); // Flashback {1}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{1}{R}"), TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{1}{R}"))); } private VolcanicSpray(final VolcanicSpray card) { diff --git a/Mage.Sets/src/mage/cards/v/VolleyOfBoulders.java b/Mage.Sets/src/mage/cards/v/VolleyOfBoulders.java index 5387b0f86a8..2918562e23c 100644 --- a/Mage.Sets/src/mage/cards/v/VolleyOfBoulders.java +++ b/Mage.Sets/src/mage/cards/v/VolleyOfBoulders.java @@ -25,7 +25,7 @@ public final class VolleyOfBoulders extends CardImpl { this.getSpellAbility().addEffect(new DamageMultiEffect(6)); this.getSpellAbility().addTarget(new TargetAnyTargetAmount(6)); // Flashback {R}{R}{R}{R}{R}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{R}{R}{R}{R}{R}{R}"),TimingRule.SORCERY)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{R}{R}{R}{R}{R}{R}"))); } private VolleyOfBoulders(final VolleyOfBoulders card) { diff --git a/Mage.Sets/src/mage/cards/w/WildHunger.java b/Mage.Sets/src/mage/cards/w/WildHunger.java index 76225d35d98..6b213997a25 100644 --- a/Mage.Sets/src/mage/cards/w/WildHunger.java +++ b/Mage.Sets/src/mage/cards/w/WildHunger.java @@ -29,7 +29,7 @@ public final class WildHunger extends CardImpl { this.getSpellAbility().addEffect(new BoostTargetEffect(3, 1, Duration.EndOfTurn)); this.getSpellAbility().addEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance(), Duration.EndOfTurn)); // Flashback {3}{R} - this.addAbility(new FlashbackAbility(new ManaCostsImpl("{3}{R}"), TimingRule.INSTANT)); + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl("{3}{R}"))); } private WildHunger(final WildHunger card) { diff --git a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java index 968ffeee61d..6347b1ef2dc 100644 --- a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java @@ -38,12 +38,12 @@ public class FlashbackAbility extends SpellAbility { private String abilityName; private SpellAbility spellAbilityToResolve; - public FlashbackAbility(Cost cost, TimingRule timingRule) { + public FlashbackAbility(Card card, Cost cost) { super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.FLASHBACK); this.setAdditionalCostsRuleVisible(false); this.name = "Flashback " + cost.getText(); this.addCost(cost); - this.timing = timingRule; + this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT; } public FlashbackAbility(final FlashbackAbility ability) { diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index c3741f4a45f..014c7400790 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -271,7 +271,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card { && mainCardState != null && !mainCardState.hasLostAllAbilities() && mainCardState.getAbilities().containsClass(FlashbackAbility.class)) { - FlashbackAbility flash = new FlashbackAbility(this.getManaCost(), this.isInstant(game) ? TimingRule.INSTANT : TimingRule.SORCERY); + FlashbackAbility flash = new FlashbackAbility(this, this.getManaCost()); flash.setSourceId(this.getId()); flash.setControllerId(this.getOwnerId()); flash.setSpellAbilityType(this.getSpellAbility().getSpellAbilityType()); diff --git a/Utils/keywords.txt b/Utils/keywords.txt index 6e62c8744db..1dc14e60261 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -43,7 +43,7 @@ Fear|instance| First strike|instance| Flanking|new| Flash|instance| -Flashback|cost| +Flashback|card, cost| Flying|instance| Forestcycling|cost| Forestwalk|new| From 7aa3e75f3a9adad3c48b1be50e4b669ef1d676b9 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Sat, 11 Sep 2021 16:45:43 -0500 Subject: [PATCH 050/231] [MID] Implemented Hallowed Respite --- .../src/mage/cards/h/HallowedRespite.java | 88 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 89 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HallowedRespite.java diff --git a/Mage.Sets/src/mage/cards/h/HallowedRespite.java b/Mage.Sets/src/mage/cards/h/HallowedRespite.java new file mode 100644 index 00000000000..c7176564068 --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HallowedRespite.java @@ -0,0 +1,88 @@ +package mage.cards.h; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetForSourceEffect; +import mage.abilities.effects.common.ReturnToBattlefieldUnderOwnerControlTargetEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SuperType; +import mage.constants.TimingRule; +import mage.counters.CounterType; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetCreaturePermanent; + +/** + * + * @author weirddan455 + */ +public final class HallowedRespite extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("nonlegendary creature"); + + static { + filter.add(Predicates.not(SuperType.LEGENDARY.getPredicate())); + } + + public HallowedRespite(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{W}{U}"); + + // Exile target nonlegendary creature, then return it to the battlefield under its owner's control. If it entered under your control, put a +1/+1 counter on it. Otherwise, tap it. + this.getSpellAbility().addEffect(new ExileTargetForSourceEffect()); + this.getSpellAbility().addEffect(new ReturnToBattlefieldUnderOwnerControlTargetEffect(false, false)); + this.getSpellAbility().addEffect(new HallowedRespiteEffect()); + this.getSpellAbility().addTarget(new TargetCreaturePermanent(filter)); + + // Flashback {1}{W}{U} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{W}{U}"))); + } + + private HallowedRespite(final HallowedRespite card) { + super(card); + } + + @Override + public HallowedRespite copy() { + return new HallowedRespite(this); + } +} + +class HallowedRespiteEffect extends OneShotEffect { + + public HallowedRespiteEffect() { + super(Outcome.Benefit); + staticText = "If it entered under your control, put a +1/+1 counter on it. Otherwise, tap it"; + } + + private HallowedRespiteEffect(final HallowedRespiteEffect effect) { + super(effect); + } + + @Override + public HallowedRespiteEffect copy() { + return new HallowedRespiteEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent == null) { + return false; + } + if (permanent.isControlledBy(source.getControllerId())) { + permanent.addCounters(CounterType.P1P1.createInstance(), source.getControllerId(), source, game); + } else { + permanent.tap(source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 1b8b9c90b2b..e6ada750ded 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -149,6 +149,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Graveyard Glutton", 104, Rarity.RARE, mage.cards.g.GraveyardGlutton.class)); cards.add(new SetCardInfo("Graveyard Trespasser", 104, Rarity.RARE, mage.cards.g.GraveyardTrespasser.class)); cards.add(new SetCardInfo("Grizzly Ghoul", 226, Rarity.UNCOMMON, mage.cards.g.GrizzlyGhoul.class)); + cards.add(new SetCardInfo("Hallowed Respite", 227, Rarity.RARE, mage.cards.h.HallowedRespite.class)); cards.add(new SetCardInfo("Harvesttide Assailant", 143, Rarity.COMMON, mage.cards.h.HarvesttideAssailant.class)); cards.add(new SetCardInfo("Harvesttide Infiltrator", 143, Rarity.COMMON, mage.cards.h.HarvesttideInfiltrator.class)); cards.add(new SetCardInfo("Harvesttide Sentry", 186, Rarity.COMMON, mage.cards.h.HarvesttideSentry.class)); From 6e3f118dae6bfa9e463c8aae398d78eda82b193b Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Sat, 11 Sep 2021 18:27:06 -0500 Subject: [PATCH 051/231] [MID] Implemented Katilda, Dawnhart Prime --- .../mage/cards/k/KatildaDawnhartPrime.java | 191 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 192 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KatildaDawnhartPrime.java diff --git a/Mage.Sets/src/mage/cards/k/KatildaDawnhartPrime.java b/Mage.Sets/src/mage/cards/k/KatildaDawnhartPrime.java new file mode 100644 index 00000000000..7f2294002c3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KatildaDawnhartPrime.java @@ -0,0 +1,191 @@ +package mage.cards.k; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import mage.MageInt; +import mage.Mana; +import mage.ObjectColor; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.counter.AddCountersAllEffect; +import mage.abilities.effects.mana.ManaEffect; +import mage.abilities.keyword.ProtectionAbility; +import mage.abilities.mana.ActivatedManaAbilityImpl; +import mage.choices.Choice; +import mage.choices.ChoiceImpl; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +/** + * + * @author weirddan455 + */ +public final class KatildaDawnhartPrime extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent(SubType.WEREWOLF, "Werewolves"); + private static final FilterControlledCreaturePermanent filter2 = new FilterControlledCreaturePermanent(SubType.HUMAN, "Human creatures you control"); + + public KatildaDawnhartPrime(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{W}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Protection from Werewolves + this.addAbility(new ProtectionAbility(filter)); + + // Human creatures you control have "{T}: Add one mana of any of this creature's colors." + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect(new KatildaDawnhartPrimeManaAbility(), Duration.WhileOnBattlefield, filter2))); + + // {4}{G}{W}, {T}: Put a +1/+1 counter on each creature you control. + this.addAbility(new SimpleActivatedAbility(new AddCountersAllEffect(CounterType.P1P1.createInstance(), StaticFilters.FILTER_CONTROLLED_CREATURE), new ManaCostsImpl<>("{4}{G}{W}"))); + } + + private KatildaDawnhartPrime(final KatildaDawnhartPrime card) { + super(card); + } + + @Override + public KatildaDawnhartPrime copy() { + return new KatildaDawnhartPrime(this); + } +} + +// Mana code based on CommanderColorIdentityManaAbility +class KatildaDawnhartPrimeManaAbility extends ActivatedManaAbilityImpl { + + public KatildaDawnhartPrimeManaAbility() { + super(Zone.BATTLEFIELD, new KatildaDawnhartPrimeManaEffect(), new TapSourceCost()); + } + + private KatildaDawnhartPrimeManaAbility(final KatildaDawnhartPrimeManaAbility ability) { + super(ability); + } + + @Override + public KatildaDawnhartPrimeManaAbility copy() { + return new KatildaDawnhartPrimeManaAbility(this); + } + + @Override + public boolean definesMana(Game game) { + return true; + } +} + +class KatildaDawnhartPrimeManaEffect extends ManaEffect { + + public KatildaDawnhartPrimeManaEffect() { + super(); + staticText = "Add one mana of any of this creature's colors"; + } + + private KatildaDawnhartPrimeManaEffect(final KatildaDawnhartPrimeManaEffect effect) { + super(effect); + } + + @Override + public KatildaDawnhartPrimeManaEffect copy() { + return new KatildaDawnhartPrimeManaEffect(this); + } + + @Override + public List getNetMana(Game game, Ability source) { + List netMana = new ArrayList<>(); + if (game != null) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent != null) { + ObjectColor color = permanent.getColor(game); + if (color.isWhite()) { + netMana.add(new Mana(ColoredManaSymbol.W)); + } + if (color.isBlue()) { + netMana.add(new Mana(ColoredManaSymbol.U)); + } + if (color.isBlack()) { + netMana.add(new Mana(ColoredManaSymbol.B)); + } + if (color.isRed()) { + netMana.add(new Mana(ColoredManaSymbol.R)); + } + if (color.isGreen()) { + netMana.add(new Mana(ColoredManaSymbol.G)); + } + } + } + return netMana; + } + + @Override + public Mana produceMana(Game game, Ability source) { + Mana mana = new Mana(); + if (game != null) { + Player controller = game.getPlayer(source.getControllerId()); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (controller != null && permanent != null) { + Choice choice = new ChoiceImpl(); + choice.setMessage("Pick a mana color"); + ObjectColor color = permanent.getColor(game); + if (color.isWhite()) { + choice.getChoices().add("White"); + } + if (color.isBlue()) { + choice.getChoices().add("Blue"); + } + if (color.isBlack()) { + choice.getChoices().add("Black"); + } + if (color.isRed()) { + choice.getChoices().add("Red"); + } + if (color.isGreen()) { + choice.getChoices().add("Green"); + } + if (!choice.getChoices().isEmpty()) { + if (choice.getChoices().size() == 1) { + choice.setChoice(choice.getChoices().iterator().next()); + } else { + controller.choose(outcome, choice, game); + } + + if (choice.getChoice() != null) { + switch (choice.getChoice()) { + case "White": + mana.setWhite(1); + break; + case "Blue": + mana.setBlue(1); + break; + case "Black": + mana.setBlack(1); + break; + case "Red": + mana.setRed(1); + break; + case "Green": + mana.setGreen(1); + break; + } + } + } + } + } + return mana; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index e6ada750ded..e7b79f4e5b9 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -171,6 +171,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Jack-o'-Lantern", 254, Rarity.COMMON, mage.cards.j.JackOLantern.class)); cards.add(new SetCardInfo("Jadar, Ghoulcaller of Nephalia", 108, Rarity.RARE, mage.cards.j.JadarGhoulcallerOfNephalia.class)); cards.add(new SetCardInfo("Join the Dance", 229, Rarity.UNCOMMON, mage.cards.j.JoinTheDance.class)); + cards.add(new SetCardInfo("Katilda, Dawnhart Prime", 230, Rarity.RARE, mage.cards.k.KatildaDawnhartPrime.class)); cards.add(new SetCardInfo("Kessig Naturalist", 231, Rarity.UNCOMMON, mage.cards.k.KessigNaturalist.class)); cards.add(new SetCardInfo("Lambholt Harrier", 145, Rarity.COMMON, mage.cards.l.LambholtHarrier.class)); cards.add(new SetCardInfo("Larder Zombie", 58, Rarity.COMMON, mage.cards.l.LarderZombie.class)); From 7774f48325a8f0e845110a19205c28379628d411 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Sat, 11 Sep 2021 19:09:01 -0500 Subject: [PATCH 052/231] [MID] Implemented Liesa, Forgotten Archangel --- .../mage/cards/l/LiesaForgottenArchangel.java | 139 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 140 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LiesaForgottenArchangel.java diff --git a/Mage.Sets/src/mage/cards/l/LiesaForgottenArchangel.java b/Mage.Sets/src/mage/cards/l/LiesaForgottenArchangel.java new file mode 100644 index 00000000000..e2c159ad2d4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LiesaForgottenArchangel.java @@ -0,0 +1,139 @@ +package mage.cards.l; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.ReturnToHandTargetEffect; +import mage.constants.*; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; + +/** + * + * @author weirddan455 + */ +public final class LiesaForgottenArchangel extends CardImpl { + + private final static FilterControlledCreaturePermanent filter + = new FilterControlledCreaturePermanent("another nontoken creature you control"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(TokenPredicate.FALSE); + } + + public LiesaForgottenArchangel(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{W}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.ANGEL); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // Whenever another nontoken creature you control dies, return that card to its owner's hand at the beginning of the next end step. + this.addAbility(new DiesCreatureTriggeredAbility(new LiesaForgottenArchangelReturnToHandEffect(), false, filter, true)); + + // If a creature an opponent controls would die, exile it instead. + this.addAbility(new SimpleStaticAbility(new LiesaForgottenArchangelReplacementEffect())); + } + + private LiesaForgottenArchangel(final LiesaForgottenArchangel card) { + super(card); + } + + @Override + public LiesaForgottenArchangel copy() { + return new LiesaForgottenArchangel(this); + } +} + +class LiesaForgottenArchangelReturnToHandEffect extends OneShotEffect { + + public LiesaForgottenArchangelReturnToHandEffect() { + super(Outcome.ReturnToHand); + staticText = "return that card to its owner's hand at the beginning of the next end step"; + } + + private LiesaForgottenArchangelReturnToHandEffect(final LiesaForgottenArchangelReturnToHandEffect effect) { + super(effect); + } + + @Override + public LiesaForgottenArchangelReturnToHandEffect copy() { + return new LiesaForgottenArchangelReturnToHandEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Effect effect = new ReturnToHandTargetEffect(); + effect.setText("return that card to its owner's hand"); + effect.setTargetPointer(targetPointer); + DelayedTriggeredAbility ability = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect); + game.addDelayedTriggeredAbility(ability, source); + return true; + } +} + +class LiesaForgottenArchangelReplacementEffect extends ReplacementEffectImpl { + + public LiesaForgottenArchangelReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Exile); + staticText = "If a creature an opponent controls would die, exile it instead"; + } + + private LiesaForgottenArchangelReplacementEffect(final LiesaForgottenArchangelReplacementEffect effect) { + super(effect); + } + + @Override + public LiesaForgottenArchangelReplacementEffect copy() { + return new LiesaForgottenArchangelReplacementEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + ((ZoneChangeEvent) event).setToZone(Zone.EXILED); + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (zEvent.isDiesEvent()) { + Permanent permanent = zEvent.getTarget(); + if (permanent != null && permanent.isCreature()) { + Player player = game.getPlayer(source.getControllerId()); + return player != null && player.hasOpponent(permanent.getControllerId(), game); + } + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index e7b79f4e5b9..57acd7213dd 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -176,6 +176,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Lambholt Harrier", 145, Rarity.COMMON, mage.cards.l.LambholtHarrier.class)); cards.add(new SetCardInfo("Larder Zombie", 58, Rarity.COMMON, mage.cards.l.LarderZombie.class)); cards.add(new SetCardInfo("Lier, Disciple of the Drowned", 59, Rarity.MYTHIC, mage.cards.l.LierDiscipleOfTheDrowned.class)); + cards.add(new SetCardInfo("Liesa, Forgotten Archangel", 232, Rarity.RARE, mage.cards.l.LiesaForgottenArchangel.class)); cards.add(new SetCardInfo("Locked in the Cemetery", 60, Rarity.COMMON, mage.cards.l.LockedInTheCemetery.class)); cards.add(new SetCardInfo("Lord of the Ulvenwald", 231, Rarity.UNCOMMON, mage.cards.l.LordOfTheUlvenwald.class)); cards.add(new SetCardInfo("Loyal Gryff", 26, Rarity.UNCOMMON, mage.cards.l.LoyalGryff.class)); From a6bf28846d5900930d9a37481a8efa64ee0c3375 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 18:15:53 -0400 Subject: [PATCH 053/231] [MIC] updated spoiler --- Utils/mtg-cards-data.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 3d9ddf2684c..c088858c8a5 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -42865,12 +42865,13 @@ Universal Automaton|Jumpstart: Historic Horizons|759|C|{1}|Artifact Creature - S Zabaz, the Glimmerwasp|Jumpstart: Historic Horizons|762|R|{1}|Legendary Artifact Creature - Insect|0|0|Modular 1$If a modular triggered ability would put one or more +1/+1 counters on a creature you control, that many plus one +1/+1 counters are put on it instead.${R}: Destroy target artifact you control.${W}: Zabaz, the Glimmerwasp gains flying until end of turn.| Khalni Garden|Jumpstart: Historic Horizons|770|C||Land|||Khalni Garden enters the battlefield tapped.$When Khalni Garden enters the battlefield, create a 0/1 green Plant creature token.${T}: Add {G}.| Sliver Hive|Jumpstart: Historic Horizons|776|R||Land|||{T}: Add {C}.${T}: Add one mana of any color. Spend this mana only to cast a Sliver spell.${5}, {T}: Create a 1/1 colorless Sliver creature token. Activate only if you control a Sliver.| -Leinore, Autumn Sovereign|Midnight Hunt Commander|1|M|{2}{G}{W}|Legendary Creature - Human Noble|0|4|Coven — At the beginning of combat ono your turn, put a +1/+1 counter on up to one target creature you control. Then if you control three or more creatures with different powers, draw a card.| -Wilhelt, the Rotcleaver|Midnight Hunt Commander|2|R|{2}{U}{B}|Legendary Creature - Zombie Warrior|3|3|Whenever another zombie you control dies, if it didn't have decayed, create a 2/2 black Zombie creature token with decayed.$At the beginning of your end step, you may sacrifice a Zombie. If you do, draw a card.| +Leinore, Autumn Sovereign|Midnight Hunt Commander|1|M|{2}{G}{W}|Legendary Creature - Human Noble|0|4|Coven — At the beginning of combat on your turn, put a +1/+1 counter on up to one target creature you control. Then if you control three or more creatures with different powers, draw a card.| +Wilhelt, the Rotcleaver|Midnight Hunt Commander|2|M|{2}{U}{B}|Legendary Creature - Zombie Warrior|3|3|Whenever another zombie you control dies, if it didn't have decayed, create a 2/2 black Zombie creature token with decayed.$At the beginning of your end step, you may sacrifice a Zombie. If you do, draw a card.| Avacyn's Memorial|Midnight Hunt Commander|31|M|{5}{W}{W}{W}|Legendary Artifact|||Indestructible$Other legendary permanents you control have indestructible.| Visions of Glory|Midnight Hunt Commander|32|R|{4}{W}|Sorcery|||Create a 1/1 white Human creature token for each creature you control.$Flashback {8}{W}{W}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| Visions of Duplicity|Midnight Hunt Commander|33|R|{2}{U}|Sorcery|||Exchange control of two target creatures you don't control.$Flashback {8}{U}{U}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| Visions of Dread|Midnight Hunt Commander|34|R|{2}{B}|Sorcery|||Target opponent puts a creature card of choice from their graveyard onto the battlefield under your control.$Flashback {8}{B}{B}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| -Curse of Obsession|Midnight Hunt Commander|35|R|{4}{R}|Enchantment - Aura Curse|||Encahnt player$At the beginning of enchanted player's draw step, that player draws two additional cards.$At the beginning of enchanted player's end step, that player discards their hand.| +Curse of Obsession|Midnight Hunt Commander|35|R|{4}{R}|Enchantment - Aura Curse|||Enchant player$At the beginning of enchanted player's draw step, that player draws two additional cards.$At the beginning of enchanted player's end step, that player discards their hand.| Visions of Ruin|Midnight Hunt Commander|36|R|{3}{R}|Sorcery|||Each opponent sacrifices an artifact. For each artifact sacrificed this way, you create a Treasure token.$Flashback {8}{R}{R}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| Visions of Dominance|Midnight Hunt Commander|37|R|{2}{G}|Sorcery|||Put a +1/+1 counter on target creature, then double the number of +1/+1 counters on it.$Flashback {8}{G}{G}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| +Lynde, Cheerful Tormenter|Midnight Hunt Commander|38|M|{1}{U}{B}{R}|Legendary Creature - Human Warlock|2|4|Deathtouch$Whenever a Curse is put into your graveyard from the battlefield, return it to the battlefield attached to you at the beginning of the next end step.$At the beginning of your upkeep, you may attach a Curse attached to you to one of your opponents. If you do, draw two cards.| From c58a4f2eb2a00fd031154761bd1b2ada5ec0afde Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 18:31:44 -0400 Subject: [PATCH 054/231] [MID] Implemented Storm the Festival --- .../src/mage/cards/s/StormTheFestival.java | 50 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 51 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StormTheFestival.java diff --git a/Mage.Sets/src/mage/cards/s/StormTheFestival.java b/Mage.Sets/src/mage/cards/s/StormTheFestival.java new file mode 100644 index 00000000000..cf0293f5fb8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StormTheFestival.java @@ -0,0 +1,50 @@ +package mage.cards.s; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.common.FilterPermanentCard; +import mage.filter.predicate.mageobject.ManaValuePredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StormTheFestival extends CardImpl { + + private static final FilterCard filter = new FilterPermanentCard( + "up to two permanent cards with mana value 5 or less" + ); + + static { + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 6)); + } + + public StormTheFestival(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{G}{G}{G}"); + + // Look at the top five cards of your library. Put up to two permanent cards with mana value 5 or less from among them onto the battlefield. Put the rest on the bottom of your library in a random order. + this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect( + 5, 2, filter, false, true, Zone.BATTLEFIELD, false + ).setBackInRandomOrder(true)); + + // Flashback {7}{G}{G}{G} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{7}{G}{G}{G}"))); + } + + private StormTheFestival(final StormTheFestival card) { + super(card); + } + + @Override + public StormTheFestival copy() { + return new StormTheFestival(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 57acd7213dd..b04b82a0d2a 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -245,6 +245,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Stalking Predator", 120, Rarity.COMMON, mage.cards.s.StalkingPredator.class)); cards.add(new SetCardInfo("Startle", 78, Rarity.COMMON, mage.cards.s.Startle.class)); cards.add(new SetCardInfo("Stolen Vitality", 161, Rarity.COMMON, mage.cards.s.StolenVitality.class)); + cards.add(new SetCardInfo("Storm the Festival", 200, Rarity.RARE, mage.cards.s.StormTheFestival.class)); cards.add(new SetCardInfo("Storm-Charged Slasher", 157, Rarity.RARE, mage.cards.s.StormChargedSlasher.class)); cards.add(new SetCardInfo("Stormrider Spirit", 79, Rarity.COMMON, mage.cards.s.StormriderSpirit.class)); cards.add(new SetCardInfo("Stromkirk Bloodthief", 123, Rarity.UNCOMMON, mage.cards.s.StromkirkBloodthief.class)); From 6200f96d93ff5ce022269bbcbb527f9385ca3d0d Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 18:47:55 -0400 Subject: [PATCH 055/231] [MID] Implemented Arlinn, the Pack's Hope / Arlinn, the Moon's Fury --- .../src/mage/cards/a/ArlinnTheMoonsFury.java | 119 ++++++++++++++++++ .../src/mage/cards/a/ArlinnThePacksHope.java | 104 +++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 225 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/ArlinnTheMoonsFury.java create mode 100644 Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java diff --git a/Mage.Sets/src/mage/cards/a/ArlinnTheMoonsFury.java b/Mage.Sets/src/mage/cards/a/ArlinnTheMoonsFury.java new file mode 100644 index 00000000000..96a462e0f81 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/ArlinnTheMoonsFury.java @@ -0,0 +1,119 @@ +package mage.cards.a; + +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.PlaneswalkerEntersWithLoyaltyCountersAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.mana.BasicManaEffect; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.keyword.NightboundAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ArlinnTheMoonsFury extends CardImpl { + + public ArlinnTheMoonsFury(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, ""); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.ARLINN); + this.addAbility(new PlaneswalkerEntersWithLoyaltyCountersAbility(4)); + this.color.setRed(true); + this.color.setGreen(true); + this.transformable = true; + this.nightCard = true; + + // Nightbound + this.addAbility(NightboundAbility.getInstance()); + + // +2: Add {R}{G}. + this.addAbility(new LoyaltyAbility(new BasicManaEffect(new Mana( + 0, 0, 0, 1, 1, 0, 0, 0 + )), 2)); + + // 0: Until end of turn, Arlinn, the Moon's Fury becomes a 5/5 Werewolf creature with trample, indestructible, and haste. + this.addAbility(new LoyaltyAbility(new ArlinnTheMoonsFuryEffect(), 0)); + } + + private ArlinnTheMoonsFury(final ArlinnTheMoonsFury card) { + super(card); + } + + @Override + public ArlinnTheMoonsFury copy() { + return new ArlinnTheMoonsFury(this); + } +} + +class ArlinnTheMoonsFuryEffect extends ContinuousEffectImpl { + + ArlinnTheMoonsFuryEffect() { + super(Duration.EndOfTurn, Outcome.Benefit); + staticText = "until end of turn, {this} becomes a 5/5 Werewolf creature with trample, indestructible, and haste"; + } + + private ArlinnTheMoonsFuryEffect(final ArlinnTheMoonsFuryEffect effect) { + super(effect); + } + + @Override + public ArlinnTheMoonsFuryEffect copy() { + return new ArlinnTheMoonsFuryEffect(this); + } + + @Override + public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null) { + discard(); + return false; + } + switch (layer) { + case TypeChangingEffects_4: + permanent.removeAllCardTypes(game); + permanent.addCardType(game, CardType.CREATURE); + permanent.removeAllCreatureTypes(game); + permanent.addSubType(game, SubType.WEREWOLF); + return true; + case AbilityAddingRemovingEffects_6: + permanent.addAbility(TrampleAbility.getInstance(), source.getSourceId(), game); + permanent.addAbility(IndestructibleAbility.getInstance(), source.getSourceId(), game); + permanent.addAbility(HasteAbility.getInstance(), source.getSourceId(), game); + return true; + case PTChangingEffects_7: + if (sublayer == SubLayer.SetPT_7b) { + permanent.getPower().setValue(5); + permanent.getToughness().setValue(5); + return true; + } + } + return false; + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean hasLayer(Layer layer) { + switch (layer) { + case TypeChangingEffects_4: + case AbilityAddingRemovingEffects_6: + case PTChangingEffects_7: + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java b/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java new file mode 100644 index 00000000000..f475991694e --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java @@ -0,0 +1,104 @@ +package mage.cards.a; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.PlaneswalkerEntersWithLoyaltyCountersAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.CastAsThoughItHadFlashAllEffect; +import mage.abilities.keyword.DayboundAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.game.events.EntersTheBattlefieldEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.WolfToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ArlinnThePacksHope extends CardImpl { + + private static final FilterCard filter = new FilterCreatureCard("creature spells"); + + public ArlinnThePacksHope(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{R}{G}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.ARLINN); + this.addAbility(new PlaneswalkerEntersWithLoyaltyCountersAbility(4)); + this.transformable = true; + this.secondSideCardClazz = mage.cards.a.ArlinnTheMoonsFury.class; + + // Daybound + this.addAbility(DayboundAbility.getInstance()); + + // +1: Until your next turn, you may cast creature spells as though they had flash, and each creature you control enters the battlefield with an additional +1/+1 counter on it. + Ability ability = new LoyaltyAbility(new CastAsThoughItHadFlashAllEffect( + Duration.UntilYourNextTurn, filter + ).setText("until your next turn, you may cast creature spells as though they had flash"), 1); + ability.addEffect(new ArlinnThePacksHopeEffect()); + this.addAbility(ability); + + // −3: Create two 2/2 green Wolf creature tokens. + this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new WolfToken(), 2), -3)); + } + + private ArlinnThePacksHope(final ArlinnThePacksHope card) { + super(card); + } + + @Override + public ArlinnThePacksHope copy() { + return new ArlinnThePacksHope(this); + } +} + +class ArlinnThePacksHopeEffect extends ReplacementEffectImpl { + + ArlinnThePacksHopeEffect() { + super(Duration.UntilYourNextTurn, Outcome.BoostCreature); + this.staticText = ", and each creature you control enters the battlefield with an additional +1/+1 counter on it"; + } + + private ArlinnThePacksHopeEffect(ArlinnThePacksHopeEffect effect) { + super(effect); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Permanent permanent = ((EntersTheBattlefieldEvent) event).getTarget(); + return permanent != null && permanent.isControlledBy(source.getControllerId()) && permanent.isCreature(game); + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Permanent target = ((EntersTheBattlefieldEvent) event).getTarget(); + if (target != null) { + target.addCounters(CounterType.P1P1.createInstance(), source.getControllerId(), source, game, event.getAppliedEffects()); + } + return false; + } + + @Override + public ArlinnThePacksHopeEffect copy() { + return new ArlinnThePacksHopeEffect(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index b04b82a0d2a..ac9ff978c03 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -38,6 +38,8 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Arcane Infusion", 210, Rarity.UNCOMMON, mage.cards.a.ArcaneInfusion.class)); cards.add(new SetCardInfo("Archive Haunt", 68, Rarity.UNCOMMON, mage.cards.a.ArchiveHaunt.class)); cards.add(new SetCardInfo("Ardent Elementalist", 128, Rarity.COMMON, mage.cards.a.ArdentElementalist.class)); + cards.add(new SetCardInfo("Arlinn, the Moon's Fury", 211, Rarity.MYTHIC, mage.cards.a.ArlinnTheMoonsFury.class)); + cards.add(new SetCardInfo("Arlinn, the Pack's Hope", 211, Rarity.MYTHIC, mage.cards.a.ArlinnThePacksHope.class)); cards.add(new SetCardInfo("Arrogant Outlaw", 84, Rarity.COMMON, mage.cards.a.ArrogantOutlaw.class)); cards.add(new SetCardInfo("Augur of Autumn", 168, Rarity.RARE, mage.cards.a.AugurOfAutumn.class)); cards.add(new SetCardInfo("Awoken Demon", 100, Rarity.COMMON, mage.cards.a.AwokenDemon.class)); From 822f8b767bfb8283bcad88ce985e402803bcad83 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 19:56:44 -0400 Subject: [PATCH 056/231] [MID] Implemented Lunarch Veteran / Luminous Phantom --- .../src/mage/cards/l/LuminousPhantom.java | 53 +++++++++++++++++++ .../src/mage/cards/l/LunarchVeteran.java | 50 +++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 105 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LuminousPhantom.java create mode 100644 Mage.Sets/src/mage/cards/l/LunarchVeteran.java diff --git a/Mage.Sets/src/mage/cards/l/LuminousPhantom.java b/Mage.Sets/src/mage/cards/l/LuminousPhantom.java new file mode 100644 index 00000000000..8819843a1ba --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LuminousPhantom.java @@ -0,0 +1,53 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.common.LeavesBattlefieldAllTriggeredAbility; +import mage.abilities.common.PutIntoGraveFromAnywhereSourceAbility; +import mage.abilities.effects.common.ExileSourceEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LuminousPhantom extends CardImpl { + + public LuminousPhantom(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + this.color.setWhite(true); + this.transformable = true; + this.nightCard = true; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever another creature you control leaves the battlefield, you gain 1 life. + this.addAbility(new LeavesBattlefieldAllTriggeredAbility( + new GainLifeEffect(1), StaticFilters.FILTER_CONTROLLED_ANOTHER_CREATURE + )); + + // If Luminous Phantom would be put into a graveyard from anywhere, exile it instead. + this.addAbility(new PutIntoGraveFromAnywhereSourceAbility(new ExileSourceEffect().setText("exile it instead"))); + } + + private LuminousPhantom(final LuminousPhantom card) { + super(card); + } + + @Override + public LuminousPhantom copy() { + return new LuminousPhantom(this); + } +} diff --git a/Mage.Sets/src/mage/cards/l/LunarchVeteran.java b/Mage.Sets/src/mage/cards/l/LunarchVeteran.java new file mode 100644 index 00000000000..61596d263f3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LunarchVeteran.java @@ -0,0 +1,50 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LunarchVeteran extends CardImpl { + + public LunarchVeteran(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + this.transformable = true; + this.secondSideCardClazz = mage.cards.l.LuminousPhantom.class; + + // Whenever another creature enters the battlefield under your control, you gain 1 life. + this.addAbility(new EntersBattlefieldControlledTriggeredAbility( + new GainLifeEffect(1), StaticFilters.FILTER_CONTROLLED_ANOTHER_CREATURE + )); + + // Disturb {1}{W} + this.addAbility(new TransformAbility()); + this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{1}{W}"))); + } + + private LunarchVeteran(final LunarchVeteran card) { + super(card); + } + + @Override + public LunarchVeteran copy() { + return new LunarchVeteran(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index ac9ff978c03..9bb13602ae1 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -182,7 +182,9 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Locked in the Cemetery", 60, Rarity.COMMON, mage.cards.l.LockedInTheCemetery.class)); cards.add(new SetCardInfo("Lord of the Ulvenwald", 231, Rarity.UNCOMMON, mage.cards.l.LordOfTheUlvenwald.class)); cards.add(new SetCardInfo("Loyal Gryff", 26, Rarity.UNCOMMON, mage.cards.l.LoyalGryff.class)); + cards.add(new SetCardInfo("Luminous Phantom", 27, Rarity.COMMON, mage.cards.l.LuminousPhantom.class)); cards.add(new SetCardInfo("Lunar Frenzy", 147, Rarity.UNCOMMON, mage.cards.l.LunarFrenzy.class)); + cards.add(new SetCardInfo("Lunarch Veteran", 27, Rarity.COMMON, mage.cards.l.LunarchVeteran.class)); cards.add(new SetCardInfo("Malevolent Hermit", 61, Rarity.RARE, mage.cards.m.MalevolentHermit.class)); cards.add(new SetCardInfo("Mask of Griselbrand", 111, Rarity.RARE, mage.cards.m.MaskOfGriselbrand.class)); cards.add(new SetCardInfo("Might of the Old Ways", 189, Rarity.COMMON, mage.cards.m.MightOfTheOldWays.class)); From bc4ec72656c632604abe55e8df68845d6645cee9 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 20:08:54 -0400 Subject: [PATCH 057/231] [MID] Implemented Devoted Grafkeeper / Departed Soulkeeper --- .../src/mage/cards/d/DepartedSoulkeeper.java | 49 ++++++++++ .../src/mage/cards/d/DevotedGrafkeeper.java | 89 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 140 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DepartedSoulkeeper.java create mode 100644 Mage.Sets/src/mage/cards/d/DevotedGrafkeeper.java diff --git a/Mage.Sets/src/mage/cards/d/DepartedSoulkeeper.java b/Mage.Sets/src/mage/cards/d/DepartedSoulkeeper.java new file mode 100644 index 00000000000..91e19c92f0f --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DepartedSoulkeeper.java @@ -0,0 +1,49 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.common.CanBlockOnlyFlyingAbility; +import mage.abilities.common.PutIntoGraveFromAnywhereSourceAbility; +import mage.abilities.effects.common.ExileSourceEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DepartedSoulkeeper extends CardImpl { + + public DepartedSoulkeeper(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.SPIRIT); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + this.color.setWhite(true); + this.color.setBlue(true); + this.transformable = true; + this.nightCard = true; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Departed Soulkeeper can block only creatures with flying. + this.addAbility(new CanBlockOnlyFlyingAbility()); + + // If Departed Soulkeeper would be put into a graveyard from anywhere, exile it instead. + this.addAbility(new PutIntoGraveFromAnywhereSourceAbility(new ExileSourceEffect().setText("exile it instead"))); + } + + private DepartedSoulkeeper(final DepartedSoulkeeper card) { + super(card); + } + + @Override + public DepartedSoulkeeper copy() { + return new DepartedSoulkeeper(this); + } +} diff --git a/Mage.Sets/src/mage/cards/d/DevotedGrafkeeper.java b/Mage.Sets/src/mage/cards/d/DevotedGrafkeeper.java new file mode 100644 index 00000000000..667d86340c4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DevotedGrafkeeper.java @@ -0,0 +1,89 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.effects.common.TapTargetEffect; +import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DevotedGrafkeeper extends CardImpl { + + public DevotedGrafkeeper(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}{U}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.PEASANT); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + this.transformable = true; + this.secondSideCardClazz = mage.cards.d.DepartedSoulkeeper.class; + + // When Devoted Grafkeeper enters the battlefield, mill two cards. + this.addAbility(new EntersBattlefieldTriggeredAbility(new MillCardsControllerEffect(2))); + + // Whenever you cast a spell from your graveyard, tap target creature you don't control. + this.addAbility(new DevotedGrafkeeperTriggeredAbility()); + + // Disturb {1}{W}{U} + this.addAbility(new TransformAbility()); + this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{1}{W}{U}"))); + } + + private DevotedGrafkeeper(final DevotedGrafkeeper card) { + super(card); + } + + @Override + public DevotedGrafkeeper copy() { + return new DevotedGrafkeeper(this); + } +} + +class DevotedGrafkeeperTriggeredAbility extends TriggeredAbilityImpl { + + DevotedGrafkeeperTriggeredAbility() { + super(Zone.BATTLEFIELD, new TapTargetEffect(), false); + this.addTarget(new TargetPermanent(StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL)); + } + + private DevotedGrafkeeperTriggeredAbility(DevotedGrafkeeperTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SPELL_CAST; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return isControlledBy(event.getPlayerId()) && event.getZone() == Zone.GRAVEYARD; + } + + @Override + public DevotedGrafkeeperTriggeredAbility copy() { + return new DevotedGrafkeeperTriggeredAbility(this); + } + + @Override + public String getRule() { + return "Whenever you cast a spell from your graveyard, tap target creature you don't control."; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 9bb13602ae1..8ee1b8b59af 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -98,8 +98,10 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Defend the Celestus", 182, Rarity.UNCOMMON, mage.cards.d.DefendTheCelestus.class)); cards.add(new SetCardInfo("Defenestrate", 95, Rarity.COMMON, mage.cards.d.Defenestrate.class)); cards.add(new SetCardInfo("Delver of Secrets", 47, Rarity.UNCOMMON, mage.cards.d.DelverOfSecrets.class)); + cards.add(new SetCardInfo("Departed Soulkeeper", 218, Rarity.UNCOMMON, mage.cards.d.DepartedSoulkeeper.class)); cards.add(new SetCardInfo("Deserted Beach", 260, Rarity.RARE, mage.cards.d.DesertedBeach.class)); cards.add(new SetCardInfo("Devious Cover-Up", 48, Rarity.COMMON, mage.cards.d.DeviousCoverUp.class)); + cards.add(new SetCardInfo("Devoted Grafkeeper", 218, Rarity.UNCOMMON, mage.cards.d.DevotedGrafkeeper.class)); cards.add(new SetCardInfo("Dire-Strain Brawler", 203, Rarity.COMMON, mage.cards.d.DireStrainBrawler.class)); cards.add(new SetCardInfo("Dire-Strain Demolisher", 174, Rarity.UNCOMMON, mage.cards.d.DireStrainDemolisher.class)); cards.add(new SetCardInfo("Dire-Strain Rampage", 219, Rarity.RARE, mage.cards.d.DireStrainRampage.class)); From f2b7434b927fa7a6c71d828abc97c52248222fce Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 20:18:29 -0400 Subject: [PATCH 058/231] [MID] Implemented The Meathook Massacre --- .../src/mage/cards/t/TheMeathookMassacre.java | 58 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 59 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TheMeathookMassacre.java diff --git a/Mage.Sets/src/mage/cards/t/TheMeathookMassacre.java b/Mage.Sets/src/mage/cards/t/TheMeathookMassacre.java new file mode 100644 index 00000000000..def254de989 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheMeathookMassacre.java @@ -0,0 +1,58 @@ +package mage.cards.t; + +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.MultipliedValue; +import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.abilities.effects.common.continuous.BoostAllEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SuperType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TheMeathookMassacre extends CardImpl { + + private static final DynamicValue xValue = new MultipliedValue(ManacostVariableValue.ETB, -1); + + public TheMeathookMassacre(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{X}{B}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + + // When The Meathook Massacre enters the battlefield, each creature gets -X/-X until end of turn. + this.addAbility(new EntersBattlefieldTriggeredAbility(new BoostAllEffect( + xValue, xValue, Duration.EndOfTurn + ).setText("each creature gets -X/-X until end of turn"))); + + // Whenever a creature you control dies, each opponent loses 1 life. + this.addAbility(new DiesCreatureTriggeredAbility( + new LoseLifeOpponentsEffect(1), false, + StaticFilters.FILTER_CONTROLLED_A_CREATURE + )); + + // Whenever a creature an opponent controls dies, you gain 1 life. + this.addAbility(new DiesCreatureTriggeredAbility( + new GainLifeEffect(1), false, + StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE + )); + } + + private TheMeathookMassacre(final TheMeathookMassacre card) { + super(card); + } + + @Override + public TheMeathookMassacre copy() { + return new TheMeathookMassacre(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 8ee1b8b59af..efc4e7c404f 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -264,6 +264,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Tainted Adversary", 124, Rarity.MYTHIC, mage.cards.t.TaintedAdversary.class)); cards.add(new SetCardInfo("Tavern Ruffian", 163, Rarity.COMMON, mage.cards.t.TavernRuffian.class)); cards.add(new SetCardInfo("Tavern Smasher", 163, Rarity.COMMON, mage.cards.t.TavernSmasher.class)); + cards.add(new SetCardInfo("The Meathook Massacre", 112, Rarity.MYTHIC, mage.cards.t.TheMeathookMassacre.class)); cards.add(new SetCardInfo("Thermo-Alchemist", 164, Rarity.UNCOMMON, mage.cards.t.ThermoAlchemist.class)); cards.add(new SetCardInfo("Thraben Exorcism", 39, Rarity.COMMON, mage.cards.t.ThrabenExorcism.class)); cards.add(new SetCardInfo("Timberland Guide", 202, Rarity.COMMON, mage.cards.t.TimberlandGuide.class)); From 655a81d2ac16576e992fa695273313b1a215dacb Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 20:23:08 -0400 Subject: [PATCH 059/231] [MID] Implemented Otherworldly Gaze --- .../src/mage/cards/o/OtherworldlyGaze.java | 41 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 42 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/o/OtherworldlyGaze.java diff --git a/Mage.Sets/src/mage/cards/o/OtherworldlyGaze.java b/Mage.Sets/src/mage/cards/o/OtherworldlyGaze.java new file mode 100644 index 00000000000..0463022624e --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OtherworldlyGaze.java @@ -0,0 +1,41 @@ +package mage.cards.o; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class OtherworldlyGaze extends CardImpl { + + public OtherworldlyGaze(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}"); + + // Look at the top three cards of your library. Put any number of them into your graveyard and the rest back on top of your library in any order. + this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect( + StaticValue.get(3), false, StaticValue.get(5), StaticFilters.FILTER_CARD_CARDS, + Zone.LIBRARY, true, false, true, Zone.GRAVEYARD, false + )); + + // Flashback {1}{U} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{U}"))); + } + + private OtherworldlyGaze(final OtherworldlyGaze card) { + super(card); + } + + @Override + public OtherworldlyGaze copy() { + return new OtherworldlyGaze(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index efc4e7c404f..4a76d3ebdc5 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -207,6 +207,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Obsessive Astronomer", 152, Rarity.UNCOMMON, mage.cards.o.ObsessiveAstronomer.class)); cards.add(new SetCardInfo("Odric's Outrider", 29, Rarity.UNCOMMON, mage.cards.o.OdricsOutrider.class)); cards.add(new SetCardInfo("Organ Hoarder", 66, Rarity.COMMON, mage.cards.o.OrganHoarder.class)); + cards.add(new SetCardInfo("Otherworldly Gaze", 67, Rarity.COMMON, mage.cards.o.OtherworldlyGaze.class)); cards.add(new SetCardInfo("Outland Liberator", 190, Rarity.UNCOMMON, mage.cards.o.OutlandLiberator.class)); cards.add(new SetCardInfo("Overgrown Farmland", 265, Rarity.RARE, mage.cards.o.OvergrownFarmland.class)); cards.add(new SetCardInfo("Overwhelmed Archivist", 68, Rarity.UNCOMMON, mage.cards.o.OverwhelmedArchivist.class)); From 849b8d19dc8763e6265d971dd462b3df162ea9cb Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 11 Sep 2021 20:43:48 -0400 Subject: [PATCH 060/231] fixed verify errors --- Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java | 2 ++ Mage.Sets/src/mage/cards/c/ChillingChronicle.java | 1 + Mage.Sets/src/mage/cards/c/CovetousGeist.java | 1 + Mage.Sets/src/mage/cards/f/FangbladeBrigand.java | 2 ++ Mage.Sets/src/mage/cards/g/Galedrifter.java | 2 ++ Mage.Sets/src/mage/cards/g/GhostlyCastigator.java | 1 + Mage.Sets/src/mage/cards/h/HarvesttideInfiltrator.java | 2 ++ Mage.Sets/src/mage/cards/m/MourningPatrol.java | 2 ++ Mage.Sets/src/mage/cards/s/ShadyTraveler.java | 2 ++ Mage.Sets/src/mage/cards/t/TirelessHauler.java | 2 ++ 10 files changed, 17 insertions(+) diff --git a/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java b/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java index f475991694e..39076e1d213 100644 --- a/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java +++ b/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java @@ -7,6 +7,7 @@ import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.continuous.CastAsThoughItHadFlashAllEffect; import mage.abilities.keyword.DayboundAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -38,6 +39,7 @@ public final class ArlinnThePacksHope extends CardImpl { this.secondSideCardClazz = mage.cards.a.ArlinnTheMoonsFury.class; // Daybound + this.addAbility(new TransformAbility()); this.addAbility(DayboundAbility.getInstance()); // +1: Until your next turn, you may cast creature spells as though they had flash, and each creature you control enters the battlefield with an additional +1/+1 counter on it. diff --git a/Mage.Sets/src/mage/cards/c/ChillingChronicle.java b/Mage.Sets/src/mage/cards/c/ChillingChronicle.java index 324529c58e8..27b55de43e1 100644 --- a/Mage.Sets/src/mage/cards/c/ChillingChronicle.java +++ b/Mage.Sets/src/mage/cards/c/ChillingChronicle.java @@ -21,6 +21,7 @@ public final class ChillingChronicle extends CardImpl { public ChillingChronicle(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, ""); + this.color.setBlue(true); this.transformable = true; this.nightCard = true; diff --git a/Mage.Sets/src/mage/cards/c/CovetousGeist.java b/Mage.Sets/src/mage/cards/c/CovetousGeist.java index f72b20fd483..7f1be068826 100644 --- a/Mage.Sets/src/mage/cards/c/CovetousGeist.java +++ b/Mage.Sets/src/mage/cards/c/CovetousGeist.java @@ -24,6 +24,7 @@ public final class CovetousGeist extends CardImpl { this.subtype.add(SubType.ROGUE); this.power = new MageInt(2); this.toughness = new MageInt(2); + this.color.setBlack(true); this.transformable = true; this.nightCard = true; diff --git a/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java b/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java index 53221949d4a..67526dfe671 100644 --- a/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java +++ b/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java @@ -8,6 +8,7 @@ import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; import mage.abilities.keyword.DayboundAbility; import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -41,6 +42,7 @@ public final class FangbladeBrigand extends CardImpl { this.addAbility(ability); // Daybound + this.addAbility(new TransformAbility()); this.addAbility(DayboundAbility.getInstance()); } diff --git a/Mage.Sets/src/mage/cards/g/Galedrifter.java b/Mage.Sets/src/mage/cards/g/Galedrifter.java index 1b644d96c38..1ef6a7f13da 100644 --- a/Mage.Sets/src/mage/cards/g/Galedrifter.java +++ b/Mage.Sets/src/mage/cards/g/Galedrifter.java @@ -4,6 +4,7 @@ import mage.MageInt; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.keyword.DisturbAbility; import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -29,6 +30,7 @@ public final class Galedrifter extends CardImpl { this.addAbility(FlyingAbility.getInstance()); // Disturb {4}{U} + this.addAbility(new TransformAbility()); this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{4}{U}"))); } diff --git a/Mage.Sets/src/mage/cards/g/GhostlyCastigator.java b/Mage.Sets/src/mage/cards/g/GhostlyCastigator.java index 5e882234492..35833d52144 100644 --- a/Mage.Sets/src/mage/cards/g/GhostlyCastigator.java +++ b/Mage.Sets/src/mage/cards/g/GhostlyCastigator.java @@ -26,6 +26,7 @@ public final class GhostlyCastigator extends CardImpl { this.subtype.add(SubType.SPIRIT); this.power = new MageInt(3); this.toughness = new MageInt(4); + this.color.setBlue(true); this.transformable = true; this.nightCard = true; diff --git a/Mage.Sets/src/mage/cards/h/HarvesttideInfiltrator.java b/Mage.Sets/src/mage/cards/h/HarvesttideInfiltrator.java index 25528bc3cb4..aff3c87fa60 100644 --- a/Mage.Sets/src/mage/cards/h/HarvesttideInfiltrator.java +++ b/Mage.Sets/src/mage/cards/h/HarvesttideInfiltrator.java @@ -3,6 +3,7 @@ package mage.cards.h; import mage.MageInt; import mage.abilities.keyword.DayboundAbility; import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -29,6 +30,7 @@ public final class HarvesttideInfiltrator extends CardImpl { this.addAbility(TrampleAbility.getInstance()); // Daybound + this.addAbility(new TransformAbility()); this.addAbility(DayboundAbility.getInstance()); } diff --git a/Mage.Sets/src/mage/cards/m/MourningPatrol.java b/Mage.Sets/src/mage/cards/m/MourningPatrol.java index 70448be2db0..8065c06f1f8 100644 --- a/Mage.Sets/src/mage/cards/m/MourningPatrol.java +++ b/Mage.Sets/src/mage/cards/m/MourningPatrol.java @@ -3,6 +3,7 @@ package mage.cards.m; import mage.MageInt; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.TransformAbility; import mage.abilities.keyword.VigilanceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -30,6 +31,7 @@ public final class MourningPatrol extends CardImpl { this.addAbility(VigilanceAbility.getInstance()); // Disturb {3}{W} + this.addAbility(new TransformAbility()); this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{3}{W}"))); } diff --git a/Mage.Sets/src/mage/cards/s/ShadyTraveler.java b/Mage.Sets/src/mage/cards/s/ShadyTraveler.java index a8cee198aa5..e9ab146127b 100644 --- a/Mage.Sets/src/mage/cards/s/ShadyTraveler.java +++ b/Mage.Sets/src/mage/cards/s/ShadyTraveler.java @@ -3,6 +3,7 @@ package mage.cards.s; import mage.MageInt; import mage.abilities.keyword.DayboundAbility; import mage.abilities.keyword.MenaceAbility; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -29,6 +30,7 @@ public final class ShadyTraveler extends CardImpl { this.addAbility(new MenaceAbility()); // Daybound + this.addAbility(new TransformAbility()); this.addAbility(DayboundAbility.getInstance()); } diff --git a/Mage.Sets/src/mage/cards/t/TirelessHauler.java b/Mage.Sets/src/mage/cards/t/TirelessHauler.java index 148791a6ed9..e4603424729 100644 --- a/Mage.Sets/src/mage/cards/t/TirelessHauler.java +++ b/Mage.Sets/src/mage/cards/t/TirelessHauler.java @@ -2,6 +2,7 @@ package mage.cards.t; import mage.MageInt; import mage.abilities.keyword.DayboundAbility; +import mage.abilities.keyword.TransformAbility; import mage.abilities.keyword.VigilanceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -29,6 +30,7 @@ public final class TirelessHauler extends CardImpl { this.addAbility(VigilanceAbility.getInstance()); // Daybound + this.addAbility(new TransformAbility()); this.addAbility(DayboundAbility.getInstance()); } From 3463b720ce6820c65098f18585d1864db14c010d Mon Sep 17 00:00:00 2001 From: spjspj Date: Sun, 12 Sep 2021 15:21:03 +1000 Subject: [PATCH 061/231] No final blank line in jumpstart Worked out it can't have the final blank line or you can get blank packs being presented (so you will get a pack of 20 cards and a pack of 0 cards). --- Mage/src/main/resources/jumpstart/jumpstart.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/Mage/src/main/resources/jumpstart/jumpstart.txt b/Mage/src/main/resources/jumpstart/jumpstart.txt index 64aba0f50de..73df97d972a 100644 --- a/Mage/src/main/resources/jumpstart/jumpstart.txt +++ b/Mage/src/main/resources/jumpstart/jumpstart.txt @@ -1212,4 +1212,3 @@ 1 M21 71 Shipwreck Dowser 1 M21 62 Read the Tides 1 M21 295 Teferi's Protege - From 4c69286c148a430ae15e975a037547df84b07a25 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 07:42:55 -0400 Subject: [PATCH 062/231] [MID] Implemented Bereaved Survivor / Dauntless Avenger --- .../mage/cards/a/AleshaWhoSmilesAtDeath.java | 61 +++++-------------- .../src/mage/cards/b/BereavedSurvivor.java | 46 ++++++++++++++ .../src/mage/cards/d/DauntlessAvenger.java | 56 +++++++++++++++++ .../src/mage/cards/y/YoreTillerNephilim.java | 52 ++-------------- .../src/mage/sets/InnistradMidnightHunt.java | 2 + ...romGraveyardToBattlefieldTargetEffect.java | 18 +++++- 6 files changed, 141 insertions(+), 94 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/b/BereavedSurvivor.java create mode 100644 Mage.Sets/src/mage/cards/d/DauntlessAvenger.java diff --git a/Mage.Sets/src/mage/cards/a/AleshaWhoSmilesAtDeath.java b/Mage.Sets/src/mage/cards/a/AleshaWhoSmilesAtDeath.java index 5821347dc85..9f76c02a339 100644 --- a/Mage.Sets/src/mage/cards/a/AleshaWhoSmilesAtDeath.java +++ b/Mage.Sets/src/mage/cards/a/AleshaWhoSmilesAtDeath.java @@ -1,38 +1,39 @@ - package mage.cards.a; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; import mage.abilities.keyword.FirstStrikeAbility; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterCard; import mage.filter.common.FilterCreatureCard; import mage.filter.predicate.mageobject.PowerPredicate; -import mage.game.Game; -import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class AleshaWhoSmilesAtDeath extends CardImpl { - private static final FilterCreatureCard filter = new FilterCreatureCard("creature card with power 2 or less"); + private static final FilterCard filter + = new FilterCreatureCard("creature card with power 2 or less from your graveyard"); static { filter.add(new PowerPredicate(ComparisonType.FEWER_THAN, 3)); } public AleshaWhoSmilesAtDeath(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{R}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); addSuperType(SuperType.LEGENDARY); this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.WARRIOR); @@ -43,7 +44,10 @@ public final class AleshaWhoSmilesAtDeath extends CardImpl { this.addAbility(FirstStrikeAbility.getInstance()); // Whenever Alesha, Who Smiles at Death attacks, you may pay {W/B}{W/B}. If you do, return target creature card with power 2 or less from your graveyard to the battlefield tapped and attacking. - Ability ability = new AttacksTriggeredAbility(new DoIfCostPaid(new AleshaWhoSmilesAtDeathEffect(), new ManaCostsImpl("{W/B}{W/B}")), false); + Ability ability = new AttacksTriggeredAbility(new DoIfCostPaid( + new ReturnFromGraveyardToBattlefieldTargetEffect(true, true), + new ManaCostsImpl<>("{W/B}{W/B}") + ), false); ability.addTarget(new TargetCardInYourGraveyard(filter)); this.addAbility(ability); } @@ -57,38 +61,3 @@ public final class AleshaWhoSmilesAtDeath extends CardImpl { return new AleshaWhoSmilesAtDeath(this); } } - -class AleshaWhoSmilesAtDeathEffect extends OneShotEffect { - - public AleshaWhoSmilesAtDeathEffect() { - super(Outcome.PutCreatureInPlay); - this.staticText = "return target creature card with power 2 or less from your graveyard to the battlefield tapped and attacking"; - } - - public AleshaWhoSmilesAtDeathEffect(final AleshaWhoSmilesAtDeathEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - - if (controller != null) { - Card card = game.getCard(getTargetPointer().getFirst(game, source)); - if (card != null) { - if (controller.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null)) { - game.getCombat().addAttackingCreature(card.getId(), game); - } - } - return true; - - } - return false; - } - - @Override - public AleshaWhoSmilesAtDeathEffect copy() { - return new AleshaWhoSmilesAtDeathEffect(this); - } - -} diff --git a/Mage.Sets/src/mage/cards/b/BereavedSurvivor.java b/Mage.Sets/src/mage/cards/b/BereavedSurvivor.java new file mode 100644 index 00000000000..453f517c4fd --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BereavedSurvivor.java @@ -0,0 +1,46 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BereavedSurvivor extends CardImpl { + + public BereavedSurvivor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.PEASANT); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + this.transformable = true; + this.secondSideCardClazz = mage.cards.d.DauntlessAvenger.class; + + // When another creature you control dies, transform Bereaved Survivor. + this.addAbility(new TransformAbility()); + this.addAbility(new DiesCreatureTriggeredAbility( + new TransformSourceEffect(true), false, + StaticFilters.FILTER_CONTROLLED_ANOTHER_CREATURE + )); + } + + private BereavedSurvivor(final BereavedSurvivor card) { + super(card); + } + + @Override + public BereavedSurvivor copy() { + return new BereavedSurvivor(this); + } +} diff --git a/Mage.Sets/src/mage/cards/d/DauntlessAvenger.java b/Mage.Sets/src/mage/cards/d/DauntlessAvenger.java new file mode 100644 index 00000000000..d7004ce3498 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DauntlessAvenger.java @@ -0,0 +1,56 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DauntlessAvenger extends CardImpl { + + private static final FilterCard filter + = new FilterCreatureCard("creature card with mana value 2 or less from your graveyard"); + + static { + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 3)); + } + + public DauntlessAvenger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + this.color.setWhite(true); + this.transformable = true; + this.nightCard = true; + + // Whenever Dauntless Avenger attacks, return target creature card with mana value 2 or less from your graveyard to the battlefield tapped and attacking. + Ability ability = new AttacksTriggeredAbility(new ReturnFromGraveyardToBattlefieldTargetEffect(true, true)); + ability.addTarget(new TargetCardInYourGraveyard(filter)); + this.addAbility(ability); + } + + private DauntlessAvenger(final DauntlessAvenger card) { + super(card); + } + + @Override + public DauntlessAvenger copy() { + return new DauntlessAvenger(this); + } +} diff --git a/Mage.Sets/src/mage/cards/y/YoreTillerNephilim.java b/Mage.Sets/src/mage/cards/y/YoreTillerNephilim.java index 163d649e4ff..cc0defe9056 100644 --- a/Mage.Sets/src/mage/cards/y/YoreTillerNephilim.java +++ b/Mage.Sets/src/mage/cards/y/YoreTillerNephilim.java @@ -1,25 +1,19 @@ package mage.cards.y; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; -import mage.abilities.effects.OneShotEffect; -import mage.cards.Card; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.SubType; -import mage.constants.Zone; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; +import java.util.UUID; + /** - * * @author fireshoes */ public final class YoreTillerNephilim extends CardImpl { @@ -31,8 +25,8 @@ public final class YoreTillerNephilim extends CardImpl { this.toughness = new MageInt(2); // Whenever Yore-Tiller Nephilim attacks, return target creature card from your graveyard to the battlefield tapped and attacking. - Ability ability = new AttacksTriggeredAbility(new YoreTillerNephilimEffect(), false); - ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE)); + Ability ability = new AttacksTriggeredAbility(new ReturnFromGraveyardToBattlefieldTargetEffect(true, true), false); + ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); this.addAbility(ability); } @@ -45,39 +39,3 @@ public final class YoreTillerNephilim extends CardImpl { return new YoreTillerNephilim(this); } } - -class YoreTillerNephilimEffect extends OneShotEffect { - - public YoreTillerNephilimEffect() { - super(Outcome.PutCreatureInPlay); - this.staticText = "return target creature card from your graveyard to the battlefield tapped and attacking"; - } - - public YoreTillerNephilimEffect(final YoreTillerNephilimEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - - if (controller != null) { - Card card = game.getCard(getTargetPointer().getFirst(game, source)); - if (card != null) { - controller.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null); - Permanent permanent = game.getPermanent(card.getId()); - if (permanent != null) { - game.getCombat().addAttackingCreature(permanent.getId(), game); - } - } - return true; - - } - return false; - } - - @Override - public YoreTillerNephilimEffect copy() { - return new YoreTillerNephilimEffect(this); - } -} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 4a76d3ebdc5..285eb059dfc 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -46,6 +46,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Baithook Angler", 42, Rarity.COMMON, mage.cards.b.BaithookAngler.class)); cards.add(new SetCardInfo("Bat Whisperer", 86, Rarity.COMMON, mage.cards.b.BatWhisperer.class)); cards.add(new SetCardInfo("Benevolent Geist", 61, Rarity.RARE, mage.cards.b.BenevolentGeist.class)); + cards.add(new SetCardInfo("Bereaved Survivor", 4, Rarity.UNCOMMON, mage.cards.b.BereavedSurvivor.class)); cards.add(new SetCardInfo("Bird Admirer", 169, Rarity.COMMON, mage.cards.b.BirdAdmirer.class)); cards.add(new SetCardInfo("Bladebrand", 87, Rarity.COMMON, mage.cards.b.Bladebrand.class)); cards.add(new SetCardInfo("Bladestitched Skaab", 212, Rarity.UNCOMMON, mage.cards.b.BladestitchedSkaab.class)); @@ -90,6 +91,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Curse of Shaken Faith", 134, Rarity.RARE, mage.cards.c.CurseOfShakenFaith.class)); cards.add(new SetCardInfo("Curse of Silence", 15, Rarity.RARE, mage.cards.c.CurseOfSilence.class)); cards.add(new SetCardInfo("Curse of Surveillance", 46, Rarity.RARE, mage.cards.c.CurseOfSurveillance.class)); + cards.add(new SetCardInfo("Dauntless Avenger", 4, Rarity.UNCOMMON, mage.cards.d.DauntlessAvenger.class)); cards.add(new SetCardInfo("Dawnhart Mentor", 179, Rarity.UNCOMMON, mage.cards.d.DawnhartMentor.class)); cards.add(new SetCardInfo("Dawnhart Rejuvenator", 180, Rarity.COMMON, mage.cards.d.DawnhartRejuvenator.class)); cards.add(new SetCardInfo("Dawnhart Wardens", 216, Rarity.UNCOMMON, mage.cards.d.DawnhartWardens.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldTargetEffect.java index 860bfef7ed2..617f651e263 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldTargetEffect.java @@ -22,19 +22,26 @@ import java.util.UUID; public class ReturnFromGraveyardToBattlefieldTargetEffect extends OneShotEffect { private final boolean tapped; + private final boolean attacking; public ReturnFromGraveyardToBattlefieldTargetEffect() { this(false); } public ReturnFromGraveyardToBattlefieldTargetEffect(boolean tapped) { + this(tapped, false); + } + + public ReturnFromGraveyardToBattlefieldTargetEffect(boolean tapped, boolean attacking) { super(Outcome.PutCreatureInPlay); this.tapped = tapped; + this.attacking = attacking; } protected ReturnFromGraveyardToBattlefieldTargetEffect(final ReturnFromGraveyardToBattlefieldTargetEffect effect) { super(effect); this.tapped = effect.tapped; + this.attacking = effect.attacking; } @Override @@ -54,6 +61,11 @@ public class ReturnFromGraveyardToBattlefieldTargetEffect extends OneShotEffect } } controller.moveCards(cardsToMove, Zone.BATTLEFIELD, source, game, tapped, false, false, null); + if (attacking) { + for (Card card : cardsToMove) { + game.getCombat().addAttackingCreature(card.getId(), game); + } + } return true; } return false; @@ -82,8 +94,12 @@ public class ReturnFromGraveyardToBattlefieldTargetEffect extends OneShotEffect } sb.append(yourGrave ? " to" : " onto"); sb.append(" the battlefield"); - if (tapped) { + if (tapped && attacking) { + sb.append(" tapped and attacking"); + } else if (tapped) { sb.append(" tapped"); + } else if (attacking) { + sb.append(" attacking"); } if (!yourGrave) { sb.append(" under your control"); From b8e9214a3fe84fffa0a1ce13a100890932c72393 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 08:05:40 -0400 Subject: [PATCH 063/231] [MID] Implemented Moonveil Regent --- .../src/mage/cards/m/MoonveilRegent.java | 141 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 142 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MoonveilRegent.java diff --git a/Mage.Sets/src/mage/cards/m/MoonveilRegent.java b/Mage.Sets/src/mage/cards/m/MoonveilRegent.java new file mode 100644 index 00000000000..b0e147769ec --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MoonveilRegent.java @@ -0,0 +1,141 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.ObjectColor; +import mage.abilities.Ability; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.costs.common.DiscardHandCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.hint.Hint; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.target.common.TargetAnyTarget; + +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author TheElk801 + */ +public final class MoonveilRegent extends CardImpl { + + public MoonveilRegent(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever you cast a spell, you may discard your hand. If you do, draw a card for each of that spell's colors. + this.addAbility(new SpellCastControllerTriggeredAbility(new DoIfCostPaid( + new DrawCardSourceControllerEffect(MoonveilRegentSpellValue.instance) + .setText("draw a card for each of that spell's colors"), + new DiscardHandCost() + ), false)); + + // When Moonveil Regent dies, it deals X damage to any target, where X is the number of colors among permanents you control. + Ability ability = new DiesSourceTriggeredAbility(new DamageTargetEffect( + MoonveilRegentColorValue.instance + ).setText("it deals X damage to any target, where X is the number of colors among permanents you control")); + ability.addTarget(new TargetAnyTarget()); + this.addAbility(ability); + } + + private MoonveilRegent(final MoonveilRegent card) { + super(card); + } + + @Override + public MoonveilRegent copy() { + return new MoonveilRegent(this); + } +} + +enum MoonveilRegentSpellValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + Spell spell = (Spell) effect.getValue("spellCast"); + return spell != null ? spell.getColor(game).getColorCount() : 0; + } + + @Override + public MoonveilRegentSpellValue copy() { + return this; + } + + @Override + public String getMessage() { + return ""; + } +} + +enum MoonveilRegentColorValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return getColorUnion(game, sourceAbility).getColorCount(); + } + + static ObjectColor getColorUnion(Game game, Ability ability) { + ObjectColor color = new ObjectColor(); + for (Permanent permanent : game.getBattlefield().getActivePermanents( + StaticFilters.FILTER_CONTROLLED_PERMANENT, ability.getControllerId(), game + )) { + color.addColor(permanent.getColor(game)); + if (color.getColorCount() >= 5) { + break; + } + } + return color; + } + + @Override + public MoonveilRegentColorValue copy() { + return this; + } + + @Override + public String getMessage() { + return ""; + } +} + +enum MoonveilRegentHint implements Hint { + instance; + + @Override + public String getText(Game game, Ability ability) { + ObjectColor color = MoonveilRegentColorValue.getColorUnion(game, ability); + return "Colors among permanents you control: " + color.getColorCount() + ( + color.getColorCount() > 0 + ? color + .getColors() + .stream() + .map(ObjectColor::getDescription) + .collect(Collectors.joining(", ", " (", ")")) : "" + ); + } + + @Override + public MoonveilRegentHint copy() { + return this; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 285eb059dfc..eca4dda085e 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -193,6 +193,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Mask of Griselbrand", 111, Rarity.RARE, mage.cards.m.MaskOfGriselbrand.class)); cards.add(new SetCardInfo("Might of the Old Ways", 189, Rarity.COMMON, mage.cards.m.MightOfTheOldWays.class)); cards.add(new SetCardInfo("Moonsilver Key", 255, Rarity.UNCOMMON, mage.cards.m.MoonsilverKey.class)); + cards.add(new SetCardInfo("Moonveil Regent", 149, Rarity.MYTHIC, mage.cards.m.MoonveilRegent.class)); cards.add(new SetCardInfo("Morbid Opportunist", 113, Rarity.UNCOMMON, mage.cards.m.MorbidOpportunist.class)); cards.add(new SetCardInfo("Morkrut Behemoth", 114, Rarity.COMMON, mage.cards.m.MorkrutBehemoth.class)); cards.add(new SetCardInfo("Morning Apparition", 28, Rarity.COMMON, mage.cards.m.MorningApparition.class)); From 13042a2bf9697860e69432169f184911cd4c8af2 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 08:10:07 -0400 Subject: [PATCH 064/231] [MID] Implemented Revenge of the Drowned --- .../src/mage/cards/r/RevengeOfTheDrowned.java | 70 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 71 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RevengeOfTheDrowned.java diff --git a/Mage.Sets/src/mage/cards/r/RevengeOfTheDrowned.java b/Mage.Sets/src/mage/cards/r/RevengeOfTheDrowned.java new file mode 100644 index 00000000000..0187a86e455 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RevengeOfTheDrowned.java @@ -0,0 +1,70 @@ +package mage.cards.r; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.PutOnLibraryTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.permanent.token.ZombieDecayedToken; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RevengeOfTheDrowned extends CardImpl { + + public RevengeOfTheDrowned(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{U}"); + + // Target creature's owner puts it on the top or bottom of their library. You create a 2/2 black Zombie creature token with decayed. + this.getSpellAbility().addEffect(new RevengeOfTheDrownedEffect()); + this.getSpellAbility().addEffect(new CreateTokenEffect(new ZombieDecayedToken()).concatBy("You")); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + } + + private RevengeOfTheDrowned(final RevengeOfTheDrowned card) { + super(card); + } + + @Override + public RevengeOfTheDrowned copy() { + return new RevengeOfTheDrowned(this); + } +} + +class RevengeOfTheDrownedEffect extends OneShotEffect { + + RevengeOfTheDrownedEffect() { + super(Outcome.Benefit); + staticText = "target creature's owner puts it on the top or bottom of their library."; + } + + private RevengeOfTheDrownedEffect(final RevengeOfTheDrownedEffect effect) { + super(effect); + } + + @Override + public RevengeOfTheDrownedEffect copy() { + return new RevengeOfTheDrownedEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(game.getOwnerId(source.getFirstTarget())); + if (player == null) { + return false; + } + if (player.chooseUse(Outcome.Detriment, "Put the targeted object on the top or bottom of your library?", + "", "Top", "Bottom", source, game)) { + return new PutOnLibraryTargetEffect(true).apply(game, source); + } + return new PutOnLibraryTargetEffect(false).apply(game, source); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index eca4dda085e..cc50eb5c5d8 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -230,6 +230,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Raze the Effigy", 156, Rarity.COMMON, mage.cards.r.RazeTheEffigy.class)); cards.add(new SetCardInfo("Reckless Stormseeker", 157, Rarity.RARE, mage.cards.r.RecklessStormseeker.class)); cards.add(new SetCardInfo("Return to Nature", 195, Rarity.COMMON, mage.cards.r.ReturnToNature.class)); + cards.add(new SetCardInfo("Revenge of the Drowned", 72, Rarity.COMMON, mage.cards.r.RevengeOfTheDrowned.class)); cards.add(new SetCardInfo("Rite of Harmony", 236, Rarity.RARE, mage.cards.r.RiteOfHarmony.class)); cards.add(new SetCardInfo("Ritual Guardian", 30, Rarity.COMMON, mage.cards.r.RitualGuardian.class)); cards.add(new SetCardInfo("Ritual of Hope", 31, Rarity.UNCOMMON, mage.cards.r.RitualOfHope.class)); From 1a5f15751ddc3e66909d72b1a195c3feade30b36 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 08:50:01 -0400 Subject: [PATCH 065/231] [MID] Implemented Storm Skreelix --- Mage.Sets/src/mage/cards/s/StormSkreelix.java | 56 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 57 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StormSkreelix.java diff --git a/Mage.Sets/src/mage/cards/s/StormSkreelix.java b/Mage.Sets/src/mage/cards/s/StormSkreelix.java new file mode 100644 index 00000000000..318b11d287a --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StormSkreelix.java @@ -0,0 +1,56 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.cost.SpellsCostReductionControllerEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.filter.common.FilterInstantOrSorceryCard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StormSkreelix extends CardImpl { + + private static final FilterCard filter = new FilterInstantOrSorceryCard("instant and sorcery spells"); + + public StormSkreelix(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{R}"); + + this.subtype.add(SubType.DRAKE); + this.subtype.add(SubType.HORROR); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Instant and sorcery spells you cast cost {1} less to cast. + this.addAbility(new SimpleStaticAbility(new SpellsCostReductionControllerEffect(filter, 1))); + + // Whenever you cast an instant or sorcery spell, Storm Skreelix gets +2/+0 until end of turn. + this.addAbility(new SpellCastControllerTriggeredAbility( + new BoostSourceEffect(2, 0, Duration.EndOfTurn), + StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false + )); + } + + private StormSkreelix(final StormSkreelix card) { + super(card); + } + + @Override + public StormSkreelix copy() { + return new StormSkreelix(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index cc50eb5c5d8..cf6ec7a4ad1 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -256,6 +256,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Stalking Predator", 120, Rarity.COMMON, mage.cards.s.StalkingPredator.class)); cards.add(new SetCardInfo("Startle", 78, Rarity.COMMON, mage.cards.s.Startle.class)); cards.add(new SetCardInfo("Stolen Vitality", 161, Rarity.COMMON, mage.cards.s.StolenVitality.class)); + cards.add(new SetCardInfo("Storm Skreelix", 243, Rarity.UNCOMMON, mage.cards.s.StormSkreelix.class)); cards.add(new SetCardInfo("Storm the Festival", 200, Rarity.RARE, mage.cards.s.StormTheFestival.class)); cards.add(new SetCardInfo("Storm-Charged Slasher", 157, Rarity.RARE, mage.cards.s.StormChargedSlasher.class)); cards.add(new SetCardInfo("Stormrider Spirit", 79, Rarity.COMMON, mage.cards.s.StormriderSpirit.class)); From e9d78fc5ac681ec5dc85b6fd4984f43b16038b98 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 08:58:52 -0400 Subject: [PATCH 066/231] [MID] Implemented Silver Bolt --- Mage.Sets/src/mage/cards/s/SilverBolt.java | 74 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../java/mage/game/permanent/Permanent.java | 4 + .../mage/game/permanent/PermanentImpl.java | 10 +++ 4 files changed, 89 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SilverBolt.java diff --git a/Mage.Sets/src/mage/cards/s/SilverBolt.java b/Mage.Sets/src/mage/cards/s/SilverBolt.java new file mode 100644 index 00000000000..136d9f4be03 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SilverBolt.java @@ -0,0 +1,74 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SilverBolt extends CardImpl { + + public SilverBolt(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}"); + + // {3}, {T}, Sacrifice Silver Bolt: It deals 3 damage to target creature. If a Werewolf is dealt damage this way, destroy it. + Ability ability = new SimpleActivatedAbility(new SilverBoltEffect(), new GenericManaCost(3)); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeSourceCost()); + ability.addTarget(new TargetCreaturePermanent()); + this.addAbility(ability); + } + + private SilverBolt(final SilverBolt card) { + super(card); + } + + @Override + public SilverBolt copy() { + return new SilverBolt(this); + } +} + +class SilverBoltEffect extends OneShotEffect { + + SilverBoltEffect() { + super(Outcome.Benefit); + staticText = "it deals 3 damage to target creature. If a Werewolf is dealt damage this way, destroy it"; + } + + private SilverBoltEffect(final SilverBoltEffect effect) { + super(effect); + } + + @Override + public SilverBoltEffect copy() { + return new SilverBoltEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent == null) { + return false; + } + if (permanent.damage(3, source, game) > 0 + && permanent.hasSubtype(SubType.WEREWOLF, game)) { + permanent.destroy(source, game, false); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index cf6ec7a4ad1..f80412c120c 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -246,6 +246,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Shipwreck Marsh", 267, Rarity.RARE, mage.cards.s.ShipwreckMarsh.class)); cards.add(new SetCardInfo("Siege Zombie", 121, Rarity.COMMON, mage.cards.s.SiegeZombie.class)); cards.add(new SetCardInfo("Sigarda, Champion of Light", 240, Rarity.MYTHIC, mage.cards.s.SigardaChampionOfLight.class)); + cards.add(new SetCardInfo("Silver Bolt", 258, Rarity.COMMON, mage.cards.s.SilverBolt.class)); cards.add(new SetCardInfo("Skaab Wrangler", 75, Rarity.UNCOMMON, mage.cards.s.SkaabWrangler.class)); cards.add(new SetCardInfo("Slaughter Specialist", 122, Rarity.RARE, mage.cards.s.SlaughterSpecialist.class)); cards.add(new SetCardInfo("Snarling Wolf", 199, Rarity.COMMON, mage.cards.s.SnarlingWolf.class)); diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index ded11c011ba..65b47042538 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -125,6 +125,8 @@ public interface Permanent extends Card, Controllable { int getDamage(); + int damage(int damage, Ability source, Game game); + int damage(int damage, UUID attackerId, Ability source, Game game); int damage(int damage, UUID attackerId, Ability source, Game game, boolean combat, boolean preventable); @@ -168,6 +170,8 @@ public interface Permanent extends Card, Controllable { MageObject getBasicMageObject(Game game); + boolean destroy(Ability source, Game game); + boolean destroy(Ability source, Game game, boolean noRegen); /** diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index b903450cc1a..d2a210af1e0 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -840,6 +840,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { return this.damage; } + @Override + public int damage(int damage, Ability source, Game game) { + return damage(damage, source.getSourceId(), source, game); + } + @Override public int damage(int damage, UUID attackerId, Ability source, Game game) { return doDamage(damage, attackerId, source, game, true, false, false, null); @@ -1165,6 +1170,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { return true; } + @Override + public boolean destroy(Ability source, Game game) { + return destroy(source, game, false); + } + @Override public boolean destroy(Ability source, Game game, boolean noRegen) { // Only permanets on the battlefield can be destroyed From 4838e7ff34f641d33ccb826cf3e76654d5820131 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 09:08:59 -0400 Subject: [PATCH 067/231] [MID] Implemented Sludge Monster --- Mage.Sets/src/mage/cards/s/SludgeMonster.java | 121 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 122 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SludgeMonster.java diff --git a/Mage.Sets/src/mage/cards/s/SludgeMonster.java b/Mage.Sets/src/mage/cards/s/SludgeMonster.java new file mode 100644 index 00000000000..29775592cd1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SludgeMonster.java @@ -0,0 +1,121 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SludgeMonster extends CardImpl { + + private static final FilterPermanent filter = new FilterCreaturePermanent("other creature"); + + static { + filter.add(AnotherPredicate.instance); + } + + public SludgeMonster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{U}"); + + this.subtype.add(SubType.HORROR); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + + // Whenever Sludge Monster enters the battlefield or attacks, put a slime counter on up to one other target creature. + Ability ability = new EntersBattlefieldOrAttacksSourceTriggeredAbility( + new AddCountersTargetEffect(CounterType.SLIME.createInstance()) + .setText("put a slime counter on up to one other target creature") + ); + ability.addTarget(new TargetPermanent(0, 1, filter)); + this.addAbility(ability); + + // Non-Horror creatures with slime counters on them lose all abilities and have base power and toughness 2/2. + this.addAbility(new SimpleStaticAbility(new SludgeMonsterEffect())); + } + + private SludgeMonster(final SludgeMonster card) { + super(card); + } + + @Override + public SludgeMonster copy() { + return new SludgeMonster(this); + } +} + +class SludgeMonsterEffect extends ContinuousEffectImpl { + + private static final FilterPermanent filter = new FilterCreaturePermanent(); + + static { + filter.add(CounterType.SLIME.getPredicate()); + filter.add(Predicates.not(SubType.HORROR.getPredicate())); + } + + SludgeMonsterEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "non-Horror creatures with slime counters on them " + + "lose all abilities and have base power and toughness 2/2"; + } + + private SludgeMonsterEffect(final SludgeMonsterEffect effect) { + super(effect); + } + + @Override + public SludgeMonsterEffect copy() { + return new SludgeMonsterEffect(this); + } + + @Override + public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { + for (Permanent permanent : game.getBattlefield().getActivePermanents( + filter, source.getControllerId(), source.getSourceId(), game + )) { + switch (layer) { + case AbilityAddingRemovingEffects_6: + permanent.removeAllAbilities(source.getSourceId(), game); + return true; + case PTChangingEffects_7: + if (sublayer == SubLayer.SetPT_7b) { + permanent.getPower().setValue(2); + permanent.getToughness().setValue(2); + return true; + } + } + } + return false; + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean hasLayer(Layer layer) { + switch (layer) { + case AbilityAddingRemovingEffects_6: + case PTChangingEffects_7: + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index f80412c120c..b32af10ceba 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -249,6 +249,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Silver Bolt", 258, Rarity.COMMON, mage.cards.s.SilverBolt.class)); cards.add(new SetCardInfo("Skaab Wrangler", 75, Rarity.UNCOMMON, mage.cards.s.SkaabWrangler.class)); cards.add(new SetCardInfo("Slaughter Specialist", 122, Rarity.RARE, mage.cards.s.SlaughterSpecialist.class)); + cards.add(new SetCardInfo("Sludge Monster", 76, Rarity.RARE, mage.cards.s.SludgeMonster.class)); cards.add(new SetCardInfo("Snarling Wolf", 199, Rarity.COMMON, mage.cards.s.SnarlingWolf.class)); cards.add(new SetCardInfo("Soul-Guide Gryff", 35, Rarity.COMMON, mage.cards.s.SoulGuideGryff.class)); cards.add(new SetCardInfo("Spectral Adversary", 77, Rarity.MYTHIC, mage.cards.s.SpectralAdversary.class)); From 72cb83e61511fd14d8488a3f2ff098a247f0b991 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 10:00:35 -0400 Subject: [PATCH 068/231] [MID] Implemented Rise of the Ants --- Mage.Sets/src/mage/cards/r/RiseOfTheAnts.java | 38 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../permanent/token/RiseOfTheAntsToken.java | 28 ++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RiseOfTheAnts.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/RiseOfTheAntsToken.java diff --git a/Mage.Sets/src/mage/cards/r/RiseOfTheAnts.java b/Mage.Sets/src/mage/cards/r/RiseOfTheAnts.java new file mode 100644 index 00000000000..afb446d9c5e --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RiseOfTheAnts.java @@ -0,0 +1,38 @@ +package mage.cards.r; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.RiseOfTheAntsToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RiseOfTheAnts extends CardImpl { + + public RiseOfTheAnts(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{G}{G}"); + + // Create two 3/3 green Insect creature tokens. You gain 2 life. + this.getSpellAbility().addEffect(new CreateTokenEffect(new RiseOfTheAntsToken(), 2)); + this.getSpellAbility().addEffect(new GainLifeEffect(2).concatBy(".")); + + // Flashback {6}{G}{G} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{6}{G}{G}"))); + } + + private RiseOfTheAnts(final RiseOfTheAnts card) { + super(card); + } + + @Override + public RiseOfTheAnts copy() { + return new RiseOfTheAnts(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index b32af10ceba..197b7299496 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -231,6 +231,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Reckless Stormseeker", 157, Rarity.RARE, mage.cards.r.RecklessStormseeker.class)); cards.add(new SetCardInfo("Return to Nature", 195, Rarity.COMMON, mage.cards.r.ReturnToNature.class)); cards.add(new SetCardInfo("Revenge of the Drowned", 72, Rarity.COMMON, mage.cards.r.RevengeOfTheDrowned.class)); + cards.add(new SetCardInfo("Rise of the Ants", 196, Rarity.UNCOMMON, mage.cards.r.RiseOfTheAnts.class)); cards.add(new SetCardInfo("Rite of Harmony", 236, Rarity.RARE, mage.cards.r.RiteOfHarmony.class)); cards.add(new SetCardInfo("Ritual Guardian", 30, Rarity.COMMON, mage.cards.r.RitualGuardian.class)); cards.add(new SetCardInfo("Ritual of Hope", 31, Rarity.UNCOMMON, mage.cards.r.RitualOfHope.class)); diff --git a/Mage/src/main/java/mage/game/permanent/token/RiseOfTheAntsToken.java b/Mage/src/main/java/mage/game/permanent/token/RiseOfTheAntsToken.java new file mode 100644 index 00000000000..81167ac5669 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/RiseOfTheAntsToken.java @@ -0,0 +1,28 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.constants.CardType; +import mage.constants.SubType; + +/** + * @author TheElk801 + */ +public final class RiseOfTheAntsToken extends TokenImpl { + + public RiseOfTheAntsToken() { + super("Insect", "3/3 green Insect creature token"); + cardType.add(CardType.CREATURE); + color.setGreen(true); + subtype.add(SubType.INSECT); + power = new MageInt(3); + toughness = new MageInt(3); + } + + public RiseOfTheAntsToken(final RiseOfTheAntsToken token) { + super(token); + } + + public RiseOfTheAntsToken copy() { + return new RiseOfTheAntsToken(this); + } +} \ No newline at end of file From 07e0a4ef54b3b2a6433bf9f76de84611bfaaaff3 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 10:59:25 -0400 Subject: [PATCH 069/231] [MID] Implemented Smoldering Egg / Ashmouth Dragon --- .../src/mage/cards/a/AshmouthDragon.java | 51 ++++++++++ Mage.Sets/src/mage/cards/s/SmolderingEgg.java | 98 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + .../main/java/mage/counters/CounterType.java | 1 + 4 files changed, 152 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/AshmouthDragon.java create mode 100644 Mage.Sets/src/mage/cards/s/SmolderingEgg.java diff --git a/Mage.Sets/src/mage/cards/a/AshmouthDragon.java b/Mage.Sets/src/mage/cards/a/AshmouthDragon.java new file mode 100644 index 00000000000..aa7fae2f965 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AshmouthDragon.java @@ -0,0 +1,51 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.target.common.TargetAnyTarget; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AshmouthDragon extends CardImpl { + + public AshmouthDragon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + this.color.setRed(true); + this.transformable = true; + this.nightCard = true; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever you cast an instant or sorcery spell, Ashmouth Dragon deals 2 damage to any target. + Ability ability = new SpellCastControllerTriggeredAbility( + new DamageTargetEffect(2), StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false + ); + ability.addTarget(new TargetAnyTarget()); + this.addAbility(ability); + } + + private AshmouthDragon(final AshmouthDragon card) { + super(card); + } + + @Override + public AshmouthDragon copy() { + return new AshmouthDragon(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SmolderingEgg.java b/Mage.Sets/src/mage/cards/s/SmolderingEgg.java new file mode 100644 index 00000000000..bb5eaec7220 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SmolderingEgg.java @@ -0,0 +1,98 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.keyword.DefenderAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.watchers.common.ManaPaidSourceWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SmolderingEgg extends CardImpl { + + public SmolderingEgg(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}"); + + this.subtype.add(SubType.DRAGON); + this.subtype.add(SubType.EGG); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + this.transformable = true; + this.secondSideCardClazz = mage.cards.a.AshmouthDragon.class; + + // Defender + this.addAbility(DefenderAbility.getInstance()); + + // Whenever you cast an instant or sorcery spell, put a number of ember counters on Smoldering Egg equal to the amount of mana spent to cast that spell. Then if Smoldering Egg has seven or more ember counters on it, remove them and transform Smoldering Egg. + this.addAbility(new TransformAbility()); + this.addAbility(new SpellCastControllerTriggeredAbility( + new SmolderingEggEffect(), StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false + )); + } + + private SmolderingEgg(final SmolderingEgg card) { + super(card); + } + + @Override + public SmolderingEgg copy() { + return new SmolderingEgg(this); + } +} + +class SmolderingEggEffect extends OneShotEffect { + + SmolderingEggEffect() { + super(Outcome.Benefit); + staticText = "put a number of ember counters on {this} equal to the amount of mana spent to cast that spell. " + + "Then if {this} has seven or more ember counters on it, remove them and transform {this}"; + } + + private SmolderingEggEffect(final SmolderingEggEffect effect) { + super(effect); + } + + @Override + public SmolderingEggEffect copy() { + return new SmolderingEggEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null) { + return false; + } + Spell spell = (Spell) getValue("spellCast"); + if (spell != null) { + permanent.addCounters( + CounterType.EMBER.createInstance( + ManaPaidSourceWatcher.getTotalPaid(spell.getId(), game) + ), source.getControllerId(), source, game + ); + } + int counters = permanent.getCounters(game).getCount(CounterType.EMBER); + if (counters < 7) { + return true; + } + permanent.removeCounters(CounterType.EMBER.createInstance(counters), source, game); + new TransformSourceEffect(true).apply(game, source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 197b7299496..4cd40b66ce3 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -41,6 +41,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Arlinn, the Moon's Fury", 211, Rarity.MYTHIC, mage.cards.a.ArlinnTheMoonsFury.class)); cards.add(new SetCardInfo("Arlinn, the Pack's Hope", 211, Rarity.MYTHIC, mage.cards.a.ArlinnThePacksHope.class)); cards.add(new SetCardInfo("Arrogant Outlaw", 84, Rarity.COMMON, mage.cards.a.ArrogantOutlaw.class)); + cards.add(new SetCardInfo("Ashmouth Dragon", 159, Rarity.RARE, mage.cards.a.AshmouthDragon.class)); cards.add(new SetCardInfo("Augur of Autumn", 168, Rarity.RARE, mage.cards.a.AugurOfAutumn.class)); cards.add(new SetCardInfo("Awoken Demon", 100, Rarity.COMMON, mage.cards.a.AwokenDemon.class)); cards.add(new SetCardInfo("Baithook Angler", 42, Rarity.COMMON, mage.cards.b.BaithookAngler.class)); @@ -251,6 +252,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Skaab Wrangler", 75, Rarity.UNCOMMON, mage.cards.s.SkaabWrangler.class)); cards.add(new SetCardInfo("Slaughter Specialist", 122, Rarity.RARE, mage.cards.s.SlaughterSpecialist.class)); cards.add(new SetCardInfo("Sludge Monster", 76, Rarity.RARE, mage.cards.s.SludgeMonster.class)); + cards.add(new SetCardInfo("Smoldering Egg", 159, Rarity.RARE, mage.cards.s.SmolderingEgg.class)); cards.add(new SetCardInfo("Snarling Wolf", 199, Rarity.COMMON, mage.cards.s.SnarlingWolf.class)); cards.add(new SetCardInfo("Soul-Guide Gryff", 35, Rarity.COMMON, mage.cards.s.SoulGuideGryff.class)); cards.add(new SetCardInfo("Spectral Adversary", 77, Rarity.MYTHIC, mage.cards.s.SpectralAdversary.class)); diff --git a/Mage/src/main/java/mage/counters/CounterType.java b/Mage/src/main/java/mage/counters/CounterType.java index 2c426b9a402..3a4612fb276 100644 --- a/Mage/src/main/java/mage/counters/CounterType.java +++ b/Mage/src/main/java/mage/counters/CounterType.java @@ -50,6 +50,7 @@ public enum CounterType { ECHO("echo"), EGG("egg"), ELIXIR("elixir"), + EMBER("ember"), ENERGY("energy"), ENLIGHTENED("enlightened"), EON("eon"), From d9dcc8d9db1103708e0b71c83e34f3426addca2d Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 11:13:38 -0400 Subject: [PATCH 070/231] [MID] Implemented Sigardian Savior --- .../src/mage/cards/s/SigardianSavior.java | 63 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 64 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SigardianSavior.java diff --git a/Mage.Sets/src/mage/cards/s/SigardianSavior.java b/Mage.Sets/src/mage/cards/s/SigardianSavior.java new file mode 100644 index 00000000000..abb9cdd8171 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SigardianSavior.java @@ -0,0 +1,63 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.CastFromEverywhereSourceCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SigardianSavior extends CardImpl { + + private static final FilterCard filter + = new FilterCreatureCard("creature cards with mana value 2 or less from your graveyard"); + + static { + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 3)); + } + + public SigardianSavior(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}{W}"); + + this.subtype.add(SubType.ANGEL); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When Sigardian Savior enters the battlefield, if you cast it, return up to two target creature cards with mana value 2 or less from your graveyard to the battlefield. + Ability ability = new ConditionalInterveningIfTriggeredAbility( + new EntersBattlefieldTriggeredAbility(new ReturnFromGraveyardToBattlefieldTargetEffect()), + CastFromEverywhereSourceCondition.instance, "When {this} enters the battlefield, " + + "if you cast it, return up to two target creature cards with mana value " + + "2 or less from your graveyard to the battlefield." + ); + ability.addTarget(new TargetCardInYourGraveyard(0, 2, filter)); + this.addAbility(ability); + } + + private SigardianSavior(final SigardianSavior card) { + super(card); + } + + @Override + public SigardianSavior copy() { + return new SigardianSavior(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 4cd40b66ce3..97d0aab3b62 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -248,6 +248,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Shipwreck Marsh", 267, Rarity.RARE, mage.cards.s.ShipwreckMarsh.class)); cards.add(new SetCardInfo("Siege Zombie", 121, Rarity.COMMON, mage.cards.s.SiegeZombie.class)); cards.add(new SetCardInfo("Sigarda, Champion of Light", 240, Rarity.MYTHIC, mage.cards.s.SigardaChampionOfLight.class)); + cards.add(new SetCardInfo("Sigardian Savior", 34, Rarity.MYTHIC, mage.cards.s.SigardianSavior.class)); cards.add(new SetCardInfo("Silver Bolt", 258, Rarity.COMMON, mage.cards.s.SilverBolt.class)); cards.add(new SetCardInfo("Skaab Wrangler", 75, Rarity.UNCOMMON, mage.cards.s.SkaabWrangler.class)); cards.add(new SetCardInfo("Slaughter Specialist", 122, Rarity.RARE, mage.cards.s.SlaughterSpecialist.class)); From 7e89d620b8303e48c49c2f1a17b9bcf90f0e3335 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 11:18:10 -0400 Subject: [PATCH 071/231] [MID] Implemented Shipwreck Sifters --- .../src/mage/cards/s/ShipwreckSifters.java | 60 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + ...DiscardCardControllerTriggeredAbility.java | 5 +- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/s/ShipwreckSifters.java diff --git a/Mage.Sets/src/mage/cards/s/ShipwreckSifters.java b/Mage.Sets/src/mage/cards/s/ShipwreckSifters.java new file mode 100644 index 00000000000..1e95e8709be --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/ShipwreckSifters.java @@ -0,0 +1,60 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.DiscardCardControllerTriggeredAbility; +import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.DisturbAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ShipwreckSifters extends CardImpl { + + private static final FilterCard filter = new FilterCard("a Spirit card or a card with disturb"); + + static { + filter.add(Predicates.or( + SubType.SPIRIT.getPredicate(), + new AbilityPredicate(DisturbAbility.class) + )); + } + + public ShipwreckSifters(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.SPIRIT); + this.power = new MageInt(1); + this.toughness = new MageInt(2); + + // When Shipwreck Sifters enters the battlefield, draw a card, then discard a card. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new DrawDiscardControllerEffect(1, 1) + )); + + // Whenever you discard a Spirit card or a card with disturb, put a +1/+1 counter on Shipwreck Sifters. + this.addAbility(new DiscardCardControllerTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), false, filter + )); + } + + private ShipwreckSifters(final ShipwreckSifters card) { + super(card); + } + + @Override + public ShipwreckSifters copy() { + return new ShipwreckSifters(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 97d0aab3b62..b3eea59b151 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -246,6 +246,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Shadowbeast Sighting", 198, Rarity.COMMON, mage.cards.s.ShadowbeastSighting.class)); cards.add(new SetCardInfo("Shady Traveler", 120, Rarity.COMMON, mage.cards.s.ShadyTraveler.class)); cards.add(new SetCardInfo("Shipwreck Marsh", 267, Rarity.RARE, mage.cards.s.ShipwreckMarsh.class)); + cards.add(new SetCardInfo("Shipwreck Sifters", 74, Rarity.COMMON, mage.cards.s.ShipwreckSifters.class)); cards.add(new SetCardInfo("Siege Zombie", 121, Rarity.COMMON, mage.cards.s.SiegeZombie.class)); cards.add(new SetCardInfo("Sigarda, Champion of Light", 240, Rarity.MYTHIC, mage.cards.s.SigardaChampionOfLight.class)); cards.add(new SetCardInfo("Sigardian Savior", 34, Rarity.MYTHIC, mage.cards.s.SigardianSavior.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/DiscardCardControllerTriggeredAbility.java b/Mage/src/main/java/mage/abilities/effects/common/DiscardCardControllerTriggeredAbility.java index 0a0914953c1..0d7a295b6cd 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/DiscardCardControllerTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/effects/common/DiscardCardControllerTriggeredAbility.java @@ -11,7 +11,6 @@ import mage.game.events.GameEvent; /** * @author TheElk801 */ - public class DiscardCardControllerTriggeredAbility extends TriggeredAbilityImpl { private final FilterCard filter; @@ -48,6 +47,6 @@ public class DiscardCardControllerTriggeredAbility extends TriggeredAbilityImpl @Override public String getTriggerPhrase() { - return "Whenever you discard " + filter.getMessage() + ", " ; + return "Whenever you discard " + filter.getMessage() + ", "; } -} \ No newline at end of file +} From eb065bc0e79df2b7ad1ca502cdabcea057a775c5 Mon Sep 17 00:00:00 2001 From: ciaccona007 Date: Sun, 12 Sep 2021 12:01:58 -0400 Subject: [PATCH 072/231] [MID] Implement Consuming Blob --- .../src/mage/cards/c/CatharCommando.java | 2 +- Mage.Sets/src/mage/cards/c/ConsumingBlob.java | 78 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../permanent/token/ConsumingBlobToken.java | 71 +++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/c/ConsumingBlob.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/ConsumingBlobToken.java diff --git a/Mage.Sets/src/mage/cards/c/CatharCommando.java b/Mage.Sets/src/mage/cards/c/CatharCommando.java index 7e29b383c96..72cb8a11621 100644 --- a/Mage.Sets/src/mage/cards/c/CatharCommando.java +++ b/Mage.Sets/src/mage/cards/c/CatharCommando.java @@ -30,7 +30,7 @@ public final class CatharCommando extends CardImpl { this.addAbility(FlashAbility.getInstance()); // {1}, Sacrifice Cathar Commando: Destroy target artifact or enchantment. - Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DestroyTargetEffect(), new ManaCostsImpl("{1}{G}")); + Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DestroyTargetEffect(), new ManaCostsImpl("{1}")); ability.addCost(new SacrificeSourceCost()); ability.addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT)); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/c/ConsumingBlob.java b/Mage.Sets/src/mage/cards/c/ConsumingBlob.java new file mode 100644 index 00000000000..b49efca1438 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ConsumingBlob.java @@ -0,0 +1,78 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.hint.common.CardTypesInGraveyardHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.permanent.token.ConsumingBlobToken; + +import java.util.UUID; + +/** + * @author ciaccona007 + */ +public final class ConsumingBlob extends CardImpl { + + public ConsumingBlob(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}{G}"); + + this.subtype.add(SubType.OOZE); + this.power = new MageInt(0); + this.toughness = new MageInt(1); + + // Consuming Blob's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new ConsumingBlobEffect()).addHint(CardTypesInGraveyardHint.YOU)); + + // At the beginning of your end step, create a green Ooze creature token with "This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1". + this.addAbility(new BeginningOfEndStepTriggeredAbility( + new CreateTokenEffect(new ConsumingBlobToken()), TargetController.YOU, false) + ); + } + + private ConsumingBlob(final ConsumingBlob card) { + super(card); + } + + @Override + public ConsumingBlob copy() { + return new ConsumingBlob(this); + } +} + +class ConsumingBlobEffect extends ContinuousEffectImpl { + + public ConsumingBlobEffect() { + super(Duration.EndOfGame, Layer.PTChangingEffects_7, SubLayer.CharacteristicDefining_7a, Outcome.BoostCreature); + staticText = "{this}'s power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1"; + } + + public ConsumingBlobEffect(final ConsumingBlobEffect effect) { + super(effect); + } + + @Override + public ConsumingBlobEffect copy() { + return new ConsumingBlobEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + MageObject target = source.getSourceObject(game); + if (target == null) { + return false; + } + int number = CardTypesInGraveyardCount.YOU.calculate(game, source, this); + target.getPower().setValue(number); + target.getToughness().setValue(number + 1); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index b3eea59b151..2d1d5fddfcb 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -81,6 +81,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Clear Shot", 176, Rarity.UNCOMMON, mage.cards.c.ClearShot.class)); cards.add(new SetCardInfo("Component Collector", 43, Rarity.COMMON, mage.cards.c.ComponentCollector.class)); cards.add(new SetCardInfo("Consider", 44, Rarity.COMMON, mage.cards.c.Consider.class)); + cards.add(new SetCardInfo("Consuming Blob", 177, Rarity.MYTHIC, mage.cards.c.ConsumingBlob.class)); cards.add(new SetCardInfo("Contortionist Troupe", 178, Rarity.UNCOMMON, mage.cards.c.ContortionistTroupe.class)); cards.add(new SetCardInfo("Corpse Cobble", 214, Rarity.UNCOMMON, mage.cards.c.CorpseCobble.class)); cards.add(new SetCardInfo("Covert Cutpurse", 92, Rarity.UNCOMMON, mage.cards.c.CovertCutpurse.class)); diff --git a/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobToken.java b/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobToken.java new file mode 100644 index 00000000000..d3d113fc666 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobToken.java @@ -0,0 +1,71 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.hint.common.CardTypesInGraveyardHint; +import mage.constants.*; +import mage.game.Game; + +/** + * @author ciaccona007 + */ +public final class ConsumingBlobToken extends TokenImpl { + + public ConsumingBlobToken() { + super("Ooze", "green Ooze creature token with \"This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1.\""); + setOriginalExpansionSetCode("MID"); + cardType.add(CardType.CREATURE); + subtype.add(SubType.OOZE); + color.setGreen(true); + + power = new MageInt(0); + toughness = new MageInt(1); + + // This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new ConsumingBlobTokenEffect()).addHint(CardTypesInGraveyardHint.YOU)); + + } + + private ConsumingBlobToken(final ConsumingBlobToken token) { + super(token); + } + + @Override + public ConsumingBlobToken copy() { + return new ConsumingBlobToken(this); + } +} + + +class ConsumingBlobTokenEffect extends ContinuousEffectImpl { + + public ConsumingBlobTokenEffect() { + super(Duration.EndOfGame, Layer.PTChangingEffects_7, SubLayer.CharacteristicDefining_7a, Outcome.BoostCreature); + staticText = "{this}'s power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1"; + } + + public ConsumingBlobTokenEffect(final ConsumingBlobTokenEffect effect) { + super(effect); + } + + @Override + public ConsumingBlobTokenEffect copy() { + return new ConsumingBlobTokenEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + MageObject target = source.getSourceObject(game); + if (target == null) { + return false; + } + int number = CardTypesInGraveyardCount.YOU.calculate(game, source, this); + target.getPower().setValue(number); + target.getToughness().setValue(number + 1); + return true; + } +} From af068e2b812f801b4935ed97b86ddbfc8495314f Mon Sep 17 00:00:00 2001 From: ciaccona007 Date: Sun, 12 Sep 2021 12:48:06 -0400 Subject: [PATCH 073/231] [MID] Implement Rite of Oblivion --- .../src/mage/cards/r/RiteOfOblivion.java | 52 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 53 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RiteOfOblivion.java diff --git a/Mage.Sets/src/mage/cards/r/RiteOfOblivion.java b/Mage.Sets/src/mage/cards/r/RiteOfOblivion.java new file mode 100644 index 00000000000..b0c85137e9b --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RiteOfOblivion.java @@ -0,0 +1,52 @@ +package mage.cards.r; + +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.target.common.TargetControlledPermanent; +import mage.target.common.TargetNonlandPermanent; + +import java.util.UUID; + +/** + * @author ciaccona007 + */ +public final class RiteOfOblivion extends CardImpl { + + private static final FilterControlledPermanent filter = new FilterControlledPermanent("nonland permanent"); + + static { + filter.add(Predicates.not(CardType.LAND.getPredicate())); + } + + public RiteOfOblivion(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{W}{B}"); + + + // As an additional cost to cast this spell, sacrifice a nonland permanent. + this.getSpellAbility().addCost(new SacrificeTargetCost(new TargetControlledPermanent(filter))); + + // Exile target nonland permanent + this.getSpellAbility().addEffect(new ExileTargetEffect()); + this.getSpellAbility().addTarget(new TargetNonlandPermanent()); + + // Flashback {2}{W}{B} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{2}{W}{B}"))); + + } + + private RiteOfOblivion(final RiteOfOblivion card) { + super(card); + } + + @Override + public RiteOfOblivion copy() { + return new RiteOfOblivion(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 2d1d5fddfcb..fd140608368 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -235,6 +235,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Revenge of the Drowned", 72, Rarity.COMMON, mage.cards.r.RevengeOfTheDrowned.class)); cards.add(new SetCardInfo("Rise of the Ants", 196, Rarity.UNCOMMON, mage.cards.r.RiseOfTheAnts.class)); cards.add(new SetCardInfo("Rite of Harmony", 236, Rarity.RARE, mage.cards.r.RiteOfHarmony.class)); + cards.add(new SetCardInfo("Rite of Oblivion", 237, Rarity.UNCOMMON, mage.cards.r.RiteOfOblivion.class)); cards.add(new SetCardInfo("Ritual Guardian", 30, Rarity.COMMON, mage.cards.r.RitualGuardian.class)); cards.add(new SetCardInfo("Ritual of Hope", 31, Rarity.UNCOMMON, mage.cards.r.RitualOfHope.class)); cards.add(new SetCardInfo("Rockfall Vale", 266, Rarity.RARE, mage.cards.r.RockfallVale.class)); From 8d26761eb511c7ec37cdbe935ffdd0b627c3136a Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 13:30:20 -0400 Subject: [PATCH 074/231] [MID] Implemented Memory Deluge --- Mage.Sets/src/mage/cards/m/MemoryDeluge.java | 44 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 45 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MemoryDeluge.java diff --git a/Mage.Sets/src/mage/cards/m/MemoryDeluge.java b/Mage.Sets/src/mage/cards/m/MemoryDeluge.java new file mode 100644 index 00000000000..76bd75248d6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MemoryDeluge.java @@ -0,0 +1,44 @@ +package mage.cards.m; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.ManaSpentToCastCount; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MemoryDeluge extends CardImpl { + + public MemoryDeluge(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{U}{U}"); + + // Look at the top X cards of your library, where X is the amount of mana spent to cast this spell. Put two of them into your hand and the rest on the bottom of your library in a random order. + this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect( + ManaSpentToCastCount.instance, false, StaticValue.get(2), + StaticFilters.FILTER_CARD, Zone.LIBRARY, false, false + ).setBackInRandomOrder(true).setText("look at the top X cards of your library, where X " + + "is the amount of mana spent to cast this spell. Put two of them into your " + + "hand and the rest on the bottom of your library in a random order")); + + // Flashback {5}{U}{U} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{5}{U}{U}"))); + } + + private MemoryDeluge(final MemoryDeluge card) { + super(card); + } + + @Override + public MemoryDeluge copy() { + return new MemoryDeluge(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index fd140608368..9c796002846 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -193,6 +193,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Lunarch Veteran", 27, Rarity.COMMON, mage.cards.l.LunarchVeteran.class)); cards.add(new SetCardInfo("Malevolent Hermit", 61, Rarity.RARE, mage.cards.m.MalevolentHermit.class)); cards.add(new SetCardInfo("Mask of Griselbrand", 111, Rarity.RARE, mage.cards.m.MaskOfGriselbrand.class)); + cards.add(new SetCardInfo("Memory Deluge", 62, Rarity.RARE, mage.cards.m.MemoryDeluge.class)); cards.add(new SetCardInfo("Might of the Old Ways", 189, Rarity.COMMON, mage.cards.m.MightOfTheOldWays.class)); cards.add(new SetCardInfo("Moonsilver Key", 255, Rarity.UNCOMMON, mage.cards.m.MoonsilverKey.class)); cards.add(new SetCardInfo("Moonveil Regent", 149, Rarity.MYTHIC, mage.cards.m.MoonveilRegent.class)); From 520528293ab53d6385d7255c2b23c8a6f38ab70e Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 13:51:47 -0400 Subject: [PATCH 075/231] [MID] Implemented Seize the Storm --- Mage.Sets/src/mage/cards/s/SeizeTheStorm.java | 80 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../permanent/token/SeizeTheStormToken.java | 43 ++++++++++ 3 files changed, 124 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SeizeTheStorm.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/SeizeTheStormToken.java diff --git a/Mage.Sets/src/mage/cards/s/SeizeTheStorm.java b/Mage.Sets/src/mage/cards/s/SeizeTheStorm.java new file mode 100644 index 00000000000..420f316c59c --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SeizeTheStorm.java @@ -0,0 +1,80 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.token.SeizeTheStormToken; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SeizeTheStorm extends CardImpl { + + private static final Hint hint = new ValueHint( + "Spells in your graveyard and flashback cards in exile", SeizeTheStormValue.instance + ); + + public SeizeTheStorm(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{R}"); + + // Create a red Elemental creature token with trample and "This creature's power and toughness are each equal to the number of instant and sorcery cards in your graveyard, plus the number of cards with flashback you own in exile." + this.getSpellAbility().addEffect(new CreateTokenEffect( + new SeizeTheStormToken(SeizeTheStormValue.instance, hint) + )); + this.getSpellAbility().addHint(hint); + + // Flashback {6}{R} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{6}{R}"))); + } + + private SeizeTheStorm(final SeizeTheStorm card) { + super(card); + } + + @Override + public SeizeTheStorm copy() { + return new SeizeTheStorm(this); + } +} + +enum SeizeTheStormValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + Player player = game.getPlayer(sourceAbility.getControllerId()); + if (player == null) { + return 0; + } + return player.getGraveyard().count( + StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY, game + ) + game.getExile() + .getAllCards(game, sourceAbility.getControllerId()) + .stream() + .filter(card -> card.getAbilities(game).containsClass(FlashbackAbility.class)) + .mapToInt(x -> 1).sum(); + } + + @Override + public SeizeTheStormValue copy() { + return this; + } + + @Override + public String getMessage() { + return "instant and sorcery cards in your graveyard, plus the number of cards with flashback you own in exile"; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 9c796002846..24241a9e772 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -246,6 +246,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Seafaring Werewolf", 80, Rarity.RARE, mage.cards.s.SeafaringWerewolf.class)); cards.add(new SetCardInfo("Seasoned Cathar", 2, Rarity.UNCOMMON, mage.cards.s.SeasonedCathar.class)); cards.add(new SetCardInfo("Secrets of the Key", 73, Rarity.COMMON, mage.cards.s.SecretsOfTheKey.class)); + cards.add(new SetCardInfo("Seize the Storm", 158, Rarity.UNCOMMON, mage.cards.s.SeizeTheStorm.class)); cards.add(new SetCardInfo("Shadowbeast Sighting", 198, Rarity.COMMON, mage.cards.s.ShadowbeastSighting.class)); cards.add(new SetCardInfo("Shady Traveler", 120, Rarity.COMMON, mage.cards.s.ShadyTraveler.class)); cards.add(new SetCardInfo("Shipwreck Marsh", 267, Rarity.RARE, mage.cards.s.ShipwreckMarsh.class)); diff --git a/Mage/src/main/java/mage/game/permanent/token/SeizeTheStormToken.java b/Mage/src/main/java/mage/game/permanent/token/SeizeTheStormToken.java new file mode 100644 index 00000000000..35004de4eef --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/SeizeTheStormToken.java @@ -0,0 +1,43 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.common.continuous.SetPowerToughnessSourceEffect; +import mage.abilities.hint.Hint; +import mage.abilities.keyword.TrampleAbility; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; + +/** + * @author TheElk801 + */ +public final class SeizeTheStormToken extends TokenImpl { + + public SeizeTheStormToken(DynamicValue xValue, Hint hint) { + super("Elemental", "red Elemental creature token with trample and " + + "\"This creature's power and toughness are each equal to the number of instant " + + "and sorcery cards in your graveyard, plus the number of cards with flashback you own in exile.\""); + cardType.add(CardType.CREATURE); + color.setRed(true); + subtype.add(SubType.ELEMENTAL); + power = new MageInt(0); + toughness = new MageInt(0); + this.addAbility(TrampleAbility.getInstance()); + this.addAbility(new SimpleStaticAbility(new SetPowerToughnessSourceEffect( + xValue, Duration.WhileOnBattlefield + ).setText("this creature's power and toughness are each equal to the number of " + + "instant and sorcery cards in your graveyard, plus the number of cards with flashback you own in exile") + ).addHint(hint)); + } + + private SeizeTheStormToken(final SeizeTheStormToken token) { + super(token); + } + + @Override + public SeizeTheStormToken copy() { + return new SeizeTheStormToken(this); + } +} From 04886d421ac55408f20079e37eaf070422d963e0 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 14:01:07 -0400 Subject: [PATCH 076/231] [MID] Implemented Tapping at the Window --- .../src/mage/cards/t/TappingAtTheWindow.java | 81 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 82 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TappingAtTheWindow.java diff --git a/Mage.Sets/src/mage/cards/t/TappingAtTheWindow.java b/Mage.Sets/src/mage/cards/t/TappingAtTheWindow.java new file mode 100644 index 00000000000..5b6fa979fbc --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TappingAtTheWindow.java @@ -0,0 +1,81 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInLibrary; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TappingAtTheWindow extends CardImpl { + + public TappingAtTheWindow(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{G}"); + + // Look at the top three cards of your library. You may reveal a creature card from among them and put it into your hand. Put the rest into your graveyard. + this.getSpellAbility().addEffect(new TappingAtTheWindowEffect()); + + // Flashback {2}{G} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{2}{G}"))); + } + + private TappingAtTheWindow(final TappingAtTheWindow card) { + super(card); + } + + @Override + public TappingAtTheWindow copy() { + return new TappingAtTheWindow(this); + } +} + +class TappingAtTheWindowEffect extends OneShotEffect { + + TappingAtTheWindowEffect() { + super(Outcome.Benefit); + staticText = "look at the top three cards of your library. You may reveal a creature card " + + "from among them and put it into your hand. Put the rest into your graveyard"; + } + + private TappingAtTheWindowEffect(final TappingAtTheWindowEffect effect) { + super(effect); + } + + @Override + public TappingAtTheWindowEffect copy() { + return new TappingAtTheWindowEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, 3)); + TargetCard target = new TargetCardInLibrary( + 0, 1, StaticFilters.FILTER_CARD_CREATURE + ); + player.choose(outcome, cards, target, game); + Card card = game.getCard(target.getFirstTarget()); + if (card != null) { + player.revealCards(source, new CardsImpl(card), game); + player.moveCards(card, Zone.HAND, source, game); + } + cards.retainZone(Zone.LIBRARY, game); + player.moveCards(card, Zone.GRAVEYARD, source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 24241a9e772..c11d20e521a 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -279,6 +279,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Suspicious Stowaway", 80, Rarity.RARE, mage.cards.s.SuspiciousStowaway.class)); cards.add(new SetCardInfo("Swamp", 272, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Tainted Adversary", 124, Rarity.MYTHIC, mage.cards.t.TaintedAdversary.class)); + cards.add(new SetCardInfo("Tapping at the Window", 201, Rarity.COMMON, mage.cards.t.TappingAtTheWindow.class)); cards.add(new SetCardInfo("Tavern Ruffian", 163, Rarity.COMMON, mage.cards.t.TavernRuffian.class)); cards.add(new SetCardInfo("Tavern Smasher", 163, Rarity.COMMON, mage.cards.t.TavernSmasher.class)); cards.add(new SetCardInfo("The Meathook Massacre", 112, Rarity.MYTHIC, mage.cards.t.TheMeathookMassacre.class)); From cf5fa8185cb716b8da34bedff38b05171ea7d85c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 15:50:39 -0400 Subject: [PATCH 077/231] [MID] Implemented Sunset Revelry --- Mage.Sets/src/mage/cards/s/SunsetRevelry.java | 105 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 106 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SunsetRevelry.java diff --git a/Mage.Sets/src/mage/cards/s/SunsetRevelry.java b/Mage.Sets/src/mage/cards/s/SunsetRevelry.java new file mode 100644 index 00000000000..59ad39611b1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SunsetRevelry.java @@ -0,0 +1,105 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.game.Controllable; +import mage.game.Game; +import mage.game.permanent.token.HumanToken; +import mage.players.Player; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author TheElk801 + */ +public final class SunsetRevelry extends CardImpl { + + public SunsetRevelry(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{W}"); + + // If an opponent has more life than you, you gain 4 life. + // If an opponent controls more creatures than you, create two 1/1 white Human creature tokens. + // If an opponent has more cards in hand than you, draw a card. + // TODO: add hints to this + this.getSpellAbility().addEffect(new SunsetRevelryEffect()); + } + + private SunsetRevelry(final SunsetRevelry card) { + super(card); + } + + @Override + public SunsetRevelry copy() { + return new SunsetRevelry(this); + } +} + +class SunsetRevelryEffect extends OneShotEffect { + + SunsetRevelryEffect() { + super(Outcome.Benefit); + staticText = "If an opponent has more life than you, you gain 4 life." + + "
If an opponent controls more creatures than you, create two 1/1 white Human creature tokens." + + "
If an opponent has more cards in hand than you, draw a card."; + } + + private SunsetRevelryEffect(final SunsetRevelryEffect effect) { + super(effect); + } + + @Override + public SunsetRevelryEffect copy() { + return new SunsetRevelryEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Set opponents = game.getOpponents(source.getControllerId()); + if (opponents.stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .mapToInt(Player::getLife) + .anyMatch(x -> x > player.getLife())) { + player.gainLife(4, game, source); + } + Map map = game + .getBattlefield() + .getActivePermanents( + StaticFilters.FILTER_PERMANENT_CREATURE, + source.getControllerId(), game + ).stream() + .filter(Objects::nonNull) + .map(Controllable::getControllerId) + .filter(uuid -> opponents.contains(uuid) || source.getControllerId().equals(uuid)) + .collect(Collectors.toMap(Function.identity(), x -> 1, Integer::sum)); + if (map.getOrDefault( + source.getControllerId(), 0 + ) < map.values().stream().mapToInt(x -> x).max().orElse(0)) { + new HumanToken().putOntoBattlefield(2, game, source, source.getControllerId()); + } + if (opponents + .stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .map(Player::getHand) + .mapToInt(Set::size) + .anyMatch(x -> x > player.getHand().size())) { + player.drawCards(1, source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index c11d20e521a..15b9981e01c 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -275,6 +275,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Stuffed Bear", 259, Rarity.COMMON, mage.cards.s.StuffedBear.class)); cards.add(new SetCardInfo("Sungold Barrage", 36, Rarity.COMMON, mage.cards.s.SungoldBarrage.class)); cards.add(new SetCardInfo("Sunrise Cavalier", 244, Rarity.UNCOMMON, mage.cards.s.SunriseCavalier.class)); + cards.add(new SetCardInfo("Sunset Revelry", 38, Rarity.UNCOMMON, mage.cards.s.SunsetRevelry.class)); cards.add(new SetCardInfo("Sunstreak Phoenix", 162, Rarity.MYTHIC, mage.cards.s.SunstreakPhoenix.class)); cards.add(new SetCardInfo("Suspicious Stowaway", 80, Rarity.RARE, mage.cards.s.SuspiciousStowaway.class)); cards.add(new SetCardInfo("Swamp", 272, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); From 6afe6c968adde8a1cc8863b907e9378ab73a5ca2 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 15:54:48 -0400 Subject: [PATCH 078/231] [MID] Implemented Unblinking Observer --- .../src/mage/cards/u/UnblinkingObserver.java | 89 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 90 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/u/UnblinkingObserver.java diff --git a/Mage.Sets/src/mage/cards/u/UnblinkingObserver.java b/Mage.Sets/src/mage/cards/u/UnblinkingObserver.java new file mode 100644 index 00000000000..8f47ae7c5ae --- /dev/null +++ b/Mage.Sets/src/mage/cards/u/UnblinkingObserver.java @@ -0,0 +1,89 @@ +package mage.cards.u; + +import mage.ConditionalMana; +import mage.MageInt; +import mage.MageObject; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.Cost; +import mage.abilities.keyword.DisturbAbility; +import mage.abilities.mana.ConditionalColoredManaAbility; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.abilities.mana.conditional.ManaCondition; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ManaType; +import mage.constants.SubType; +import mage.game.Game; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class UnblinkingObserver extends CardImpl { + + public UnblinkingObserver(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.HOMUNCULUS); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // {T}: Add {U}. Spend this mana only to pay a disturb cost or cast an instant or sorcery spell. + this.addAbility(new ConditionalColoredManaAbility( + new Mana(ManaType.BLUE, 1), new UnblinkingObserverManaBuilder() + )); + } + + private UnblinkingObserver(final UnblinkingObserver card) { + super(card); + } + + @Override + public UnblinkingObserver copy() { + return new UnblinkingObserver(this); + } +} + +class UnblinkingObserverManaBuilder extends ConditionalManaBuilder { + + @Override + public ConditionalMana build(Object... options) { + return new UnblinkingObserverConditionalMana(this.mana); + } + + @Override + public String getRule() { + return "Spend this mana only to pay a disturb cost or cast an instant or sorcery spell."; + } +} + +class UnblinkingObserverConditionalMana extends ConditionalMana { + + UnblinkingObserverConditionalMana(Mana mana) { + super(mana); + staticText = "Spend this mana only to pay a disturb cost or cast an instant or sorcery spell."; + addCondition(new UnblinkingObserverManaCondition()); + } +} + +class UnblinkingObserverManaCondition extends ManaCondition implements Condition { + + @Override + public boolean apply(Game game, Ability source) { + if (source instanceof SpellAbility) { + MageObject object = game.getObject(source.getSourceId()); + return object != null && object.isInstantOrSorcery(game); + } + return source instanceof DisturbAbility; + } + + @Override + public boolean apply(Game game, Ability source, UUID originalId, Cost costsToPay) { + return apply(game, source); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 15b9981e01c..4a9d4d35a59 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -292,6 +292,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Tovolar's Packleader", 204, Rarity.RARE, mage.cards.t.TovolarsPackleader.class)); cards.add(new SetCardInfo("Triskaidekaphile", 81, Rarity.RARE, mage.cards.t.Triskaidekaphile.class)); cards.add(new SetCardInfo("Turn the Earth", 205, Rarity.UNCOMMON, mage.cards.t.TurnTheEarth.class)); + cards.add(new SetCardInfo("Unblinking Observer", 82, Rarity.COMMON, mage.cards.u.UnblinkingObserver.class)); cards.add(new SetCardInfo("Unnatural Growth", 206, Rarity.RARE, mage.cards.u.UnnaturalGrowth.class)); cards.add(new SetCardInfo("Unruly Mob", 40, Rarity.COMMON, mage.cards.u.UnrulyMob.class)); cards.add(new SetCardInfo("Untamed Pup", 187, Rarity.UNCOMMON, mage.cards.u.UntamedPup.class)); From 887cbe8ca1d16570b321014bf5e7ce62e3404577 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 16:09:13 -0400 Subject: [PATCH 079/231] [MID] Implemented Search Party Captain --- .../src/mage/cards/s/SearchPartyCaptain.java | 80 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 81 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SearchPartyCaptain.java diff --git a/Mage.Sets/src/mage/cards/s/SearchPartyCaptain.java b/Mage.Sets/src/mage/cards/s/SearchPartyCaptain.java new file mode 100644 index 00000000000..ae1ee2a13d6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SearchPartyCaptain.java @@ -0,0 +1,80 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.watchers.common.PlayerAttackedWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SearchPartyCaptain extends CardImpl { + + private static final Hint hint = new ValueHint( + "Creatures you attacked with this turn", SearchPartyCaptainValue.instance + ); + + public SearchPartyCaptain(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // This spell costs {1} less to cast for each creature you attacked with this turn. + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new SpellCostReductionSourceEffect(SearchPartyCaptainValue.instance) + .setText("this spell costs {1} less to cast for each creature you attacked with this turn") + ).addHint(hint), new PlayerAttackedWatcher()); + + // When Search Party Captain enters the battlefield, draw a card. + this.addAbility(new EntersBattlefieldTriggeredAbility(new DrawCardSourceControllerEffect(1))); + } + + private SearchPartyCaptain(final SearchPartyCaptain card) { + super(card); + } + + @Override + public SearchPartyCaptain copy() { + return new SearchPartyCaptain(this); + } +} + +enum SearchPartyCaptainValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return game + .getState() + .getWatcher(PlayerAttackedWatcher.class) + .getNumberOfAttackersCurrentTurn(sourceAbility.getControllerId()); + } + + @Override + public SearchPartyCaptainValue copy() { + return this; + } + + @Override + public String getMessage() { + return ""; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 4a9d4d35a59..b3fb957fd0a 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -244,6 +244,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Sacred Fire", 239, Rarity.UNCOMMON, mage.cards.s.SacredFire.class)); cards.add(new SetCardInfo("Saryth, the Viper's Fang", 197, Rarity.RARE, mage.cards.s.SarythTheVipersFang.class)); cards.add(new SetCardInfo("Seafaring Werewolf", 80, Rarity.RARE, mage.cards.s.SeafaringWerewolf.class)); + cards.add(new SetCardInfo("Search Party Captain", 32, Rarity.COMMON, mage.cards.s.SearchPartyCaptain.class)); cards.add(new SetCardInfo("Seasoned Cathar", 2, Rarity.UNCOMMON, mage.cards.s.SeasonedCathar.class)); cards.add(new SetCardInfo("Secrets of the Key", 73, Rarity.COMMON, mage.cards.s.SecretsOfTheKey.class)); cards.add(new SetCardInfo("Seize the Storm", 158, Rarity.UNCOMMON, mage.cards.s.SeizeTheStorm.class)); From ce0c6fa34e0eab5bb29b3c6cd9aa00e41a70c43f Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 16:18:59 -0400 Subject: [PATCH 080/231] [MID] Implemented Brutal Cathar / Moonrage Brute --- Mage.Sets/src/mage/cards/b/BrutalCathar.java | 99 +++++++++++++++++++ Mage.Sets/src/mage/cards/m/MoonrageBrute.java | 48 +++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 149 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/b/BrutalCathar.java create mode 100644 Mage.Sets/src/mage/cards/m/MoonrageBrute.java diff --git a/Mage.Sets/src/mage/cards/b/BrutalCathar.java b/Mage.Sets/src/mage/cards/b/BrutalCathar.java new file mode 100644 index 00000000000..1640c63563a --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BrutalCathar.java @@ -0,0 +1,99 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.delayed.OnLeaveReturnExiledToBattlefieldAbility; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; +import mage.abilities.effects.common.ExileUntilSourceLeavesEffect; +import mage.abilities.keyword.DayboundAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.common.TargetOpponentsCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BrutalCathar extends CardImpl { + + public BrutalCathar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.subtype.add(SubType.WEREWOLF); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + this.transformable = true; + this.secondSideCardClazz = mage.cards.m.MoonrageBrute.class; + + // When this creature enters the battlefield or transforms into Brutal Cathar, exile target creature an opponent controls until this creature leaves the battlefield. + this.addAbility(new BrutalCatharTriggeredAbility()); + + // Daybound + this.addAbility(new TransformAbility()); + this.addAbility(DayboundAbility.getInstance()); + } + + private BrutalCathar(final BrutalCathar card) { + super(card); + } + + @Override + public BrutalCathar copy() { + return new BrutalCathar(this); + } +} + +class BrutalCatharTriggeredAbility extends TriggeredAbilityImpl { + + public BrutalCatharTriggeredAbility() { + super(Zone.BATTLEFIELD, new ExileUntilSourceLeavesEffect("creature an opponent controls"), false); + this.addTarget(new TargetOpponentsCreaturePermanent()); + this.addEffect(new CreateDelayedTriggeredAbilityEffect(new OnLeaveReturnExiledToBattlefieldAbility())); + } + + public BrutalCatharTriggeredAbility(final BrutalCatharTriggeredAbility ability) { + super(ability); + } + + @Override + public BrutalCatharTriggeredAbility copy() { + return new BrutalCatharTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.TRANSFORMED + || event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!event.getTargetId().equals(this.getSourceId())) { + return false; + } + switch (event.getType()) { + case TRANSFORMED: + Permanent permanent = getSourcePermanentIfItStillExists(game); + return permanent != null && !permanent.isTransformed(); + case ENTERS_THE_BATTLEFIELD: + return true; + } + return false; + } + + @Override + public String getRule() { + return "When this creature enters the battlefield or transforms into {this}, " + + "exile target creature an opponent controls until this creature leaves the battlefield."; + } +} diff --git a/Mage.Sets/src/mage/cards/m/MoonrageBrute.java b/Mage.Sets/src/mage/cards/m/MoonrageBrute.java new file mode 100644 index 00000000000..5df9cb9e93b --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MoonrageBrute.java @@ -0,0 +1,48 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.NightboundAbility; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MoonrageBrute extends CardImpl { + + public MoonrageBrute(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.subtype.add(SubType.WEREWOLF); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + this.color.setRed(true); + this.transformable = true; + this.nightCard = true; + + // First strike + this.addAbility(FirstStrikeAbility.getInstance()); + + // Ward—Pay 3 life. + this.addAbility(new WardAbility(new PayLifeCost(3))); + + // Nightbound + this.addAbility(NightboundAbility.getInstance()); + } + + private MoonrageBrute(final MoonrageBrute card) { + super(card); + } + + @Override + public MoonrageBrute copy() { + return new MoonrageBrute(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index b3fb957fd0a..4a64c5f9a55 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -62,6 +62,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Briarbridge Tracker", 172, Rarity.RARE, mage.cards.b.BriarbridgeTracker.class)); cards.add(new SetCardInfo("Brimstone Vandal", 130, Rarity.COMMON, mage.cards.b.BrimstoneVandal.class)); cards.add(new SetCardInfo("Brood Weaver", 173, Rarity.UNCOMMON, mage.cards.b.BroodWeaver.class)); + cards.add(new SetCardInfo("Brutal Cathar", 7, Rarity.RARE, mage.cards.b.BrutalCathar.class)); cards.add(new SetCardInfo("Burly Breaker", 174, Rarity.UNCOMMON, mage.cards.b.BurlyBreaker.class)); cards.add(new SetCardInfo("Burn Down the House", 131, Rarity.RARE, mage.cards.b.BurnDownTheHouse.class)); cards.add(new SetCardInfo("Burn the Accursed", 132, Rarity.COMMON, mage.cards.b.BurnTheAccursed.class)); @@ -195,6 +196,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Mask of Griselbrand", 111, Rarity.RARE, mage.cards.m.MaskOfGriselbrand.class)); cards.add(new SetCardInfo("Memory Deluge", 62, Rarity.RARE, mage.cards.m.MemoryDeluge.class)); cards.add(new SetCardInfo("Might of the Old Ways", 189, Rarity.COMMON, mage.cards.m.MightOfTheOldWays.class)); + cards.add(new SetCardInfo("Moonrage Brute", 7, Rarity.RARE, mage.cards.m.MoonrageBrute.class)); cards.add(new SetCardInfo("Moonsilver Key", 255, Rarity.UNCOMMON, mage.cards.m.MoonsilverKey.class)); cards.add(new SetCardInfo("Moonveil Regent", 149, Rarity.MYTHIC, mage.cards.m.MoonveilRegent.class)); cards.add(new SetCardInfo("Morbid Opportunist", 113, Rarity.UNCOMMON, mage.cards.m.MorbidOpportunist.class)); From 165a347092a511563348e7e0fa97ed801ae88d72 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 17:46:43 -0400 Subject: [PATCH 081/231] [MID] Implemented Moonrager's Slash --- .../src/mage/cards/m/MoonragersSlash.java | 42 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../condition/common/NightCondition.java | 23 ++++++++++ .../mage/abilities/hint/common/NightHint.java | 27 ++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MoonragersSlash.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/NightCondition.java create mode 100644 Mage/src/main/java/mage/abilities/hint/common/NightHint.java diff --git a/Mage.Sets/src/mage/cards/m/MoonragersSlash.java b/Mage.Sets/src/mage/cards/m/MoonragersSlash.java new file mode 100644 index 00000000000..68b745297e6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MoonragersSlash.java @@ -0,0 +1,42 @@ +package mage.cards.m; + +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.NightCondition; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.abilities.hint.common.NightHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.target.common.TargetAnyTarget; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MoonragersSlash extends CardImpl { + + public MoonragersSlash(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{R}"); + + // This spell costs {2} less to cast if it's night. + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new SpellCostReductionSourceEffect(2, NightCondition.instance) + ).addHint(NightHint.instance)); + + // Moonrager's Slash deals 3 damage to any target. + this.getSpellAbility().addEffect(new DamageTargetEffect(3)); + this.getSpellAbility().addTarget(new TargetAnyTarget()); + } + + private MoonragersSlash(final MoonragersSlash card) { + super(card); + } + + @Override + public MoonragersSlash copy() { + return new MoonragersSlash(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 4a64c5f9a55..9572d5223ef 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -197,6 +197,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Memory Deluge", 62, Rarity.RARE, mage.cards.m.MemoryDeluge.class)); cards.add(new SetCardInfo("Might of the Old Ways", 189, Rarity.COMMON, mage.cards.m.MightOfTheOldWays.class)); cards.add(new SetCardInfo("Moonrage Brute", 7, Rarity.RARE, mage.cards.m.MoonrageBrute.class)); + cards.add(new SetCardInfo("Moonrager's Slash", 148, Rarity.COMMON, mage.cards.m.MoonragersSlash.class)); cards.add(new SetCardInfo("Moonsilver Key", 255, Rarity.UNCOMMON, mage.cards.m.MoonsilverKey.class)); cards.add(new SetCardInfo("Moonveil Regent", 149, Rarity.MYTHIC, mage.cards.m.MoonveilRegent.class)); cards.add(new SetCardInfo("Morbid Opportunist", 113, Rarity.UNCOMMON, mage.cards.m.MorbidOpportunist.class)); diff --git a/Mage/src/main/java/mage/abilities/condition/common/NightCondition.java b/Mage/src/main/java/mage/abilities/condition/common/NightCondition.java new file mode 100644 index 00000000000..29e5c709d64 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/NightCondition.java @@ -0,0 +1,23 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; + +/** + * @author TheElk801 + * TODO: Implement this + */ +public enum NightCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public String toString() { + return "it's night"; + } +} diff --git a/Mage/src/main/java/mage/abilities/hint/common/NightHint.java b/Mage/src/main/java/mage/abilities/hint/common/NightHint.java new file mode 100644 index 00000000000..afc7f2631dd --- /dev/null +++ b/Mage/src/main/java/mage/abilities/hint/common/NightHint.java @@ -0,0 +1,27 @@ +package mage.abilities.hint.common; + +import mage.abilities.Ability; +import mage.abilities.condition.common.NightCondition; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.game.Game; + +/** + * @author TheElk801 + */ +public enum NightHint implements Hint { + instance; + private static final Hint hint = new ConditionHint( + NightCondition.instance, "It's currently night" + ); + + @Override + public String getText(Game game, Ability ability) { + return hint.getText(game, ability); + } + + @Override + public Hint copy() { + return this; + } +} From 4902a31720239322f8baba5215242b3b53358553 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 17:49:56 -0400 Subject: [PATCH 082/231] [MID] Implemented Olivia's Midnight Ambush --- .../mage/cards/o/OliviasMidnightAmbush.java | 64 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 65 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/o/OliviasMidnightAmbush.java diff --git a/Mage.Sets/src/mage/cards/o/OliviasMidnightAmbush.java b/Mage.Sets/src/mage/cards/o/OliviasMidnightAmbush.java new file mode 100644 index 00000000000..872ea104fa6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OliviasMidnightAmbush.java @@ -0,0 +1,64 @@ +package mage.cards.o; + +import mage.abilities.Ability; +import mage.abilities.condition.common.NightCondition; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.hint.common.NightHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class OliviasMidnightAmbush extends CardImpl { + + public OliviasMidnightAmbush(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{B}"); + + // Target creature gets -2/-2 until end of turn. If it's night, that creature gets -13/-13 until end of turn instead. + this.getSpellAbility().addEffect(new OliviasMidnightAmbushEffect()); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.getSpellAbility().addHint(NightHint.instance); + } + + private OliviasMidnightAmbush(final OliviasMidnightAmbush card) { + super(card); + } + + @Override + public OliviasMidnightAmbush copy() { + return new OliviasMidnightAmbush(this); + } +} + +class OliviasMidnightAmbushEffect extends OneShotEffect { + + OliviasMidnightAmbushEffect() { + super(Outcome.Benefit); + staticText = "target creature gets -2/-2 until end of turn. " + + "If it's night, that creature gets -13/-13 until end of turn instead"; + } + + private OliviasMidnightAmbushEffect(final OliviasMidnightAmbushEffect effect) { + super(effect); + } + + @Override + public OliviasMidnightAmbushEffect copy() { + return new OliviasMidnightAmbushEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + int boost = NightCondition.instance.apply(game, source) ? -13 : -2; + game.addEffect(new BoostTargetEffect(boost, boost), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 9572d5223ef..0b7f79bcf2c 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -215,6 +215,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Novice Occultist", 117, Rarity.COMMON, mage.cards.n.NoviceOccultist.class)); cards.add(new SetCardInfo("Obsessive Astronomer", 152, Rarity.UNCOMMON, mage.cards.o.ObsessiveAstronomer.class)); cards.add(new SetCardInfo("Odric's Outrider", 29, Rarity.UNCOMMON, mage.cards.o.OdricsOutrider.class)); + cards.add(new SetCardInfo("Olivia's Midnight Ambush", 118, Rarity.COMMON, mage.cards.o.OliviasMidnightAmbush.class)); cards.add(new SetCardInfo("Organ Hoarder", 66, Rarity.COMMON, mage.cards.o.OrganHoarder.class)); cards.add(new SetCardInfo("Otherworldly Gaze", 67, Rarity.COMMON, mage.cards.o.OtherworldlyGaze.class)); cards.add(new SetCardInfo("Outland Liberator", 190, Rarity.UNCOMMON, mage.cards.o.OutlandLiberator.class)); From 5f32fe53085d39602d9cf0a799ab7ac11f656bdb Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 12 Sep 2021 18:06:46 -0400 Subject: [PATCH 083/231] [MID] changed daybound/nightbound from being singleton --- .../src/mage/cards/a/ArlinnTheMoonsFury.java | 2 +- .../src/mage/cards/a/ArlinnThePacksHope.java | 2 +- Mage.Sets/src/mage/cards/b/BirdAdmirer.java | 2 +- Mage.Sets/src/mage/cards/b/BrutalCathar.java | 2 +- Mage.Sets/src/mage/cards/b/BurlyBreaker.java | 2 +- .../src/mage/cards/d/DireStrainBrawler.java | 2 +- .../mage/cards/d/DireStrainDemolisher.java | 2 +- .../src/mage/cards/f/FangbladeBrigand.java | 2 +- .../mage/cards/f/FangbladeEviscerator.java | 2 +- .../src/mage/cards/f/FrenziedTrapbreaker.java | 2 +- .../src/mage/cards/g/GraveyardGlutton.java | 2 +- .../src/mage/cards/g/GraveyardTrespasser.java | 2 +- .../mage/cards/h/HarvesttideAssailant.java | 2 +- .../mage/cards/h/HarvesttideInfiltrator.java | 2 +- Mage.Sets/src/mage/cards/h/HoundTamer.java | 2 +- .../src/mage/cards/k/KessigNaturalist.java | 2 +- .../src/mage/cards/l/LordOfTheUlvenwald.java | 2 +- Mage.Sets/src/mage/cards/m/MoonrageBrute.java | 2 +- .../src/mage/cards/o/OutlandLiberator.java | 2 +- .../src/mage/cards/r/RecklessStormseeker.java | 2 +- .../src/mage/cards/s/SeafaringWerewolf.java | 2 +- Mage.Sets/src/mage/cards/s/ShadyTraveler.java | 2 +- .../src/mage/cards/s/SpellruneHowler.java | 2 +- .../src/mage/cards/s/SpellrunePainter.java | 2 +- .../src/mage/cards/s/StalkingPredator.java | 2 +- .../src/mage/cards/s/StormChargedSlasher.java | 2 +- .../src/mage/cards/s/SuspiciousStowaway.java | 2 +- Mage.Sets/src/mage/cards/t/TavernRuffian.java | 2 +- Mage.Sets/src/mage/cards/t/TavernSmasher.java | 2 +- .../src/mage/cards/t/TirelessHauler.java | 2 +- .../src/mage/cards/t/TovolarsHuntmaster.java | 2 +- .../src/mage/cards/t/TovolarsPackleader.java | 2 +- Mage.Sets/src/mage/cards/u/UntamedPup.java | 2 +- .../src/mage/cards/v/VillageReavers.java | 2 +- Mage.Sets/src/mage/cards/v/VillageWatch.java | 2 +- Mage.Sets/src/mage/cards/w/WingShredder.java | 2 +- .../abilities/keyword/DayboundAbility.java | 26 +++++-------------- .../abilities/keyword/NightboundAbility.java | 26 +++++-------------- Utils/keywords.txt | 4 +-- 39 files changed, 50 insertions(+), 78 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/ArlinnTheMoonsFury.java b/Mage.Sets/src/mage/cards/a/ArlinnTheMoonsFury.java index 96a462e0f81..98e064a041b 100644 --- a/Mage.Sets/src/mage/cards/a/ArlinnTheMoonsFury.java +++ b/Mage.Sets/src/mage/cards/a/ArlinnTheMoonsFury.java @@ -35,7 +35,7 @@ public final class ArlinnTheMoonsFury extends CardImpl { this.nightCard = true; // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); // +2: Add {R}{G}. this.addAbility(new LoyaltyAbility(new BasicManaEffect(new Mana( diff --git a/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java b/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java index 39076e1d213..ee4f826916a 100644 --- a/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java +++ b/Mage.Sets/src/mage/cards/a/ArlinnThePacksHope.java @@ -40,7 +40,7 @@ public final class ArlinnThePacksHope extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); // +1: Until your next turn, you may cast creature spells as though they had flash, and each creature you control enters the battlefield with an additional +1/+1 counter on it. Ability ability = new LoyaltyAbility(new CastAsThoughItHadFlashAllEffect( diff --git a/Mage.Sets/src/mage/cards/b/BirdAdmirer.java b/Mage.Sets/src/mage/cards/b/BirdAdmirer.java index 23cd8f80e6c..3ee4bc0bd70 100644 --- a/Mage.Sets/src/mage/cards/b/BirdAdmirer.java +++ b/Mage.Sets/src/mage/cards/b/BirdAdmirer.java @@ -32,7 +32,7 @@ public final class BirdAdmirer extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private BirdAdmirer(final BirdAdmirer card) { diff --git a/Mage.Sets/src/mage/cards/b/BrutalCathar.java b/Mage.Sets/src/mage/cards/b/BrutalCathar.java index 1640c63563a..902e0277d02 100644 --- a/Mage.Sets/src/mage/cards/b/BrutalCathar.java +++ b/Mage.Sets/src/mage/cards/b/BrutalCathar.java @@ -40,7 +40,7 @@ public final class BrutalCathar extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private BrutalCathar(final BrutalCathar card) { diff --git a/Mage.Sets/src/mage/cards/b/BurlyBreaker.java b/Mage.Sets/src/mage/cards/b/BurlyBreaker.java index e81de9308fb..4f327728240 100644 --- a/Mage.Sets/src/mage/cards/b/BurlyBreaker.java +++ b/Mage.Sets/src/mage/cards/b/BurlyBreaker.java @@ -32,7 +32,7 @@ public final class BurlyBreaker extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private BurlyBreaker(final BurlyBreaker card) { diff --git a/Mage.Sets/src/mage/cards/d/DireStrainBrawler.java b/Mage.Sets/src/mage/cards/d/DireStrainBrawler.java index f6d61205137..c7116d20e7f 100644 --- a/Mage.Sets/src/mage/cards/d/DireStrainBrawler.java +++ b/Mage.Sets/src/mage/cards/d/DireStrainBrawler.java @@ -29,7 +29,7 @@ public final class DireStrainBrawler extends CardImpl { this.addAbility(VigilanceAbility.getInstance()); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private DireStrainBrawler(final DireStrainBrawler card) { diff --git a/Mage.Sets/src/mage/cards/d/DireStrainDemolisher.java b/Mage.Sets/src/mage/cards/d/DireStrainDemolisher.java index 40ee7cbd748..51fcdcc7077 100644 --- a/Mage.Sets/src/mage/cards/d/DireStrainDemolisher.java +++ b/Mage.Sets/src/mage/cards/d/DireStrainDemolisher.java @@ -30,7 +30,7 @@ public final class DireStrainDemolisher extends CardImpl { this.addAbility(new WardAbility(new ManaCostsImpl<>("{3}"))); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private DireStrainDemolisher(final DireStrainDemolisher card) { diff --git a/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java b/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java index 67526dfe671..c8bab4a3313 100644 --- a/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java +++ b/Mage.Sets/src/mage/cards/f/FangbladeBrigand.java @@ -43,7 +43,7 @@ public final class FangbladeBrigand extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private FangbladeBrigand(final FangbladeBrigand card) { diff --git a/Mage.Sets/src/mage/cards/f/FangbladeEviscerator.java b/Mage.Sets/src/mage/cards/f/FangbladeEviscerator.java index 2bf456d84b0..f3a9ba0926a 100644 --- a/Mage.Sets/src/mage/cards/f/FangbladeEviscerator.java +++ b/Mage.Sets/src/mage/cards/f/FangbladeEviscerator.java @@ -47,7 +47,7 @@ public final class FangbladeEviscerator extends CardImpl { ), new ManaCostsImpl<>("{4}{R}"))); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private FangbladeEviscerator(final FangbladeEviscerator card) { diff --git a/Mage.Sets/src/mage/cards/f/FrenziedTrapbreaker.java b/Mage.Sets/src/mage/cards/f/FrenziedTrapbreaker.java index 9dbe001be71..3eaf2dae55e 100644 --- a/Mage.Sets/src/mage/cards/f/FrenziedTrapbreaker.java +++ b/Mage.Sets/src/mage/cards/f/FrenziedTrapbreaker.java @@ -52,7 +52,7 @@ public final class FrenziedTrapbreaker extends CardImpl { this.addAbility(ability); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private FrenziedTrapbreaker(final FrenziedTrapbreaker card) { diff --git a/Mage.Sets/src/mage/cards/g/GraveyardGlutton.java b/Mage.Sets/src/mage/cards/g/GraveyardGlutton.java index 9e700d816c4..8cd92dd191b 100644 --- a/Mage.Sets/src/mage/cards/g/GraveyardGlutton.java +++ b/Mage.Sets/src/mage/cards/g/GraveyardGlutton.java @@ -46,7 +46,7 @@ public final class GraveyardGlutton extends CardImpl { this.addAbility(ability); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private GraveyardGlutton(final GraveyardGlutton card) { diff --git a/Mage.Sets/src/mage/cards/g/GraveyardTrespasser.java b/Mage.Sets/src/mage/cards/g/GraveyardTrespasser.java index f74e5125d35..4b505e9d417 100644 --- a/Mage.Sets/src/mage/cards/g/GraveyardTrespasser.java +++ b/Mage.Sets/src/mage/cards/g/GraveyardTrespasser.java @@ -48,7 +48,7 @@ public final class GraveyardTrespasser extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private GraveyardTrespasser(final GraveyardTrespasser card) { diff --git a/Mage.Sets/src/mage/cards/h/HarvesttideAssailant.java b/Mage.Sets/src/mage/cards/h/HarvesttideAssailant.java index 4c888f9ad8f..55c37c19d41 100644 --- a/Mage.Sets/src/mage/cards/h/HarvesttideAssailant.java +++ b/Mage.Sets/src/mage/cards/h/HarvesttideAssailant.java @@ -29,7 +29,7 @@ public final class HarvesttideAssailant extends CardImpl { this.addAbility(TrampleAbility.getInstance()); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private HarvesttideAssailant(final HarvesttideAssailant card) { diff --git a/Mage.Sets/src/mage/cards/h/HarvesttideInfiltrator.java b/Mage.Sets/src/mage/cards/h/HarvesttideInfiltrator.java index aff3c87fa60..66e8715dc57 100644 --- a/Mage.Sets/src/mage/cards/h/HarvesttideInfiltrator.java +++ b/Mage.Sets/src/mage/cards/h/HarvesttideInfiltrator.java @@ -31,7 +31,7 @@ public final class HarvesttideInfiltrator extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private HarvesttideInfiltrator(final HarvesttideInfiltrator card) { diff --git a/Mage.Sets/src/mage/cards/h/HoundTamer.java b/Mage.Sets/src/mage/cards/h/HoundTamer.java index 02d1b4f3cbb..d45833ecbd5 100644 --- a/Mage.Sets/src/mage/cards/h/HoundTamer.java +++ b/Mage.Sets/src/mage/cards/h/HoundTamer.java @@ -44,7 +44,7 @@ public final class HoundTamer extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private HoundTamer(final HoundTamer card) { diff --git a/Mage.Sets/src/mage/cards/k/KessigNaturalist.java b/Mage.Sets/src/mage/cards/k/KessigNaturalist.java index c9e52efc30b..bc9096b8f54 100644 --- a/Mage.Sets/src/mage/cards/k/KessigNaturalist.java +++ b/Mage.Sets/src/mage/cards/k/KessigNaturalist.java @@ -38,7 +38,7 @@ public final class KessigNaturalist extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private KessigNaturalist(final KessigNaturalist card) { diff --git a/Mage.Sets/src/mage/cards/l/LordOfTheUlvenwald.java b/Mage.Sets/src/mage/cards/l/LordOfTheUlvenwald.java index 4bd34e1408c..05f56567b31 100644 --- a/Mage.Sets/src/mage/cards/l/LordOfTheUlvenwald.java +++ b/Mage.Sets/src/mage/cards/l/LordOfTheUlvenwald.java @@ -56,7 +56,7 @@ public final class LordOfTheUlvenwald extends CardImpl { this.addAbility(new AttacksTriggeredAbility(new LordOfTheUlvenwaldEffect())); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private LordOfTheUlvenwald(final LordOfTheUlvenwald card) { diff --git a/Mage.Sets/src/mage/cards/m/MoonrageBrute.java b/Mage.Sets/src/mage/cards/m/MoonrageBrute.java index 5df9cb9e93b..bbd90c57b43 100644 --- a/Mage.Sets/src/mage/cards/m/MoonrageBrute.java +++ b/Mage.Sets/src/mage/cards/m/MoonrageBrute.java @@ -34,7 +34,7 @@ public final class MoonrageBrute extends CardImpl { this.addAbility(new WardAbility(new PayLifeCost(3))); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private MoonrageBrute(final MoonrageBrute card) { diff --git a/Mage.Sets/src/mage/cards/o/OutlandLiberator.java b/Mage.Sets/src/mage/cards/o/OutlandLiberator.java index b4a2e9affc0..3e27af77112 100644 --- a/Mage.Sets/src/mage/cards/o/OutlandLiberator.java +++ b/Mage.Sets/src/mage/cards/o/OutlandLiberator.java @@ -37,7 +37,7 @@ public final class OutlandLiberator extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private OutlandLiberator(final OutlandLiberator card) { diff --git a/Mage.Sets/src/mage/cards/r/RecklessStormseeker.java b/Mage.Sets/src/mage/cards/r/RecklessStormseeker.java index da11e7ace7f..8b78f15c067 100644 --- a/Mage.Sets/src/mage/cards/r/RecklessStormseeker.java +++ b/Mage.Sets/src/mage/cards/r/RecklessStormseeker.java @@ -47,7 +47,7 @@ public final class RecklessStormseeker extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private RecklessStormseeker(final RecklessStormseeker card) { diff --git a/Mage.Sets/src/mage/cards/s/SeafaringWerewolf.java b/Mage.Sets/src/mage/cards/s/SeafaringWerewolf.java index f2789e2301d..c075a58ed96 100644 --- a/Mage.Sets/src/mage/cards/s/SeafaringWerewolf.java +++ b/Mage.Sets/src/mage/cards/s/SeafaringWerewolf.java @@ -36,7 +36,7 @@ public final class SeafaringWerewolf extends CardImpl { )); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private SeafaringWerewolf(final SeafaringWerewolf card) { diff --git a/Mage.Sets/src/mage/cards/s/ShadyTraveler.java b/Mage.Sets/src/mage/cards/s/ShadyTraveler.java index e9ab146127b..f2424f30024 100644 --- a/Mage.Sets/src/mage/cards/s/ShadyTraveler.java +++ b/Mage.Sets/src/mage/cards/s/ShadyTraveler.java @@ -31,7 +31,7 @@ public final class ShadyTraveler extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private ShadyTraveler(final ShadyTraveler card) { diff --git a/Mage.Sets/src/mage/cards/s/SpellruneHowler.java b/Mage.Sets/src/mage/cards/s/SpellruneHowler.java index c5c63b6da66..a7419fbe12c 100644 --- a/Mage.Sets/src/mage/cards/s/SpellruneHowler.java +++ b/Mage.Sets/src/mage/cards/s/SpellruneHowler.java @@ -35,7 +35,7 @@ public final class SpellruneHowler extends CardImpl { )); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private SpellruneHowler(final SpellruneHowler card) { diff --git a/Mage.Sets/src/mage/cards/s/SpellrunePainter.java b/Mage.Sets/src/mage/cards/s/SpellrunePainter.java index 3e46dc3ef0b..8a7b20c3458 100644 --- a/Mage.Sets/src/mage/cards/s/SpellrunePainter.java +++ b/Mage.Sets/src/mage/cards/s/SpellrunePainter.java @@ -38,7 +38,7 @@ public final class SpellrunePainter extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private SpellrunePainter(final SpellrunePainter card) { diff --git a/Mage.Sets/src/mage/cards/s/StalkingPredator.java b/Mage.Sets/src/mage/cards/s/StalkingPredator.java index 9721d074f27..98ce16017b5 100644 --- a/Mage.Sets/src/mage/cards/s/StalkingPredator.java +++ b/Mage.Sets/src/mage/cards/s/StalkingPredator.java @@ -29,7 +29,7 @@ public final class StalkingPredator extends CardImpl { this.addAbility(new MenaceAbility()); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private StalkingPredator(final StalkingPredator card) { diff --git a/Mage.Sets/src/mage/cards/s/StormChargedSlasher.java b/Mage.Sets/src/mage/cards/s/StormChargedSlasher.java index 4b0e5f52872..93ba611b364 100644 --- a/Mage.Sets/src/mage/cards/s/StormChargedSlasher.java +++ b/Mage.Sets/src/mage/cards/s/StormChargedSlasher.java @@ -49,7 +49,7 @@ public final class StormChargedSlasher extends CardImpl { this.addAbility(ability); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private StormChargedSlasher(final StormChargedSlasher card) { diff --git a/Mage.Sets/src/mage/cards/s/SuspiciousStowaway.java b/Mage.Sets/src/mage/cards/s/SuspiciousStowaway.java index 8b09363b0cb..83c217f6fc6 100644 --- a/Mage.Sets/src/mage/cards/s/SuspiciousStowaway.java +++ b/Mage.Sets/src/mage/cards/s/SuspiciousStowaway.java @@ -39,7 +39,7 @@ public final class SuspiciousStowaway extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private SuspiciousStowaway(final SuspiciousStowaway card) { diff --git a/Mage.Sets/src/mage/cards/t/TavernRuffian.java b/Mage.Sets/src/mage/cards/t/TavernRuffian.java index fa0f278d608..b06f875bd57 100644 --- a/Mage.Sets/src/mage/cards/t/TavernRuffian.java +++ b/Mage.Sets/src/mage/cards/t/TavernRuffian.java @@ -29,7 +29,7 @@ public final class TavernRuffian extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private TavernRuffian(final TavernRuffian card) { diff --git a/Mage.Sets/src/mage/cards/t/TavernSmasher.java b/Mage.Sets/src/mage/cards/t/TavernSmasher.java index bb399b15cc0..8a3b1258e57 100644 --- a/Mage.Sets/src/mage/cards/t/TavernSmasher.java +++ b/Mage.Sets/src/mage/cards/t/TavernSmasher.java @@ -28,7 +28,7 @@ public final class TavernSmasher extends CardImpl { this.toughness = new MageInt(5); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private TavernSmasher(final TavernSmasher card) { diff --git a/Mage.Sets/src/mage/cards/t/TirelessHauler.java b/Mage.Sets/src/mage/cards/t/TirelessHauler.java index e4603424729..c50d0ac4ca8 100644 --- a/Mage.Sets/src/mage/cards/t/TirelessHauler.java +++ b/Mage.Sets/src/mage/cards/t/TirelessHauler.java @@ -31,7 +31,7 @@ public final class TirelessHauler extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private TirelessHauler(final TirelessHauler card) { diff --git a/Mage.Sets/src/mage/cards/t/TovolarsHuntmaster.java b/Mage.Sets/src/mage/cards/t/TovolarsHuntmaster.java index 40ffabbe628..728ea25305f 100644 --- a/Mage.Sets/src/mage/cards/t/TovolarsHuntmaster.java +++ b/Mage.Sets/src/mage/cards/t/TovolarsHuntmaster.java @@ -33,7 +33,7 @@ public final class TovolarsHuntmaster extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private TovolarsHuntmaster(final TovolarsHuntmaster card) { diff --git a/Mage.Sets/src/mage/cards/t/TovolarsPackleader.java b/Mage.Sets/src/mage/cards/t/TovolarsPackleader.java index b167aff49c2..ce1a3bff2a2 100644 --- a/Mage.Sets/src/mage/cards/t/TovolarsPackleader.java +++ b/Mage.Sets/src/mage/cards/t/TovolarsPackleader.java @@ -61,7 +61,7 @@ public final class TovolarsPackleader extends CardImpl { ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL)); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private TovolarsPackleader(final TovolarsPackleader card) { diff --git a/Mage.Sets/src/mage/cards/u/UntamedPup.java b/Mage.Sets/src/mage/cards/u/UntamedPup.java index 89289789654..519b2ef3353 100644 --- a/Mage.Sets/src/mage/cards/u/UntamedPup.java +++ b/Mage.Sets/src/mage/cards/u/UntamedPup.java @@ -61,7 +61,7 @@ public final class UntamedPup extends CardImpl { this.addAbility(ability); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private UntamedPup(final UntamedPup card) { diff --git a/Mage.Sets/src/mage/cards/v/VillageReavers.java b/Mage.Sets/src/mage/cards/v/VillageReavers.java index 23a0a1ebd73..efa4fe5e3ff 100644 --- a/Mage.Sets/src/mage/cards/v/VillageReavers.java +++ b/Mage.Sets/src/mage/cards/v/VillageReavers.java @@ -49,7 +49,7 @@ public final class VillageReavers extends CardImpl { ))); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private VillageReavers(final VillageReavers card) { diff --git a/Mage.Sets/src/mage/cards/v/VillageWatch.java b/Mage.Sets/src/mage/cards/v/VillageWatch.java index 4641353d28f..c620c73046f 100644 --- a/Mage.Sets/src/mage/cards/v/VillageWatch.java +++ b/Mage.Sets/src/mage/cards/v/VillageWatch.java @@ -32,7 +32,7 @@ public final class VillageWatch extends CardImpl { // Daybound this.addAbility(new TransformAbility()); - this.addAbility(DayboundAbility.getInstance()); + this.addAbility(new DayboundAbility()); } private VillageWatch(final VillageWatch card) { diff --git a/Mage.Sets/src/mage/cards/w/WingShredder.java b/Mage.Sets/src/mage/cards/w/WingShredder.java index 33e792c600a..e1946dd6e5b 100644 --- a/Mage.Sets/src/mage/cards/w/WingShredder.java +++ b/Mage.Sets/src/mage/cards/w/WingShredder.java @@ -30,7 +30,7 @@ public final class WingShredder extends CardImpl { this.addAbility(ReachAbility.getInstance()); // Nightbound - this.addAbility(NightboundAbility.getInstance()); + this.addAbility(new NightboundAbility()); } private WingShredder(final WingShredder card) { diff --git a/Mage/src/main/java/mage/abilities/keyword/DayboundAbility.java b/Mage/src/main/java/mage/abilities/keyword/DayboundAbility.java index 968846cc88c..1e8761d6b4c 100644 --- a/Mage/src/main/java/mage/abilities/keyword/DayboundAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/DayboundAbility.java @@ -1,34 +1,20 @@ package mage.abilities.keyword; -import mage.abilities.MageSingleton; import mage.abilities.StaticAbility; import mage.constants.Zone; -import java.io.ObjectStreamException; - /** * @author TheElk801 * TODO: Implement this */ -public class DayboundAbility extends StaticAbility implements MageSingleton { +public class DayboundAbility extends StaticAbility { - private static final DayboundAbility instance; - - static { - instance = new DayboundAbility(); - // instance.addIcon(DayboundAbilityIcon.instance); (needs to be added) + public DayboundAbility() { + super(Zone.BATTLEFIELD, null); } - private Object readResolve() throws ObjectStreamException { - return instance; - } - - public static DayboundAbility getInstance() { - return instance; - } - - private DayboundAbility() { - super(Zone.ALL, null); + private DayboundAbility(final DayboundAbility ability) { + super(ability); } @Override @@ -38,6 +24,6 @@ public class DayboundAbility extends StaticAbility implements MageSingleton { @Override public DayboundAbility copy() { - return instance; + return new DayboundAbility(this); } } diff --git a/Mage/src/main/java/mage/abilities/keyword/NightboundAbility.java b/Mage/src/main/java/mage/abilities/keyword/NightboundAbility.java index 13f232d50d8..804b63c83cc 100644 --- a/Mage/src/main/java/mage/abilities/keyword/NightboundAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/NightboundAbility.java @@ -1,34 +1,20 @@ package mage.abilities.keyword; -import mage.abilities.MageSingleton; import mage.abilities.StaticAbility; import mage.constants.Zone; -import java.io.ObjectStreamException; - /** * @author TheElk801 * TODO: Implement this */ -public class NightboundAbility extends StaticAbility implements MageSingleton { +public class NightboundAbility extends StaticAbility { - private static final NightboundAbility instance; - - static { - instance = new NightboundAbility(); - // instance.addIcon(NightboundAbilityIcon.instance); (needs to be added) + public NightboundAbility() { + super(Zone.BATTLEFIELD, null); } - private Object readResolve() throws ObjectStreamException { - return instance; - } - - public static NightboundAbility getInstance() { - return instance; - } - - private NightboundAbility() { - super(Zone.ALL, null); + private NightboundAbility(final NightboundAbility ability) { + super(ability); } @Override @@ -38,6 +24,6 @@ public class NightboundAbility extends StaticAbility implements MageSingleton { @Override public NightboundAbility copy() { - return instance; + return new NightboundAbility(this); } } diff --git a/Utils/keywords.txt b/Utils/keywords.txt index 1dc14e60261..617997e4f49 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -16,7 +16,7 @@ Crew|number| Cumulative upkeep|cost| Cycling|cost| Dash|card, manaString| -Daybound|instance| +Daybound|new| Deathtouch|instance| Demonstrate|new| Delve|new| @@ -73,7 +73,7 @@ Mountainwalk|new| Morph|card, cost| Mutate|card, manaString| Myriad|new| -Nightbound|instance| +Nightbound|new| Ninjutsu|cost| Outlast|cost| Partner|instance| From 26e9a92ea7e78bade4bc6366a7e0aa6465bd7cc5 Mon Sep 17 00:00:00 2001 From: jeffwadsworth Date: Mon, 13 Sep 2021 11:03:25 -0500 Subject: [PATCH 084/231] - Fixed #8257 --- Mage.Sets/src/mage/cards/o/OboshThePreypiercer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/o/OboshThePreypiercer.java b/Mage.Sets/src/mage/cards/o/OboshThePreypiercer.java index acce61c7468..9f2a8a9f13f 100644 --- a/Mage.Sets/src/mage/cards/o/OboshThePreypiercer.java +++ b/Mage.Sets/src/mage/cards/o/OboshThePreypiercer.java @@ -88,7 +88,7 @@ class OboshThePreypiercerEffect extends ReplacementEffectImpl { @Override public boolean checksEventType(GameEvent event, Game game) { return event.getType().equals(GameEvent.EventType.DAMAGE_PLAYER) - || event.getType().equals(GameEvent.EventType.DAMAGED_PERMANENT); + || event.getType().equals(GameEvent.EventType.DAMAGE_PERMANENT); } @Override From 1b27fdd6ba65903aa02ddd19689f3c787a9aa7eb Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Mon, 13 Sep 2021 12:23:16 -0400 Subject: [PATCH 085/231] Update Commander banned list --- .../Mage.Deck.Constructed/src/mage/deck/Commander.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java index 6a81bbc6b07..9dccf0fde5f 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java @@ -46,6 +46,7 @@ public class Commander extends Constructed { banned.add("Fastbond"); banned.add("Flash"); banned.add("Gifts Ungiven"); + banned.add("Golos, Tireless Pilgrim"); banned.add("Griselbrand"); banned.add("Hullbreacher"); banned.add("Iona, Shield of Emeria"); @@ -74,7 +75,6 @@ public class Commander extends Constructed { banned.add("Tolarian Academy"); banned.add("Trade Secrets"); banned.add("Upheaval"); - banned.add("Worldfire"); banned.add("Yawgmoth's Bargain"); } From 5cc6e326d550903dcb4a37917e1384bafa2619b3 Mon Sep 17 00:00:00 2001 From: jeffwadsworth Date: Mon, 13 Sep 2021 11:40:31 -0500 Subject: [PATCH 086/231] - Fixed #8237 --- .../mage/cards/n/NilsDisciplineEnforcer.java | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/Mage.Sets/src/mage/cards/n/NilsDisciplineEnforcer.java b/Mage.Sets/src/mage/cards/n/NilsDisciplineEnforcer.java index 6378da0aede..4375cbb4c98 100644 --- a/Mage.Sets/src/mage/cards/n/NilsDisciplineEnforcer.java +++ b/Mage.Sets/src/mage/cards/n/NilsDisciplineEnforcer.java @@ -19,11 +19,9 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; -import mage.target.Target; import mage.target.TargetPermanent; import mage.target.targetadjustment.TargetAdjuster; -import java.util.Objects; import java.util.UUID; /** @@ -67,15 +65,14 @@ enum NilsDisciplineEnforcerAdjuster implements TargetAdjuster { @Override public void adjustTargets(Ability ability, Game game) { ability.getTargets().clear(); - for (UUID playerId : game.getState().getPlayersInRange(ability.getControllerId(), game)) { + game.getState().getPlayersInRange(ability.getControllerId(), game).forEach(playerId -> { Player player = game.getPlayer(playerId); - if (player == null) { - continue; + if (!(player == null)) { + FilterPermanent filter = new FilterCreaturePermanent("creature controlled by " + player.getName()); + filter.add(new ControllerIdPredicate(playerId)); + ability.addTarget(new TargetPermanent(0, 1, filter)); } - FilterPermanent filter = new FilterCreaturePermanent("creature controlled by " + player.getName()); - filter.add(new ControllerIdPredicate(playerId)); - ability.addTarget(new TargetPermanent(0, 1, filter)); - } + }); } } @@ -99,13 +96,14 @@ class NilsDisciplineEnforcerCountersEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { source.getTargets() .stream() - .map(Target::getFirstTarget) - .map(game::getPermanent) - .filter(Objects::nonNull) - .map(permanent -> permanent.addCounters( - CounterType.P1P1.createInstance(), - source.getControllerId(), source, game - )); + .filter( + t -> (t != null)) + .map(t -> game.getPermanent(t.getFirstTarget())) + .filter(targetedPermanent + -> (targetedPermanent != null)) + .forEachOrdered(targetedPermanent -> { + targetedPermanent.addCounters(CounterType.P1P1.createInstance(), source.getControllerId(), source, game); + }); return true; } } @@ -114,8 +112,8 @@ class NilsDisciplineEnforcerEffect extends CantAttackYouUnlessPayManaAllEffect { NilsDisciplineEnforcerEffect() { super(null, true); - staticText = "Each creature with one or more counters on it can't attack you or planeswalkers you control " + - "unless its controller pays {X}, where X is the number of counters on that creature."; + staticText = "Each creature with one or more counters on it can't attack you or planeswalkers you control " + + "unless its controller pays {X}, where X is the number of counters on that creature."; } private NilsDisciplineEnforcerEffect(NilsDisciplineEnforcerEffect effect) { From ce2f166c53d7f7efe6034827c2e431364da075a7 Mon Sep 17 00:00:00 2001 From: jeffwadsworth Date: Mon, 13 Sep 2021 15:59:46 -0500 Subject: [PATCH 087/231] - Fixed #8185 --- Mage.Sets/src/mage/cards/s/ScreechingBat.java | 13 +-- .../src/mage/cards/s/StalkingVampire.java | 80 +++++++++++++++++-- 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/Mage.Sets/src/mage/cards/s/ScreechingBat.java b/Mage.Sets/src/mage/cards/s/ScreechingBat.java index 63b29ed5cea..8c1c31fd8b7 100644 --- a/Mage.Sets/src/mage/cards/s/ScreechingBat.java +++ b/Mage.Sets/src/mage/cards/s/ScreechingBat.java @@ -1,15 +1,13 @@ - package mage.cards.s; import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.condition.common.TransformedCondition; import mage.abilities.costs.Cost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.TransformSourceEffect; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; @@ -20,7 +18,6 @@ import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.game.permanent.Permanent; /** @@ -29,7 +26,7 @@ import mage.game.permanent.Permanent; public final class ScreechingBat extends CardImpl { public ScreechingBat(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); this.subtype.add(SubType.BAT); this.transformable = true; @@ -42,7 +39,7 @@ public final class ScreechingBat extends CardImpl { // At the beginning of your upkeep, you may pay {2}{B}{B}. If you do, transform Screeching Bat. this.addAbility(new TransformAbility()); - this.addAbility(new ConditionalTriggeredAbility(new ScreechingBatBeginningOfUpkeepTriggeredAbility(), new TransformedCondition(true), "")); + this.addAbility(new ScreechingBatBeginningOfUpkeepTriggeredAbility()); } private ScreechingBat(final ScreechingBat card) { @@ -108,9 +105,7 @@ class ScreechingBatTransformSourceEffect extends OneShotEffect { if (permanent != null) { Cost cost = new ManaCostsImpl("{2}{B}{B}"); if (cost.pay(source, game, source, permanent.getControllerId(), false, null)) { - if (permanent.isTransformable()) { - permanent.setTransformed(!permanent.isTransformed()); - } + new TransformSourceEffect(true).apply(game, source); } return true; } diff --git a/Mage.Sets/src/mage/cards/s/StalkingVampire.java b/Mage.Sets/src/mage/cards/s/StalkingVampire.java index 888b4567855..66065d0fbac 100644 --- a/Mage.Sets/src/mage/cards/s/StalkingVampire.java +++ b/Mage.Sets/src/mage/cards/s/StalkingVampire.java @@ -1,15 +1,23 @@ - package mage.cards.s; import mage.MageInt; -import mage.abilities.condition.common.TransformedCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.costs.Cost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; /** * @author nantuko @@ -17,7 +25,7 @@ import java.util.UUID; public final class StalkingVampire extends CardImpl { public StalkingVampire(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},""); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); this.subtype.add(SubType.VAMPIRE); this.color.setBlack(true); @@ -28,7 +36,7 @@ public final class StalkingVampire extends CardImpl { this.toughness = new MageInt(5); // At the beginning of your upkeep, you may pay {2}{B}{B}. If you do, transform Stalking Vampire. - this.addAbility(new ConditionalTriggeredAbility(new ScreechingBatBeginningOfUpkeepTriggeredAbility(), new TransformedCondition(), "")); + this.addAbility(new StalkingVampireBeginningOfUpkeepTriggeredAbility()); } private StalkingVampire(final StalkingVampire card) { @@ -40,3 +48,65 @@ public final class StalkingVampire extends CardImpl { return new StalkingVampire(this); } } + +class StalkingVampireBeginningOfUpkeepTriggeredAbility extends TriggeredAbilityImpl { + + public StalkingVampireBeginningOfUpkeepTriggeredAbility() { + super(Zone.BATTLEFIELD, new StalkingVampireTransformSourceEffect(), true); + } + + public StalkingVampireBeginningOfUpkeepTriggeredAbility(final StalkingVampireBeginningOfUpkeepTriggeredAbility ability) { + super(ability); + } + + @Override + public StalkingVampireBeginningOfUpkeepTriggeredAbility copy() { + return new StalkingVampireBeginningOfUpkeepTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.UPKEEP_STEP_PRE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.getPlayerId().equals(this.controllerId); + } + + @Override + public String getRule() { + return "At the beginning of your upkeep, you may pay {2}{B}{B}. If you do, transform {this}."; + } +} + +class StalkingVampireTransformSourceEffect extends OneShotEffect { + + public StalkingVampireTransformSourceEffect() { + super(Outcome.Transform); + staticText = "transform {this}"; + } + + public StalkingVampireTransformSourceEffect(final StalkingVampireTransformSourceEffect effect) { + super(effect); + } + + @Override + public StalkingVampireTransformSourceEffect copy() { + return new StalkingVampireTransformSourceEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getSourceId()); + if (permanent != null) { + Cost cost = new ManaCostsImpl("{2}{B}{B}"); + if (cost.pay(source, game, source, permanent.getControllerId(), false, null)) { + new TransformSourceEffect(false).apply(game, source); + } + return true; + } + return false; + } + +} From 8b50f24afbaf942a0793828216a676d32c75aadf Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 14 Sep 2021 01:03:15 +0400 Subject: [PATCH 088/231] * The Deck of Many Things - fixed that it can choose card without random on 1-9 (#8254); --- Mage.Sets/src/mage/cards/t/TheDeckOfManyThings.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/t/TheDeckOfManyThings.java b/Mage.Sets/src/mage/cards/t/TheDeckOfManyThings.java index 074542096b4..9d65ea25439 100644 --- a/Mage.Sets/src/mage/cards/t/TheDeckOfManyThings.java +++ b/Mage.Sets/src/mage/cards/t/TheDeckOfManyThings.java @@ -114,7 +114,8 @@ class TheDeckOfManyThingsRandomEffect extends OneShotEffect { TargetCard target = new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD); target.setRandom(true); target.setNotTarget(true); - player.chooseTarget(outcome, target, source, game); + target.chooseTarget(outcome, player.getId(), source, game); + Card card = game.getCard(target.getFirstTarget()); return card != null && player.moveCards(card, Zone.HAND, source, game); } From c7c0c479ad1fe4afb209f87ea1cc15e156e39a76 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Mon, 13 Sep 2021 19:35:18 -0500 Subject: [PATCH 089/231] [MID] Implemented Florian, Voldaren Scion --- .../mage/cards/f/FlorianVoldarenScion.java | 102 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 103 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FlorianVoldarenScion.java diff --git a/Mage.Sets/src/mage/cards/f/FlorianVoldarenScion.java b/Mage.Sets/src/mage/cards/f/FlorianVoldarenScion.java new file mode 100644 index 00000000000..e9c9c7355cd --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FlorianVoldarenScion.java @@ -0,0 +1,102 @@ +package mage.cards.f; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfPostCombatMainTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; +import mage.cards.*; +import mage.constants.*; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.watchers.common.PlayerLostLifeWatcher; + +/** + * + * @author weirddan455 + */ +public final class FlorianVoldarenScion extends CardImpl { + + public FlorianVoldarenScion(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}{R}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.VAMPIRE); + this.subtype.add(SubType.NOBLE); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // First strike + this.addAbility(FirstStrikeAbility.getInstance()); + + // At the beginning of your postcombat main phase, look at the top X cards of your library, where X is the total amount of life your opponents lost this turn. + // Exile one of those cards and put the rest on the bottom of your library in a random order. You may play the exiled card this turn. + this.addAbility(new BeginningOfPostCombatMainTriggeredAbility(new FlorianVoldarenScionEffect(), TargetController.YOU, false)); + } + + private FlorianVoldarenScion(final FlorianVoldarenScion card) { + super(card); + } + + @Override + public FlorianVoldarenScion copy() { + return new FlorianVoldarenScion(this); + } +} + +class FlorianVoldarenScionEffect extends OneShotEffect { + + public FlorianVoldarenScionEffect() { + super(Outcome.Benefit); + staticText = "look at the top X cards of your library, where X is the total amount of life your opponents lost this turn. " + + "Exile one of those cards and put the rest on the bottom of your library in a random order. You may play the exiled card this turn"; + } + + private FlorianVoldarenScionEffect(final FlorianVoldarenScionEffect effect) { + super(effect); + } + + @Override + public FlorianVoldarenScionEffect copy() { + return new FlorianVoldarenScionEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + PlayerLostLifeWatcher watcher = game.getState().getWatcher(PlayerLostLifeWatcher.class); + if (controller != null && watcher != null) { + int lifeLost = watcher.getAllOppLifeLost(controller.getId(), game); + if (lifeLost > 0) { + Cards cards = new CardsImpl(controller.getLibrary().getTopCards(game, lifeLost)); + int numCards = cards.size(); + if (numCards > 0) { + controller.lookAtCards(source, null, cards, game); + Card selectedCard; + if (numCards == 1) { + selectedCard = game.getCard(cards.iterator().next()); + } else { + TargetCard target = new TargetCard(Zone.LIBRARY, StaticFilters.FILTER_CARD); + controller.chooseTarget(outcome, cards, target, source, game); + selectedCard = game.getCard(target.getFirstTarget()); + } + if (selectedCard != null) { + cards.remove(selectedCard); + PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile( + game, source, selectedCard, TargetController.YOU, Duration.EndOfTurn, false, false, false + ); + } + if (!cards.isEmpty()) { + controller.putCardsOnBottomOfLibrary(cards, game, source, false); + } + return true; + } + } + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 0b7f79bcf2c..8b186f6fc2c 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -141,6 +141,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Flare of Faith", 19, Rarity.COMMON, mage.cards.f.FlareOfFaith.class)); cards.add(new SetCardInfo("Fleshtaker", 222, Rarity.UNCOMMON, mage.cards.f.Fleshtaker.class)); cards.add(new SetCardInfo("Flip the Switch", 54, Rarity.COMMON, mage.cards.f.FlipTheSwitch.class)); + cards.add(new SetCardInfo("Florian, Voldaren Scion", 223, Rarity.RARE, mage.cards.f.FlorianVoldarenScion.class)); cards.add(new SetCardInfo("Forest", 276, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Foul Play", 101, Rarity.UNCOMMON, mage.cards.f.FoulPlay.class)); cards.add(new SetCardInfo("Frenzied Trapbreaker", 190, Rarity.UNCOMMON, mage.cards.f.FrenziedTrapbreaker.class)); From dcfdea0a382e1f98736861ddd0056e421bd4eb29 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 14 Sep 2021 08:45:21 -0400 Subject: [PATCH 090/231] [MIC] updated spoiler and reprints --- .../src/mage/sets/MidnightHuntCommander.java | 1 + Utils/mtg-cards-data.txt | 35 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index acee3f4116b..76a3bfb4f74 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -20,5 +20,6 @@ public final class MidnightHuntCommander extends ExpansionSet { this.hasBasicLands = false; cards.add(new SetCardInfo("Avacyn's Memorial", 31, Rarity.MYTHIC, mage.cards.a.AvacynsMemorial.class)); + cards.add(new SetCardInfo("Zombie Apocalypse", 131, Rarity.RARE, mage.cards.z.ZombieApocalypse.class)); } } diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index c088858c8a5..df9124fd748 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -42866,12 +42866,41 @@ Zabaz, the Glimmerwasp|Jumpstart: Historic Horizons|762|R|{1}|Legendary Artifact Khalni Garden|Jumpstart: Historic Horizons|770|C||Land|||Khalni Garden enters the battlefield tapped.$When Khalni Garden enters the battlefield, create a 0/1 green Plant creature token.${T}: Add {G}.| Sliver Hive|Jumpstart: Historic Horizons|776|R||Land|||{T}: Add {C}.${T}: Add one mana of any color. Spend this mana only to cast a Sliver spell.${5}, {T}: Create a 1/1 colorless Sliver creature token. Activate only if you control a Sliver.| Leinore, Autumn Sovereign|Midnight Hunt Commander|1|M|{2}{G}{W}|Legendary Creature - Human Noble|0|4|Coven — At the beginning of combat on your turn, put a +1/+1 counter on up to one target creature you control. Then if you control three or more creatures with different powers, draw a card.| -Wilhelt, the Rotcleaver|Midnight Hunt Commander|2|M|{2}{U}{B}|Legendary Creature - Zombie Warrior|3|3|Whenever another zombie you control dies, if it didn't have decayed, create a 2/2 black Zombie creature token with decayed.$At the beginning of your end step, you may sacrifice a Zombie. If you do, draw a card.| +Wilhelt, the Rotcleaver|Midnight Hunt Commander|2|M|{2}{U}{B}|Legendary Creature - Zombie Warrior|3|3|Whenever another Zombie you control dies, if it didn't have decayed, create a 2/2 black Zombie creature token with decayed.$At the beginning of your end step, you may sacrifice a Zombie. If you do, draw a card.| +Eloise, Nephalia Sleuth|Midnight Hunt Commander|3|M|{3}{U}{B}|Legendary Creature - Human Rogue|4|4|Whenever another creature you control dies, investigate.$Whenever you sacrifice a token, surveil 1.| +Kyler, Sigardian Emissary|Midnight Hunt Commander|4|M|{3}{G}{W}|Legendary Creature - Human Cleric|2|2|Whenever another Human enters the battlefield under your control, put a +1/+1 counter on Kyler, Sigardian Emissary.$Other humans you control get +1/+1 for each counter on Kyler, Sigardian Emissary.| +Celestial Judgment|Midnight Hunt Commander|5|R|{4}{W}{W}|Sorcery|||For each different power among creatures on the battlefield, choose a creature with that power. Destroy each creature not chosen this way.| +Curse of Conformity|Midnight Hunt Commander|6|R|{4}{W}|Enchantment - Aura Curse|||Enchant player$Nonlegendary creatures enchanted player controls have base power and toughness 3/3 and lose all creature types.| +Moorland Rescuer|Midnight Hunt Commander|7|R|{5}{W}|Creature - Human Knight|4|4|When Moorland Rescuer dies, return any number of other creature cards with total power X or less from your graveyard to the battlefield, where X is Moorland Rescuer's power. Exile Moorland Rescuer.| +Sigarda's Vanguard|Midnight Hunt Commander|8|R|{4}{W}|Creature - Angel|3|3|Flash$Flying$Whenever Sigarda's Vanguard enters the battlefield or attacks, choose any number of creatures with different powers. Those creatures gain double strike until end of turn.| +Stalwart Pathlighter|Midnight Hunt Commander|9|R|{2}{W}|Creature - Human Soldier|3|1|Vigilance$Coven — At the beginning of combat on your turn, if you control three or more creatures with different powers, creatures you control gain indestructible until end of turn.| +Wall of Mourning|Midnight Hunt Commander|10|R|{1}{W}|Creature - Wall|0|4|Defender$When Wall of Mourning enters the battlefield, exile a card from the top of your library face down for each opponent you have.$Coven — At the beginning of your end step, if you control three or more creatures with different powers, put a card exiled with Wall of Mourning into its owner's hand.| +Cleaver Skaab|Midnight Hunt Commander|11|R|{3}{U}|Creature - Zombie Horror|2|4|{3}, {T}, Sacrifice another Zombie: Create two tokens that are copies of the sacrificed creature.| +Curse of Unbinding|Midnight Hunt Commander|12|R|{6}{U}|Enchantment - Aura Curse|||Enchant player$At the beginning of enchanted player's upkeep, that player reveals cards from the top of their library until they reveal a creature card. Put that card onto the battlefield under your control. That player puts the rest of the revealed cards into their graveyard.| +Drown in Dreams|Midnight Hunt Commander|13|R|{X}{2}{U}|Instant|||Choose one. If you control a commander as you cast this spell, you may choose both.$• Target player draws X cards.$• Target player mills twice X cards.| +Empty the Laboratory|Midnight Hunt Commander|14|R|{X}{U}{U}|Sorcery|||Sacrifice X Zombies, then reveal cards from the top of your library until you reveal a number of Zombie creature cards equal to the number of Zombies sacrificed this way. Put those cards onto the battlefield and the rest on the bottom of your library in a random order.| +Hordewing Skaab|Midnight Hunt Commander|15|R|{4}{U}|Creature - Zombie Horror|3|3|Flying$Other Zombies you control have flying.$Whenever one or more Zombies you control deal combat damage to one or more of your opponents, you may draw cards equal to the number of opponents dealt damage this way. If you do, discard that many cards.| +Shadow Kin|Midnight Hunt Commander|16|R|{3}{U}|Creature - Shapeshifter|2|2|Flash$At the beginning of your upkeep, each player mills three cards. You may exile a creature card from among the cards milled this way. If you do, Shadow Kin becomes a copy of that card, except it has this ability.| +Crowded Crypt|Midnight Hunt Commander|17|R|{2}{B}|Artifact|||{T}: Add {B}.$Whenever a creature you control dies, put a corpse counter on Crowded Crypt.${4}{B}{B}, {T}, Sacrifice Crowded Crypt: Create a 2/2 black Zombie creature token with decayed for each corpse counter on Crowded Crypt.| +Curse of the Restless Dead|Midnight Hunt Commander|18|R|{2}{B}|Enchantment - Aura Curse|||Enchant player$Whenever a land enters the battlefield under enchanted player's control, you create a 2/2 black Zombie creature token with decayed.| +Ghouls' Night Out|Midnight Hunt Commander|19|R|{3}{B}{B}|Sorcery|||For each player, choose a creature card in that player's graveyard. Put those cards onto the battlefield under your control. They're black Zombies in addition to their other colors and types and they gain decayed.| +Gorex, the Tombshell|Midnight Hunt Commander|20|R|{6}{B}{B}|Legendary Creature - Zombie Turtle|4|4|As an additional cost to cast this spell, you may exile a number of creature cards from your graveyard. This spell costs {2} less to cast for each card exiled this way.$Deathtouch$Whenever Gorex, the Tombshell attacks or dies, choose a card at random exiled with Gorex and put that card into its owner's hand.| +Prowling Geistcatcher|Midnight Hunt Commander|21|R|{3}{B}|Creature - Human Rogue|2|4|Whenever you sacrifice another creature, exile it. If that creature was a token, put a +1/+1 counter on Prowling Geistcatcher.$When Prowling Geistcatcher leaves the battlefield, return each card exiled with it to the battlefield under your control.| +Ravenous Rotbelly|Midnight Hunt Commander|22|R|{4}{B}|Creature - Zombie Horror|4|5|When Ravenous Rotbelly enters the battlefield, you may sacrifice up to three Zombies. When you sacrifice one or more Zombies this way, each opponent sacrifices that many creatures.| +Tomb Tyrant|Midnight Hunt Commander|23|R|{3}{B}|Creature - Zombie Noble|3|3|Other Zombies you control get +1/+1.${2}{B}, {T}, Sacrifice a creature: Return a Zombie creature card at random from your graveyard to the battlefield. Activate only during your turn and only if there are at least three Zombie creature cards in your graveyard.| +Celebrate the Harvest|Midnight Hunt Commander|24|R|{3}{G}|Sorcery|||Search your library for up to X basic land cards, where X is the number of different powers among creatures you control. Put those cards onto the battlefield tapped, then shuffle.| +Curse of Clinging Webs|Midnight Hunt Commander|25|R|{2}{G}|Enchantment - Aura Curse|||Enchant player$Whenever a nontoken creature enchanted player controls dies, exile it and you create a 1/2 green Spider creature token with reach.| +Heronblade Elite|Midnight Hunt Commander|26|R|{2}{G}|Creature - Human Warrior|1|1|Vigilance$Whenever another Human enters the battlefield under your control, put a +1/+1 counter on Heronblade Elite.${T}: Add X mana of any one color, where X is Heronblade Elite's power.| +Kurbis, Harvest Celebrant|Midnight Hunt Commander|27|U|{X}{G}{G}|Legendary Creature - Treefolk|0|0|Kurbis, Harvest Celebrant enters the battlefield with a number of +1/+1 counters on it equal to the amount of mana spent to cast it.$Remove a +1/+1 counter from Kurbis: Prevent all damage that would be dealt this turn to another target creature with a +1/+1 counter on it.| +Ruinous Intrusion|Midnight Hunt Commander|28|R|{3}{G}|Instant|||Exile target artifact or enchantment. Put X +1/+1 counters on target creature you control, where X is the mana value of the permanent exiled this way.| +Siguardian Zealout|Midnight Hunt Commander|29|R|{4}{G}|Creature - Human Cleric|3|3|At the beginning of combat on your turn, choose any number of creatures with different powers. Each of them gets +X/+X and gains vigilance until end of turn, where X is Siguardian Zealout's power.| +Somberwald Beastmaster|Midnight Hunt Commander|30|R|{6}{G}|Creature - Human Ranger|1|1|When Somberwald Beastmaster enters the battlefield, create a 2/2 green Wolf creature token, a 3/3 green Beast creature token, and a 4/4 green Beast creature token.$Creature tokens you control have deathtouch.| Avacyn's Memorial|Midnight Hunt Commander|31|M|{5}{W}{W}{W}|Legendary Artifact|||Indestructible$Other legendary permanents you control have indestructible.| Visions of Glory|Midnight Hunt Commander|32|R|{4}{W}|Sorcery|||Create a 1/1 white Human creature token for each creature you control.$Flashback {8}{W}{W}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| Visions of Duplicity|Midnight Hunt Commander|33|R|{2}{U}|Sorcery|||Exchange control of two target creatures you don't control.$Flashback {8}{U}{U}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| -Visions of Dread|Midnight Hunt Commander|34|R|{2}{B}|Sorcery|||Target opponent puts a creature card of choice from their graveyard onto the battlefield under your control.$Flashback {8}{B}{B}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| +Visions of Dread|Midnight Hunt Commander|34|R|{2}{B}|Sorcery|||Target opponent puts a creature card of their choice from their graveyard onto the battlefield under your control.$Flashback {8}{B}{B}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| Curse of Obsession|Midnight Hunt Commander|35|R|{4}{R}|Enchantment - Aura Curse|||Enchant player$At the beginning of enchanted player's draw step, that player draws two additional cards.$At the beginning of enchanted player's end step, that player discards their hand.| Visions of Ruin|Midnight Hunt Commander|36|R|{3}{R}|Sorcery|||Each opponent sacrifices an artifact. For each artifact sacrificed this way, you create a Treasure token.$Flashback {8}{R}{R}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| Visions of Dominance|Midnight Hunt Commander|37|R|{2}{G}|Sorcery|||Put a +1/+1 counter on target creature, then double the number of +1/+1 counters on it.$Flashback {8}{G}{G}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| -Lynde, Cheerful Tormenter|Midnight Hunt Commander|38|M|{1}{U}{B}{R}|Legendary Creature - Human Warlock|2|4|Deathtouch$Whenever a Curse is put into your graveyard from the battlefield, return it to the battlefield attached to you at the beginning of the next end step.$At the beginning of your upkeep, you may attach a Curse attached to you to one of your opponents. If you do, draw two cards.| +Lynde, Cheerful Tormentor|Midnight Hunt Commander|38|M|{1}{U}{B}{R}|Legendary Creature - Human Warlock|2|4|Deathtouch$Whenever a Curse is put into your graveyard from the battlefield, return it to the battlefield attached to you at the beginning of the next end step.$At the beginning of your upkeep, you may attach a Curse attached to you to one of your opponents. If you do, draw two cards.| +Zombie Apocalypse|Midnight Hunt Commander|131|R|{3}{B}{B}{B}|Sorcery|||Return all Zombie creature cards from your graveyard to the battlefield tapped, then destroy all Humans.| From 2ab6ec8a1867d20a0413b2cd82b5e676540e450d Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 14 Sep 2021 09:02:17 -0400 Subject: [PATCH 091/231] [MIC] Implemented Wilhelt, the Rotcleaver --- .../mage/cards/w/WilheltTheRotcleaver.java | 71 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 72 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WilheltTheRotcleaver.java diff --git a/Mage.Sets/src/mage/cards/w/WilheltTheRotcleaver.java b/Mage.Sets/src/mage/cards/w/WilheltTheRotcleaver.java new file mode 100644 index 00000000000..19506338b12 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WilheltTheRotcleaver.java @@ -0,0 +1,71 @@ +package mage.cards.w; + +import mage.MageInt; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.DecayedAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.TargetController; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.permanent.token.ZombieDecayedToken; +import mage.target.common.TargetControlledPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WilheltTheRotcleaver extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledPermanent(SubType.ZOMBIE); + private static final FilterControlledPermanent filter2 + = new FilterControlledPermanent(SubType.ZOMBIE, "a Zombie"); + + static { + filter.add(Predicates.not(new AbilityPredicate(DecayedAbility.class))); + } + + public WilheltTheRotcleaver(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Whenever another Zombie you control dies, if it didn't have decayed, create a 2/2 black Zombie creature token with decayed. + this.addAbility(new DiesCreatureTriggeredAbility( + new CreateTokenEffect(new ZombieDecayedToken()) + .concatBy(", if it didn't have decayed, "), + false, filter + )); + + // At the beginning of your end step, you may sacrifice a Zombie. If you do, draw a card. + this.addAbility(new BeginningOfEndStepTriggeredAbility(new DoIfCostPaid( + new DrawCardSourceControllerEffect(1), + new SacrificeTargetCost(new TargetControlledPermanent(filter2)) + ), TargetController.YOU, false)); + } + + private WilheltTheRotcleaver(final WilheltTheRotcleaver card) { + super(card); + } + + @Override + public WilheltTheRotcleaver copy() { + return new WilheltTheRotcleaver(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 76a3bfb4f74..19ab73e48bd 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -20,6 +20,7 @@ public final class MidnightHuntCommander extends ExpansionSet { this.hasBasicLands = false; cards.add(new SetCardInfo("Avacyn's Memorial", 31, Rarity.MYTHIC, mage.cards.a.AvacynsMemorial.class)); + cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); cards.add(new SetCardInfo("Zombie Apocalypse", 131, Rarity.RARE, mage.cards.z.ZombieApocalypse.class)); } } From 45a6e7ec50d0b7c216eb96442f7b10500272debf Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 14 Sep 2021 09:14:10 -0400 Subject: [PATCH 092/231] [MIC] Implemented Leinore, Autumn Sovereign --- .../mage/cards/l/LeinoreAutumnSovereign.java | 54 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 55 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LeinoreAutumnSovereign.java diff --git a/Mage.Sets/src/mage/cards/l/LeinoreAutumnSovereign.java b/Mage.Sets/src/mage/cards/l/LeinoreAutumnSovereign.java new file mode 100644 index 00000000000..3621947141f --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LeinoreAutumnSovereign.java @@ -0,0 +1,54 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfCombatTriggeredAbility; +import mage.abilities.condition.common.CovenCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.hint.common.CovenHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LeinoreAutumnSovereign extends CardImpl { + + public LeinoreAutumnSovereign(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}{W}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.NOBLE); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + + // Coven — At the beginning of combat on your turn, put a +1/+1 counter on up to one target creature you control. Then if you control three or more creatures with different powers, draw a card. + Ability ability = new BeginningOfCombatTriggeredAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance()), + TargetController.YOU, false + ); + ability.addEffect(new ConditionalOneShotEffect( + new DrawCardSourceControllerEffect(1), CovenCondition.instance, + "Then if you control three or more creatures with different powers, draw a card" + )); + ability.addTarget(new TargetControlledCreaturePermanent(0, 1)); + this.addAbility(ability.addHint(CovenHint.instance).setAbilityWord(AbilityWord.COVEN)); + } + + private LeinoreAutumnSovereign(final LeinoreAutumnSovereign card) { + super(card); + } + + @Override + public LeinoreAutumnSovereign copy() { + return new LeinoreAutumnSovereign(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 19ab73e48bd..b4bbb82f663 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -20,6 +20,7 @@ public final class MidnightHuntCommander extends ExpansionSet { this.hasBasicLands = false; cards.add(new SetCardInfo("Avacyn's Memorial", 31, Rarity.MYTHIC, mage.cards.a.AvacynsMemorial.class)); + cards.add(new SetCardInfo("Leinore, Autumn Sovereign", 1, Rarity.MYTHIC, mage.cards.l.LeinoreAutumnSovereign.class)); cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); cards.add(new SetCardInfo("Zombie Apocalypse", 131, Rarity.RARE, mage.cards.z.ZombieApocalypse.class)); } From b58f7dbfc47400af4d63e6c3748a70f644a755e1 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 14 Sep 2021 09:28:59 -0400 Subject: [PATCH 093/231] [MIC] Implemented Kyler, Sigardian Emissary --- .../mage/cards/k/KylerSigardianEmissary.java | 123 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 124 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KylerSigardianEmissary.java diff --git a/Mage.Sets/src/mage/cards/k/KylerSigardianEmissary.java b/Mage.Sets/src/mage/cards/k/KylerSigardianEmissary.java new file mode 100644 index 00000000000..cac51bcf28e --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KylerSigardianEmissary.java @@ -0,0 +1,123 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.continuous.BoostAllEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.Hint; +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.Counter; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KylerSigardianEmissary extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledPermanent(SubType.HUMAN, "another Human"); + private static final FilterCreaturePermanent filter2 = new FilterCreaturePermanent(SubType.HUMAN, ""); + + static { + filter.add(AnotherPredicate.instance); + filter2.add(AnotherPredicate.instance); + } + + public KylerSigardianEmissary(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}{W}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Whenever another Human enters the battlefield under your control, put a +1/+1 counter on Kyler, Sigardian Emissary. + this.addAbility(new EntersBattlefieldControlledTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), filter + )); + + // Other Humans you control get +1/+1 for each counter on Kyler, Sigardian Emissary. + this.addAbility(new SimpleStaticAbility(new BoostAllEffect( + KylerSigardianEmissaryValue.instance, KylerSigardianEmissaryValue.instance, + Duration.WhileOnBattlefield, filter2, true, + "Other Humans you control get +1/+1 for each counter on {this}" + )).addHint(KylerSigardianEmissaryHint.instance)); + } + + private KylerSigardianEmissary(final KylerSigardianEmissary card) { + super(card); + } + + @Override + public KylerSigardianEmissary copy() { + return new KylerSigardianEmissary(this); + } +} + +enum KylerSigardianEmissaryValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + Permanent permanent = sourceAbility.getSourcePermanentIfItStillExists(game); + return permanent != null + ? permanent + .getCounters(game) + .values() + .stream() + .mapToInt(Counter::getCount) + .sum() : 0; + } + + @Override + public KylerSigardianEmissaryValue copy() { + return this; + } + + @Override + public String getMessage() { + return ""; + } +} + +enum KylerSigardianEmissaryHint implements Hint { + instance; + + @Override + public String getText(Game game, Ability ability) { + Permanent permanent = ability.getSourcePermanentIfItStillExists(game); + if (permanent == null) { + return null; + } + return "Counters on " + permanent.getName() + ": " + + permanent + .getCounters(game) + .values() + .stream() + .mapToInt(Counter::getCount) + .sum(); + } + + @Override + public KylerSigardianEmissaryHint copy() { + return this; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index b4bbb82f663..322f2910b5a 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -20,6 +20,7 @@ public final class MidnightHuntCommander extends ExpansionSet { this.hasBasicLands = false; cards.add(new SetCardInfo("Avacyn's Memorial", 31, Rarity.MYTHIC, mage.cards.a.AvacynsMemorial.class)); + cards.add(new SetCardInfo("Kyler, Sigardian Emissary", 4, Rarity.MYTHIC, mage.cards.k.KylerSigardianEmissary.class)); cards.add(new SetCardInfo("Leinore, Autumn Sovereign", 1, Rarity.MYTHIC, mage.cards.l.LeinoreAutumnSovereign.class)); cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); cards.add(new SetCardInfo("Zombie Apocalypse", 131, Rarity.RARE, mage.cards.z.ZombieApocalypse.class)); From c935520570e959b3c3d2b7611cdb00095e83c061 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 14 Sep 2021 09:38:28 -0400 Subject: [PATCH 094/231] [MIC] Implemented Ravenous Rotbelly --- .../src/mage/cards/r/RavenousRotbelly.java | 95 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 96 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RavenousRotbelly.java diff --git a/Mage.Sets/src/mage/cards/r/RavenousRotbelly.java b/Mage.Sets/src/mage/cards/r/RavenousRotbelly.java new file mode 100644 index 00000000000..0c90d344ad4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RavenousRotbelly.java @@ -0,0 +1,95 @@ +package mage.cards.r; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.SacrificeOpponentsEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RavenousRotbelly extends CardImpl { + + public RavenousRotbelly(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{B}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.HORROR); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // When Ravenous Rotbelly enters the battlefield, you may sacrifice up to three Zombies. When you sacrifice one or more Zombies this way, each opponent sacrifices that many creatures. + this.addAbility(new EntersBattlefieldTriggeredAbility(new RavenousRotbellyEffect())); + } + + private RavenousRotbelly(final RavenousRotbelly card) { + super(card); + } + + @Override + public RavenousRotbelly copy() { + return new RavenousRotbelly(this); + } +} + +class RavenousRotbellyEffect extends OneShotEffect { + + private static final FilterPermanent filter + = new FilterControlledPermanent(SubType.ZOMBIE, "Zombies you control"); + + RavenousRotbellyEffect() { + super(Outcome.Benefit); + staticText = "you may sacrifice up to three Zombies. When you sacrifice " + + "one or more Zombies this way, each opponent sacrifices that many creatures"; + } + + private RavenousRotbellyEffect(final RavenousRotbellyEffect effect) { + super(effect); + } + + @Override + public RavenousRotbellyEffect copy() { + return new RavenousRotbellyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetPermanent target = new TargetPermanent(0, 3, filter, true); + player.choose(outcome, target, source.getSourceId(), game); + int amount = 0; + for (UUID permanentId : target.getTargets()) { + Permanent permanent = game.getPermanent(permanentId); + if (permanent != null && permanent.sacrifice(source, game)) { + amount++; + } + } + if (amount < 1) { + return false; + } + game.fireReflexiveTriggeredAbility(new ReflexiveTriggeredAbility( + new SacrificeOpponentsEffect(amount, StaticFilters.FILTER_PERMANENT_CREATURES), + false, "each opponent sacrifices that many creatures" + ), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 322f2910b5a..eaeecae3ae5 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -22,6 +22,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Avacyn's Memorial", 31, Rarity.MYTHIC, mage.cards.a.AvacynsMemorial.class)); cards.add(new SetCardInfo("Kyler, Sigardian Emissary", 4, Rarity.MYTHIC, mage.cards.k.KylerSigardianEmissary.class)); cards.add(new SetCardInfo("Leinore, Autumn Sovereign", 1, Rarity.MYTHIC, mage.cards.l.LeinoreAutumnSovereign.class)); + cards.add(new SetCardInfo("Ravenous Rotbelly", 22, Rarity.RARE, mage.cards.r.RavenousRotbelly.class)); cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); cards.add(new SetCardInfo("Zombie Apocalypse", 131, Rarity.RARE, mage.cards.z.ZombieApocalypse.class)); } From dbd4b32e6bd2cf10846f9ae00c2e7314e664090c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 14 Sep 2021 09:43:36 -0400 Subject: [PATCH 095/231] [MIC] Implemented Somberwald Beastmaster --- .../mage/cards/s/SomberwaldBeastmaster.java | 55 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 56 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SomberwaldBeastmaster.java diff --git a/Mage.Sets/src/mage/cards/s/SomberwaldBeastmaster.java b/Mage.Sets/src/mage/cards/s/SomberwaldBeastmaster.java new file mode 100644 index 00000000000..f9e1d8fb970 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SomberwaldBeastmaster.java @@ -0,0 +1,55 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.permanent.token.BeastToken; +import mage.game.permanent.token.BeastToken2; +import mage.game.permanent.token.WolfToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SomberwaldBeastmaster extends CardImpl { + + public SomberwaldBeastmaster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{6}{G}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.RANGER); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // When Somberwald Beastmaster enters the battlefield, create a 2/2 green Wolf creature token, a 3/3 green Beast creature token, and a 4/4 green Beast creature token. + Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new WolfToken())); + ability.addEffect(new CreateTokenEffect(new BeastToken()).setText(", a 3/3 green Beast creature token")); + ability.addEffect(new CreateTokenEffect(new BeastToken2()).setText(", and a 4/4 green Beast creature token")); + this.addAbility(ability); + + // Creature tokens you control have deathtouch. + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( + DeathtouchAbility.getInstance(), Duration.WhileOnBattlefield, StaticFilters.FILTER_CREATURE_TOKENS + ))); + } + + private SomberwaldBeastmaster(final SomberwaldBeastmaster card) { + super(card); + } + + @Override + public SomberwaldBeastmaster copy() { + return new SomberwaldBeastmaster(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index eaeecae3ae5..719feda40ed 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -23,6 +23,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Kyler, Sigardian Emissary", 4, Rarity.MYTHIC, mage.cards.k.KylerSigardianEmissary.class)); cards.add(new SetCardInfo("Leinore, Autumn Sovereign", 1, Rarity.MYTHIC, mage.cards.l.LeinoreAutumnSovereign.class)); cards.add(new SetCardInfo("Ravenous Rotbelly", 22, Rarity.RARE, mage.cards.r.RavenousRotbelly.class)); + cards.add(new SetCardInfo("Somberwald Beastmaster", 30, Rarity.RARE, mage.cards.s.SomberwaldBeastmaster.class)); cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); cards.add(new SetCardInfo("Zombie Apocalypse", 131, Rarity.RARE, mage.cards.z.ZombieApocalypse.class)); } From 1b5ad66ae58fcd8831c751504aeb6c673789c32c Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Tue, 14 Sep 2021 18:17:22 -0500 Subject: [PATCH 096/231] [MID] Implemented Light Up the Night --- .../src/mage/cards/l/LightUpTheNight.java | 89 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../RemoveVariableCountersTargetCost.java | 21 ++++- 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/l/LightUpTheNight.java diff --git a/Mage.Sets/src/mage/cards/l/LightUpTheNight.java b/Mage.Sets/src/mage/cards/l/LightUpTheNight.java new file mode 100644 index 00000000000..29fb8a01c09 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LightUpTheNight.java @@ -0,0 +1,89 @@ +package mage.cards.l; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.costs.common.RemoveVariableCountersTargetCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetAnyTarget; + +/** + * + * @author weirddan455 + */ +public final class LightUpTheNight extends CardImpl { + + public LightUpTheNight(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}"); + + // Light Up the Night deals X damage to any target. It deals X plus 1 damage instead if that target is a creature or planeswalker. + this.getSpellAbility().addEffect(new LightUpTheNightEffect()); + this.getSpellAbility().addTarget(new TargetAnyTarget()); + + // Flashback—{3}{R}, Remove X loyalty counters from among planeswalkers you control. If you cast this spell this way, X can't be 0. + Ability ability = new FlashbackAbility(this, new ManaCostsImpl<>("{3}{R}")); + ability.addCost(new RemoveVariableCountersTargetCost( + StaticFilters.FILTER_CONTROLLED_PERMANENT_PLANESWALKER, CounterType.LOYALTY, "X", 1, + "Remove X loyalty counters from among planeswalkers you control. If you cast this spell this way, X can't be 0" + )); + this.addAbility(ability); + } + + private LightUpTheNight(final LightUpTheNight card) { + super(card); + } + + @Override + public LightUpTheNight copy() { + return new LightUpTheNight(this); + } +} + +class LightUpTheNightEffect extends OneShotEffect { + + public LightUpTheNightEffect() { + super(Outcome.Damage); + staticText = "{this} deals X damage to any target. It deals X plus 1 damage instead if that target is a creature or planeswalker"; + } + + private LightUpTheNightEffect(final LightUpTheNightEffect effect) { + super(effect); + } + + @Override + public LightUpTheNightEffect copy() { + return new LightUpTheNightEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + // Normal cast + int damage = source.getManaCostsToPay().getX(); + // Flashback cast + damage += GetXValue.instance.calculate(game, source, this); + UUID targetId = source.getFirstTarget(); + Player player = game.getPlayer(targetId); + if (player != null) { + player.damage(damage, source.getSourceId(), source, game); + return true; + } + Permanent permanent = game.getPermanent(targetId); + if (permanent != null) { + permanent.damage(damage + 1, source.getSourceId(), source, game); + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 8b186f6fc2c..4280787eb0e 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -187,6 +187,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Larder Zombie", 58, Rarity.COMMON, mage.cards.l.LarderZombie.class)); cards.add(new SetCardInfo("Lier, Disciple of the Drowned", 59, Rarity.MYTHIC, mage.cards.l.LierDiscipleOfTheDrowned.class)); cards.add(new SetCardInfo("Liesa, Forgotten Archangel", 232, Rarity.RARE, mage.cards.l.LiesaForgottenArchangel.class)); + cards.add(new SetCardInfo("Light Up the Night", 146, Rarity.RARE, mage.cards.l.LightUpTheNight.class)); cards.add(new SetCardInfo("Locked in the Cemetery", 60, Rarity.COMMON, mage.cards.l.LockedInTheCemetery.class)); cards.add(new SetCardInfo("Lord of the Ulvenwald", 231, Rarity.UNCOMMON, mage.cards.l.LordOfTheUlvenwald.class)); cards.add(new SetCardInfo("Loyal Gryff", 26, Rarity.UNCOMMON, mage.cards.l.LoyalGryff.class)); diff --git a/Mage/src/main/java/mage/abilities/costs/common/RemoveVariableCountersTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/RemoveVariableCountersTargetCost.java index 0b017a680e6..2bf8e091e15 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/RemoveVariableCountersTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/RemoveVariableCountersTargetCost.java @@ -31,16 +31,25 @@ public class RemoveVariableCountersTargetCost extends VariableCostImpl { } public RemoveVariableCountersTargetCost(FilterPermanent filter, CounterType counterTypeToRemove, String xText, int minValue) { + this(filter, counterTypeToRemove, xText, minValue, null); + } + + public RemoveVariableCountersTargetCost(FilterPermanent filter, CounterType counterTypeToRemove, String xText, int minValue, String text) { super(VariableCostType.NORMAL, xText, new StringBuilder(counterTypeToRemove != null ? counterTypeToRemove.getName() + ' ' : "").append("counters to remove").toString()); this.filter = filter; this.counterTypeToRemove = counterTypeToRemove; - this.text = setText(); + if (text != null && !text.isEmpty()) { + this.text = text; + } else { + this.text = setText(); + } this.minValue = minValue; } public RemoveVariableCountersTargetCost(final RemoveVariableCountersTargetCost cost) { super(cost); this.filter = cost.filter; + this.counterTypeToRemove = cost.counterTypeToRemove; this.minValue = cost.minValue; } @@ -63,6 +72,16 @@ public class RemoveVariableCountersTargetCost extends VariableCostImpl { return new RemoveVariableCountersTargetCost(this); } + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + return getMaxValue(source, game) >= minValue; + } + + @Override + public int getMinValue(Ability source, Game game) { + return minValue; + } + @Override public int getMaxValue(Ability source, Game game) { int maxValue = 0; From a5038f2409aff6c17259d5200b00c2a9c3b7b7f5 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Tue, 14 Sep 2021 18:48:04 -0500 Subject: [PATCH 097/231] [ALL] Sheltered Valley - Fixed intervening if ability (fixes #8265) --- Mage.Sets/src/mage/cards/s/ShelteredValley.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Mage.Sets/src/mage/cards/s/ShelteredValley.java b/Mage.Sets/src/mage/cards/s/ShelteredValley.java index a5721941a6e..51176f197a3 100644 --- a/Mage.Sets/src/mage/cards/s/ShelteredValley.java +++ b/Mage.Sets/src/mage/cards/s/ShelteredValley.java @@ -5,10 +5,9 @@ import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.Condition; import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; import mage.abilities.costs.common.SacrificeAllCost; -import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.Effect; import mage.abilities.effects.common.EnterBattlefieldPayCostOrPutGraveyardEffect; import mage.abilities.effects.common.GainLifeEffect; @@ -30,7 +29,7 @@ import mage.filter.predicate.mageobject.NamePredicate; public final class ShelteredValley extends CardImpl { private static final FilterPermanent filterShelteredValley = new FilterPermanent("permanent named Sheltered Valley"); - + static { filterShelteredValley.add(new NamePredicate("Sheltered Valley")); } @@ -43,13 +42,13 @@ public final class ShelteredValley extends CardImpl { effect.setText("If {this} would enter the battlefield, instead sacrifice each other permanent named {this} you control, then put {this} onto the battlefield."); Ability ability = new SimpleStaticAbility(Zone.ALL, effect); this.addAbility(ability); - + // At the beginning of your upkeep, if you control three or fewer lands, you gain 1 life. - Condition controls = new PermanentsOnTheBattlefieldCondition(StaticFilters.FILTER_LANDS, ComparisonType.FEWER_THAN, 4); - effect = new ConditionalOneShotEffect(new GainLifeEffect(1), controls); - effect.setText("if you control three or fewer lands, you gain 1 life"); - ability = new BeginningOfUpkeepTriggeredAbility(Zone.BATTLEFIELD, effect, TargetController.YOU, false); - this.addAbility(ability); + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new BeginningOfUpkeepTriggeredAbility(Zone.BATTLEFIELD, new GainLifeEffect(1), TargetController.YOU, false), + new PermanentsOnTheBattlefieldCondition(StaticFilters.FILTER_LANDS, ComparisonType.FEWER_THAN, 4), + "At the beginning of your upkeep, if you control three or fewer lands, you gain 1 life." + )); // {tap}: Add {C}. this.addAbility(new ColorlessManaAbility()); } From f901026af58e164b1a8c4701d72a99ebc1237b82 Mon Sep 17 00:00:00 2001 From: "Raphael \"who?!\" Kehldorfer" Date: Wed, 15 Sep 2021 03:48:10 +0200 Subject: [PATCH 098/231] [AFR] Fix Spare Dagger not dealing 1 damage if sacrificed (#8273) --- Mage.Sets/src/mage/cards/s/SpareDagger.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mage.Sets/src/mage/cards/s/SpareDagger.java b/Mage.Sets/src/mage/cards/s/SpareDagger.java index 2fef867de9a..42611a2ba0b 100644 --- a/Mage.Sets/src/mage/cards/s/SpareDagger.java +++ b/Mage.Sets/src/mage/cards/s/SpareDagger.java @@ -16,6 +16,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.game.Game; +import mage.target.common.TargetAnyTarget; import java.util.UUID; @@ -75,6 +76,8 @@ class SpareDaggerEffect extends GainAbilityWithAttachmentEffect { new DamageTargetEffect(1), false, "This creature deals 1 damage to any target" ); + ability.addTarget(new TargetAnyTarget()); + return new AttacksTriggeredAbility(new DoWhenCostPaid( ability, useAttachedCost.copy().setMageObjectReference(source, game), "Sacrifice " + sourceName + "?" From 46aa282218f013e4413096a514c1be0288f8784e Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 15 Sep 2021 06:47:37 -0400 Subject: [PATCH 099/231] [MIC] updated spoiler and reprints --- .../src/mage/sets/MidnightHuntCommander.java | 110 +++++++++++++++++ Utils/mtg-cards-data.txt | 116 +++++++++++++++++- 2 files changed, 223 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 719feda40ed..25caacc5f8f 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -19,12 +19,122 @@ public final class MidnightHuntCommander extends ExpansionSet { super("Midnight Hunt Commander", "MIC", ExpansionSet.buildDate(2021, 9, 24), SetType.SUPPLEMENTAL); this.hasBasicLands = false; + cards.add(new SetCardInfo("Abzan Falconer", 77, Rarity.UNCOMMON, mage.cards.a.AbzanFalconer.class)); + cards.add(new SetCardInfo("Aetherspouts", 97, Rarity.RARE, mage.cards.a.Aetherspouts.class)); + cards.add(new SetCardInfo("Ainok Bond-Kin", 78, Rarity.COMMON, mage.cards.a.AinokBondKin.class)); + cards.add(new SetCardInfo("Angel of Glory's Rise", 79, Rarity.RARE, mage.cards.a.AngelOfGlorysRise.class)); + cards.add(new SetCardInfo("Arcane Signet", 157, Rarity.COMMON, mage.cards.a.ArcaneSignet.class)); + cards.add(new SetCardInfo("Army of the Damned", 106, Rarity.MYTHIC, mage.cards.a.ArmyOfTheDamned.class)); cards.add(new SetCardInfo("Avacyn's Memorial", 31, Rarity.MYTHIC, mage.cards.a.AvacynsMemorial.class)); + cards.add(new SetCardInfo("Avacyn's Pilgrim", 132, Rarity.COMMON, mage.cards.a.AvacynsPilgrim.class)); + cards.add(new SetCardInfo("Bastion Protector", 80, Rarity.RARE, mage.cards.b.BastionProtector.class)); + cards.add(new SetCardInfo("Beast Within", 133, Rarity.UNCOMMON, mage.cards.b.BeastWithin.class)); + cards.add(new SetCardInfo("Bestial Menace", 134, Rarity.UNCOMMON, mage.cards.b.BestialMenace.class)); + cards.add(new SetCardInfo("Biogenic Upgrade", 135, Rarity.UNCOMMON, mage.cards.b.BiogenicUpgrade.class)); + cards.add(new SetCardInfo("Blighted Woodland", 166, Rarity.UNCOMMON, mage.cards.b.BlightedWoodland.class)); + cards.add(new SetCardInfo("Bojuka Bog", 167, Rarity.COMMON, mage.cards.b.BojukaBog.class)); + cards.add(new SetCardInfo("Butcher of Malakir", 107, Rarity.RARE, mage.cards.b.ButcherOfMalakir.class)); + cards.add(new SetCardInfo("Canopy Vista", 168, Rarity.RARE, mage.cards.c.CanopyVista.class)); + cards.add(new SetCardInfo("Cemetery Reaper", 108, Rarity.RARE, mage.cards.c.CemeteryReaper.class)); + cards.add(new SetCardInfo("Champion of Lambholt", 136, Rarity.RARE, mage.cards.c.ChampionOfLambholt.class)); + cards.add(new SetCardInfo("Charcoal Diamond", 158, Rarity.COMMON, mage.cards.c.CharcoalDiamond.class)); + cards.add(new SetCardInfo("Choked Estuary", 169, Rarity.RARE, mage.cards.c.ChokedEstuary.class)); + cards.add(new SetCardInfo("Citadel Siege", 81, Rarity.RARE, mage.cards.c.CitadelSiege.class)); + cards.add(new SetCardInfo("Cleansing Nova", 82, Rarity.RARE, mage.cards.c.CleansingNova.class)); + cards.add(new SetCardInfo("Command Tower", 170, Rarity.COMMON, mage.cards.c.CommandTower.class)); + cards.add(new SetCardInfo("Commander's Sphere", 159, Rarity.COMMON, mage.cards.c.CommandersSphere.class)); + cards.add(new SetCardInfo("Corpse Augur", 109, Rarity.UNCOMMON, mage.cards.c.CorpseAugur.class)); + cards.add(new SetCardInfo("Custodi Soulbinders", 83, Rarity.RARE, mage.cards.c.CustodiSoulbinders.class)); + cards.add(new SetCardInfo("Dark Salvation", 110, Rarity.RARE, mage.cards.d.DarkSalvation.class)); + cards.add(new SetCardInfo("Darkwater Catacombs", 171, Rarity.RARE, mage.cards.d.DarkwaterCatacombs.class)); + cards.add(new SetCardInfo("Dearly Departed", 84, Rarity.RARE, mage.cards.d.DearlyDeparted.class)); + cards.add(new SetCardInfo("Death Baron", 111, Rarity.RARE, mage.cards.d.DeathBaron.class)); + cards.add(new SetCardInfo("Death's Presence", 137, Rarity.RARE, mage.cards.d.DeathsPresence.class)); + cards.add(new SetCardInfo("Dimir Aqueduct", 172, Rarity.UNCOMMON, mage.cards.d.DimirAqueduct.class)); + cards.add(new SetCardInfo("Diregraf Captain", 148, Rarity.UNCOMMON, mage.cards.d.DiregrafCaptain.class)); + cards.add(new SetCardInfo("Diregraf Colossus", 112, Rarity.RARE, mage.cards.d.DiregrafColossus.class)); + cards.add(new SetCardInfo("Distant Melody", 98, Rarity.COMMON, mage.cards.d.DistantMelody.class)); + cards.add(new SetCardInfo("Dread Summons", 113, Rarity.RARE, mage.cards.d.DreadSummons.class)); + cards.add(new SetCardInfo("Dreadhorde Invasion", 114, Rarity.RARE, mage.cards.d.DreadhordeInvasion.class)); + cards.add(new SetCardInfo("Eater of Hope", 115, Rarity.RARE, mage.cards.e.EaterOfHope.class)); + cards.add(new SetCardInfo("Elite Scaleguard", 85, Rarity.UNCOMMON, mage.cards.e.EliteScaleguard.class)); + cards.add(new SetCardInfo("Endless Ranks of the Dead", 116, Rarity.RARE, mage.cards.e.EndlessRanksOfTheDead.class)); + cards.add(new SetCardInfo("Enduring Scalelord", 149, Rarity.UNCOMMON, mage.cards.e.EnduringScalelord.class)); + cards.add(new SetCardInfo("Eternal Skylord", 99, Rarity.UNCOMMON, mage.cards.e.EternalSkylord.class)); + cards.add(new SetCardInfo("Eternal Witness", 138, Rarity.UNCOMMON, mage.cards.e.EternalWitness.class)); + cards.add(new SetCardInfo("Exotic Orchard", 173, Rarity.RARE, mage.cards.e.ExoticOrchard.class)); + cards.add(new SetCardInfo("Feed the Swarm", 117, Rarity.COMMON, mage.cards.f.FeedTheSwarm.class)); + cards.add(new SetCardInfo("Fleshbag Marauder", 118, Rarity.UNCOMMON, mage.cards.f.FleshbagMarauder.class)); + cards.add(new SetCardInfo("Forgotten Creation", 100, Rarity.RARE, mage.cards.f.ForgottenCreation.class)); + cards.add(new SetCardInfo("Fortified Village", 174, Rarity.RARE, mage.cards.f.FortifiedVillage.class)); + cards.add(new SetCardInfo("Gisa and Geralf", 150, Rarity.MYTHIC, mage.cards.g.GisaAndGeralf.class)); + cards.add(new SetCardInfo("Gleaming Overseer", 151, Rarity.UNCOMMON, mage.cards.g.GleamingOverseer.class)); + cards.add(new SetCardInfo("Go for the Throat", 119, Rarity.UNCOMMON, mage.cards.g.GoForTheThroat.class)); + cards.add(new SetCardInfo("Gravespawn Sovereign", 120, Rarity.RARE, mage.cards.g.GravespawnSovereign.class)); + cards.add(new SetCardInfo("Growth Spasm", 139, Rarity.COMMON, mage.cards.g.GrowthSpasm.class)); + cards.add(new SetCardInfo("Gyre Sage", 140, Rarity.RARE, mage.cards.g.GyreSage.class)); + cards.add(new SetCardInfo("Havengul Runebinder", 101, Rarity.RARE, mage.cards.h.HavengulRunebinder.class)); + cards.add(new SetCardInfo("Herald of War", 86, Rarity.RARE, mage.cards.h.HeraldOfWar.class)); + cards.add(new SetCardInfo("Heron's Grace Champion", 152, Rarity.RARE, mage.cards.h.HeronsGraceChampion.class)); + cards.add(new SetCardInfo("Hour of Eternity", 102, Rarity.RARE, mage.cards.h.HourOfEternity.class)); + cards.add(new SetCardInfo("Hour of Reckoning", 87, Rarity.RARE, mage.cards.h.HourOfReckoning.class)); + cards.add(new SetCardInfo("Inspiring Call", 141, Rarity.UNCOMMON, mage.cards.i.InspiringCall.class)); + cards.add(new SetCardInfo("Juniper Order Ranger", 153, Rarity.UNCOMMON, mage.cards.j.JuniperOrderRanger.class)); + cards.add(new SetCardInfo("Kessig Cagebreakers", 142, Rarity.RARE, mage.cards.k.KessigCagebreakers.class)); + cards.add(new SetCardInfo("Knight of the White Orchid", 88, Rarity.RARE, mage.cards.k.KnightOfTheWhiteOrchid.class)); + cards.add(new SetCardInfo("Krosan Verge", 175, Rarity.UNCOMMON, mage.cards.k.KrosanVerge.class)); cards.add(new SetCardInfo("Kyler, Sigardian Emissary", 4, Rarity.MYTHIC, mage.cards.k.KylerSigardianEmissary.class)); cards.add(new SetCardInfo("Leinore, Autumn Sovereign", 1, Rarity.MYTHIC, mage.cards.l.LeinoreAutumnSovereign.class)); + cards.add(new SetCardInfo("Lifecrafter's Bestiary", 160, Rarity.RARE, mage.cards.l.LifecraftersBestiary.class)); + cards.add(new SetCardInfo("Liliana's Devotee", 122, Rarity.UNCOMMON, mage.cards.l.LilianasDevotee.class)); + cards.add(new SetCardInfo("Liliana's Mastery", 123, Rarity.RARE, mage.cards.l.LilianasMastery.class)); + cards.add(new SetCardInfo("Liliana, Death's Majesty", 121, Rarity.MYTHIC, mage.cards.l.LilianaDeathsMajesty.class)); + cards.add(new SetCardInfo("Lord of the Accursed", 124, Rarity.UNCOMMON, mage.cards.l.LordOfTheAccursed.class)); + cards.add(new SetCardInfo("Midnight Reaper", 125, Rarity.RARE, mage.cards.m.MidnightReaper.class)); + cards.add(new SetCardInfo("Mikaeus, the Lunarch", 89, Rarity.MYTHIC, mage.cards.m.MikaeusTheLunarch.class)); + cards.add(new SetCardInfo("Mortuary Mire", 176, Rarity.COMMON, mage.cards.m.MortuaryMire.class)); + cards.add(new SetCardInfo("Myriad Landscape", 177, Rarity.UNCOMMON, mage.cards.m.MyriadLandscape.class)); + cards.add(new SetCardInfo("Odric, Master Tactician", 90, Rarity.RARE, mage.cards.o.OdricMasterTactician.class)); + cards.add(new SetCardInfo("Open the Graves", 126, Rarity.RARE, mage.cards.o.OpenTheGraves.class)); + cards.add(new SetCardInfo("Orzhov Advokist", 91, Rarity.UNCOMMON, mage.cards.o.OrzhovAdvokist.class)); + cards.add(new SetCardInfo("Overseer of the Damned", 127, Rarity.RARE, mage.cards.o.OverseerOfTheDamned.class)); + cards.add(new SetCardInfo("Path of Ancestry", 178, Rarity.COMMON, mage.cards.p.PathOfAncestry.class)); cards.add(new SetCardInfo("Ravenous Rotbelly", 22, Rarity.RARE, mage.cards.r.RavenousRotbelly.class)); + cards.add(new SetCardInfo("Return to Dust", 92, Rarity.UNCOMMON, mage.cards.r.ReturnToDust.class)); + cards.add(new SetCardInfo("Riders of Gavony", 93, Rarity.RARE, mage.cards.r.RidersOfGavony.class)); + cards.add(new SetCardInfo("Rogue's Passage", 179, Rarity.UNCOMMON, mage.cards.r.RoguesPassage.class)); + cards.add(new SetCardInfo("Rooftop Storm", 103, Rarity.RARE, mage.cards.r.RooftopStorm.class)); + cards.add(new SetCardInfo("Ruthless Deathfang", 154, Rarity.UNCOMMON, mage.cards.r.RuthlessDeathfang.class)); + cards.add(new SetCardInfo("Selesnya Sanctuary", 180, Rarity.UNCOMMON, mage.cards.s.SelesnyaSanctuary.class)); + cards.add(new SetCardInfo("Shamanic Revelation", 143, Rarity.RARE, mage.cards.s.ShamanicRevelation.class)); + cards.add(new SetCardInfo("Sigarda, Heron's Grace", 155, Rarity.MYTHIC, mage.cards.s.SigardaHeronsGrace.class)); + cards.add(new SetCardInfo("Sky Diamond", 161, Rarity.COMMON, mage.cards.s.SkyDiamond.class)); + cards.add(new SetCardInfo("Sol Ring", 162, Rarity.UNCOMMON, mage.cards.s.SolRing.class)); cards.add(new SetCardInfo("Somberwald Beastmaster", 30, Rarity.RARE, mage.cards.s.SomberwaldBeastmaster.class)); + cards.add(new SetCardInfo("Somberwald Sage", 144, Rarity.RARE, mage.cards.s.SomberwaldSage.class)); + cards.add(new SetCardInfo("Spark Reaper", 128, Rarity.COMMON, mage.cards.s.SparkReaper.class)); + cards.add(new SetCardInfo("Stitcher Geralf", 104, Rarity.MYTHIC, mage.cards.s.StitcherGeralf.class)); + cards.add(new SetCardInfo("Sungrass Prairie", 181, Rarity.RARE, mage.cards.s.SungrassPrairie.class)); + cards.add(new SetCardInfo("Sunken Hollow", 182, Rarity.RARE, mage.cards.s.SunkenHollow.class)); + cards.add(new SetCardInfo("Swiftfoot Boots", 163, Rarity.UNCOMMON, mage.cards.s.SwiftfootBoots.class)); + cards.add(new SetCardInfo("Swords to Plowshares", 94, Rarity.UNCOMMON, mage.cards.s.SwordsToPlowshares.class)); + cards.add(new SetCardInfo("Syphon Flesh", 129, Rarity.UNCOMMON, mage.cards.s.SyphonFlesh.class)); + cards.add(new SetCardInfo("Tainted Isle", 183, Rarity.UNCOMMON, mage.cards.t.TaintedIsle.class)); + cards.add(new SetCardInfo("Talisman of Dominance", 164, Rarity.UNCOMMON, mage.cards.t.TalismanOfDominance.class)); + cards.add(new SetCardInfo("Talisman of Unity", 165, Rarity.UNCOMMON, mage.cards.t.TalismanOfUnity.class)); + cards.add(new SetCardInfo("Temple of Deceit", 184, Rarity.RARE, mage.cards.t.TempleOfDeceit.class)); + cards.add(new SetCardInfo("Temple of Plenty", 185, Rarity.RARE, mage.cards.t.TempleOfPlenty.class)); + cards.add(new SetCardInfo("Temple of the False God", 186, Rarity.UNCOMMON, mage.cards.t.TempleOfTheFalseGod.class)); + cards.add(new SetCardInfo("Trostani's Summoner", 156, Rarity.UNCOMMON, mage.cards.t.TrostanisSummoner.class)); + cards.add(new SetCardInfo("Unbreakable Formation", 95, Rarity.RARE, mage.cards.u.UnbreakableFormation.class)); + cards.add(new SetCardInfo("Unclaimed Territory", 187, Rarity.UNCOMMON, mage.cards.u.UnclaimedTerritory.class)); + cards.add(new SetCardInfo("Undead Alchemist", 105, Rarity.RARE, mage.cards.u.UndeadAlchemist.class)); + cards.add(new SetCardInfo("Undead Augur", 130, Rarity.UNCOMMON, mage.cards.u.UndeadAugur.class)); + cards.add(new SetCardInfo("Verdurous Gearhulk", 145, Rarity.MYTHIC, mage.cards.v.VerdurousGearhulk.class)); + cards.add(new SetCardInfo("Victory's Envoy", 96, Rarity.RARE, mage.cards.v.VictorysEnvoy.class)); + cards.add(new SetCardInfo("Wild Beastmaster", 146, Rarity.RARE, mage.cards.w.WildBeastmaster.class)); cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); + cards.add(new SetCardInfo("Yavimaya Elder", 147, Rarity.COMMON, mage.cards.y.YavimayaElder.class)); cards.add(new SetCardInfo("Zombie Apocalypse", 131, Rarity.RARE, mage.cards.z.ZombieApocalypse.class)); } } diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index df9124fd748..a0457dc5f02 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -42868,7 +42868,7 @@ Sliver Hive|Jumpstart: Historic Horizons|776|R||Land|||{T}: Add {C}.${T}: Add on Leinore, Autumn Sovereign|Midnight Hunt Commander|1|M|{2}{G}{W}|Legendary Creature - Human Noble|0|4|Coven — At the beginning of combat on your turn, put a +1/+1 counter on up to one target creature you control. Then if you control three or more creatures with different powers, draw a card.| Wilhelt, the Rotcleaver|Midnight Hunt Commander|2|M|{2}{U}{B}|Legendary Creature - Zombie Warrior|3|3|Whenever another Zombie you control dies, if it didn't have decayed, create a 2/2 black Zombie creature token with decayed.$At the beginning of your end step, you may sacrifice a Zombie. If you do, draw a card.| Eloise, Nephalia Sleuth|Midnight Hunt Commander|3|M|{3}{U}{B}|Legendary Creature - Human Rogue|4|4|Whenever another creature you control dies, investigate.$Whenever you sacrifice a token, surveil 1.| -Kyler, Sigardian Emissary|Midnight Hunt Commander|4|M|{3}{G}{W}|Legendary Creature - Human Cleric|2|2|Whenever another Human enters the battlefield under your control, put a +1/+1 counter on Kyler, Sigardian Emissary.$Other humans you control get +1/+1 for each counter on Kyler, Sigardian Emissary.| +Kyler, Sigardian Emissary|Midnight Hunt Commander|4|M|{3}{G}{W}|Legendary Creature - Human Cleric|2|2|Whenever another Human enters the battlefield under your control, put a +1/+1 counter on Kyler, Sigardian Emissary.$Other Humans you control get +1/+1 for each counter on Kyler, Sigardian Emissary.| Celestial Judgment|Midnight Hunt Commander|5|R|{4}{W}{W}|Sorcery|||For each different power among creatures on the battlefield, choose a creature with that power. Destroy each creature not chosen this way.| Curse of Conformity|Midnight Hunt Commander|6|R|{4}{W}|Enchantment - Aura Curse|||Enchant player$Nonlegendary creatures enchanted player controls have base power and toughness 3/3 and lose all creature types.| Moorland Rescuer|Midnight Hunt Commander|7|R|{5}{W}|Creature - Human Knight|4|4|When Moorland Rescuer dies, return any number of other creature cards with total power X or less from your graveyard to the battlefield, where X is Moorland Rescuer's power. Exile Moorland Rescuer.| @@ -42891,9 +42891,9 @@ Tomb Tyrant|Midnight Hunt Commander|23|R|{3}{B}|Creature - Zombie Noble|3|3|Othe Celebrate the Harvest|Midnight Hunt Commander|24|R|{3}{G}|Sorcery|||Search your library for up to X basic land cards, where X is the number of different powers among creatures you control. Put those cards onto the battlefield tapped, then shuffle.| Curse of Clinging Webs|Midnight Hunt Commander|25|R|{2}{G}|Enchantment - Aura Curse|||Enchant player$Whenever a nontoken creature enchanted player controls dies, exile it and you create a 1/2 green Spider creature token with reach.| Heronblade Elite|Midnight Hunt Commander|26|R|{2}{G}|Creature - Human Warrior|1|1|Vigilance$Whenever another Human enters the battlefield under your control, put a +1/+1 counter on Heronblade Elite.${T}: Add X mana of any one color, where X is Heronblade Elite's power.| -Kurbis, Harvest Celebrant|Midnight Hunt Commander|27|U|{X}{G}{G}|Legendary Creature - Treefolk|0|0|Kurbis, Harvest Celebrant enters the battlefield with a number of +1/+1 counters on it equal to the amount of mana spent to cast it.$Remove a +1/+1 counter from Kurbis: Prevent all damage that would be dealt this turn to another target creature with a +1/+1 counter on it.| +Kurbis, Harvest Celebrant|Midnight Hunt Commander|27|R|{X}{G}{G}|Legendary Creature - Treefolk|0|0|Kurbis, Harvest Celebrant enters the battlefield with a number of +1/+1 counters on it equal to the amount of mana spent to cast it.$Remove a +1/+1 counter from Kurbis: Prevent all damage that would be dealt this turn to another target creature with a +1/+1 counter on it.| Ruinous Intrusion|Midnight Hunt Commander|28|R|{3}{G}|Instant|||Exile target artifact or enchantment. Put X +1/+1 counters on target creature you control, where X is the mana value of the permanent exiled this way.| -Siguardian Zealout|Midnight Hunt Commander|29|R|{4}{G}|Creature - Human Cleric|3|3|At the beginning of combat on your turn, choose any number of creatures with different powers. Each of them gets +X/+X and gains vigilance until end of turn, where X is Siguardian Zealout's power.| +Sigardian Zealot|Midnight Hunt Commander|29|R|{4}{G}|Creature - Human Cleric|3|3|At the beginning of combat on your turn, choose any number of creatures with different powers. Each of them gets +X/+X and gains vigilance until end of turn, where X is Sigardian Zealot's power.| Somberwald Beastmaster|Midnight Hunt Commander|30|R|{6}{G}|Creature - Human Ranger|1|1|When Somberwald Beastmaster enters the battlefield, create a 2/2 green Wolf creature token, a 3/3 green Beast creature token, and a 4/4 green Beast creature token.$Creature tokens you control have deathtouch.| Avacyn's Memorial|Midnight Hunt Commander|31|M|{5}{W}{W}{W}|Legendary Artifact|||Indestructible$Other legendary permanents you control have indestructible.| Visions of Glory|Midnight Hunt Commander|32|R|{4}{W}|Sorcery|||Create a 1/1 white Human creature token for each creature you control.$Flashback {8}{W}{W}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| @@ -42903,4 +42903,114 @@ Curse of Obsession|Midnight Hunt Commander|35|R|{4}{R}|Enchantment - Aura Curse| Visions of Ruin|Midnight Hunt Commander|36|R|{3}{R}|Sorcery|||Each opponent sacrifices an artifact. For each artifact sacrificed this way, you create a Treasure token.$Flashback {8}{R}{R}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| Visions of Dominance|Midnight Hunt Commander|37|R|{2}{G}|Sorcery|||Put a +1/+1 counter on target creature, then double the number of +1/+1 counters on it.$Flashback {8}{G}{G}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone.| Lynde, Cheerful Tormentor|Midnight Hunt Commander|38|M|{1}{U}{B}{R}|Legendary Creature - Human Warlock|2|4|Deathtouch$Whenever a Curse is put into your graveyard from the battlefield, return it to the battlefield attached to you at the beginning of the next end step.$At the beginning of your upkeep, you may attach a Curse attached to you to one of your opponents. If you do, draw two cards.| +Abzan Falconer|Midnight Hunt Commander|77|U|{2}{W}|Creature - Human Soldier|2|3|Outlast {W}$Each creature you control with a +1/+1 counter on it has flying.| +Ainok Bond-Kin|Midnight Hunt Commander|78|C|{1}{W}|Creature - Dog Soldier|2|1|Outlast {1}{W}$Each creature you control with a +1/+1 counter on it has first strike.| +Angel of Glory's Rise|Midnight Hunt Commander|79|R|{5}{W}{W}|Creature - Angel|4|6|Flying$When Angel of Glory's Rise enters the battlefield, exile all Zombies, then return all Human creature cards from your graveyard to the battlefield.| +Bastion Protector|Midnight Hunt Commander|80|R|{2}{W}|Creature - Human Soldier|3|3|Commander creatures you control get +2/+2 and have indestructible.| +Citadel Siege|Midnight Hunt Commander|81|R|{2}{W}{W}|Enchantment|||As Citadel Siege enters the battlefield, choose Khans or Dragons.$• Khans — At the beginning of combat on your turn, put two +1/+1 counters on target creature you control.$• Dragons — At the beginning of combat on each opponent's turn, tap target creature that player controls.| +Cleansing Nova|Midnight Hunt Commander|82|R|{3}{W}{W}|Sorcery|||Choose one —$• Destroy all creatures.$• Destroy all artifacts and enchantments.| +Custodi Soulbinders|Midnight Hunt Commander|83|R|{3}{W}|Creature - Human Cleric|0|0|Custodi Soulbinders enters the battlefield with X +1/+1 counters on it, where X is the number of other creatures on the battlefield.${2}{W}, Remove a +1/+1 counter from Custodi Soulbinders: Create a 1/1 white Spirit creature token with flying.| +Dearly Departed|Midnight Hunt Commander|84|R|{4}{W}{W}|Creature - Spirit|5|5|Flying$As long as Dearly Departed is in your graveyard, each Human creature you control enters the battlefield with an additional +1/+1 counter on it.| +Elite Scaleguard|Midnight Hunt Commander|85|U|{4}{W}|Creature - Human Soldier|2|3|When Elite Scaleguard enters the battlefield, bolster 2.$Whenever a creature you control with a +1/+1 counter on it attacks, tap target creature defending player controls.| +Herald of War|Midnight Hunt Commander|86|R|{3}{W}{W}|Creature - Angel|3|3|Flying$Whenever Herald of War attacks, put a +1/+1 counter on it.$Angel spells and Human spells you cast cost {1} less to cast for each +1/+1 counter on Herald of War.| +Hour of Reckoning|Midnight Hunt Commander|87|R|{4}{W}{W}{W}|Sorcery|||Convoke$Destroy all nontoken creatures.| +Knight of the White Orchid|Midnight Hunt Commander|88|R|{W}{W}|Creature - Human Knight|2|2|First strike$When Knight of the White Orchid enters the battlefield, if an opponent controls more lands than you, you may search your library for a Plains card, put it onto the battlefield, then shuffle.| +Mikaeus, the Lunarch|Midnight Hunt Commander|89|M|{X}{W}|Legendary Creature - Human Cleric|0|0|Mikaeus, the Lunarch enters the battlefield with X +1/+1 counters on it.${T}: Put a +1/+1 counter on Mikaeus.${T}, Remove a +1/+1 counter from Mikaeus: Put a +1/+1 counter on each other creature you control.| +Odric, Master Tactician|Midnight Hunt Commander|90|R|{2}{W}{W}|Legendary Creature - Human Soldier|3|4|First strike$Whenever Odric, Master Tactician and at least three other creatures attack, you choose which creatures block this combat and how those creatures block.| +Orzhov Advokist|Midnight Hunt Commander|91|U|{2}{W}|Creature - Human Advisor|1|4|At the beginning of your upkeep, each player may put two +1/+1 counters on a creature they control. If a player does, creatures that player controls can't attack you or planeswalkers you control until your next turn.| +Return to Dust|Midnight Hunt Commander|92|U|{2}{W}{W}|Instant|||Exile target artifact or enchantment. If you cast this spell during your main phase, you may exile up to one other target artifact or enchantment.| +Riders of Gavony|Midnight Hunt Commander|93|R|{2}{W}{W}|Creature - Human Knight|3|3|Vigilance$As Riders of Gavony enters the battlefield, choose a creature type.$Human creatures you control have protection from creatures of the chosen type.| +Swords to Plowshares|Midnight Hunt Commander|94|U|{W}|Instant|||Exile target creature. Its controller gains life equal to its power.| +Unbreakable Formation|Midnight Hunt Commander|95|R|{2}{W}|Instant|||Creatures you control gain indestructible until end of turn.$Addendum — If you cast this spell during your main phase, put a +1/+1 counter on each of those creatures and they gain vigilance until end of turn.| +Victory's Envoy|Midnight Hunt Commander|96|R|{3}{W}{W}|Creature - Human Cleric|3|3|At the beginning of your upkeep, put a +1/+1 counter on each other creature you control.| +Aetherspouts|Midnight Hunt Commander|97|R|{3}{U}{U}|Instant|||For each attacking creature, its owner puts it on the top or bottom of their library.| +Distant Melody|Midnight Hunt Commander|98|C|{3}{U}|Sorcery|||Choose a creature type. Draw a card for each permanent you control of that type.| +Eternal Skylord|Midnight Hunt Commander|99|U|{4}{U}|Creature - Zombie Wizard|3|3|When Eternal Skylord enters the battlefield, amass 2.$Zombie tokens you control have flying.| +Forgotten Creation|Midnight Hunt Commander|100|R|{3}{U}|Creature - Zombie Horror|3|3|Skulk$At the beginning of your upkeep, you may discard all the cards in your hand. If you do, draw that many cards.| +Havengul Runebinder|Midnight Hunt Commander|101|R|{2}{U}{U}|Creature - Human Wizard|2|2|{2}{U}, {T}, Exile a creature card from your graveyard: Create a 2/2 black Zombie creature token, then put a +1/+1 counter on each Zombie creature you control.| +Hour of Eternity|Midnight Hunt Commander|102|R|{X}{X}{U}{U}{U}|Sorcery|||Exile X target creature cards from your graveyard. For each card exiled this way, create a token that's a copy of that card, except it's a 4/4 black Zombie.| +Rooftop Storm|Midnight Hunt Commander|103|R|{5}{U}|Enchantment|||You may pay {0} rather than pay the mana cost for Zombie creature spells you cast.| +Stitcher Geralf|Midnight Hunt Commander|104|M|{3}{U}{U}|Legendary Creature - Human Wizard|3|4|{2}{U}, {T}: Each player mills three cards. Exile up to two creature cards put into graveyards this way. Create an X/X blue Zombie creature token, where X is the total power of the cards exiled this way.| +Undead Alchemist|Midnight Hunt Commander|105|R|{3}{U}|Creature - Zombie|4|2|If a Zombie you control would deal combat damage to a player, instead that player mills that many cards.$Whenever a creature card is put into an opponent's graveyard from their library, exile that card and create a 2/2 black Zombie creature token.| +Army of the Damned|Midnight Hunt Commander|106|M|{5}{B}{B}{B}|Sorcery|||Create thirteen tapped 2/2 black Zombie creature tokens.$Flashback {7}{B}{B}{B}| +Butcher of Malakir|Midnight Hunt Commander|107|R|{5}{B}{B}|Creature - Vampire Warrior|5|4|Flying$Whenever Butcher of Malakir or another creature you control dies, each opponent sacrifices a creature.| +Cemetery Reaper|Midnight Hunt Commander|108|R|{1}{B}{B}|Creature - Zombie|2|2|Other Zombie creatures you control get +1/+1.${2}{B}, {T}: Exile target creature card from a graveyard. Create a 2/2 black Zombie creature token.| +Corpse Augur|Midnight Hunt Commander|109|U|{3}{B}|Creature - Zombie Wizard|4|2|When Corpse Augur dies, you draw X cards and you lose X life, where X is the number of creature cards in target player's graveyard.| +Dark Salvation|Midnight Hunt Commander|110|R|{X}{X}{B}|Sorcery|||Target player creates X 2/2 black Zombie creature tokens, then up to one target creature gets -1/-1 until end of turn for each Zombie that player controls.| +Death Baron|Midnight Hunt Commander|111|R|{1}{B}{B}|Creature - Zombie Wizard|2|2|Skeletons you control and other Zombies you control get +1/+1 and have deathtouch.| +Diregraf Colossus|Midnight Hunt Commander|112|R|{2}{B}|Creature - Zombie Giant|2|2|Diregraf Colossus enters the battlefield with a +1/+1 counter on it for each Zombie card in your graveyard.$Whenever you cast a Zombie spell, create a tapped 2/2 black Zombie creature token.| +Dread Summons|Midnight Hunt Commander|113|R|{X}{B}{B}|Sorcery|||Each player mills X cards. For each creature card put into a graveyard this way, you create a tapped 2/2 black Zombie creature token.| +Dreadhorde Invasion|Midnight Hunt Commander|114|R|{1}{B}|Enchantment|||At the beginning of your upkeep, you lose 1 life and amass 1.$Whenever a Zombie token you control with power 6 or greater attacks, it gains lifelink until end of turn.| +Eater of Hope|Midnight Hunt Commander|115|R|{5}{B}{B}|Creature - Demon|6|4|Flying${B}, Sacrifice another creature: Regenerate Eater of Hope.${2}{B}, Sacrifice two other creatures: Destroy target creature.| +Endless Ranks of the Dead|Midnight Hunt Commander|116|R|{2}{B}{B}|Enchantment|||At the beginning of your upkeep, create X 2/2 black Zombie creature tokens, where X is half the number of Zombies you control, rounded down.| +Feed the Swarm|Midnight Hunt Commander|117|C|{1}{B}|Sorcery|||Destroy target creature or enchantment an opponent controls. You lose life equal to that permanent's mana value.| +Fleshbag Marauder|Midnight Hunt Commander|118|U|{2}{B}|Creature - Zombie Warrior|3|1|When Fleshbag Marauder enters the battlefield, each player sacrifices a creature.| +Go for the Throat|Midnight Hunt Commander|119|U|{1}{B}|Instant|||Destroy target nonartifact creature.| +Gravespawn Sovereign|Midnight Hunt Commander|120|R|{4}{B}{B}|Creature - Zombie|3|3|Tap five untapped Zombies you control: Put target creature card from a graveyard onto the battlefield under your control.| +Liliana, Death's Majesty|Midnight Hunt Commander|121|M|{3}{B}{B}|Legendary Planeswalker - Liliana|5|+1: Create a 2/2 black Zombie creature token. Mill two cards.$−3: Return target creature card from your graveyard to the battlefield. That creature is a black Zombie in addition to its other colors and types.$−7: Destroy all non-Zombie creatures.| +Liliana's Devotee|Midnight Hunt Commander|122|U|{2}{B}|Creature - Human Warlock|2|3|Zombies you control get +1/+0.$At the beginning of your end step, if a creature died this turn, you may pay {1}{B}. If you do, create a 2/2 black Zombie creature token.| +Liliana's Mastery|Midnight Hunt Commander|123|R|{3}{B}{B}|Enchantment|||Zombies you control get +1/+1.$When Liliana's Mastery enters the battlefield, create two 2/2 black Zombie creature tokens.| +Lord of the Accursed|Midnight Hunt Commander|124|U|{2}{B}|Creature - Zombie|2|3|Other Zombies you control get +1/+1.${1}{B}, {T}: All Zombies gain menace until end of turn.| +Midnight Reaper|Midnight Hunt Commander|125|R|{2}{B}|Creature - Zombie Knight|3|2|Whenever a nontoken creature you control dies, Midnight Reaper deals 1 damage to you and you draw a card.| +Open the Graves|Midnight Hunt Commander|126|R|{3}{B}{B}|Enchantment|||Whenever a nontoken creature you control dies, create a 2/2 black Zombie creature token.| +Overseer of the Damned|Midnight Hunt Commander|127|R|{5}{B}{B}|Creature - Demon|5|5|Flying$When Overseer of the Damned enters the battlefield, you may destroy target creature.$Whenever a nontoken creature an opponent controls dies, create a tapped 2/2 black Zombie creature token.| +Spark Reaper|Midnight Hunt Commander|128|C|{2}{B}|Creature - Zombie|2|3|{3}, Sacrifice a creature or planeswalker: You gain 1 life and draw a card.| +Syphon Flesh|Midnight Hunt Commander|129|U|{4}{B}|Sorcery|||Each other player sacrifices a creature. You create a 2/2 black Zombie creature token for each creature sacrificed this way.| +Undead Augur|Midnight Hunt Commander|130|U|{B}{B}|Creature - Zombie Wizard|2|2|Whenever Undead Augur or another Zombie you control dies, you draw a card and you lose 1 life.| Zombie Apocalypse|Midnight Hunt Commander|131|R|{3}{B}{B}{B}|Sorcery|||Return all Zombie creature cards from your graveyard to the battlefield tapped, then destroy all Humans.| +Avacyn's Pilgrim|Midnight Hunt Commander|132|C|{G}|Creature - Human Monk|1|1|{T}: Add {W}.| +Beast Within|Midnight Hunt Commander|133|U|{2}{G}|Instant|||Destroy target permanent. Its controller creates a 3/3 green Beast creature token.| +Bestial Menace|Midnight Hunt Commander|134|U|{3}{G}{G}|Sorcery|||Create a 1/1 green Snake creature token, a 2/2 green Wolf creature token, and a 3/3 green Elephant creature token.| +Biogenic Upgrade|Midnight Hunt Commander|135|U|{4}{G}{G}|Sorcery|||Distribute three +1/+1 counters among one, two, or three target creatures, then double the number of +1/+1 counters on each of those creatures.| +Champion of Lambholt|Midnight Hunt Commander|136|R|{1}{G}{G}|Creature - Human Warrior|1|1|Creatures with power less than Champion of Lambholt's power can't block creatures you control.$Whenever another creature enters the battlefield under your control, put a +1/+1 counter on Champion of Lambholt.| +Death's Presence|Midnight Hunt Commander|137|R|{5}{G}|Enchantment|||Whenever a creature you control dies, put X +1/+1 counters on target creature you control, where X is the power of the creature that died.| +Eternal Witness|Midnight Hunt Commander|138|U|{1}{G}{G}|Creature - Human Shaman|2|1|When Eternal Witness enters the battlefield, you may return target card from your graveyard to your hand.| +Growth Spasm|Midnight Hunt Commander|139|C|{2}{G}|Sorcery|||Search your library for a basic land card, put it onto the battlefield tapped, then shuffle. Create a 0/1 colorless Eldrazi Spawn creature token. It has "Sacrifice this creature: Add {C}."| +Gyre Sage|Midnight Hunt Commander|140|R|{1}{G}|Creature - Elf Druid|1|2|Evolve${T}: Add {G} for each +1/+1 counter on Gyre Sage.| +Inspiring Call|Midnight Hunt Commander|141|U|{2}{G}|Instant|||Draw a card for each creature you control with a +1/+1 counter on it. Those creatures gain indestructible until end of turn.| +Kessig Cagebreakers|Midnight Hunt Commander|142|R|{4}{G}|Creature - Human Rogue|3|4|Whenever Kessig Cagebreakers attacks, create a 2/2 green Wolf creature token that's tapped and attacking for each creature card in your graveyard.| +Shamanic Revelation|Midnight Hunt Commander|143|R|{3}{G}{G}|Sorcery|||Draw a card for each creature you control.$Ferocious — You gain 4 life for each creature you control with power 4 or greater.| +Somberwald Sage|Midnight Hunt Commander|144|R|{2}{G}|Creature - Human Druid|0|1|{T}: Add three mana of any one color. Spend this mana only to cast creature spells.| +Verdurous Gearhulk|Midnight Hunt Commander|145|M|{3}{G}{G}|Artifact Creature - Construct|4|4|Trample$When Verdurous Gearhulk enters the battlefield, distribute four +1/+1 counters among any number of target creatures you control.| +Wild Beastmaster|Midnight Hunt Commander|146|R|{2}{G}|Creature - Human Shaman|1|1|Whenever Wild Beastmaster attacks, each other creature you control gets +X/+X until end of turn, where X is Wild Beastmaster's power.| +Yavimaya Elder|Midnight Hunt Commander|147|C|{1}{G}{G}|Creature - Human Druid|2|1|When Yavimaya Elder dies, you may search your library for up to two basic land cards, reveal them, put them into your hand, then shuffle.${2}, Sacrifice Yavimaya Elder: Draw a card.| +Diregraf Captain|Midnight Hunt Commander|148|U|{1}{U}{B}|Creature - Zombie Soldier|2|2|Deathtouch$Other Zombie creatures you control get +1/+1.$Whenever another Zombie you control dies, target opponent loses 1 life.| +Enduring Scalelord|Midnight Hunt Commander|149|U|{4}{G}{W}|Creature - Dragon|4|4|Flying$Whenever one or more +1/+1 counters are put on another creature you control, you may put a +1/+1 counter on Enduring Scalelord.| +Gisa and Geralf|Midnight Hunt Commander|150|M|{2}{U}{B}|Legendary Creature - Human Wizard|4|4|When Gisa and Geralf enters the battlefield, mill four cards.$During each of your turns, you may cast a Zombie creature spell from your graveyard.| +Gleaming Overseer|Midnight Hunt Commander|151|U|{1}{U}{B}|Creature - Zombie Wizard|1|4|When Gleaming Overseer enters the battlefield, amass 1.$Zombie tokens you control have hexproof and menace.| +Heron's Grace Champion|Midnight Hunt Commander|152|R|{2}{G}{W}|Creature - Human Knight|3|3|Flash$Lifelink$When Heron's Grace Champion enters the battlefield, other Humans you control get +1/+1 and gain lifelink until end of turn.| +Juniper Order Ranger|Midnight Hunt Commander|153|U|{3}{G}{W}|Creature - Human Knight Ranger|2|4|Whenever another creature enters the battlefield under your control, put a +1/+1 counter on that creature and a +1/+1 counter on Juniper Order Ranger.| +Ruthless Deathfang|Midnight Hunt Commander|154|U|{4}{U}{B}|Creature - Dragon|4|4|Flying$Whenever you sacrifice a creature, target opponent sacrifices a creature.| +Sigarda, Heron's Grace|Midnight Hunt Commander|155|M|{3}{G}{W}|Legendary Creature - Angel|4|5|Flying$You and Humans you control have hexproof.${2}, Exile a card from your graveyard: Create a 1/1 white Human Soldier creature token.| +Trostani's Summoner|Midnight Hunt Commander|156|U|{5}{G}{W}|Creature - Elf Shaman|1|1|When Trostani's Summoner enters the battlefield, create a 2/2 white Knight creature token with vigilance, a 3/3 green Centaur creature token, and a 4/4 green Rhino creature token with trample.| +Arcane Signet|Midnight Hunt Commander|157|C|{2}|Artifact|||{T}: Add one mana of any color in your commander's color identity.| +Charcoal Diamond|Midnight Hunt Commander|158|C|{2}|Artifact|||Charcoal Diamond enters the battlefield tapped.${T}: Add {B}.| +Commander's Sphere|Midnight Hunt Commander|159|C|{3}|Artifact|||{T}: Add one mana of any color in your commander's color identity.$Sacrifice Commander's Sphere: Draw a card.| +Lifecrafter's Bestiary|Midnight Hunt Commander|160|R|{3}|Artifact|||At the beginning of your upkeep, scry 1.$Whenever you cast a creature spell, you may pay {G}. If you do, draw a card.| +Sky Diamond|Midnight Hunt Commander|161|C|{2}|Artifact|||Sky Diamond enters the battlefield tapped.${T}: Add {U}.| +Sol Ring|Midnight Hunt Commander|162|U|{1}|Artifact|||{T}: Add {C}{C}.| +Swiftfoot Boots|Midnight Hunt Commander|163|U|{2}|Artifact - Equipment|||Equipped creature has hexproof and haste.$Equip {1}| +Talisman of Dominance|Midnight Hunt Commander|164|U|{2}|Artifact|||{T}: Add {C}.${T}: Add {U} or {B}. Talisman of Dominance deals 1 damage to you.| +Talisman of Unity|Midnight Hunt Commander|165|U|{2}|Artifact|||{T}: Add {C}.${T}: Add {G} or {W}. Talisman of Unity deals 1 damage to you.| +Blighted Woodland|Midnight Hunt Commander|166|U||Land|||{T}: Add {C}.${3}{G}, {T}, Sacrifice Blighted Woodland: Search your library for up to two basic land cards, put them onto the battlefield tapped, then shuffle.| +Bojuka Bog|Midnight Hunt Commander|167|C||Land|||Bojuka Bog enters the battlefield tapped.$When Bojuka Bog enters the battlefield, exile all cards from target player's graveyard.${T}: Add {B}.| +Canopy Vista|Midnight Hunt Commander|168|R||Land - Forest Plains|||({T}: Add {G} or {W}.)$Canopy Vista enters the battlefield tapped unless you control two or more basic lands.| +Choked Estuary|Midnight Hunt Commander|169|R||Land|||As Choked Estuary enters the battlefield, you may reveal an Island or Swamp card from your hand. If you don't, Choked Estuary enters the battlefield tapped.${T}: Add {U} or {B}.| +Command Tower|Midnight Hunt Commander|170|C||Land|||{T}: Add one mana of any color in your commander's color identity.| +Darkwater Catacombs|Midnight Hunt Commander|171|R||Land|||{1}, {T}: Add {U}{B}.| +Dimir Aqueduct|Midnight Hunt Commander|172|U||Land|||Dimir Aqueduct enters the battlefield tapped.$When Dimir Aqueduct enters the battlefield, return a land you control to its owner's hand.${T}: Add {U}{B}.| +Exotic Orchard|Midnight Hunt Commander|173|R||Land|||{T}: Add one mana of any color that a land an opponent controls could produce.| +Fortified Village|Midnight Hunt Commander|174|R||Land|||As Fortified Village enters the battlefield, you may reveal a Forest or Plains card from your hand. If you don't, Fortified Village enters the battlefield tapped.${T}: Add {G} or {W}.| +Krosan Verge|Midnight Hunt Commander|175|U||Land|||Krosan Verge enters the battlefield tapped.${T}: Add {C}.${2}, {T}, Sacrifice Krosan Verge: Search your library for a Forest card and a Plains card, put them onto the battlefield tapped, then shuffle.| +Mortuary Mire|Midnight Hunt Commander|176|C||Land|||Mortuary Mire enters the battlefield tapped.$When Mortuary Mire enters the battlefield, you may put target creature card from your graveyard on top of your library.${T}: Add {B}.| +Myriad Landscape|Midnight Hunt Commander|177|U||Land|||Myriad Landscape enters the battlefield tapped.${T}: Add {C}.${2}, {T}, Sacrifice Myriad Landscape: Search your library for up to two basic land cards that share a land type, put them onto the battlefield tapped, then shuffle.| +Path of Ancestry|Midnight Hunt Commander|178|C||Land|||Path of Ancestry enters the battlefield tapped.${T}: Add one mana of any color in your commander's color identity. When that mana is spent to cast a creature spell that shares a creature type with your commander, scry 1.| +Rogue's Passage|Midnight Hunt Commander|179|U||Land|||{T}: Add {C}.${4}, {T}: Target creature can't be blocked this turn.| +Selesnya Sanctuary|Midnight Hunt Commander|180|U||Land|||Selesnya Sanctuary enters the battlefield tapped.$When Selesnya Sanctuary enters the battlefield, return a land you control to its owner's hand.${T}: Add {G}{W}.| +Sungrass Prairie|Midnight Hunt Commander|181|R||Land|||{1}, {T}: Add {G}{W}.| +Sunken Hollow|Midnight Hunt Commander|182|R||Land - Island Swamp|||({T}: Add {U} or {B}.)$Sunken Hollow enters the battlefield tapped unless you control two or more basic lands.| +Tainted Isle|Midnight Hunt Commander|183|U||Land|||{T}: Add {C}.${T}: Add {U} or {B}. Activate only if you control a Swamp.| +Temple of Deceit|Midnight Hunt Commander|184|R||Land|||Temple of Deceit enters the battlefield tapped.$When Temple of Deceit enters the battlefield, scry 1.${T}: Add {U} or {B}.| +Temple of Plenty|Midnight Hunt Commander|185|R||Land|||Temple of Plenty enters the battlefield tapped.$When Temple of Plenty enters the battlefield, scry 1.${T}: Add {G} or {W}.| +Temple of the False God|Midnight Hunt Commander|186|U||Land|||{T}: Add {C}{C}. Activate only if you control five or more lands.| +Unclaimed Territory|Midnight Hunt Commander|187|U||Land|||As Unclaimed Territory enters the battlefield, choose a creature type.${T}: Add {C}.${T}: Add one mana of any color. Spend this mana only to cast a creature spell of the chosen type.| From 698079f480501a81a7bb99185c160e8facbd4fc0 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 15 Sep 2021 08:32:11 -0400 Subject: [PATCH 100/231] [MIC] Implemented Eloise, Nephalia Sleuth --- .../mage/cards/e/EloiseNephaliaSleuth.java | 55 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + .../effects/keyword/InvestigateEffect.java | 2 +- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/e/EloiseNephaliaSleuth.java diff --git a/Mage.Sets/src/mage/cards/e/EloiseNephaliaSleuth.java b/Mage.Sets/src/mage/cards/e/EloiseNephaliaSleuth.java new file mode 100644 index 00000000000..17f13811056 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EloiseNephaliaSleuth.java @@ -0,0 +1,55 @@ +package mage.cards.e; + +import mage.MageInt; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.common.SacrificePermanentTriggeredAbility; +import mage.abilities.effects.keyword.InvestigateEffect; +import mage.abilities.effects.keyword.SurveilEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterPermanent; +import mage.filter.predicate.permanent.TokenPredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EloiseNephaliaSleuth extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("a token"); + + static { + filter.add(TokenPredicate.TRUE); + } + + public EloiseNephaliaSleuth(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Whenever another creature you control dies, investigate. + this.addAbility(new DiesCreatureTriggeredAbility( + new InvestigateEffect(1), false, true + )); + + // Whenever you sacrifice a token, surveil 1. + this.addAbility(new SacrificePermanentTriggeredAbility(new SurveilEffect(1), filter)); + } + + private EloiseNephaliaSleuth(final EloiseNephaliaSleuth card) { + super(card); + } + + @Override + public EloiseNephaliaSleuth copy() { + return new EloiseNephaliaSleuth(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 25caacc5f8f..62c769e91f5 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -58,6 +58,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Dreadhorde Invasion", 114, Rarity.RARE, mage.cards.d.DreadhordeInvasion.class)); cards.add(new SetCardInfo("Eater of Hope", 115, Rarity.RARE, mage.cards.e.EaterOfHope.class)); cards.add(new SetCardInfo("Elite Scaleguard", 85, Rarity.UNCOMMON, mage.cards.e.EliteScaleguard.class)); + cards.add(new SetCardInfo("Eloise, Nephalia Sleuth", 3, Rarity.MYTHIC, mage.cards.e.EloiseNephaliaSleuth.class)); cards.add(new SetCardInfo("Endless Ranks of the Dead", 116, Rarity.RARE, mage.cards.e.EndlessRanksOfTheDead.class)); cards.add(new SetCardInfo("Enduring Scalelord", 149, Rarity.UNCOMMON, mage.cards.e.EnduringScalelord.class)); cards.add(new SetCardInfo("Eternal Skylord", 99, Rarity.UNCOMMON, mage.cards.e.EternalSkylord.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/InvestigateEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/InvestigateEffect.java index 41f7b3100f8..8a6de130bf8 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/InvestigateEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/InvestigateEffect.java @@ -60,7 +60,7 @@ public class InvestigateEffect extends OneShotEffect { default: message = CardUtil.numberToText(amount) + " times. (To investigate, c"; } - return "investigate " + message + "reate a colorless Clue artifact token " + + return "investigate" + message + "reate a colorless Clue artifact token " + "with \"{2}, Sacrifice this artifact: Draw a card.\")"; } } From 789b7473d0c1b27f07b90c524421ec1cb1ec4698 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 15 Sep 2021 08:36:56 -0400 Subject: [PATCH 101/231] [MIC] Implemented Drown in Dreams --- Mage.Sets/src/mage/cards/d/DrownInDreams.java | 51 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 52 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DrownInDreams.java diff --git a/Mage.Sets/src/mage/cards/d/DrownInDreams.java b/Mage.Sets/src/mage/cards/d/DrownInDreams.java new file mode 100644 index 00000000000..3d716d0eba5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DrownInDreams.java @@ -0,0 +1,51 @@ +package mage.cards.d; + +import mage.abilities.Mode; +import mage.abilities.condition.common.ControlACommanderCondition; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.MultipliedValue; +import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.common.DrawCardTargetEffect; +import mage.abilities.effects.common.MillCardsTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.TargetPlayer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DrownInDreams extends CardImpl { + + private static final DynamicValue xValue = new MultipliedValue(ManacostVariableValue.REGULAR, 2); + + public DrownInDreams(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{X}{2}{U}"); + + // Choose one. If you control a commander as you cast this spell, you may choose both. + this.getSpellAbility().getModes().setChooseText( + "Choose one. If you control a commander as you cast this spell, you may choose both." + ); + this.getSpellAbility().getModes().setMoreCondition(ControlACommanderCondition.instance); + + // • Target player draws X cards. + this.getSpellAbility().addEffect(new DrawCardTargetEffect(ManacostVariableValue.REGULAR)); + this.getSpellAbility().addTarget(new TargetPlayer()); + + // • Target player mills twice X cards. + Mode mode = new Mode(new MillCardsTargetEffect(xValue).setText("target player mills twice X cards")); + mode.addTarget(new TargetPlayer()); + this.getSpellAbility().addMode(mode); + } + + private DrownInDreams(final DrownInDreams card) { + super(card); + } + + @Override + public DrownInDreams copy() { + return new DrownInDreams(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 62c769e91f5..730397a72f6 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -56,6 +56,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Distant Melody", 98, Rarity.COMMON, mage.cards.d.DistantMelody.class)); cards.add(new SetCardInfo("Dread Summons", 113, Rarity.RARE, mage.cards.d.DreadSummons.class)); cards.add(new SetCardInfo("Dreadhorde Invasion", 114, Rarity.RARE, mage.cards.d.DreadhordeInvasion.class)); + cards.add(new SetCardInfo("Drown in Dreams", 13, Rarity.RARE, mage.cards.d.DrownInDreams.class)); cards.add(new SetCardInfo("Eater of Hope", 115, Rarity.RARE, mage.cards.e.EaterOfHope.class)); cards.add(new SetCardInfo("Elite Scaleguard", 85, Rarity.UNCOMMON, mage.cards.e.EliteScaleguard.class)); cards.add(new SetCardInfo("Eloise, Nephalia Sleuth", 3, Rarity.MYTHIC, mage.cards.e.EloiseNephaliaSleuth.class)); From cbe511ebcef1fcaf4378fdaf69d6dc7223f20e20 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 15 Sep 2021 08:45:37 -0400 Subject: [PATCH 102/231] [MIC] Implemented Sigarda's Vanguard --- .../src/mage/cards/s/SigardasVanguard.java | 99 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 100 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SigardasVanguard.java diff --git a/Mage.Sets/src/mage/cards/s/SigardasVanguard.java b/Mage.Sets/src/mage/cards/s/SigardasVanguard.java new file mode 100644 index 00000000000..6e6ef33732e --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SigardasVanguard.java @@ -0,0 +1,99 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.hint.common.CovenHint; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.common.FilterCreaturePermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.Objects; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SigardasVanguard extends CardImpl { + + public SigardasVanguard(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{W}"); + + this.subtype.add(SubType.ANGEL); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever Sigarda's Vanguard enters the battlefield or attacks, choose any number of creatures with different powers. Those creatures gain double strike until end of turn. + Ability ability = new EntersBattlefieldOrAttacksSourceTriggeredAbility( + new GainAbilityTargetEffect( + DoubleStrikeAbility.getInstance(), Duration.EndOfTurn + ).setText("choose any number of creatures with different powers. " + + "Those creatures gain double strike until end of turn") + ); + ability.addTarget(new SigardasVanguardTarget()); + this.addAbility(ability.addHint(CovenHint.instance)); + } + + private SigardasVanguard(final SigardasVanguard card) { + super(card); + } + + @Override + public SigardasVanguard copy() { + return new SigardasVanguard(this); + } +} + +class SigardasVanguardTarget extends TargetCreaturePermanent { + + private static final FilterCreaturePermanent filter + = new FilterCreaturePermanent("creatures with different powers"); + + SigardasVanguardTarget() { + super(0, Integer.MAX_VALUE, filter, false); + } + + private SigardasVanguardTarget(final SigardasVanguardTarget target) { + super(target); + } + + @Override + public SigardasVanguardTarget copy() { + return new SigardasVanguardTarget(this); + } + + @Override + public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + if (!super.canTarget(controllerId, id, source, game)) { + return false; + } + Permanent creature = game.getPermanent(id); + if (creature == null) { + return false; + } + return this.getTargets() + .stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .noneMatch(p -> creature.getPower().getValue() == p); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 730397a72f6..006b0d437db 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -109,6 +109,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Ruthless Deathfang", 154, Rarity.UNCOMMON, mage.cards.r.RuthlessDeathfang.class)); cards.add(new SetCardInfo("Selesnya Sanctuary", 180, Rarity.UNCOMMON, mage.cards.s.SelesnyaSanctuary.class)); cards.add(new SetCardInfo("Shamanic Revelation", 143, Rarity.RARE, mage.cards.s.ShamanicRevelation.class)); + cards.add(new SetCardInfo("Sigarda's Vanguard", 8, Rarity.RARE, mage.cards.s.SigardasVanguard.class)); cards.add(new SetCardInfo("Sigarda, Heron's Grace", 155, Rarity.MYTHIC, mage.cards.s.SigardaHeronsGrace.class)); cards.add(new SetCardInfo("Sky Diamond", 161, Rarity.COMMON, mage.cards.s.SkyDiamond.class)); cards.add(new SetCardInfo("Sol Ring", 162, Rarity.UNCOMMON, mage.cards.s.SolRing.class)); From 0fc29302f41d2f751c1dc63a9f893273607a9ce1 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 15 Sep 2021 08:54:51 -0400 Subject: [PATCH 103/231] [MIC] Implemented Celebrate the Harvest --- .../src/mage/cards/c/CelebrateTheHarvest.java | 96 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 97 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CelebrateTheHarvest.java diff --git a/Mage.Sets/src/mage/cards/c/CelebrateTheHarvest.java b/Mage.Sets/src/mage/cards/c/CelebrateTheHarvest.java new file mode 100644 index 00000000000..47fcbcb28f7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CelebrateTheHarvest.java @@ -0,0 +1,96 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.hint.common.CovenHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInLibrary; + +import java.util.Objects; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CelebrateTheHarvest extends CardImpl { + + public CelebrateTheHarvest(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{G}"); + + // Search your library for up to X basic land cards, where X is the number of different powers among creatures you control. Put those cards onto the battlefield tapped, then shuffle. + this.getSpellAbility().addEffect(new CelebrateTheHarvestEffect()); + this.getSpellAbility().addHint(CovenHint.instance); + } + + private CelebrateTheHarvest(final CelebrateTheHarvest card) { + super(card); + } + + @Override + public CelebrateTheHarvest copy() { + return new CelebrateTheHarvest(this); + } +} + +class CelebrateTheHarvestEffect extends OneShotEffect { + + CelebrateTheHarvestEffect() { + super(Outcome.Benefit); + staticText = "search your library for up to X basic land cards, where X is the number of different powers " + + "among creatures you control. Put those cards onto the battlefield tapped, then shuffle"; + } + + private CelebrateTheHarvestEffect(final CelebrateTheHarvestEffect effect) { + super(effect); + } + + @Override + public CelebrateTheHarvestEffect copy() { + return new CelebrateTheHarvestEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + int powerCount = game + .getBattlefield() + .getActivePermanents( + StaticFilters.FILTER_CONTROLLED_CREATURE, + source.getControllerId(), source.getSourceId(), game + ) + .stream() + .filter(Objects::nonNull) + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .distinct() + .map(x -> 1) + .sum(); + TargetCardInLibrary target = new TargetCardInLibrary(0, powerCount, StaticFilters.FILTER_CARD_BASIC_LAND); + player.searchLibrary(target, source, game); + Cards cards = new CardsImpl(); + target.getTargets() + .stream() + .map(cardId -> player.getLibrary().getCard(cardId, game)) + .forEach(cards::add); + player.moveCards( + cards.getCards(game), Zone.BATTLEFIELD, source, game, + true, false, true, null + ); + player.shuffleLibrary(source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 006b0d437db..10cb23af0f5 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -35,6 +35,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Bojuka Bog", 167, Rarity.COMMON, mage.cards.b.BojukaBog.class)); cards.add(new SetCardInfo("Butcher of Malakir", 107, Rarity.RARE, mage.cards.b.ButcherOfMalakir.class)); cards.add(new SetCardInfo("Canopy Vista", 168, Rarity.RARE, mage.cards.c.CanopyVista.class)); + cards.add(new SetCardInfo("Celebrate the Harvest", 24, Rarity.RARE, mage.cards.c.CelebrateTheHarvest.class)); cards.add(new SetCardInfo("Cemetery Reaper", 108, Rarity.RARE, mage.cards.c.CemeteryReaper.class)); cards.add(new SetCardInfo("Champion of Lambholt", 136, Rarity.RARE, mage.cards.c.ChampionOfLambholt.class)); cards.add(new SetCardInfo("Charcoal Diamond", 158, Rarity.COMMON, mage.cards.c.CharcoalDiamond.class)); From 08e8a34e8bf8394757aae142243ef5bd9b74e4ff Mon Sep 17 00:00:00 2001 From: ciaccona007 Date: Wed, 15 Sep 2021 12:50:30 -0400 Subject: [PATCH 104/231] [MID] Implement Ominous Roost --- Mage.Sets/src/mage/cards/o/OminousRoost.java | 80 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../permanent/token/OminousRoostToken.java | 31 +++++++ 3 files changed, 112 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/o/OminousRoost.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/OminousRoostToken.java diff --git a/Mage.Sets/src/mage/cards/o/OminousRoost.java b/Mage.Sets/src/mage/cards/o/OminousRoost.java new file mode 100644 index 00000000000..1c2d18c6760 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OminousRoost.java @@ -0,0 +1,80 @@ +package mage.cards.o; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.ReturnToHandTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.token.OminousRoostToken; +import mage.game.stack.Spell; +import mage.target.common.TargetNonlandPermanent; + +import java.util.UUID; + +/** + * @author ciaccona007 + */ +public final class OminousRoost extends CardImpl { + + public OminousRoost(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}"); + + // When Ominous Roost enters the battlefield or whenever you cast a spell from your graveyard, create a 1/1 blue Bird creature token with flying and "This creature can block only creatures with flying." + this.addAbility(new OminousRoostTriggeredAbility()); + } + + private OminousRoost(final OminousRoost card) { + super(card); + } + + @Override + public OminousRoost copy() { + return new OminousRoost(this); + } +} + +class OminousRoostTriggeredAbility extends TriggeredAbilityImpl { + + OminousRoostTriggeredAbility() { + super(Zone.ALL, new CreateTokenEffect(new OminousRoostToken())); + } + + private OminousRoostTriggeredAbility(final OminousRoostTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SPELL_CAST + || event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + switch (event.getType()) { + case SPELL_CAST: + if (event.getPlayerId().equals(controllerId) && event.getZone() == Zone.GRAVEYARD) { + return true; + } + case ENTERS_THE_BATTLEFIELD: + return event.getTargetId().equals(getSourceId()); + default: + return false; + } + } + + @Override + public String getRule() { + return "When {this} enters the battlefield or whenever you cast a spell from your graveyard, create a " + + "1/1 blue Bird creature token with flying and \"This creature can block only creatures with flying.\""; + } + + @Override + public OminousRoostTriggeredAbility copy() { + return new OminousRoostTriggeredAbility(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 4280787eb0e..eb97394bca3 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -218,6 +218,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Obsessive Astronomer", 152, Rarity.UNCOMMON, mage.cards.o.ObsessiveAstronomer.class)); cards.add(new SetCardInfo("Odric's Outrider", 29, Rarity.UNCOMMON, mage.cards.o.OdricsOutrider.class)); cards.add(new SetCardInfo("Olivia's Midnight Ambush", 118, Rarity.COMMON, mage.cards.o.OliviasMidnightAmbush.class)); + cards.add(new SetCardInfo("Ominous Roost", 65, Rarity.UNCOMMON, mage.cards.o.OminousRoost.class)); cards.add(new SetCardInfo("Organ Hoarder", 66, Rarity.COMMON, mage.cards.o.OrganHoarder.class)); cards.add(new SetCardInfo("Otherworldly Gaze", 67, Rarity.COMMON, mage.cards.o.OtherworldlyGaze.class)); cards.add(new SetCardInfo("Outland Liberator", 190, Rarity.UNCOMMON, mage.cards.o.OutlandLiberator.class)); diff --git a/Mage/src/main/java/mage/game/permanent/token/OminousRoostToken.java b/Mage/src/main/java/mage/game/permanent/token/OminousRoostToken.java new file mode 100644 index 00000000000..17e645c212b --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/OminousRoostToken.java @@ -0,0 +1,31 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.common.CanBlockOnlyFlyingAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.constants.CardType; +import mage.constants.SubType; + +public class OminousRoostToken extends TokenImpl { + + public OminousRoostToken() { + super("Bird", "1/1 blue Bird creature token with flying and \"This creature can block only creatures with flying\""); + cardType.add(CardType.CREATURE); + color.setBlue(true); + subtype.add(SubType.BIRD); + power = new MageInt(1); + toughness = new MageInt(1); + + this.addAbility(FlyingAbility.getInstance()); + this.addAbility(new CanBlockOnlyFlyingAbility()); + } + + public OminousRoostToken(final OminousRoostToken token) { + super(token); + } + + @Override + public Token copy() { + return new OminousRoostToken(this); + } +} From e72f13c0e964f1e45ce84b5f8d9bf9a57b8b460d Mon Sep 17 00:00:00 2001 From: ciaccona007 Date: Wed, 15 Sep 2021 13:23:33 -0400 Subject: [PATCH 105/231] [MID] Implement Necrosynthesis --- .../src/mage/cards/n/Necrosynthesis.java | 105 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 106 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/n/Necrosynthesis.java diff --git a/Mage.Sets/src/mage/cards/n/Necrosynthesis.java b/Mage.Sets/src/mage/cards/n/Necrosynthesis.java new file mode 100644 index 00000000000..e3338e09093 --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/Necrosynthesis.java @@ -0,0 +1,105 @@ +package mage.cards.n; + +import mage.abilities.Ability; +import mage.abilities.common.DiesAttachedTriggeredAbility; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author ciaccona007 + */ +public final class Necrosynthesis extends CardImpl { + + public Necrosynthesis(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}"); + + this.subtype.add(SubType.AURA); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + Ability ability = new EnchantAbility(auraTarget.getTargetName()); + this.addAbility(ability); + + // Enchanted creature has "Whenever another creature dies, put a +1/+1 counter on this creature." + Effect effect = new GainAbilityAttachedEffect(new DiesCreatureTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), false), AttachmentType.AURA); + effect.setText("Enchanted creature has \"Whenever another creature dies, put a +1/+1 counter on this creature.\""); + this.addAbility(new SimpleStaticAbility(effect)); + + // When enchanted creature dies, look at the top X cards of your library, where X is its power. Put one of those cards into your hand and the rest on the bottom of your library in a random order. + DynamicValue attachedPower = new NecrosynthesisAttachedPermanentPowerCount(); + effect = new LookLibraryAndPickControllerEffect( + attachedPower, false, StaticValue.get(1), StaticFilters.FILTER_CARD, false, + false + ).setBackInRandomOrder(true); + effect.setText("look at the top X cards of your library, where X is its power. " + + "Put one of those cards into your hand and the rest on the bottom of your library in a random order"); + ability = new DiesAttachedTriggeredAbility(effect, "enchanted creature"); + this.addAbility(ability); + } + + private Necrosynthesis(final Necrosynthesis card) { + super(card); + } + + @Override + public Necrosynthesis copy() { + return new Necrosynthesis(this); + } +} + +class NecrosynthesisAttachedPermanentPowerCount implements DynamicValue { + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + Permanent attachmentPermanent = game.getPermanent(sourceAbility.getSourceId()); + if (attachmentPermanent == null) { + attachmentPermanent = (Permanent) game.getLastKnownInformation(sourceAbility.getSourceId(), Zone.BATTLEFIELD, sourceAbility.getSourceObjectZoneChangeCounter()); + } + if (attachmentPermanent != null && attachmentPermanent.getAttachedTo() != null) { + if (effect.getValue("attachedTo") != null) { + Permanent attached = (Permanent) effect.getValue("attachedTo"); + if (attached != null) { + return attached.getPower().getValue(); + } + } + } + return 0; + } + + @Override + public NecrosynthesisAttachedPermanentPowerCount copy() { + return new NecrosynthesisAttachedPermanentPowerCount(); + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return "its power"; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index eb97394bca3..523849b2716 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -212,6 +212,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Mystic Monstrosity", 256, Rarity.UNCOMMON, mage.cards.m.MysticMonstrosity.class)); cards.add(new SetCardInfo("Mystic Skull", 256, Rarity.UNCOMMON, mage.cards.m.MysticSkull.class)); cards.add(new SetCardInfo("Nebelgast Intruder", 64, Rarity.UNCOMMON, mage.cards.n.NebelgastIntruder.class)); + cards.add(new SetCardInfo("Necrosynthesis", 115, Rarity.UNCOMMON, mage.cards.n.Necrosynthesis.class)); cards.add(new SetCardInfo("Neonate's Rush", 151, Rarity.COMMON, mage.cards.n.NeonatesRush.class)); cards.add(new SetCardInfo("No Way Out", 116, Rarity.COMMON, mage.cards.n.NoWayOut.class)); cards.add(new SetCardInfo("Novice Occultist", 117, Rarity.COMMON, mage.cards.n.NoviceOccultist.class)); From e19abadee47ad67ff8d9209af42c600fddbb5d2b Mon Sep 17 00:00:00 2001 From: ciaccona007 Date: Wed, 15 Sep 2021 14:03:22 -0400 Subject: [PATCH 106/231] [MID] Implement Old Stickfingers --- .../src/mage/cards/o/OldStickfingers.java | 97 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 98 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/o/OldStickfingers.java diff --git a/Mage.Sets/src/mage/cards/o/OldStickfingers.java b/Mage.Sets/src/mage/cards/o/OldStickfingers.java new file mode 100644 index 00000000000..09c0cc178d3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OldStickfingers.java @@ -0,0 +1,97 @@ +package mage.cards.o; + +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CastSourceTriggeredAbility; +import mage.abilities.effects.common.continuous.SetPowerToughnessSourceEffect; +import mage.cards.*; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author ciaccona007 + */ +public final class OldStickfingers extends CardImpl { + + public OldStickfingers(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{X}{B}{G}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HORROR); + + // When you cast this spell, reveal cards from the top of your library until you reveal X creature cards. Put all the creature cards revealed this way into your graveyard and the rest on the bottom of your library in a random order. + this.addAbility(new CastSourceTriggeredAbility(new OldStickfingersEffect())); + + // Old Stickfingers' power and toughness are equal to the number of creature cards in your graveyard. + DynamicValue value = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURE); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetPowerToughnessSourceEffect(value, Duration.EndOfGame))); + } + + private OldStickfingers(final OldStickfingers card) { + super(card); + } + + @Override + public OldStickfingers copy() { + return new OldStickfingers(this); + } +} + +class OldStickfingersEffect extends OneShotEffect { + + public OldStickfingersEffect() { + super(Outcome.Discard); + this.staticText = "reveal cards from the top of your library until you reveal X creature cards. Put all the creature cards revealed this way into your graveyard and the rest on the bottom of your library in a random order"; + } + + public OldStickfingersEffect(final OldStickfingersEffect effect) { + super(effect); + } + + @Override + public OldStickfingersEffect copy() { + return new OldStickfingersEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Object obj = getValue(CastSourceTriggeredAbility.SOURCE_CAST_SPELL_ABILITY); + if (!(obj instanceof SpellAbility)) { + return false; + } + int xValue = ((SpellAbility) obj).getManaCostsToPay().getX(); + if (xValue < 1) { + return false; + } + Player controller = game.getPlayer(source.getControllerId()); + + Cards revealed = new CardsImpl(); + Cards otherCards = new CardsImpl(); + Set creatureCards = new LinkedHashSet<>(); + for (Card card : controller.getLibrary().getCards(game)) { + revealed.add(card); + if (card.isCreature(game)) { + creatureCards.add(card); + if(creatureCards.size() == xValue) { + break; + } + } else { + otherCards.add(card); + } + } + controller.revealCards(source, revealed, game); + controller.moveCards(creatureCards, Zone.GRAVEYARD, source, game); + controller.putCardsOnBottomOfLibrary(otherCards, game, source, false); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 523849b2716..3ba34f84bf0 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -218,6 +218,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Novice Occultist", 117, Rarity.COMMON, mage.cards.n.NoviceOccultist.class)); cards.add(new SetCardInfo("Obsessive Astronomer", 152, Rarity.UNCOMMON, mage.cards.o.ObsessiveAstronomer.class)); cards.add(new SetCardInfo("Odric's Outrider", 29, Rarity.UNCOMMON, mage.cards.o.OdricsOutrider.class)); + cards.add(new SetCardInfo("Old Stickfingers", 234, Rarity.RARE, mage.cards.o.OldStickfingers.class)); cards.add(new SetCardInfo("Olivia's Midnight Ambush", 118, Rarity.COMMON, mage.cards.o.OliviasMidnightAmbush.class)); cards.add(new SetCardInfo("Ominous Roost", 65, Rarity.UNCOMMON, mage.cards.o.OminousRoost.class)); cards.add(new SetCardInfo("Organ Hoarder", 66, Rarity.COMMON, mage.cards.o.OrganHoarder.class)); From 20fbde2860849cb661f82d127ae8a09c43293ad6 Mon Sep 17 00:00:00 2001 From: ciaccona007 Date: Wed, 15 Sep 2021 14:34:12 -0400 Subject: [PATCH 107/231] [MID] Implement Slogurk, the Overslime --- .../src/mage/cards/s/SlogurkTheOverslime.java | 65 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 66 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SlogurkTheOverslime.java diff --git a/Mage.Sets/src/mage/cards/s/SlogurkTheOverslime.java b/Mage.Sets/src/mage/cards/s/SlogurkTheOverslime.java new file mode 100644 index 00000000000..292337f839e --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SlogurkTheOverslime.java @@ -0,0 +1,65 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.LeavesBattlefieldTriggeredAbility; +import mage.abilities.common.PutCardIntoGraveFromAnywhereAllTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.RemoveCountersSourceCost; +import mage.abilities.effects.common.ReturnToHandSourceEffect; +import mage.abilities.effects.common.ReturnToHandTargetEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterLandCard; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author ciaccona007 + */ +public final class SlogurkTheOverslime extends CardImpl { + + public SlogurkTheOverslime(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}{U}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.OOZE); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Whenever a land card is put into your graveyard from anywhere, put a +1/+1 counter on Slogurk, the Overslime. + this.addAbility(new PutCardIntoGraveFromAnywhereAllTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), + false, StaticFilters.FILTER_CARD_LAND_A, TargetController.YOU + )); + + // Remove three +1/+1 counters from Slogurk: Return it to its owner's hand. + this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new ReturnToHandSourceEffect(), + new RemoveCountersSourceCost(CounterType.P1P1.createInstance(3)))); + + // When Slogurk leaves the battlefield, return up to three target land cards from your graveyard to your hand. + Ability ability = new LeavesBattlefieldTriggeredAbility(new ReturnToHandTargetEffect() + .setText("return up to three target land cards from your graveyard to your hand"), false); + ability.addTarget(new TargetCardInYourGraveyard( + 0, 3, new FilterLandCard("land cards from your graveyard"))); + this.addAbility(ability); + } + + private SlogurkTheOverslime(final SlogurkTheOverslime card) { + super(card); + } + + @Override + public SlogurkTheOverslime copy() { + return new SlogurkTheOverslime(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 3ba34f84bf0..70800252423 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -267,6 +267,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Silver Bolt", 258, Rarity.COMMON, mage.cards.s.SilverBolt.class)); cards.add(new SetCardInfo("Skaab Wrangler", 75, Rarity.UNCOMMON, mage.cards.s.SkaabWrangler.class)); cards.add(new SetCardInfo("Slaughter Specialist", 122, Rarity.RARE, mage.cards.s.SlaughterSpecialist.class)); + cards.add(new SetCardInfo("Slogurk, the Overslime", 242, Rarity.RARE, mage.cards.s.SlogurkTheOverslime.class)); cards.add(new SetCardInfo("Sludge Monster", 76, Rarity.RARE, mage.cards.s.SludgeMonster.class)); cards.add(new SetCardInfo("Smoldering Egg", 159, Rarity.RARE, mage.cards.s.SmolderingEgg.class)); cards.add(new SetCardInfo("Snarling Wolf", 199, Rarity.COMMON, mage.cards.s.SnarlingWolf.class)); From a2ebbda567907a54217a6c527f106c4555bbc441 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Wed, 15 Sep 2021 16:01:53 -0500 Subject: [PATCH 108/231] [MID] Implemented Rem Karolus, Stalwart Slayer --- .../cards/r/RemKarolusStalwartSlayer.java | 149 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 150 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RemKarolusStalwartSlayer.java diff --git a/Mage.Sets/src/mage/cards/r/RemKarolusStalwartSlayer.java b/Mage.Sets/src/mage/cards/r/RemKarolusStalwartSlayer.java new file mode 100644 index 00000000000..daf36be95e4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RemKarolusStalwartSlayer.java @@ -0,0 +1,149 @@ +package mage.cards.r; + +import java.util.Set; +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.PreventionEffectImpl; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.constants.*; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; +import mage.util.CardUtil; + +/** + * + * @author weirddan455 + */ +public final class RemKarolusStalwartSlayer extends CardImpl { + + public RemKarolusStalwartSlayer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}{W}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.KNIGHT); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // If a spell would deal damage to you or another permanent you control, prevent that damage. + this.addAbility(new SimpleStaticAbility(new RemKarolusStalwartSlayerPreventionEffect())); + + // If a spell would deal damage to an opponent or a permanent an opponent controls, it deals that much damage plus 1 instead. + this.addAbility(new SimpleStaticAbility(new RemKarolusStalwartSlayerReplacementEffect())); + } + + private RemKarolusStalwartSlayer(final RemKarolusStalwartSlayer card) { + super(card); + } + + @Override + public RemKarolusStalwartSlayer copy() { + return new RemKarolusStalwartSlayer(this); + } +} + +class RemKarolusStalwartSlayerPreventionEffect extends PreventionEffectImpl { + + public RemKarolusStalwartSlayerPreventionEffect() { + super(Duration.WhileOnBattlefield, Integer.MAX_VALUE, false, false); + staticText = "If a spell would deal damage to you or another permanent you control, prevent that damage"; + } + + private RemKarolusStalwartSlayerPreventionEffect(final RemKarolusStalwartSlayerPreventionEffect effect) { + super(effect); + } + + @Override + public RemKarolusStalwartSlayerPreventionEffect copy() { + return new RemKarolusStalwartSlayerPreventionEffect(this); + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + UUID targetId = event.getTargetId(); + if (targetId.equals(source.getSourceId())) { + return false; + } + UUID controllerId = source.getControllerId(); + if (!targetId.equals(controllerId)) { + Permanent permanent = game.getPermanent(targetId); + if (permanent == null || !permanent.isControlledBy(controllerId)) { + return false; + } + } + StackObject stackObject = game.getStack().getStackObject(event.getSourceId()); + if (stackObject == null) { + stackObject = (StackObject) game.getLastKnownInformation(event.getSourceId(), Zone.STACK); + } + if (stackObject instanceof Spell) { + return super.applies(event, source, game); + } + return false; + } +} + +class RemKarolusStalwartSlayerReplacementEffect extends ReplacementEffectImpl { + + public RemKarolusStalwartSlayerReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Damage); + staticText = "If a spell would deal damage to an opponent or a permanent an opponent controls, it deals that much damage plus 1 instead"; + } + + private RemKarolusStalwartSlayerReplacementEffect(final RemKarolusStalwartSlayerReplacementEffect effect) { + super(effect); + } + + @Override + public RemKarolusStalwartSlayerReplacementEffect copy() { + return new RemKarolusStalwartSlayerReplacementEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + event.setAmount(CardUtil.overflowInc(event.getAmount(), 1)); + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + switch(event.getType()) { + case DAMAGE_PERMANENT: + case DAMAGE_PLAYER: + return true; + default: + return false; + } + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + UUID targetId = event.getTargetId(); + Set opponents = game.getOpponents(source.getControllerId()); + if (!opponents.contains(targetId)) { + Permanent permanent = game.getPermanent(targetId); + if (permanent == null || !opponents.contains(permanent.getControllerId())) { + return false; + } + } + StackObject stackObject = game.getStack().getStackObject(event.getSourceId()); + if (stackObject == null) { + stackObject = (StackObject) game.getLastKnownInformation(event.getSourceId(), Zone.STACK); + } + return stackObject instanceof Spell; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 70800252423..bab56d7fdff 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -241,6 +241,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Purifying Dragon", 155, Rarity.UNCOMMON, mage.cards.p.PurifyingDragon.class)); cards.add(new SetCardInfo("Raze the Effigy", 156, Rarity.COMMON, mage.cards.r.RazeTheEffigy.class)); cards.add(new SetCardInfo("Reckless Stormseeker", 157, Rarity.RARE, mage.cards.r.RecklessStormseeker.class)); + cards.add(new SetCardInfo("Rem Karolus, Stalwart Slayer", 235, Rarity.RARE, mage.cards.r.RemKarolusStalwartSlayer.class)); cards.add(new SetCardInfo("Return to Nature", 195, Rarity.COMMON, mage.cards.r.ReturnToNature.class)); cards.add(new SetCardInfo("Revenge of the Drowned", 72, Rarity.COMMON, mage.cards.r.RevengeOfTheDrowned.class)); cards.add(new SetCardInfo("Rise of the Ants", 196, Rarity.UNCOMMON, mage.cards.r.RiseOfTheAnts.class)); From a44f43163430df82adcddcd04b59882772936252 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 08:42:33 -0400 Subject: [PATCH 109/231] [MID] fixed Sigarda, Champion of Light allowing controller to pick any card (fixes #8282) --- Mage.Sets/src/mage/cards/s/SigardaChampionOfLight.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/s/SigardaChampionOfLight.java b/Mage.Sets/src/mage/cards/s/SigardaChampionOfLight.java index 51b3ab9f0cd..7875c82d9c8 100644 --- a/Mage.Sets/src/mage/cards/s/SigardaChampionOfLight.java +++ b/Mage.Sets/src/mage/cards/s/SigardaChampionOfLight.java @@ -29,7 +29,7 @@ public final class SigardaChampionOfLight extends CardImpl { private static final FilterCard filter2 = new FilterCreatureCard("Human creature card"); static { - filter.add(SubType.HUMAN.getPredicate()); + filter2.add(SubType.HUMAN.getPredicate()); } public SigardaChampionOfLight(UUID ownerId, CardSetInfo setInfo) { From 6a174d58cf8ca941a395c6ea5da5830749dff1ae Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 08:44:42 -0400 Subject: [PATCH 110/231] [MID] fixed Kyler, Sigardian Emissary boosting opponent's creatures (fixes #8281) --- Mage.Sets/src/mage/cards/k/KylerSigardianEmissary.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Mage.Sets/src/mage/cards/k/KylerSigardianEmissary.java b/Mage.Sets/src/mage/cards/k/KylerSigardianEmissary.java index cac51bcf28e..1bab0e90675 100644 --- a/Mage.Sets/src/mage/cards/k/KylerSigardianEmissary.java +++ b/Mage.Sets/src/mage/cards/k/KylerSigardianEmissary.java @@ -11,10 +11,7 @@ import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.hint.Hint; 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.constants.*; import mage.counters.Counter; import mage.counters.CounterType; import mage.filter.FilterPermanent; @@ -38,6 +35,7 @@ public final class KylerSigardianEmissary extends CardImpl { static { filter.add(AnotherPredicate.instance); filter2.add(AnotherPredicate.instance); + filter2.add(TargetController.YOU.getControllerPredicate()); } public KylerSigardianEmissary(UUID ownerId, CardSetInfo setInfo) { From 244cacfe3b4ed28a38c576c670379c1e208dd1d2 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 09:09:41 -0400 Subject: [PATCH 111/231] [MIC] Implemented Sigardian Zealot --- .../src/mage/cards/s/SigardasVanguard.java | 61 ++++++------ .../src/mage/cards/s/SigardianZealot.java | 93 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + .../TargetCreaturesWithDifferentPowers.java | 53 +++++++++++ 4 files changed, 174 insertions(+), 34 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/s/SigardianZealot.java create mode 100644 Mage/src/main/java/mage/target/common/TargetCreaturesWithDifferentPowers.java diff --git a/Mage.Sets/src/mage/cards/s/SigardasVanguard.java b/Mage.Sets/src/mage/cards/s/SigardasVanguard.java index 6e6ef33732e..848778c0dce 100644 --- a/Mage.Sets/src/mage/cards/s/SigardasVanguard.java +++ b/Mage.Sets/src/mage/cards/s/SigardasVanguard.java @@ -1,9 +1,9 @@ package mage.cards.s; import mage.MageInt; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.abilities.hint.common.CovenHint; import mage.abilities.keyword.DoubleStrikeAbility; @@ -11,15 +11,17 @@ import mage.abilities.keyword.FlashAbility; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.cards.CardsImpl; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.Outcome; import mage.constants.SubType; -import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.target.common.TargetCreaturePermanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturesWithDifferentPowers; +import mage.target.targetpointer.FixedTargets; -import java.util.Objects; import java.util.UUID; /** @@ -41,14 +43,7 @@ public final class SigardasVanguard extends CardImpl { this.addAbility(FlyingAbility.getInstance()); // Whenever Sigarda's Vanguard enters the battlefield or attacks, choose any number of creatures with different powers. Those creatures gain double strike until end of turn. - Ability ability = new EntersBattlefieldOrAttacksSourceTriggeredAbility( - new GainAbilityTargetEffect( - DoubleStrikeAbility.getInstance(), Duration.EndOfTurn - ).setText("choose any number of creatures with different powers. " + - "Those creatures gain double strike until end of turn") - ); - ability.addTarget(new SigardasVanguardTarget()); - this.addAbility(ability.addHint(CovenHint.instance)); + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new SigardasVanguardEffect()).addHint(CovenHint.instance)); } private SigardasVanguard(final SigardasVanguard card) { @@ -61,39 +56,37 @@ public final class SigardasVanguard extends CardImpl { } } -class SigardasVanguardTarget extends TargetCreaturePermanent { +class SigardasVanguardEffect extends OneShotEffect { - private static final FilterCreaturePermanent filter - = new FilterCreaturePermanent("creatures with different powers"); - - SigardasVanguardTarget() { - super(0, Integer.MAX_VALUE, filter, false); + SigardasVanguardEffect() { + super(Outcome.Benefit); + staticText = "choose any number of creatures with different powers. " + + "Those creatures gain double strike until end of turn"; } - private SigardasVanguardTarget(final SigardasVanguardTarget target) { - super(target); + private SigardasVanguardEffect(final SigardasVanguardEffect effect) { + super(effect); } @Override - public SigardasVanguardTarget copy() { - return new SigardasVanguardTarget(this); + public SigardasVanguardEffect copy() { + return new SigardasVanguardEffect(this); } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { return false; } - Permanent creature = game.getPermanent(id); - if (creature == null) { + TargetPermanent target = new TargetCreaturesWithDifferentPowers(); + player.choose(outcome, target, source.getSourceId(), game); + if (target.getTargets().isEmpty()) { return false; } - return this.getTargets() - .stream() - .map(game::getPermanent) - .filter(Objects::nonNull) - .map(MageObject::getPower) - .mapToInt(MageInt::getValue) - .noneMatch(p -> creature.getPower().getValue() == p); + game.addEffect(new GainAbilityTargetEffect( + DoubleStrikeAbility.getInstance(), Duration.EndOfTurn + ).setTargetPointer(new FixedTargets(new CardsImpl(target.getTargets()), game)), source); + return true; } } diff --git a/Mage.Sets/src/mage/cards/s/SigardianZealot.java b/Mage.Sets/src/mage/cards/s/SigardianZealot.java new file mode 100644 index 00000000000..8a1ba21af81 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SigardianZealot.java @@ -0,0 +1,93 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfCombatTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.*; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturesWithDifferentPowers; +import mage.target.targetpointer.FixedTargets; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SigardianZealot extends CardImpl { + + public SigardianZealot(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // At the beginning of combat on your turn, choose any number of creatures with different powers. Each of them gets +X/+X and gains vigilance until end of turn, where X is Sigardian Zealot's power. + this.addAbility(new BeginningOfCombatTriggeredAbility( + new SigardianZealotEffect(), TargetController.YOU, false + )); + } + + private SigardianZealot(final SigardianZealot card) { + super(card); + } + + @Override + public SigardianZealot copy() { + return new SigardianZealot(this); + } +} + +class SigardianZealotEffect extends OneShotEffect { + + SigardianZealotEffect() { + super(Outcome.Benefit); + staticText = "choose any number of creatures with different powers. " + + "Each of them gets +X/+X and gains vigilance until end of turn, where X is {this}'s power"; + } + + private SigardianZealotEffect(final SigardianZealotEffect effect) { + super(effect); + } + + @Override + public SigardianZealotEffect copy() { + return new SigardianZealotEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = source.getSourcePermanentOrLKI(game); + if (player == null || permanent == null) { + return false; + } + int power = permanent.getPower().getValue(); + if (power == 0) { + return false; + } + TargetPermanent target = new TargetCreaturesWithDifferentPowers(); + player.choose(outcome, target, source.getSourceId(), game); + Cards cards = new CardsImpl(target.getTargets()); + if (cards.isEmpty()) { + return false; + } + game.addEffect(new BoostTargetEffect(power, power).setTargetPointer(new FixedTargets(cards, game)), source); + game.addEffect(new GainAbilityTargetEffect( + VigilanceAbility.getInstance(), Duration.EndOfTurn + ).setTargetPointer(new FixedTargets(cards, game)), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 10cb23af0f5..31c501a21b7 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -112,6 +112,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Shamanic Revelation", 143, Rarity.RARE, mage.cards.s.ShamanicRevelation.class)); cards.add(new SetCardInfo("Sigarda's Vanguard", 8, Rarity.RARE, mage.cards.s.SigardasVanguard.class)); cards.add(new SetCardInfo("Sigarda, Heron's Grace", 155, Rarity.MYTHIC, mage.cards.s.SigardaHeronsGrace.class)); + cards.add(new SetCardInfo("Sigardian Zealot", 29, Rarity.RARE, mage.cards.s.SigardianZealot.class)); cards.add(new SetCardInfo("Sky Diamond", 161, Rarity.COMMON, mage.cards.s.SkyDiamond.class)); cards.add(new SetCardInfo("Sol Ring", 162, Rarity.UNCOMMON, mage.cards.s.SolRing.class)); cards.add(new SetCardInfo("Somberwald Beastmaster", 30, Rarity.RARE, mage.cards.s.SomberwaldBeastmaster.class)); diff --git a/Mage/src/main/java/mage/target/common/TargetCreaturesWithDifferentPowers.java b/Mage/src/main/java/mage/target/common/TargetCreaturesWithDifferentPowers.java new file mode 100644 index 00000000000..121b2e8ca52 --- /dev/null +++ b/Mage/src/main/java/mage/target/common/TargetCreaturesWithDifferentPowers.java @@ -0,0 +1,53 @@ +package mage.target.common; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; + +import java.util.Objects; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class TargetCreaturesWithDifferentPowers extends TargetPermanent { + + private static final FilterPermanent filter + = new FilterCreaturePermanent("creatures with different powers"); + + public TargetCreaturesWithDifferentPowers() { + super(0, Integer.MAX_VALUE, filter, false); + } + + private TargetCreaturesWithDifferentPowers(final TargetCreaturesWithDifferentPowers target) { + super(target); + } + + @Override + public TargetCreaturesWithDifferentPowers copy() { + return new TargetCreaturesWithDifferentPowers(this); + } + + @Override + public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + if (!super.canTarget(controllerId, id, source, game)) { + return false; + } + Permanent creature = game.getPermanent(id); + if (creature == null) { + return false; + } + return this.getTargets() + .stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .noneMatch(p -> creature.getPower().getValue() == p); + } +} From 645779376fee846f9026e0d14265622759eb0477 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 09:34:22 -0400 Subject: [PATCH 112/231] [MIC] Implemented Visions of Glory --- Mage.Sets/src/mage/cards/s/StingingStudy.java | 2 +- .../src/mage/cards/v/VisionsOfGlory.java | 43 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + .../src/main/java/mage/abilities/Ability.java | 2 +- .../main/java/mage/abilities/AbilityImpl.java | 3 +- .../CommanderManaValueAdjuster.java | 35 +++++++++++++++ .../abilities/keyword/FlashbackAbility.java | 3 +- .../java/mage/game/stack/StackAbility.java | 3 +- 8 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/v/VisionsOfGlory.java create mode 100644 Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java diff --git a/Mage.Sets/src/mage/cards/s/StingingStudy.java b/Mage.Sets/src/mage/cards/s/StingingStudy.java index ccee4735dec..f70ead86aee 100644 --- a/Mage.Sets/src/mage/cards/s/StingingStudy.java +++ b/Mage.Sets/src/mage/cards/s/StingingStudy.java @@ -68,7 +68,7 @@ class StingingStudyEffect extends OneShotEffect { for (Card commander : game.getCommanderCardsFromAnyZones(player, CommanderCardType.ANY, Zone.BATTLEFIELD, Zone.COMMAND)) { manaValues.add(commander.getManaValue()); } - int chosenValue = 0; + int chosenValue; if (manaValues.size() > 1) { Choice choice = new ChoiceImpl(true); choice.setChoices(manaValues.stream().map(x -> "" + x).collect(Collectors.toSet())); diff --git a/Mage.Sets/src/mage/cards/v/VisionsOfGlory.java b/Mage.Sets/src/mage/cards/v/VisionsOfGlory.java new file mode 100644 index 00000000000..5115f463d53 --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VisionsOfGlory.java @@ -0,0 +1,43 @@ +package mage.cards.v; + +import mage.abilities.costs.costadjusters.CommanderManaValueAdjuster; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.CreaturesYouControlCount; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.HumanToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VisionsOfGlory extends CardImpl { + + public VisionsOfGlory(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{W}"); + + // Create a 1/1 white Human creature token for each creature you control. + this.getSpellAbility().addEffect(new CreateTokenEffect( + new HumanToken(), CreaturesYouControlCount.instance + ).setText("create a 1/1 white Human creature token for each creature you control")); + + // Flashback {8}{W}{W}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone. + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{8}{W}{W}")) + .setAbilityName("This spell costs {X} less to cast this way, where X is the greatest mana value " + + "of a commander you own on the battlefield or in the command zone.") + .setCostAdjuster(CommanderManaValueAdjuster.instance)); + } + + private VisionsOfGlory(final VisionsOfGlory card) { + super(card); + } + + @Override + public VisionsOfGlory copy() { + return new VisionsOfGlory(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 31c501a21b7..9c95ebee01b 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -137,6 +137,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Undead Augur", 130, Rarity.UNCOMMON, mage.cards.u.UndeadAugur.class)); cards.add(new SetCardInfo("Verdurous Gearhulk", 145, Rarity.MYTHIC, mage.cards.v.VerdurousGearhulk.class)); cards.add(new SetCardInfo("Victory's Envoy", 96, Rarity.RARE, mage.cards.v.VictorysEnvoy.class)); + cards.add(new SetCardInfo("Visions of Glory", 32, Rarity.RARE, mage.cards.v.VisionsOfGlory.class)); cards.add(new SetCardInfo("Wild Beastmaster", 146, Rarity.RARE, mage.cards.w.WildBeastmaster.class)); cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); cards.add(new SetCardInfo("Yavimaya Elder", 147, Rarity.COMMON, mage.cards.y.YavimayaElder.class)); diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index 1af6b5e8f81..63cb477b0c7 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -543,7 +543,7 @@ public interface Ability extends Controllable, Serializable { void adjustTargets(Game game); - void setCostAdjuster(CostAdjuster costAdjuster); + Ability setCostAdjuster(CostAdjuster costAdjuster); CostAdjuster getCostAdjuster(); diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index dceaba8b20a..5b07c7502e5 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1323,8 +1323,9 @@ public abstract class AbilityImpl implements Ability { * @param costAdjuster */ @Override - public void setCostAdjuster(CostAdjuster costAdjuster) { + public AbilityImpl setCostAdjuster(CostAdjuster costAdjuster) { this.costAdjuster = costAdjuster; + return this; } @Override diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java new file mode 100644 index 00000000000..df1c58ca0ad --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java @@ -0,0 +1,35 @@ +package mage.abilities.costs.costadjusters; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.costs.CostAdjuster; +import mage.constants.CommanderCardType; +import mage.constants.Zone; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +/** + * @author TheElk801 + */ +public enum CommanderManaValueAdjuster implements CostAdjuster { + instance; + + @Override + public void adjustCosts(Ability ability, Game game) { + Player player = game.getPlayer(ability.getControllerId()); + if (player == null) { + return; + } + int maxValue = game + .getCommanderCardsFromAnyZones( + player, CommanderCardType.ANY, + Zone.BATTLEFIELD, Zone.COMMAND + ) + .stream() + .mapToInt(MageObject::getManaValue) + .max() + .orElse(0); + CardUtil.reduceCost(ability, maxValue); + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java index 6347b1ef2dc..22ca67bc198 100644 --- a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java @@ -177,8 +177,9 @@ public class FlashbackAbility extends SpellAbility { * * @param abilityName */ - public void setAbilityName(String abilityName) { + public FlashbackAbility setAbilityName(String abilityName) { this.abilityName = abilityName; + return this; } } diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index 98df1330603..e9493bcb317 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -664,8 +664,9 @@ public class StackAbility extends StackObjectImpl implements Ability { } @Override - public void setCostAdjuster(CostAdjuster costAdjuster) { + public StackAbility setCostAdjuster(CostAdjuster costAdjuster) { this.costAdjuster = costAdjuster; + return this; } @Override From 9e864facd3c74a27c4c91c968ee1fbb326617c90 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 09:38:04 -0400 Subject: [PATCH 113/231] [MIC] Implemented Visions of Duplicity --- .../src/mage/cards/v/VisionsOfDuplicity.java | 47 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 48 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/v/VisionsOfDuplicity.java diff --git a/Mage.Sets/src/mage/cards/v/VisionsOfDuplicity.java b/Mage.Sets/src/mage/cards/v/VisionsOfDuplicity.java new file mode 100644 index 00000000000..5e9a9ac27fb --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VisionsOfDuplicity.java @@ -0,0 +1,47 @@ +package mage.cards.v; + +import mage.abilities.costs.costadjusters.CommanderManaValueAdjuster; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.ExchangeControlTargetEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VisionsOfDuplicity extends CardImpl { + + public VisionsOfDuplicity(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{U}"); + + // Exchange control of two target creatures you don't control. + this.getSpellAbility().addEffect(new ExchangeControlTargetEffect( + Duration.EndOfGame, "exchange control of two target creatures you don't control" + )); + this.getSpellAbility().addTarget(new TargetPermanent( + 2, StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL + )); + + // Flashback {8}{U}{U}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone. + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{8}{U}{U}")) + .setAbilityName("This spell costs {X} less to cast this way, where X is the greatest mana value " + + "of a commander you own on the battlefield or in the command zone.") + .setCostAdjuster(CommanderManaValueAdjuster.instance)); + } + + private VisionsOfDuplicity(final VisionsOfDuplicity card) { + super(card); + } + + @Override + public VisionsOfDuplicity copy() { + return new VisionsOfDuplicity(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 9c95ebee01b..52722f629ad 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -137,6 +137,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Undead Augur", 130, Rarity.UNCOMMON, mage.cards.u.UndeadAugur.class)); cards.add(new SetCardInfo("Verdurous Gearhulk", 145, Rarity.MYTHIC, mage.cards.v.VerdurousGearhulk.class)); cards.add(new SetCardInfo("Victory's Envoy", 96, Rarity.RARE, mage.cards.v.VictorysEnvoy.class)); + cards.add(new SetCardInfo("Visions of Duplicity", 33, Rarity.RARE, mage.cards.v.VisionsOfDuplicity.class)); cards.add(new SetCardInfo("Visions of Glory", 32, Rarity.RARE, mage.cards.v.VisionsOfGlory.class)); cards.add(new SetCardInfo("Wild Beastmaster", 146, Rarity.RARE, mage.cards.w.WildBeastmaster.class)); cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); From e11c5fc5c82934d3e9c815acfcf64b6ccb37a630 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 09:40:37 -0400 Subject: [PATCH 114/231] [MIC] Implemented Stalwart Pathlighter --- .../src/mage/cards/s/StalwartPathlighter.java | 53 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 54 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StalwartPathlighter.java diff --git a/Mage.Sets/src/mage/cards/s/StalwartPathlighter.java b/Mage.Sets/src/mage/cards/s/StalwartPathlighter.java new file mode 100644 index 00000000000..97185c634fc --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StalwartPathlighter.java @@ -0,0 +1,53 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.BeginningOfCombatTriggeredAbility; +import mage.abilities.condition.common.CovenCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.hint.common.CovenHint; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StalwartPathlighter extends CardImpl { + + public StalwartPathlighter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Coven — At the beginning of combat on your turn, if you control three or more creatures with different powers, creatures you control gain indestructible until end of turn. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new BeginningOfCombatTriggeredAbility(new GainAbilityControlledEffect( + IndestructibleAbility.getInstance(), Duration.EndOfTurn, + StaticFilters.FILTER_CONTROLLED_CREATURE + ), TargetController.YOU, false), CovenCondition.instance, "At the beginning " + + "of combat on your turn, if you control three or more creatures with different powers, " + + "creatures you control gain indestructible until end of turn." + ).addHint(CovenHint.instance).setAbilityWord(AbilityWord.COVEN)); + } + + private StalwartPathlighter(final StalwartPathlighter card) { + super(card); + } + + @Override + public StalwartPathlighter copy() { + return new StalwartPathlighter(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 52722f629ad..14cd84472bf 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -118,6 +118,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Somberwald Beastmaster", 30, Rarity.RARE, mage.cards.s.SomberwaldBeastmaster.class)); cards.add(new SetCardInfo("Somberwald Sage", 144, Rarity.RARE, mage.cards.s.SomberwaldSage.class)); cards.add(new SetCardInfo("Spark Reaper", 128, Rarity.COMMON, mage.cards.s.SparkReaper.class)); + cards.add(new SetCardInfo("Stalwart Pathlighter", 9, Rarity.RARE, mage.cards.s.StalwartPathlighter.class)); cards.add(new SetCardInfo("Stitcher Geralf", 104, Rarity.MYTHIC, mage.cards.s.StitcherGeralf.class)); cards.add(new SetCardInfo("Sungrass Prairie", 181, Rarity.RARE, mage.cards.s.SungrassPrairie.class)); cards.add(new SetCardInfo("Sunken Hollow", 182, Rarity.RARE, mage.cards.s.SunkenHollow.class)); From a1c067a8fbb05a4ab5aff954625116cc5ef06b35 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 09:56:53 -0400 Subject: [PATCH 115/231] [MIC] Implemented Celestial Judgment --- .../src/mage/cards/c/CelestialJudgment.java | 102 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 103 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CelestialJudgment.java diff --git a/Mage.Sets/src/mage/cards/c/CelestialJudgment.java b/Mage.Sets/src/mage/cards/c/CelestialJudgment.java new file mode 100644 index 00000000000..5ac6057ad24 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CelestialJudgment.java @@ -0,0 +1,102 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Outcome; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author TheElk801 + */ +public final class CelestialJudgment extends CardImpl { + + public CelestialJudgment(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{W}{W}"); + + // For each different power among creatures on the battlefield, choose a creature with that power. Destroy each creature not chosen this way. + this.getSpellAbility().addEffect(new CelestialJudgmentEffect()); + } + + private CelestialJudgment(final CelestialJudgment card) { + super(card); + } + + @Override + public CelestialJudgment copy() { + return new CelestialJudgment(this); + } +} + +class CelestialJudgmentEffect extends OneShotEffect { + + CelestialJudgmentEffect() { + super(Outcome.Benefit); + staticText = "for each different power among creatures on the battlefield, " + + "choose a creature with that power. Destroy each creature not chosen this way"; + } + + private CelestialJudgmentEffect(final CelestialJudgmentEffect effect) { + super(effect); + } + + @Override + public CelestialJudgmentEffect copy() { + return new CelestialJudgmentEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + List permanents = game + .getBattlefield() + .getActivePermanents( + StaticFilters.FILTER_PERMANENT_CREATURE, + source.getControllerId(), source.getSourceId(), game + ); + Map> powerMap = permanents + .stream() + .collect(Collectors.toMap( + permanent -> permanent.getPower().getValue(), + permanent -> Arrays.asList(permanent), + (a1, a2) -> { + a1.addAll(a2); + return a1; + })); + Set toKeep = new HashSet<>(); + for (Map.Entry> entry : powerMap.entrySet()) { + if (entry.getValue().size() == 1) { + toKeep.add(entry.getValue().get(0).getId()); + continue; + } + FilterPermanent filter = new FilterCreaturePermanent("creature with power " + entry.getKey() + " to save"); + filter.add(new PowerPredicate(ComparisonType.EQUAL_TO, entry.getKey())); + TargetPermanent target = new TargetPermanent(filter); + target.setNotTarget(true); + player.choose(outcome, target, source.getSourceId(), game); + toKeep.add(target.getFirstTarget()); + } + for (Permanent permanent : permanents) { + if (!toKeep.contains(permanent.getId())) { + permanent.destroy(source, game); + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 14cd84472bf..3365f86286d 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -36,6 +36,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Butcher of Malakir", 107, Rarity.RARE, mage.cards.b.ButcherOfMalakir.class)); cards.add(new SetCardInfo("Canopy Vista", 168, Rarity.RARE, mage.cards.c.CanopyVista.class)); cards.add(new SetCardInfo("Celebrate the Harvest", 24, Rarity.RARE, mage.cards.c.CelebrateTheHarvest.class)); + cards.add(new SetCardInfo("Celestial Judgment", 5, Rarity.RARE, mage.cards.c.CelestialJudgment.class)); cards.add(new SetCardInfo("Cemetery Reaper", 108, Rarity.RARE, mage.cards.c.CemeteryReaper.class)); cards.add(new SetCardInfo("Champion of Lambholt", 136, Rarity.RARE, mage.cards.c.ChampionOfLambholt.class)); cards.add(new SetCardInfo("Charcoal Diamond", 158, Rarity.COMMON, mage.cards.c.CharcoalDiamond.class)); From 025395788c565111440f849705bac8986262ed30 Mon Sep 17 00:00:00 2001 From: jeffwadsworth Date: Thu, 16 Sep 2021 11:05:32 -0500 Subject: [PATCH 116/231] - Fixed #8165. There are other cards that handle exiling sequentially, but this is a refactoring starting point. --- Mage.Sets/src/mage/cards/t/TaintedPact.java | 2 ++ .../abilities/keyword/CascadeAbility.java | 36 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Mage.Sets/src/mage/cards/t/TaintedPact.java b/Mage.Sets/src/mage/cards/t/TaintedPact.java index 5213e246034..5027296f817 100644 --- a/Mage.Sets/src/mage/cards/t/TaintedPact.java +++ b/Mage.Sets/src/mage/cards/t/TaintedPact.java @@ -67,7 +67,9 @@ class TaintedPactEffect extends OneShotEffect { && controller.getLibrary().hasCards()) { Card card = controller.getLibrary().getFromTop(game); if (card != null) { + // the card move is sequential, not all at once. controller.moveCards(card, Zone.EXILED, source, game); + game.getState().processAction(game); // Laelia, the Blade Reforged // Checks if there was already exiled a card with the same name if (names.contains(card.getName())) { break; diff --git a/Mage/src/main/java/mage/abilities/keyword/CascadeAbility.java b/Mage/src/main/java/mage/abilities/keyword/CascadeAbility.java index ae1c543c08c..3e459f37658 100644 --- a/Mage/src/main/java/mage/abilities/keyword/CascadeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/CascadeAbility.java @@ -20,23 +20,27 @@ import java.util.List; import java.util.stream.Collectors; /** - * Cascade - * A keyword ability that may let a player cast a random extra spell for no cost. See rule 702.84, “Cascade.” + * Cascade A keyword ability that may let a player cast a random extra spell for + * no cost. See rule 702.84, “Cascade.” *

* 702.84. Cascade *

- * 702.84a Cascade is a triggered ability that functions only while the spell with cascade is on the stack. - * “Cascade” means “When you cast this spell, exile cards from the top of your library until you exile a - * nonland card whose converted mana cost is less than this spell’s converted mana cost. You may cast that - * card without paying its mana cost. Then put all cards exiled this way that weren’t cast on the bottom - * of your library in a random order.” + * 702.84a Cascade is a triggered ability that functions only while the spell + * with cascade is on the stack. “Cascade” means “When you cast this spell, + * exile cards from the top of your library until you exile a nonland card whose + * converted mana cost is less than this spell’s converted mana cost. You may + * cast that card without paying its mana cost. Then put all cards exiled this + * way that weren’t cast on the bottom of your library in a random order.” *

- * 702.84b If an effect allows a player to take an action with one or more of the exiled cards “as you cascade,” - * the player may take that action after they have finished exiling cards due to the cascade ability. This action - * is taken before choosing whether to cast the last exiled card or, if no appropriate card was exiled, before - * putting the exiled cards on the bottom of their library in a random order. + * 702.84b If an effect allows a player to take an action with one or more of + * the exiled cards “as you cascade,” the player may take that action after they + * have finished exiling cards due to the cascade ability. This action is taken + * before choosing whether to cast the last exiled card or, if no appropriate + * card was exiled, before putting the exiled cards on the bottom of their + * library in a random order. *

- * 702.84c If a spell has multiple instances of cascade, each triggers separately. + * 702.84c If a spell has multiple instances of cascade, each triggers + * separately. * * @author BetaSteward_at_googlemail.com */ @@ -46,7 +50,6 @@ public class CascadeAbility extends TriggeredAbilityImpl { // can't use singletone due rules: // 702.84c If a spell has multiple instances of cascade, each triggers separately. - private static final String REMINDERTEXT = " (When you cast this spell, " + "exile cards from the top of your library until you exile a " + "nonland card whose mana value is less than this spell's mana value. " @@ -124,12 +127,15 @@ class CascadeEffect extends OneShotEffect { Card cardToCast = null; for (Card card : controller.getLibrary().getCards(game)) { cardsToExile.add(card); - if (!card.isLand(game) && card.getManaValue() < sourceCost) { + // the card move is sequential, not all at once. + controller.moveCards(card, Zone.EXILED, source, game); + game.getState().processAction(game); // Laelia, the Blade Reforged + if (!card.isLand(game) + && card.getManaValue() < sourceCost) { cardToCast = card; break; } } - controller.moveCards(cardsToExile, Zone.EXILED, source, game); controller.getLibrary().reset(); // set back empty draw state if that caused an empty draw // additional replacement effect: As you cascade, you may put a land card from among the exiled cards onto the battlefield tapped From 9afa2ce4d053216fb1ebb84850960c8db3a5024c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 17:19:56 -0400 Subject: [PATCH 117/231] [MIC] Implemented Crowded Crypt --- Mage.Sets/src/mage/cards/c/CrowdedCrypt.java | 61 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 62 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CrowdedCrypt.java diff --git a/Mage.Sets/src/mage/cards/c/CrowdedCrypt.java b/Mage.Sets/src/mage/cards/c/CrowdedCrypt.java new file mode 100644 index 00000000000..e5bc3fcd83b --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CrowdedCrypt.java @@ -0,0 +1,61 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CountersSourceCount; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.mana.BlackManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.permanent.token.ZombieDecayedToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CrowdedCrypt extends CardImpl { + + private static final DynamicValue xValue = new CountersSourceCount(CounterType.CORPSE); + + public CrowdedCrypt(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{B}"); + + // {T}: Add {B}. + this.addAbility(new BlackManaAbility()); + + // Whenever a creature you control dies, put a corpse counter on Crowded Crypt. + this.addAbility(new DiesCreatureTriggeredAbility( + new AddCountersSourceEffect(CounterType.CORPSE.createInstance()), + false, StaticFilters.FILTER_CONTROLLED_A_CREATURE + )); + + // {4}{B}{B}, {T}, Sacrifice Crowded Crypt: Create a 2/2 black Zombie creature token with decayed for each corpse counter on Crowded Crypt. + Ability ability = new SimpleActivatedAbility( + new CreateTokenEffect(new ZombieDecayedToken(), xValue) + .setText("create a 2/2 black Zombie creature token with decayed for each corpse counter on {this}"), + new ManaCostsImpl<>("{4}{B}{B}") + ); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeSourceCost()); + this.addAbility(ability); + } + + private CrowdedCrypt(final CrowdedCrypt card) { + super(card); + } + + @Override + public CrowdedCrypt copy() { + return new CrowdedCrypt(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 3365f86286d..f5fb60237b5 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -46,6 +46,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Command Tower", 170, Rarity.COMMON, mage.cards.c.CommandTower.class)); cards.add(new SetCardInfo("Commander's Sphere", 159, Rarity.COMMON, mage.cards.c.CommandersSphere.class)); cards.add(new SetCardInfo("Corpse Augur", 109, Rarity.UNCOMMON, mage.cards.c.CorpseAugur.class)); + cards.add(new SetCardInfo("Crowded Crypt", 17, Rarity.RARE, mage.cards.c.CrowdedCrypt.class)); cards.add(new SetCardInfo("Custodi Soulbinders", 83, Rarity.RARE, mage.cards.c.CustodiSoulbinders.class)); cards.add(new SetCardInfo("Dark Salvation", 110, Rarity.RARE, mage.cards.d.DarkSalvation.class)); cards.add(new SetCardInfo("Darkwater Catacombs", 171, Rarity.RARE, mage.cards.d.DarkwaterCatacombs.class)); From 0b67caab8903c42ea46acb55e4d98d3a6c075d8c Mon Sep 17 00:00:00 2001 From: jeffwadsworth Date: Thu, 16 Sep 2021 16:53:32 -0500 Subject: [PATCH 118/231] - Fixed #8125 --- Mage.Sets/src/mage/cards/c/CalixDestinysHand.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/cards/c/CalixDestinysHand.java b/Mage.Sets/src/mage/cards/c/CalixDestinysHand.java index 767f3873d6e..689f1617f5e 100644 --- a/Mage.Sets/src/mage/cards/c/CalixDestinysHand.java +++ b/Mage.Sets/src/mage/cards/c/CalixDestinysHand.java @@ -29,6 +29,7 @@ import java.util.UUID; import java.util.stream.Collectors; import static mage.constants.Outcome.Benefit; +import mage.util.CardUtil; /** * @author TheElk801 @@ -105,21 +106,24 @@ class CalixDestinysHandExileEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); + Player controller = game.getPlayer(source.getControllerId()); if (source.getTargets().size() > 2) { return false; } source.getTargets(); Permanent theirPerm = game.getPermanent(source.getTargets().get(0).getFirstTarget()); Permanent myPerm = game.getPermanent(source.getTargets().get(1).getFirstTarget()); - if (player == null || theirPerm == null || myPerm == null) { + if (controller == null + || theirPerm == null + || myPerm == null) { return false; } MageObjectReference theirMor = new MageObjectReference( theirPerm.getId(), theirPerm.getZoneChangeCounter(game) + 1, game ); MageObjectReference myMor = new MageObjectReference(myPerm, game); - player.moveCards(theirPerm, Zone.EXILED, source, game); + UUID exileId = CardUtil.getExileZoneId(game, source); + controller.moveCardsToExile(theirPerm, source, game, true, exileId, myPerm.getLogName()); game.addDelayedTriggeredAbility(new CalixDestinysHandDelayedTriggeredAbility(theirMor, myMor), source); return true; } From 10eafcdcd311f932d21be0a7a35547a80848915a Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 18:55:35 -0400 Subject: [PATCH 119/231] [MID] Implemented Gisa, Glorious Resurrector --- .../mage/cards/g/GisaGloriousResurrector.java | 146 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 147 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/g/GisaGloriousResurrector.java diff --git a/Mage.Sets/src/mage/cards/g/GisaGloriousResurrector.java b/Mage.Sets/src/mage/cards/g/GisaGloriousResurrector.java new file mode 100644 index 00000000000..cd7b87e4a31 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GisaGloriousResurrector.java @@ -0,0 +1,146 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.DecayedAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.ExileZone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.PermanentToken; +import mage.players.Player; +import mage.target.targetpointer.FixedTargets; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GisaGloriousResurrector extends CardImpl { + + public GisaGloriousResurrector(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // If a creature an opponent controls would die, exile it instead. + this.addAbility(new SimpleStaticAbility(new GisaGloriousResurrectorExileEffect())); + + // At the beginning of your upkeep, put all creature cards exiled with Gisa, Glorious Resurrector onto the battlefield under your control. They gain decayed. + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new GisaGloriousResurrectorReturnEffect(), TargetController.YOU, false + )); + } + + private GisaGloriousResurrector(final GisaGloriousResurrector card) { + super(card); + } + + @Override + public GisaGloriousResurrector copy() { + return new GisaGloriousResurrector(this); + } +} + +class GisaGloriousResurrectorExileEffect extends ReplacementEffectImpl { + + GisaGloriousResurrectorExileEffect() { + super(Duration.WhileOnBattlefield, Outcome.Exile); + staticText = "if a creature an opponent controls would die, exile it instead"; + } + + private GisaGloriousResurrectorExileEffect(final GisaGloriousResurrectorExileEffect effect) { + super(effect); + } + + @Override + public GisaGloriousResurrectorExileEffect copy() { + return new GisaGloriousResurrectorExileEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (zEvent.getTarget() instanceof PermanentToken) { + return player.moveCards(zEvent.getTarget(), Zone.EXILED, source, game); + } + return player.moveCardsToExile( + zEvent.getTarget(), source, game, false, + CardUtil.getExileZoneId(game, source), null + ); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + return zEvent.isDiesEvent() + && zEvent.getTarget() != null + && zEvent.getTarget().isCreature(game) + && game.getOpponents(zEvent.getTarget().getControllerId()).contains(source.getControllerId()); + } +} + +class GisaGloriousResurrectorReturnEffect extends OneShotEffect { + + GisaGloriousResurrectorReturnEffect() { + super(Outcome.Benefit); + staticText = "put all creature cards exiled with {this} " + + "onto the battlefield under your control. They gain decayed"; + } + + private GisaGloriousResurrectorReturnEffect(final GisaGloriousResurrectorReturnEffect effect) { + super(effect); + } + + @Override + public GisaGloriousResurrectorReturnEffect copy() { + return new GisaGloriousResurrectorReturnEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + ExileZone exileZone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source)); + if (player == null || exileZone == null || exileZone.isEmpty()) { + return false; + } + Cards cards = new CardsImpl(exileZone.getCards(StaticFilters.FILTER_CARD_CREATURE, game)); + if (cards.isEmpty()) { + return false; + } + player.moveCards(cards, Zone.BATTLEFIELD, source, game); + cards.retainZone(Zone.BATTLEFIELD, game); + if (cards.isEmpty()) { + return false; + } + game.addEffect(new GainAbilityTargetEffect( + new DecayedAbility(), Duration.Custom + ).setTargetPointer(new FixedTargets(cards, game)), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index bab56d7fdff..b51312aacb2 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -155,6 +155,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Ghostly Castigator", 45, Rarity.UNCOMMON, mage.cards.g.GhostlyCastigator.class)); cards.add(new SetCardInfo("Ghoulcaller's Harvest", 225, Rarity.RARE, mage.cards.g.GhoulcallersHarvest.class)); cards.add(new SetCardInfo("Ghoulish Procession", 102, Rarity.UNCOMMON, mage.cards.g.GhoulishProcession.class)); + cards.add(new SetCardInfo("Gisa, Glorious Resurrector", 103, Rarity.RARE, mage.cards.g.GisaGloriousResurrector.class)); cards.add(new SetCardInfo("Grafted Identity", 57, Rarity.RARE, mage.cards.g.GraftedIdentity.class)); cards.add(new SetCardInfo("Graveyard Glutton", 104, Rarity.RARE, mage.cards.g.GraveyardGlutton.class)); cards.add(new SetCardInfo("Graveyard Trespasser", 104, Rarity.RARE, mage.cards.g.GraveyardTrespasser.class)); From c1d16a376357c62d1e0456fd19814e2d19079409 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 20:29:06 -0400 Subject: [PATCH 120/231] [AFC] fixed Maddening Hex not attaching to another player (fixes #8284) --- Mage.Sets/src/mage/cards/m/MaddeningHex.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/m/MaddeningHex.java b/Mage.Sets/src/mage/cards/m/MaddeningHex.java index fcb9610174a..d1bf2ec0e8c 100644 --- a/Mage.Sets/src/mage/cards/m/MaddeningHex.java +++ b/Mage.Sets/src/mage/cards/m/MaddeningHex.java @@ -131,9 +131,11 @@ class MaddeningHexEffect extends OneShotEffect { if (player != null) { opponents.remove(player.getId()); } - if (!opponents.isEmpty()) { - permanent.attachTo(RandomUtil.randomFromCollection(opponents), source, game); + Player opponent = game.getPlayer(RandomUtil.randomFromCollection(opponents)); + if (opponent == null) { + return true; } + opponent.addAttachment(permanent.getId(), source, game); return true; } } From c7e7d371f86e943fc68238c585cbd14a946b2f46 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 20:42:53 -0400 Subject: [PATCH 121/231] [MID] Implemented Lord of the Forsaken --- .../src/mage/cards/l/LordOfTheForsaken.java | 111 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 112 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LordOfTheForsaken.java diff --git a/Mage.Sets/src/mage/cards/l/LordOfTheForsaken.java b/Mage.Sets/src/mage/cards/l/LordOfTheForsaken.java new file mode 100644 index 00000000000..04041271e1e --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LordOfTheForsaken.java @@ -0,0 +1,111 @@ +package mage.cards.l; + +import mage.ConditionalMana; +import mage.MageInt; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.MillCardsTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.mana.ConditionalColorlessManaAbility; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.target.TargetPlayer; +import mage.target.common.TargetControlledPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LordOfTheForsaken extends CardImpl { + + public LordOfTheForsaken(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{B}{B}"); + + this.subtype.add(SubType.DEMON); + this.power = new MageInt(6); + this.toughness = new MageInt(6); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // {B}, Sacrifice another creature: Target player mills three cards. + Ability ability = new SimpleActivatedAbility( + new MillCardsTargetEffect(3), new ManaCostsImpl<>("{B}") + ); + ability.addCost(new SacrificeTargetCost(new TargetControlledPermanent( + StaticFilters.FILTER_CONTROLLED_ANOTHER_CREATURE + ))); + ability.addTarget(new TargetPlayer()); + this.addAbility(ability); + + // Pay 1 life: Add {C}. Spend this mana only to cast a spell from your graveyard. + this.addAbility(new ConditionalColorlessManaAbility( + new PayLifeCost(1), 1, new LordOfTheForsakenManaBuilder() + )); + } + + private LordOfTheForsaken(final LordOfTheForsaken card) { + super(card); + } + + @Override + public LordOfTheForsaken copy() { + return new LordOfTheForsaken(this); + } +} + +class LordOfTheForsakenManaBuilder extends ConditionalManaBuilder { + + @Override + public ConditionalMana build(Object... options) { + return new LordOfTheForsakenConditionalMana(this.mana); + } + + @Override + public String getRule() { + return "Spend this mana only to cast a spell from a graveyard"; + } +} + +class LordOfTheForsakenConditionalMana extends ConditionalMana { + + public LordOfTheForsakenConditionalMana(Mana mana) { + super(mana); + staticText = "Spend this mana only to cast a spell from a graveyard"; + addCondition(LordOfTheForsakenManaCondition.instance); + } +} + +enum LordOfTheForsakenManaCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + if (game == null || !game.inCheckPlayableState()) { + return false; + } + if (game.getCard(source.getSourceId()) != null + && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { + return true; + } + Spell spell = game.getSpell(source.getSourceId()); + return spell != null && spell.getFromZone() == Zone.GRAVEYARD; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index b51312aacb2..b2efb349027 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -190,6 +190,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Liesa, Forgotten Archangel", 232, Rarity.RARE, mage.cards.l.LiesaForgottenArchangel.class)); cards.add(new SetCardInfo("Light Up the Night", 146, Rarity.RARE, mage.cards.l.LightUpTheNight.class)); cards.add(new SetCardInfo("Locked in the Cemetery", 60, Rarity.COMMON, mage.cards.l.LockedInTheCemetery.class)); + cards.add(new SetCardInfo("Lord of the Forsaken", 110, Rarity.MYTHIC, mage.cards.l.LordOfTheForsaken.class)); cards.add(new SetCardInfo("Lord of the Ulvenwald", 231, Rarity.UNCOMMON, mage.cards.l.LordOfTheUlvenwald.class)); cards.add(new SetCardInfo("Loyal Gryff", 26, Rarity.UNCOMMON, mage.cards.l.LoyalGryff.class)); cards.add(new SetCardInfo("Luminous Phantom", 27, Rarity.COMMON, mage.cards.l.LuminousPhantom.class)); From 5068339db35a454c8c89371df173ad2d811f1d9c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 16 Sep 2021 21:10:54 -0400 Subject: [PATCH 122/231] [MID] Implemented Jerren, Corrupted Bishop / Ormendahl, the Corrupter --- .../mage/cards/j/JerrenCorruptedBishop.java | 139 ++++++++++++++++++ .../mage/cards/o/OrmendahlTheCorrupter.java | 62 ++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 203 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/j/JerrenCorruptedBishop.java create mode 100644 Mage.Sets/src/mage/cards/o/OrmendahlTheCorrupter.java diff --git a/Mage.Sets/src/mage/cards/j/JerrenCorruptedBishop.java b/Mage.Sets/src/mage/cards/j/JerrenCorruptedBishop.java new file mode 100644 index 00000000000..aafbae17164 --- /dev/null +++ b/Mage.Sets/src/mage/cards/j/JerrenCorruptedBishop.java @@ -0,0 +1,139 @@ +package mage.cards.j; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.PermanentToken; +import mage.game.permanent.token.HumanToken; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class JerrenCorruptedBishop extends CardImpl { + + private static final FilterPermanent filter = new FilterControlledPermanent(SubType.HUMAN); + + public JerrenCorruptedBishop(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + this.transformable = true; + this.secondSideCardClazz = mage.cards.o.OrmendahlTheCorrupter.class; + + // Whenever Jerren, Corrupted Bishop enters the battlefield or another nontoken Human you control dies, you lose 1 life and create a 1/1 white Human creature token. + this.addAbility(new JerrenCorruptedBishopTriggeredAbility()); + + // {2}: Target Human you control gains lifelink until end of turn. + Ability ability = new SimpleActivatedAbility(new GainAbilityTargetEffect( + LifelinkAbility.getInstance(), Duration.EndOfTurn + ), new GenericManaCost(2)); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + + // At the beginning of your end step, if you have exactly 13 life, you may pay {4}{B}{B}. If you do, transform Jerren. + this.addAbility(new TransformAbility()); + this.addAbility(new BeginningOfEndStepTriggeredAbility(Zone.BATTLEFIELD, new DoIfCostPaid( + new TransformSourceEffect(true), new ManaCostsImpl<>("{4}{B}{B}") + ), TargetController.YOU, JerrenCorruptedBishopCondition.instance, false)); + } + + private JerrenCorruptedBishop(final JerrenCorruptedBishop card) { + super(card); + } + + @Override + public JerrenCorruptedBishop copy() { + return new JerrenCorruptedBishop(this); + } +} + +enum JerrenCorruptedBishopCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + return player != null && player.getLife() == 13; + } + + @Override + public String toString() { + return "if you have exactly 13 life"; + } +} + +class JerrenCorruptedBishopTriggeredAbility extends TriggeredAbilityImpl { + + JerrenCorruptedBishopTriggeredAbility() { + super(Zone.BATTLEFIELD, new LoseLifeSourceControllerEffect(1)); + this.addEffect(new CreateTokenEffect(new HumanToken())); + } + + private JerrenCorruptedBishopTriggeredAbility(final JerrenCorruptedBishopTriggeredAbility ability) { + super(ability); + } + + @Override + public JerrenCorruptedBishopTriggeredAbility copy() { + return new JerrenCorruptedBishopTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD + || event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + switch (event.getType()) { + case ENTERS_THE_BATTLEFIELD: + return event.getSourceId().equals(getSourceId()); + case ZONE_CHANGE: + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + return zEvent.isDiesEvent() + && zEvent.getTarget() != null + && !zEvent.getTarget().getId().equals(getSourceId()) + && zEvent.getTarget().isControlledBy(getControllerId()) + && !(zEvent.getTarget() instanceof PermanentToken) + && zEvent.getTarget().hasSubtype(SubType.HUMAN, game); + + default: + return false; + } + } + + @Override + public String getRule() { + return "Whenever {this} enters the battlefield or another nontoken Human you control dies, " + + "you lose 1 life and create a 1/1 white Human creature token."; + } +} diff --git a/Mage.Sets/src/mage/cards/o/OrmendahlTheCorrupter.java b/Mage.Sets/src/mage/cards/o/OrmendahlTheCorrupter.java new file mode 100644 index 00000000000..59c786aa5df --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OrmendahlTheCorrupter.java @@ -0,0 +1,62 @@ +package mage.cards.o; + +import mage.MageInt; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.StaticFilters; +import mage.target.common.TargetControlledPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class OrmendahlTheCorrupter extends CardImpl { + + public OrmendahlTheCorrupter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.DEMON); + this.power = new MageInt(6); + this.toughness = new MageInt(6); + this.color.setBlack(true); + this.transformable = true; + this.nightCard = true; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // Sacrifice another creature: Draw a card. + this.addAbility(new SimpleActivatedAbility( + new DrawCardSourceControllerEffect(1), + new SacrificeTargetCost(new TargetControlledPermanent( + StaticFilters.FILTER_CONTROLLED_ANOTHER_CREATURE + )) + )); + } + + private OrmendahlTheCorrupter(final OrmendahlTheCorrupter card) { + super(card); + } + + @Override + public OrmendahlTheCorrupter copy() { + return new OrmendahlTheCorrupter(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index b2efb349027..1c2a799e55c 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -181,6 +181,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Island", 270, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Jack-o'-Lantern", 254, Rarity.COMMON, mage.cards.j.JackOLantern.class)); cards.add(new SetCardInfo("Jadar, Ghoulcaller of Nephalia", 108, Rarity.RARE, mage.cards.j.JadarGhoulcallerOfNephalia.class)); + cards.add(new SetCardInfo("Jerren, Corrupted Bishop", 109, Rarity.MYTHIC, mage.cards.j.JerrenCorruptedBishop.class)); cards.add(new SetCardInfo("Join the Dance", 229, Rarity.UNCOMMON, mage.cards.j.JoinTheDance.class)); cards.add(new SetCardInfo("Katilda, Dawnhart Prime", 230, Rarity.RARE, mage.cards.k.KatildaDawnhartPrime.class)); cards.add(new SetCardInfo("Kessig Naturalist", 231, Rarity.UNCOMMON, mage.cards.k.KessigNaturalist.class)); @@ -224,6 +225,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Olivia's Midnight Ambush", 118, Rarity.COMMON, mage.cards.o.OliviasMidnightAmbush.class)); cards.add(new SetCardInfo("Ominous Roost", 65, Rarity.UNCOMMON, mage.cards.o.OminousRoost.class)); cards.add(new SetCardInfo("Organ Hoarder", 66, Rarity.COMMON, mage.cards.o.OrganHoarder.class)); + cards.add(new SetCardInfo("Ormendahl, the Corrupter", 109, Rarity.MYTHIC, mage.cards.o.OrmendahlTheCorrupter.class)); cards.add(new SetCardInfo("Otherworldly Gaze", 67, Rarity.COMMON, mage.cards.o.OtherworldlyGaze.class)); cards.add(new SetCardInfo("Outland Liberator", 190, Rarity.UNCOMMON, mage.cards.o.OutlandLiberator.class)); cards.add(new SetCardInfo("Overgrown Farmland", 265, Rarity.RARE, mage.cards.o.OvergrownFarmland.class)); From c5572cb41f27240a905eea70a2fd3eca36e0679c Mon Sep 17 00:00:00 2001 From: "Raphael \"who?!\" Kehldorfer" Date: Fri, 17 Sep 2021 14:06:02 +0200 Subject: [PATCH 123/231] [MID] Implemented Winterthorn Blessing (#8268) * [MID] Implemented Winterthorn Blessing * [MID] Refactored Winterthorn Blessing: + Added Min / Max to TargetControlledCreaturePermanent + Added TargetPointers * [MID] Refactored Winterthorn Blessing: + Added Min / Max to TargetControlledCreaturePermanent + Added TargetPointers * [MID] Refactored Winterthorn Blessing + Implemented suggested changes --- .../src/mage/cards/w/WinterthornBlessing.java | 48 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 49 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WinterthornBlessing.java diff --git a/Mage.Sets/src/mage/cards/w/WinterthornBlessing.java b/Mage.Sets/src/mage/cards/w/WinterthornBlessing.java new file mode 100644 index 00000000000..1f7fdb490c7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WinterthornBlessing.java @@ -0,0 +1,48 @@ +package mage.cards.w; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DontUntapInControllersNextUntapStepTargetEffect; +import mage.abilities.effects.common.TapTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.targetpointer.FirstTargetPointer; +import mage.target.targetpointer.SecondTargetPointer; + +import java.util.UUID; + +/** + * @author LePwnerer + */ +public final class WinterthornBlessing extends CardImpl { + + public WinterthornBlessing(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{G}{U}"); + + // Put a +1/+1 counter on up to one target creature you control. Tap up to one target creature you don't control, and that creature doesn't untap during its controller's next untap step. + this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent(0, 1)); + this.getSpellAbility().addTarget(new TargetPermanent(0, 1, StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL)); + this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance()).setTargetPointer(new FirstTargetPointer())); + this.getSpellAbility().addEffect(new TapTargetEffect().setTargetPointer(new SecondTargetPointer()).setText("tap up to one target creature you don't control")); + this.getSpellAbility().addEffect(new DontUntapInControllersNextUntapStepTargetEffect().setTargetPointer(new SecondTargetPointer()).setText("that creature doesn't untap during its controller's next untap step")); + + // Flashback {1}{G}{U} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{G}{U}"))); + + } + + private WinterthornBlessing(final WinterthornBlessing card) { + super(card); + } + + @Override + public WinterthornBlessing copy() { + return new WinterthornBlessing(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 1c2a799e55c..917f5eb6e5f 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -324,6 +324,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Waildrifter", 55, Rarity.COMMON, mage.cards.w.Waildrifter.class)); cards.add(new SetCardInfo("Willow Geist", 207, Rarity.RARE, mage.cards.w.WillowGeist.class)); cards.add(new SetCardInfo("Wing Shredder", 169, Rarity.COMMON, mage.cards.w.WingShredder.class)); + cards.add(new SetCardInfo("Winterthorn Blessing", 251, Rarity.UNCOMMON, mage.cards.w.WinterthornBlessing.class)); cards.add(new SetCardInfo("Wrenn and Seven", 208, Rarity.MYTHIC, mage.cards.w.WrennAndSeven.class)); cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); // remove when mechanic is fully implemented From cf93e5d572246ed6eab98ba7abe44daafd3568fa Mon Sep 17 00:00:00 2001 From: "Raphael \"who?!\" Kehldorfer" Date: Fri, 17 Sep 2021 14:07:54 +0200 Subject: [PATCH 124/231] [MID] Implemented Wake to Slaughter (#8270) * [MID] Implemented Wake to Slaughter * [MID] Refactored Wake to Slaughter: + Reworked WakeToSlaughterEffect + Added Min / Max * [MID] Refactored Wake to Slaughter + Added missing ability and trigger --- .../src/mage/cards/w/WakeToSlaughter.java | 126 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 127 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WakeToSlaughter.java diff --git a/Mage.Sets/src/mage/cards/w/WakeToSlaughter.java b/Mage.Sets/src/mage/cards/w/WakeToSlaughter.java new file mode 100644 index 00000000000..cdb70557850 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WakeToSlaughter.java @@ -0,0 +1,126 @@ +package mage.cards.w; + +import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.abilities.keyword.HasteAbility; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.Target; +import mage.target.TargetCard; +import mage.target.common.TargetCardInGraveyard; +import mage.target.common.TargetOpponent; +import mage.target.targetpointer.FixedTarget; + +import java.util.Set; +import java.util.UUID; + +/** + * @author LePwnerer + */ +public final class WakeToSlaughter extends CardImpl { + + public WakeToSlaughter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}{R}"); + + // Choose up to two target creature cards in your graveyard. An opponent chooses one of them. Return that card to your hand. Return the other to the battlefield under your control. It gains haste. Exile it at the beginning of the next end step. + this.getSpellAbility().addTarget(new TargetCardInGraveyard(0, 2, StaticFilters.FILTER_CARD_CREATURE)); + this.getSpellAbility().addEffect(new WakeToSlaughterEffect()); + + // Flashback {4}{B}{R} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{4}{B}{R}"))); + + } + + private WakeToSlaughter(final WakeToSlaughter card) { + super(card); + } + + @Override + public WakeToSlaughter copy() { + return new WakeToSlaughter(this); + } +} + +class WakeToSlaughterEffect extends OneShotEffect { + + public WakeToSlaughterEffect() { + super(Outcome.Benefit); + this.staticText = "Choose up to two target creature cards in your graveyard. " + + "An opponent chooses one of them. " + + "Return that card to your hand. " + + "Return the other to the battlefield under your control. " + + "It gains haste. " + + "Exile it at the beginning of the next end step."; + } + + public WakeToSlaughterEffect(final mage.cards.w.WakeToSlaughterEffect effect) { + super(effect); + } + + @Override + public mage.cards.w.WakeToSlaughterEffect copy() { + return new mage.cards.w.WakeToSlaughterEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Cards pickedCards = new CardsImpl(getTargetPointer().getTargets(game, source)); + if (player != null && !pickedCards.isEmpty()) { + Card cardToHand; + if (pickedCards.size() == 1) { + cardToHand = pickedCards.getRandom(game); + } else { + Player opponent; + Set opponents = game.getOpponents(player.getId()); + if (opponents.size() == 1) { + opponent = game.getPlayer(opponents.iterator().next()); + } else { + Target targetOpponent = new TargetOpponent(true); + player.chooseTarget(Outcome.Detriment, targetOpponent, source, game); + opponent = game.getPlayer(targetOpponent.getFirstTarget()); + } + + TargetCard target = new TargetCard(1, Zone.GRAVEYARD, new FilterCard()); + target.withChooseHint("Card to go to opponent's hand (other goes to battlefield)"); + opponent.chooseTarget(outcome, pickedCards, target, source, game); + cardToHand = game.getCard(target.getFirstTarget()); + } + for (Card card : pickedCards.getCards(game)) { + if (card == cardToHand) { + player.moveCards(cardToHand, Zone.HAND, source, game); + } else { + player.moveCards(card, Zone.BATTLEFIELD, source, game); + + FixedTarget fixedTarget = new FixedTarget(card, game); + ContinuousEffect effect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.EndOfGame); + effect.setTargetPointer(fixedTarget); + game.addEffect(effect, source); + + ExileTargetEffect exileEffect = new ExileTargetEffect(null, null, Zone.BATTLEFIELD); + exileEffect.setTargetPointer(fixedTarget); + DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(exileEffect); + game.addDelayedTriggeredAbility(delayedAbility, source); + } + } + pickedCards.clear(); + return true; + } + + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 917f5eb6e5f..d65369018c9 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -322,6 +322,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Voldaren Ambusher", 166, Rarity.UNCOMMON, mage.cards.v.VoldarenAmbusher.class)); cards.add(new SetCardInfo("Voldaren Stinger", 167, Rarity.COMMON, mage.cards.v.VoldarenStinger.class)); cards.add(new SetCardInfo("Waildrifter", 55, Rarity.COMMON, mage.cards.w.Waildrifter.class)); + cards.add(new SetCardInfo("Wake to Slaughter", 250, Rarity.RARE, mage.cards.w.WakeToSlaughter.class)); cards.add(new SetCardInfo("Willow Geist", 207, Rarity.RARE, mage.cards.w.WillowGeist.class)); cards.add(new SetCardInfo("Wing Shredder", 169, Rarity.COMMON, mage.cards.w.WingShredder.class)); cards.add(new SetCardInfo("Winterthorn Blessing", 251, Rarity.UNCOMMON, mage.cards.w.WinterthornBlessing.class)); From c610807d514cc7a6ecde3932a6093ea8f21be099 Mon Sep 17 00:00:00 2001 From: "Raphael \"who?!\" Kehldorfer" Date: Fri, 17 Sep 2021 14:11:33 +0200 Subject: [PATCH 125/231] [MID] Implemented Dennick, Pious Apprentice // Dennick, Pious Apparition (#8279) * [MID] Implemented Dennick, Pious Apprentice // Dennick, Pious Apparition * [MID] Refactored Dennick, Pious Apprentice // Dennick, Pious Apparition * small change Co-authored-by: Evan Kranzler --- .../mage/cards/d/DennickPiousApparition.java | 53 ++++++++++++++++++ .../mage/cards/d/DennickPiousApprentice.java | 55 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 110 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DennickPiousApparition.java create mode 100644 Mage.Sets/src/mage/cards/d/DennickPiousApprentice.java diff --git a/Mage.Sets/src/mage/cards/d/DennickPiousApparition.java b/Mage.Sets/src/mage/cards/d/DennickPiousApparition.java new file mode 100644 index 00000000000..ec7c305155d --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DennickPiousApparition.java @@ -0,0 +1,53 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.common.PutCardIntoGraveFromAnywhereAllTriggeredAbility; +import mage.abilities.common.PutIntoGraveFromAnywhereSourceAbility; +import mage.abilities.effects.common.ExileSourceEffect; +import mage.abilities.effects.keyword.InvestigateEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.TargetController; + +import java.util.UUID; + +/** + * @author LePwnerer + */ +public final class DennickPiousApparition extends CardImpl { + + public DennickPiousApparition(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + this.color.setWhite(true); + this.color.setBlue(true); + this.nightCard = true; + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever one or more creature cards are put into graveyards from anywhere, investigate. This ability triggers only once each turn. + this.addAbility(new PutCardIntoGraveFromAnywhereAllTriggeredAbility(new InvestigateEffect(1), false, TargetController.ANY).setTriggersOnce(true)); + + // If Dennick, Pious Apparition would be put into a graveyard from anywhere, exile it instead. + this.addAbility(new PutIntoGraveFromAnywhereSourceAbility(new ExileSourceEffect().setText("exile it instead"))); + } + + private DennickPiousApparition(final DennickPiousApparition card) { + super(card); + } + + @Override + public DennickPiousApparition copy() { + return new DennickPiousApparition(this); + } +} diff --git a/Mage.Sets/src/mage/cards/d/DennickPiousApprentice.java b/Mage.Sets/src/mage/cards/d/DennickPiousApprentice.java new file mode 100644 index 00000000000..20107974ca3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DennickPiousApprentice.java @@ -0,0 +1,55 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CantBeTargetedCardsGraveyardsEffect; +import mage.abilities.keyword.DisturbAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; + +import java.util.UUID; + +/** + * @author LePwnerer + */ +public final class DennickPiousApprentice extends CardImpl { + + public DennickPiousApprentice(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}{U}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + this.transformable = true; + this.secondSideCardClazz = mage.cards.d.DennickPiousApparition.class; + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // Cards in graveyards can't be the targets of spells or abilities. + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new CantBeTargetedCardsGraveyardsEffect())); + + // Disturb {2}{W}{U} + this.addAbility(new TransformAbility()); + this.addAbility(new DisturbAbility(new ManaCostsImpl<>("{2}{W}{U}"))); + + } + + private DennickPiousApprentice(final DennickPiousApprentice card) { + super(card); + } + + @Override + public DennickPiousApprentice copy() { + return new DennickPiousApprentice(this); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index d65369018c9..75c5f49087a 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -103,6 +103,8 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Defend the Celestus", 182, Rarity.UNCOMMON, mage.cards.d.DefendTheCelestus.class)); cards.add(new SetCardInfo("Defenestrate", 95, Rarity.COMMON, mage.cards.d.Defenestrate.class)); cards.add(new SetCardInfo("Delver of Secrets", 47, Rarity.UNCOMMON, mage.cards.d.DelverOfSecrets.class)); + cards.add(new SetCardInfo("Dennick, Pious Apparition", 217, Rarity.RARE, mage.cards.d.DennickPiousApparition.class)); + cards.add(new SetCardInfo("Dennick, Pious Apprentice", 217, Rarity.RARE, mage.cards.d.DennickPiousApprentice.class)); cards.add(new SetCardInfo("Departed Soulkeeper", 218, Rarity.UNCOMMON, mage.cards.d.DepartedSoulkeeper.class)); cards.add(new SetCardInfo("Deserted Beach", 260, Rarity.RARE, mage.cards.d.DesertedBeach.class)); cards.add(new SetCardInfo("Devious Cover-Up", 48, Rarity.COMMON, mage.cards.d.DeviousCoverUp.class)); From 6dd0cb69e72c70646657ff70c50ab9c73fc6dd55 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 17 Sep 2021 08:11:51 -0400 Subject: [PATCH 126/231] [EMN] simplified implementation of Emrakul, the Promised End --- .../mage/cards/e/EmrakulThePromisedEnd.java | 60 ++++--------------- 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/Mage.Sets/src/mage/cards/e/EmrakulThePromisedEnd.java b/Mage.Sets/src/mage/cards/e/EmrakulThePromisedEnd.java index f7f99c4ab4b..3c23183b322 100644 --- a/Mage.Sets/src/mage/cards/e/EmrakulThePromisedEnd.java +++ b/Mage.Sets/src/mage/cards/e/EmrakulThePromisedEnd.java @@ -1,18 +1,16 @@ - package mage.cards.e; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.SpellAbility; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CastSourceTriggeredAbility; -import mage.abilities.effects.common.cost.CostModificationEffectImpl; +import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.ProtectionAbility; import mage.abilities.keyword.TrampleAbility; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -21,10 +19,7 @@ import mage.game.Game; import mage.game.turn.TurnMod; import mage.players.Player; import mage.target.common.TargetOpponent; -import mage.util.CardUtil; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; /** @@ -46,12 +41,15 @@ public final class EmrakulThePromisedEnd extends CardImpl { this.toughness = new MageInt(13); // Emrakul, the Promised End costs {1} less to cast for each card type among cards in your graveyard. - Ability ability = new SimpleStaticAbility(Zone.ALL, new EmrakulThePromisedEndCostReductionEffect()); - ability.setRuleAtTheTop(true); - this.addAbility(ability.addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(new SimpleStaticAbility( + Zone.ALL, + new SpellCostReductionForEachSourceEffect( + 1, CardTypesInGraveyardCount.YOU + ).setText("this spell costs {1} less to cast for each card type among cards in your graveyard") + ).setRuleAtTheTop(true).addHint(CardTypesInGraveyardHint.YOU)); // When you cast Emrakul, you gain control of target opponent during that player's next turn. After that turn, that player takes an extra turn. - ability = new CastSourceTriggeredAbility(new EmrakulThePromisedEndGainControlEffect()); + Ability ability = new CastSourceTriggeredAbility(new EmrakulThePromisedEndGainControlEffect()); ability.addTarget(new TargetOpponent()); this.addAbility(ability); @@ -75,44 +73,6 @@ public final class EmrakulThePromisedEnd extends CardImpl { } } -class EmrakulThePromisedEndCostReductionEffect extends CostModificationEffectImpl { - - EmrakulThePromisedEndCostReductionEffect() { - super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST); - staticText = "this spell costs {1} less to cast for each card type among cards in your graveyard"; - } - - EmrakulThePromisedEndCostReductionEffect(EmrakulThePromisedEndCostReductionEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Set foundCardTypes = new HashSet<>(8); - for (Card card : controller.getGraveyard().getCards(game)) { - foundCardTypes.addAll(card.getCardType(game)); - } - CardUtil.reduceCost(abilityToModify, foundCardTypes.size()); - return true; - } - return false; - } - - @Override - public boolean applies(Ability abilityToModify, Ability source, Game game) { - return abilityToModify instanceof SpellAbility - && abilityToModify.getSourceId().equals(source.getSourceId()) - && game.getCard(abilityToModify.getSourceId()) != null; - } - - @Override - public EmrakulThePromisedEndCostReductionEffect copy() { - return new EmrakulThePromisedEndCostReductionEffect(this); - } -} - class EmrakulThePromisedEndGainControlEffect extends OneShotEffect { EmrakulThePromisedEndGainControlEffect() { @@ -120,7 +80,7 @@ class EmrakulThePromisedEndGainControlEffect extends OneShotEffect { this.staticText = "you gain control of target opponent during that player's next turn. After that turn, that player takes an extra turn"; } - EmrakulThePromisedEndGainControlEffect(final EmrakulThePromisedEndGainControlEffect effect) { + private EmrakulThePromisedEndGainControlEffect(final EmrakulThePromisedEndGainControlEffect effect) { super(effect); } From 7c2db3c5fc38ee12eb399248567298ef1364af10 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 17 Sep 2021 08:43:49 -0400 Subject: [PATCH 127/231] [MID] Implemented Sungold Sentinel --- .../src/mage/cards/s/SungoldSentinel.java | 94 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../keyword/HexproofBaseAbility.java | 40 ++++++++ .../keyword/HexproofFromGreenAbility.java | 48 ++++++++++ .../keyword/HexproofFromRedAbility.java | 48 ++++++++++ 5 files changed, 231 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SungoldSentinel.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/HexproofFromGreenAbility.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/HexproofFromRedAbility.java diff --git a/Mage.Sets/src/mage/cards/s/SungoldSentinel.java b/Mage.Sets/src/mage/cards/s/SungoldSentinel.java new file mode 100644 index 00000000000..1951791b24f --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SungoldSentinel.java @@ -0,0 +1,94 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.condition.common.CovenCondition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalActivatedAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.combat.CantBeBlockedByAllSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.hint.common.CovenHint; +import mage.abilities.keyword.HexproofBaseAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.choices.ChoiceColor; +import mage.constants.*; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.ColorPredicate; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SungoldSentinel extends CardImpl { + + public SungoldSentinel(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // Whenever Sungold Sentinel enters the battlefield or attacks, exile up to one target card from a graveyard. + Ability ability = new EntersBattlefieldOrAttacksSourceTriggeredAbility(new ExileTargetEffect()); + ability.addTarget(new TargetCardInGraveyard(0, 1)); + this.addAbility(ability); + + // Coven — {1}{W}: Choose a color. Sungold Sentinel gains hexproof from that color until end of turn and can't be blocked by creatures of that color this turn. Activate only if you control three or more creatures with different powers. + this.addAbility(new ConditionalActivatedAbility( + Zone.BATTLEFIELD, new SungoldSentinelEffect(), + new ManaCostsImpl<>("{1}{W}"), CovenCondition.instance + ).addHint(CovenHint.instance).setAbilityWord(AbilityWord.COVEN)); + } + + private SungoldSentinel(final SungoldSentinel card) { + super(card); + } + + @Override + public SungoldSentinel copy() { + return new SungoldSentinel(this); + } +} + +class SungoldSentinelEffect extends OneShotEffect { + + SungoldSentinelEffect() { + super(Outcome.Benefit); + staticText = "choose a color. {this} gains hexproof from that color until end of turn " + + "and can't be blocked by creatures of that color this turn"; + } + + private SungoldSentinelEffect(final SungoldSentinelEffect effect) { + super(effect); + } + + @Override + public SungoldSentinelEffect copy() { + return new SungoldSentinelEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + ChoiceColor choice = new ChoiceColor(true); + player.choose(outcome, choice, game); + Ability ability = HexproofBaseAbility.getFirstFromColor(choice.getColor()); + game.addEffect(new GainAbilitySourceEffect(ability, Duration.EndOfTurn), source); + FilterCreaturePermanent filter = new FilterCreaturePermanent(); + filter.add(new ColorPredicate(choice.getColor())); + game.addEffect(new CantBeBlockedByAllSourceEffect(filter, Duration.EndOfTurn), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 75c5f49087a..74150f18fc8 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -292,6 +292,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Stromkirk Bloodthief", 123, Rarity.UNCOMMON, mage.cards.s.StromkirkBloodthief.class)); cards.add(new SetCardInfo("Stuffed Bear", 259, Rarity.COMMON, mage.cards.s.StuffedBear.class)); cards.add(new SetCardInfo("Sungold Barrage", 36, Rarity.COMMON, mage.cards.s.SungoldBarrage.class)); + cards.add(new SetCardInfo("Sungold Sentinel", 37, Rarity.RARE, mage.cards.s.SungoldSentinel.class)); cards.add(new SetCardInfo("Sunrise Cavalier", 244, Rarity.UNCOMMON, mage.cards.s.SunriseCavalier.class)); cards.add(new SetCardInfo("Sunset Revelry", 38, Rarity.UNCOMMON, mage.cards.s.SunsetRevelry.class)); cards.add(new SetCardInfo("Sunstreak Phoenix", 162, Rarity.MYTHIC, mage.cards.s.SunstreakPhoenix.class)); diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofBaseAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofBaseAbility.java index dea7eea8176..bf87e48188a 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofBaseAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofBaseAbility.java @@ -1,12 +1,16 @@ package mage.abilities.keyword; import mage.MageObject; +import mage.ObjectColor; import mage.abilities.MageSingleton; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.icon.abilities.HexproofAbilityIcon; import mage.constants.Zone; import mage.game.Game; +import java.util.HashSet; +import java.util.Set; + /** * an abstract base class for hexproof abilities * @@ -20,4 +24,40 @@ public abstract class HexproofBaseAbility extends SimpleStaticAbility implements } public abstract boolean checkObject(MageObject source, Game game); + + public static Set getFromColor(ObjectColor color) { + Set abilities = new HashSet<>(); + if (color.isWhite()) { + abilities.add(HexproofFromWhiteAbility.getInstance()); + } + if (color.isBlue()) { + abilities.add(HexproofFromBlueAbility.getInstance()); + } + if (color.isBlack()) { + abilities.add(HexproofFromBlackAbility.getInstance()); + } + if (color.isRed()) { + abilities.add(HexproofFromRedAbility.getInstance()); + } + if (color.isGreen()) { + abilities.add(HexproofFromGreenAbility.getInstance()); + } + return abilities; + } + + public static HexproofBaseAbility getFirstFromColor(ObjectColor color) { + if (color.isWhite()) { + return HexproofFromWhiteAbility.getInstance(); + } else if (color.isBlue()) { + return HexproofFromBlueAbility.getInstance(); + } else if (color.isBlack()) { + return HexproofFromBlackAbility.getInstance(); + } else if (color.isRed()) { + return HexproofFromRedAbility.getInstance(); + } else if (color.isGreen()) { + return HexproofFromGreenAbility.getInstance(); + } else { + return null; + } + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromGreenAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromGreenAbility.java new file mode 100644 index 00000000000..3534c4b72d5 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromGreenAbility.java @@ -0,0 +1,48 @@ +package mage.abilities.keyword; + +import mage.MageObject; +import mage.game.Game; + +import java.io.ObjectStreamException; + +/** + * Hexproof from green (This creature or player can't be the target of green + * spells or abilities your opponents control.) + * + * @author igoudt + */ +public class HexproofFromGreenAbility extends HexproofBaseAbility { + + private static final HexproofFromGreenAbility instance; + + static { + instance = new HexproofFromGreenAbility(); + } + + private Object readResolve() throws ObjectStreamException { + return instance; + } + + public static HexproofFromGreenAbility getInstance() { + return instance; + } + + private HexproofFromGreenAbility() { + super(); + } + + @Override + public boolean checkObject(MageObject source, Game game) { + return source.getColor(game).isGreen(); + } + + @Override + public HexproofFromGreenAbility copy() { + return instance; + } + + @Override + public String getRule() { + return "hexproof from green (This creature can't be the target of green spells or abilities your opponents control.)"; + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromRedAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromRedAbility.java new file mode 100644 index 00000000000..b65634b118d --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromRedAbility.java @@ -0,0 +1,48 @@ +package mage.abilities.keyword; + +import mage.MageObject; +import mage.game.Game; + +import java.io.ObjectStreamException; + +/** + * Hexproof from red (This creature or player can't be the target of red + * spells or abilities your opponents control.) + * + * @author igoudt + */ +public class HexproofFromRedAbility extends HexproofBaseAbility { + + private static final HexproofFromRedAbility instance; + + static { + instance = new HexproofFromRedAbility(); + } + + private Object readResolve() throws ObjectStreamException { + return instance; + } + + public static HexproofFromRedAbility getInstance() { + return instance; + } + + private HexproofFromRedAbility() { + super(); + } + + @Override + public boolean checkObject(MageObject source, Game game) { + return source.getColor(game).isRed(); + } + + @Override + public HexproofFromRedAbility copy() { + return instance; + } + + @Override + public String getRule() { + return "hexproof from red (This creature can't be the target of red spells or abilities your opponents control.)"; + } +} From aed6a4ac3db7869d361110996d7d6ce1259b50db Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 17 Sep 2021 09:12:30 -0400 Subject: [PATCH 128/231] [MID] Implemented Vengeful Strangler / Strangling Grasp --- Mage.Sets/src/mage/cards/a/AccursedWitch.java | 2 +- .../src/mage/cards/s/StranglingGrasp.java | 115 ++++++++++++++++++ .../src/mage/cards/v/VengefulStrangler.java | 105 ++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 4 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/s/StranglingGrasp.java create mode 100644 Mage.Sets/src/mage/cards/v/VengefulStrangler.java diff --git a/Mage.Sets/src/mage/cards/a/AccursedWitch.java b/Mage.Sets/src/mage/cards/a/AccursedWitch.java index 1275d59be45..85743de74eb 100644 --- a/Mage.Sets/src/mage/cards/a/AccursedWitch.java +++ b/Mage.Sets/src/mage/cards/a/AccursedWitch.java @@ -59,7 +59,7 @@ class AccursedWitchReturnTransformedEffect extends OneShotEffect { AccursedWitchReturnTransformedEffect() { super(Outcome.PutCardInPlay); - this.staticText = "Put {this} from your graveyard onto the battlefield transformed under your control attached to target opponent"; + this.staticText = "return it to the battlefield transformed under your control attached to target opponent"; } private AccursedWitchReturnTransformedEffect(final AccursedWitchReturnTransformedEffect effect) { diff --git a/Mage.Sets/src/mage/cards/s/StranglingGrasp.java b/Mage.Sets/src/mage/cards/s/StranglingGrasp.java new file mode 100644 index 00000000000..90644555175 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StranglingGrasp.java @@ -0,0 +1,115 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent; +import mage.filter.common.FilterNonlandPermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StranglingGrasp extends CardImpl { + + private static final FilterPermanent filter + = new FilterCreatureOrPlaneswalkerPermanent("creature or planeswalker an opponent controls"); + + static { + filter.add(TargetController.OPPONENT.getControllerPredicate()); + } + + public StranglingGrasp(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, ""); + + this.subtype.add(SubType.AURA); + this.color.setBlack(true); + this.transformable = true; + this.nightCard = true; + + // Enchant creature or planeswalker an opponent controls + TargetPermanent auraTarget = new TargetPermanent(filter); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + Ability ability = new EnchantAbility(auraTarget.getTargetName()); + this.addAbility(ability); + + // At the beginning of your upkeep, enchanted permanent's controller sacrifices a nonland permanent and loses 1 life. + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new StranglingGraspEffect(), TargetController.YOU, false + )); + } + + private StranglingGrasp(final StranglingGrasp card) { + super(card); + } + + @Override + public StranglingGrasp copy() { + return new StranglingGrasp(this); + } +} + +class StranglingGraspEffect extends OneShotEffect { + + private static final FilterPermanent filter = new FilterNonlandPermanent("nonland permanent you control"); + + static { + filter.add(TargetController.YOU.getControllerPredicate()); + } + + StranglingGraspEffect() { + super(Outcome.Benefit); + staticText = "enchanted permanent's controller sacrifices a nonland permanent and loses 1 life"; + } + + private StranglingGraspEffect(final StranglingGraspEffect effect) { + super(effect); + } + + @Override + public StranglingGraspEffect copy() { + return new StranglingGraspEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent sourcePermanent = source.getSourcePermanentOrLKI(game); + if (sourcePermanent == null) { + return false; + } + Permanent attachedTo = game.getPermanentOrLKIBattlefield(sourcePermanent.getAttachedTo()); + if (attachedTo == null) { + return false; + } + Player player = game.getPlayer(attachedTo.getControllerId()); + if (player == null) { + return false; + } + TargetPermanent target = new TargetPermanent(filter); + target.setNotTarget(true); + if (target.canChoose(source.getSourceId(), player.getId(), game)) { + player.choose(outcome, target, source.getSourceId(), game); + Permanent permanent = game.getPermanent(target.getFirstTarget()); + if (permanent != null) { + permanent.sacrifice(source, game); + } + } + player.loseLife(1, game, source, false); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/v/VengefulStrangler.java b/Mage.Sets/src/mage/cards/v/VengefulStrangler.java new file mode 100644 index 00000000000..b7d56d257f6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VengefulStrangler.java @@ -0,0 +1,105 @@ +package mage.cards.v; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.CantBlockAbility; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.TransformAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VengefulStrangler extends CardImpl { + + private static final FilterPermanent filter + = new FilterCreatureOrPlaneswalkerPermanent("creature or planeswalker an opponent controls"); + + static { + filter.add(TargetController.OPPONENT.getControllerPredicate()); + } + + public VengefulStrangler(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + this.transformable = true; + this.secondSideCardClazz = mage.cards.s.StranglingGrasp.class; + + // Vengeful Strangler can't block. + this.addAbility(new CantBlockAbility()); + + // When Vengeful Strangler dies, return it to the battlefield transformed under your control attached to target creature or planeswalker an opponent controls. + this.addAbility(new TransformAbility()); + Ability ability = new DiesSourceTriggeredAbility(new VengefulStranglerEffect()); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private VengefulStrangler(final VengefulStrangler card) { + super(card); + } + + @Override + public VengefulStrangler copy() { + return new VengefulStrangler(this); + } +} + +class VengefulStranglerEffect extends OneShotEffect { + + VengefulStranglerEffect() { + super(Outcome.PutCardInPlay); + this.staticText = "return it to the battlefield transformed under your control " + + "attached to target creature or planeswalker an opponent controls"; + } + + private VengefulStranglerEffect(final VengefulStranglerEffect effect) { + super(effect); + } + + @Override + public VengefulStranglerEffect copy() { + return new VengefulStranglerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (controller == null || permanent == null + || game.getState().getZone(source.getSourceId()) != Zone.GRAVEYARD) { + return false; + } + game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + source.getSourceId(), Boolean.TRUE); + UUID secondFaceId = game.getCard(source.getSourceId()).getSecondCardFace().getId(); + game.getState().setValue("attachTo:" + secondFaceId, permanent.getId()); + Card card = game.getCard(source.getSourceId()); + if (card == null) { + return false; + } + controller.moveCards(card, Zone.BATTLEFIELD, source, game); + Permanent sourcePermanent = game.getPermanent(card.getId()); + if (sourcePermanent == null) { + return false; + } + permanent.addAttachment(card.getId(), source, game); + return true; + } +} + diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 74150f18fc8..c26d43e754a 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -289,6 +289,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Storm the Festival", 200, Rarity.RARE, mage.cards.s.StormTheFestival.class)); cards.add(new SetCardInfo("Storm-Charged Slasher", 157, Rarity.RARE, mage.cards.s.StormChargedSlasher.class)); cards.add(new SetCardInfo("Stormrider Spirit", 79, Rarity.COMMON, mage.cards.s.StormriderSpirit.class)); + cards.add(new SetCardInfo("Strangling Grasp", 126, Rarity.UNCOMMON, mage.cards.s.StranglingGrasp.class)); cards.add(new SetCardInfo("Stromkirk Bloodthief", 123, Rarity.UNCOMMON, mage.cards.s.StromkirkBloodthief.class)); cards.add(new SetCardInfo("Stuffed Bear", 259, Rarity.COMMON, mage.cards.s.StuffedBear.class)); cards.add(new SetCardInfo("Sungold Barrage", 36, Rarity.COMMON, mage.cards.s.SungoldBarrage.class)); @@ -319,6 +320,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Vampire Interloper", 125, Rarity.COMMON, mage.cards.v.VampireInterloper.class)); cards.add(new SetCardInfo("Vampire Socialite", 249, Rarity.UNCOMMON, mage.cards.v.VampireSocialite.class)); cards.add(new SetCardInfo("Vanquish the Horde", 41, Rarity.RARE, mage.cards.v.VanquishTheHorde.class)); + cards.add(new SetCardInfo("Vengeful Strangler", 126, Rarity.UNCOMMON, mage.cards.v.VengefulStrangler.class)); cards.add(new SetCardInfo("Village Reavers", 165, Rarity.UNCOMMON, mage.cards.v.VillageReavers.class)); cards.add(new SetCardInfo("Village Watch", 165, Rarity.UNCOMMON, mage.cards.v.VillageWatch.class)); cards.add(new SetCardInfo("Vivisection", 83, Rarity.UNCOMMON, mage.cards.v.Vivisection.class)); From 021a2d251cf77ddb9be9e13432717856c5eefebd Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 17 Sep 2021 21:21:37 +0400 Subject: [PATCH 129/231] Improves in some effects: * ConditionalAsThoughEffect - improved support with target effects (see comments from e6e802033b52475b22f3b0afc41cfbef7d5a237f); * TargetCardInLibrary - added additional checks on wrong usage (must be used inside effects only, not as ability's target); * PlayFromNotOwnHandZone - fixed wrong playable mark on battlefield's permanents (AI related too); --- .../conditional/ConditionalAsThoughTest.java | 136 ++++++++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 9 +- .../main/java/mage/abilities/AbilityImpl.java | 8 ++ .../decorator/ConditionalAsThoughEffect.java | 13 ++ .../abilities/effects/AsThoughEffect.java | 1 + .../PlayFromNotOwnHandZoneTargetEffect.java | 2 + .../main/java/mage/players/PlayerImpl.java | 4 +- .../target/common/TargetCardInLibrary.java | 3 + 8 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/conditional/ConditionalAsThoughTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/conditional/ConditionalAsThoughTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/conditional/ConditionalAsThoughTest.java new file mode 100644 index 00000000000..6077a23cf19 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/conditional/ConditionalAsThoughTest.java @@ -0,0 +1,136 @@ +package org.mage.test.cards.conditional; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.effects.common.InfoEffect; +import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneAllEffect; +import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.target.common.TargetCardInLibrary; +import mage.target.common.TargetCardInOpponentsGraveyard; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class ConditionalAsThoughTest extends CardTestPlayerBase { + + @Test + public void test_PlayFromNotOwnHandZoneAllEffect() { + removeAllCardsFromLibrary(playerA); + removeAllCardsFromLibrary(playerB); + + addCustomCardWithAbility("play any library on any creature", playerA, new SimpleStaticAbility( + Zone.ALL, + new ConditionalAsThoughEffect( + new PlayFromNotOwnHandZoneAllEffect( + StaticFilters.FILTER_CARD, + Zone.LIBRARY, + false, + TargetController.ANY, + Duration.EndOfTurn + ), + new PermanentsOnTheBattlefieldCondition( + StaticFilters.FILTER_PERMANENT_CREATURE, + ComparisonType.MORE_THAN, + 0 + ) + ) + )); + addCard(Zone.HAND, playerA, "Grizzly Bears"); + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); + addCard(Zone.LIBRARY, playerB, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + + // can't play lib's card before good condition + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", true); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", false); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + + // make good condition - now we can play any lib's card + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPlayableAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", false); + checkPlayableAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", true); + checkPlayableAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + } + + @Test(expected = IllegalArgumentException.class) + public void test_TargetCardInLibrary_CantUseAsAbilityTarget() { + Ability ability = new SimpleActivatedAbility( + Zone.ALL, + new InfoEffect("test"), + new ManaCostsImpl("{R}") + ); + ability.addTarget(new TargetCardInLibrary()); + } + + @Test + public void test_PlayFromNotOwnHandZoneTargetEffect() { + Ability ability = new SimpleActivatedAbility( + Zone.ALL, + new ConditionalAsThoughEffect( + new PlayFromNotOwnHandZoneTargetEffect( + Zone.GRAVEYARD, + TargetController.ANY, + Duration.EndOfTurn + ), + new PermanentsOnTheBattlefieldCondition( + StaticFilters.FILTER_PERMANENT_CREATURE, + ComparisonType.MORE_THAN, + 0 + ) + ).setText("allow target cast"), + new ManaCostsImpl("{R}") + ); + ability.addTarget(new TargetCardInOpponentsGraveyard(StaticFilters.FILTER_CARD)); + addCustomCardWithAbility("play any opponent hand", playerA, ability); + + addCard(Zone.HAND, playerA, "Grizzly Bears"); + addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion"); + addCard(Zone.GRAVEYARD, playerB, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + + // can't play grave before + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", true); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", false); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + + // activate target effect + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: Allow"); + addTarget(playerA, "Lightning Bolt"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // can't play grave after but without good condition + checkPlayableAbility("after bad", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", true); + checkPlayableAbility("after bad", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", false); + checkPlayableAbility("after bad", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + + // make good condition - now we can play opponent's grave + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPlayableAbility("after good", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", false); + checkPlayableAbility("after good", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", false); + checkPlayableAbility("after good", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index c211a2fa8bf..ed258018eb6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -2400,7 +2400,7 @@ public class TestPlayer implements Player { } // card in hand (only own hand supports here) - // TODO: add not own hand too, example + // cards from non-own hand must be targeted through revealed cards if (target.getOriginalTarget() instanceof TargetCardInHand || target.getOriginalTarget() instanceof TargetDiscard || (target.getOriginalTarget() instanceof TargetCard && target.getOriginalTarget().getZone() == Zone.HAND)) { @@ -2569,6 +2569,13 @@ public class TestPlayer implements Player { } } + // library + if (target.getOriginalTarget() instanceof TargetCardInLibrary + || (target.getOriginalTarget() instanceof TargetCard && target.getOriginalTarget().getZone() == Zone.LIBRARY)) { + // user don't have access to library, so it must be targeted through list/revealed cards + Assert.fail("Library zone is private, you must target through cards list, e.g. revealed: " + target.getOriginalTarget().getClass().getCanonicalName()); + } + // uninplemented TargetCard's zone if (target.getOriginalTarget() instanceof TargetCard && !targetCardZonesChecked.contains(target.getOriginalTarget().getZone())) { Assert.fail("Found unimplemented TargetCard's zone or TargetCard's extented class: " diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 5b07c7502e5..44306218224 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -26,7 +26,9 @@ import mage.game.stack.Spell; import mage.game.stack.StackAbility; import mage.players.Player; import mage.target.Target; +import mage.target.TargetCard; import mage.target.Targets; +import mage.target.common.TargetCardInLibrary; import mage.target.targetadjustment.TargetAdjuster; import mage.util.CardUtil; import mage.util.GameLog; @@ -881,6 +883,12 @@ public abstract class AbilityImpl implements Ability { @Override public void addTarget(Target target) { + // verify check + if (target instanceof TargetCardInLibrary + || (target instanceof TargetCard && target.getZone().equals(Zone.LIBRARY))) { + throw new IllegalArgumentException("Wrong usage of TargetCardInLibrary - you must use it with SearchLibrary only"); + } + if (target != null) { getTargets().add(target); } diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalAsThoughEffect.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalAsThoughEffect.java index 2ad82b0b8be..fdaee170b57 100644 --- a/Mage/src/main/java/mage/abilities/decorator/ConditionalAsThoughEffect.java +++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalAsThoughEffect.java @@ -65,6 +65,19 @@ public class ConditionalAsThoughEffect extends AsThoughEffectImpl { return false; } + @Override + public boolean applies(UUID sourceId, Ability affectedAbility, Ability source, Game game, UUID playerId) { + conditionState = condition.apply(game, source); + if (conditionState) { + effect.setTargetPointer(this.targetPointer); + return effect.applies(sourceId, affectedAbility, source, game, playerId); + } else if (otherwiseEffect != null) { + otherwiseEffect.setTargetPointer(this.targetPointer); + return otherwiseEffect.applies(sourceId, affectedAbility, source, game, playerId); + } + return false; + } + @Override public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { conditionState = condition.apply(game, source); diff --git a/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java b/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java index bc4d6d4f6c2..e3e5beb5556 100644 --- a/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java @@ -15,6 +15,7 @@ public interface AsThoughEffect extends ContinuousEffect { * Apply to ONE affected ability from the object (sourceId) *

* Warning, if you don't need ability to check then ignore it (by default it calls full object check) + * Warning, if you use conditional effect then you must override both applies methods to support different types * * @param sourceId * @param affectedAbility ability to check (example: check if spell ability can be cast from non hand) diff --git a/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneTargetEffect.java index c9e7f2f5c4e..9386508f97b 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneTargetEffect.java @@ -85,6 +85,8 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl { if (affectedAbility == null) { // ContinuousEffects.asThough already checks affectedAbility, so that error must never be called here // PLAY_FROM_NOT_OWN_HAND_ZONE must applies to affectedAbility only + // If you see it then parent conditional effect must override both applies methods to support different + // AsThough effect types in one conditional effect throw new IllegalArgumentException("ERROR, can't call applies method on empty affectedAbility"); } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 6be8a2d3289..89140dc946f 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3794,8 +3794,8 @@ public abstract class PlayerImpl implements Player, Serializable { } ApprovingObject approvingObject; - if (isPlaySpell || isPlayLand) { - // play hand from non hand zone + if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) { + // play hand from non hand zone (except battlefield - you can't play already played permanents) approvingObject = game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); } else { diff --git a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java index 5fa73b53a11..59b39f4ba89 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java @@ -19,6 +19,9 @@ import java.util.List; import java.util.UUID; /** + * + * Can be used with SearchLibrary only. User hasn't access to libs. + * * @author BetaSteward_at_googlemail.com */ public class TargetCardInLibrary extends TargetCard { From c87477178a30bd25b90759f41fbdae727d1f35b8 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 17 Sep 2021 22:57:49 +0400 Subject: [PATCH 130/231] CI: added maven cache to travis builds (#8289) --- .travis.yml | 5 ++++- Mage.Sets/src/mage/cards/a/AetherInspector.java | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 28ddf0984a4..056fa03710c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,7 @@ sudo: false dist: trusty language: java before_install: - - echo "MAVEN_OPTS='-Xmx2g'" > ~/.mavenrc \ No newline at end of file + - echo "MAVEN_OPTS='-Xmx2g'" > ~/.mavenrc +cache: + directories: + - $HOME/.m2 \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/AetherInspector.java b/Mage.Sets/src/mage/cards/a/AetherInspector.java index 9347c671e49..6ba31ee9731 100644 --- a/Mage.Sets/src/mage/cards/a/AetherInspector.java +++ b/Mage.Sets/src/mage/cards/a/AetherInspector.java @@ -1,4 +1,3 @@ - package mage.cards.a; import java.util.UUID; From f107c1e4db5aa30906fb5328264dfcf8ed85ab17 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 18 Sep 2021 00:24:10 +0400 Subject: [PATCH 131/231] Tests: improved performance in game tests; --- .../base/impl/CardTestPlayerAPIImpl.java | 3 ++- .../mage/cards/repository/CardRepository.java | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index b15a0320844..ae6a38570e5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -672,7 +672,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement Assert.fail("Can't add card " + cardName + " - alias " + aliasName + " already exists for " + player.getName()); } - CardInfo cardInfo = CardRepository.instance.findCard(cardName); + // game tests don't need cards from a specific set, so it can be from any set + CardInfo cardInfo = CardRepository.instance.findCard(cardName, true); if (cardInfo == null) { throw new IllegalArgumentException("[TEST] Couldn't find a card: " + cardName); } diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java index fcfc1ce01a4..15de5a22743 100644 --- a/Mage/src/main/java/mage/cards/repository/CardRepository.java +++ b/Mage/src/main/java/mage/cards/repository/CardRepository.java @@ -369,12 +369,18 @@ public enum CardRepository { return Collections.emptyList(); } + public CardInfo findCard(String name) { + return findCard(name, false); + } + /** * @param name + * @param returnAnySet return card from first available set (WARNING, it's a performance optimization for tests, + * don't use it in real games - users must get random set) * @return random card with the provided name or null if none is found */ - public CardInfo findCard(String name) { - List cards = findCards(name); + public CardInfo findCard(String name, boolean returnAnySet) { + List cards = returnAnySet ? findCards(name, 1) : findCards(name); if (!cards.isEmpty()) { return cards.get(RandomUtil.nextInt(cards.size())); } @@ -447,9 +453,23 @@ public enum CardRepository { } public List findCards(String name) { + return findCards(name, 0); + } + + /** + * Find card's reprints from all sets + * + * @param name + * @param limitByMaxAmount return max amount of different cards (if 0 then return card from all sets) + * @return + */ + public List findCards(String name, long limitByMaxAmount) { try { QueryBuilder queryBuilder = cardDao.queryBuilder(); queryBuilder.where().eq("name", new SelectArg(name)); + if (limitByMaxAmount > 0) { + queryBuilder.limit(limitByMaxAmount); + } return cardDao.query(queryBuilder.prepare()); } catch (SQLException ex) { } From 10c85e40eb312e1f112a46f68f47e42b0970085b Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 17 Sep 2021 18:18:45 -0400 Subject: [PATCH 132/231] [MID] Implemented Rootcoil Creeper --- .../src/mage/cards/r/RootcoilCreeper.java | 112 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 113 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RootcoilCreeper.java diff --git a/Mage.Sets/src/mage/cards/r/RootcoilCreeper.java b/Mage.Sets/src/mage/cards/r/RootcoilCreeper.java new file mode 100644 index 00000000000..5439d14ac18 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RootcoilCreeper.java @@ -0,0 +1,112 @@ +package mage.cards.r; + +import mage.ConditionalMana; +import mage.MageInt; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.common.ExileSourceCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.ReturnToHandTargetEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.abilities.mana.AnyColorManaAbility; +import mage.abilities.mana.ConditionalAnyColorManaAbility; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.common.FilterOwnedCard; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.target.common.TargetCardInExile; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RootcoilCreeper extends CardImpl { + + private static final FilterCard filter = new FilterOwnedCard("card with flashback you own in exile"); + + static { + filter.add(new AbilityPredicate(FlashbackAbility.class)); + } + + public RootcoilCreeper(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{U}"); + + this.subtype.add(SubType.PLANT); + this.subtype.add(SubType.HORROR); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // {T}: Add one mana of any color. + this.addAbility(new AnyColorManaAbility()); + + // {T}: Add two mana of any one color. Spend this mana only to cast spells from your graveyard. + this.addAbility(new ConditionalAnyColorManaAbility(2, new RootcoilCreeperManaBuilder())); + + // {G}{U}, {T}, Exile Rootcoil Creeper: Return target card with flashback you own in exile to your hand. + Ability ability = new SimpleActivatedAbility(new ReturnToHandTargetEffect(), new ManaCostsImpl<>("{G}{U}")); + ability.addCost(new TapSourceCost()); + ability.addCost(new ExileSourceCost()); + ability.addTarget(new TargetCardInExile(filter)); + this.addAbility(ability); + } + + private RootcoilCreeper(final RootcoilCreeper card) { + super(card); + } + + @Override + public RootcoilCreeper copy() { + return new RootcoilCreeper(this); + } +} + +class RootcoilCreeperManaBuilder extends ConditionalManaBuilder { + + @Override + public ConditionalMana build(Object... options) { + return new RootcoilCreeperConditionalMana(this.mana); + } + + @Override + public String getRule() { + return "Spend this mana only to cast spells from your graveyard"; + } +} + +class RootcoilCreeperConditionalMana extends ConditionalMana { + + public RootcoilCreeperConditionalMana(Mana mana) { + super(mana); + staticText = "Spend this mana only to cast spells from your graveyard"; + addCondition(RootcoilCreeperManaCondition.instance); + } +} + +enum RootcoilCreeperManaCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + if (game == null || !game.inCheckPlayableState() + || !source.isControlledBy(game.getOwnerId(source.getSourceId()))) { + return false; + } + if (game.getCard(source.getSourceId()) != null + && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { + return true; + } + Spell spell = game.getSpell(source.getSourceId()); + return spell != null && spell.getFromZone() == Zone.GRAVEYARD; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index c26d43e754a..112a0de37f2 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -256,6 +256,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Ritual Guardian", 30, Rarity.COMMON, mage.cards.r.RitualGuardian.class)); cards.add(new SetCardInfo("Ritual of Hope", 31, Rarity.UNCOMMON, mage.cards.r.RitualOfHope.class)); cards.add(new SetCardInfo("Rockfall Vale", 266, Rarity.RARE, mage.cards.r.RockfallVale.class)); + cards.add(new SetCardInfo("Rootcoil Creeper", 238, Rarity.UNCOMMON, mage.cards.r.RootcoilCreeper.class)); cards.add(new SetCardInfo("Rotten Reunion", 119, Rarity.COMMON, mage.cards.r.RottenReunion.class)); cards.add(new SetCardInfo("Sacred Fire", 239, Rarity.UNCOMMON, mage.cards.s.SacredFire.class)); cards.add(new SetCardInfo("Saryth, the Viper's Fang", 197, Rarity.RARE, mage.cards.s.SarythTheVipersFang.class)); From 2c8314932e4b913065ec916676e3d62652080b52 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 17 Sep 2021 18:36:39 -0400 Subject: [PATCH 133/231] [MID] Implemented Sigarda's Splendor --- .../src/mage/cards/s/SigardasSplendor.java | 141 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 2 files changed, 142 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SigardasSplendor.java diff --git a/Mage.Sets/src/mage/cards/s/SigardasSplendor.java b/Mage.Sets/src/mage/cards/s/SigardasSplendor.java new file mode 100644 index 00000000000..8a4e4b1bea1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SigardasSplendor.java @@ -0,0 +1,141 @@ +package mage.cards.s; + +import mage.ObjectColor; +import mage.abilities.Ability; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.hint.Hint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.TargetController; +import mage.filter.FilterSpell; +import mage.filter.predicate.mageobject.ColorPredicate; +import mage.game.Game; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SigardasSplendor extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("a white spell"); + + static { + filter.add(new ColorPredicate(ObjectColor.WHITE)); + } + + public SigardasSplendor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}{W}"); + + // As Sigarda's Splendor enters the battlefield, note your life total. + this.addAbility(new AsEntersBattlefieldAbility(new SigardasSplendorNoteEffect())); + + // At the beginning of your upkeep, draw a card if your life total is greater than or equal to the last noted life total for Sigarda's Splendor. Then note your life total. + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new SigardasSplendorDrawEffect(), TargetController.YOU, false + ).addHint(SigardasSplendorHint.instance)); + + // Whenever you cast a white spell, you gain 1 life. + this.addAbility(new SpellCastControllerTriggeredAbility(new GainLifeEffect(1), filter, false)); + } + + private SigardasSplendor(final SigardasSplendor card) { + super(card); + } + + @Override + public SigardasSplendor copy() { + return new SigardasSplendor(this); + } + + static String getKey(Ability source, int offset) { + return "SigardasSplendor_" + source.getControllerId() + "_" + source.getSourceId() + + "_" + (source.getSourceObjectZoneChangeCounter() + offset); + } +} + +enum SigardasSplendorHint implements Hint { + instance; + + @Override + public String getText(Game game, Ability ability) { + if (ability.getSourcePermanentIfItStillExists(game) == null) { + return null; + } + Object object = game.getState().getValue(SigardasSplendor.getKey(ability, 0)); + return "Last noted life total: " + (object != null ? (Integer) object : "None"); + } + + @Override + public SigardasSplendorHint copy() { + return this; + } +} + +class SigardasSplendorNoteEffect extends OneShotEffect { + + SigardasSplendorNoteEffect() { + super(Outcome.Benefit); + staticText = "note your life total"; + } + + private SigardasSplendorNoteEffect(final SigardasSplendorNoteEffect effect) { + super(effect); + } + + @Override + public SigardasSplendorNoteEffect copy() { + return new SigardasSplendorNoteEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + game.getState().setValue(SigardasSplendor.getKey(source, -1), player.getLife()); + return true; + } +} + +class SigardasSplendorDrawEffect extends OneShotEffect { + + SigardasSplendorDrawEffect() { + super(Outcome.Benefit); + staticText = "draw a card if your life total is greater than or equal " + + "to the last noted life total for {this}. Then note your life total"; + } + + private SigardasSplendorDrawEffect(final SigardasSplendorDrawEffect effect) { + super(effect); + } + + @Override + public SigardasSplendorDrawEffect copy() { + return new SigardasSplendorDrawEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + String key = SigardasSplendor.getKey(source, 0); + Object object = game.getState().getValue(key); + int notedLife = object instanceof Integer ? (Integer) object : Integer.MIN_VALUE; + if (player.getLife() >= notedLife) { + player.drawCards(1, source, game); + } + game.getState().setValue(key, player.getLife()); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 112a0de37f2..8fb83395431 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -270,6 +270,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Shipwreck Marsh", 267, Rarity.RARE, mage.cards.s.ShipwreckMarsh.class)); cards.add(new SetCardInfo("Shipwreck Sifters", 74, Rarity.COMMON, mage.cards.s.ShipwreckSifters.class)); cards.add(new SetCardInfo("Siege Zombie", 121, Rarity.COMMON, mage.cards.s.SiegeZombie.class)); + cards.add(new SetCardInfo("Sigarda's Splendor", 33, Rarity.RARE, mage.cards.s.SigardasSplendor.class)); cards.add(new SetCardInfo("Sigarda, Champion of Light", 240, Rarity.MYTHIC, mage.cards.s.SigardaChampionOfLight.class)); cards.add(new SetCardInfo("Sigardian Savior", 34, Rarity.MYTHIC, mage.cards.s.SigardianSavior.class)); cards.add(new SetCardInfo("Silver Bolt", 258, Rarity.COMMON, mage.cards.s.SilverBolt.class)); From 2fa76fc7bee1f60d79cbd6637dcacbf19e870aae Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 18 Sep 2021 11:08:17 -0400 Subject: [PATCH 134/231] reworked Clockwork creature implementations (fixes #8292) --- .../src/mage/cards/c/ClockworkAvian.java | 86 +++++++++--------- .../src/mage/cards/c/ClockworkBeast.java | 89 +++++++++---------- .../src/mage/cards/c/ClockworkSteed.java | 87 +++++++++--------- .../src/mage/cards/c/ClockworkSwarm.java | 88 +++++++++--------- Mage/src/main/java/mage/cards/Card.java | 2 + Mage/src/main/java/mage/cards/CardImpl.java | 11 ++- .../java/mage/cards/ModalDoubleFacesCard.java | 4 +- Mage/src/main/java/mage/game/stack/Spell.java | 5 ++ .../AttackedOrBlockedThisCombatWatcher.java | 24 ++--- 9 files changed, 211 insertions(+), 185 deletions(-) diff --git a/Mage.Sets/src/mage/cards/c/ClockworkAvian.java b/Mage.Sets/src/mage/cards/c/ClockworkAvian.java index ca67107e045..1b7c5f3ea57 100644 --- a/Mage.Sets/src/mage/cards/c/ClockworkAvian.java +++ b/Mage.Sets/src/mage/cards/c/ClockworkAvian.java @@ -1,7 +1,5 @@ - package mage.cards.c; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EndOfCombatTriggeredAbility; @@ -12,26 +10,23 @@ import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalActivatedAbility; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.effects.common.counter.RemoveCounterSourceEffect; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.Counter; +import mage.constants.*; import mage.counters.CounterType; -import mage.counters.Counters; import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; import mage.watchers.common.AttackedOrBlockedThisCombatWatcher; +import java.util.UUID; + /** - * - * @author MarcoMarin + * @author TheElk801 */ public final class ClockworkAvian extends CardImpl { @@ -52,23 +47,16 @@ public final class ClockworkAvian extends CardImpl { // At end of combat, if Clockwork Avian attacked or blocked this combat, remove a +1/+0 counter from it. this.addAbility(new ConditionalInterveningIfTriggeredAbility( - new EndOfCombatTriggeredAbility(new RemoveCounterSourceEffect(CounterType.P1P0.createInstance()), false), - AttackedOrBlockedThisCombatSourceCondition.instance, - "At end of combat, if {this} attacked or blocked this combat, remove a +1/+0 counter from it."), - new AttackedOrBlockedThisCombatWatcher() - ); + new EndOfCombatTriggeredAbility( + new RemoveCounterSourceEffect(CounterType.P1P0.createInstance()), false + ), AttackedOrBlockedThisCombatSourceCondition.instance, "At end of combat, " + + "if {this} attacked or blocked this combat, remove a +1/+0 counter from it." + ), new AttackedOrBlockedThisCombatWatcher()); // {X}, {tap}: Put up to X +1/+0 counters on Clockwork Avian. This ability can't cause the total number of +1/+0 counters on Clockwork Avian to be greater than four. Activate this ability only during your upkeep. Ability ability = new ConditionalActivatedAbility( - Zone.BATTLEFIELD, - new AvianAddCountersSourceEffect( - CounterType.P1P0.createInstance(), - ManacostVariableValue.REGULAR, - true, true - ), - new ManaCostsImpl("{X}"), - new IsStepCondition(PhaseStep.UPKEEP), - null + Zone.BATTLEFIELD, new ClockworkAvianEffect(), + new ManaCostsImpl<>("{X}"), new IsStepCondition(PhaseStep.UPKEEP) ); ability.addCost(new TapSourceCost()); this.addAbility(ability); @@ -84,24 +72,42 @@ public final class ClockworkAvian extends CardImpl { } } -class AvianAddCountersSourceEffect extends AddCountersSourceEffect { +class ClockworkAvianEffect extends OneShotEffect { - public AvianAddCountersSourceEffect(Counter counter, DynamicValue amount, boolean informPlayers, boolean putOnCard) { - super(counter, amount, informPlayers, putOnCard); - staticText = "Put up to X +1/+0 counters on {this}. This ability can't cause the total number of +1/+0 counters on {this} to be greater than four."; + ClockworkAvianEffect() { + super(Outcome.Benefit); + staticText = "put up to X +1/+0 counters on {this}. This ability can't cause " + + "the total number of +1/+0 counters on {this} to be greater than four"; + } + + private ClockworkAvianEffect(final ClockworkAvianEffect effect) { + super(effect); + } + + @Override + public ClockworkAvianEffect copy() { + return new ClockworkAvianEffect(this); } @Override public boolean apply(Game game, Ability source) { - //record how many counters - Counters permCounters = game.getPermanent(source.getSourceId()).getCounters(game); - int countersWas = permCounters.getCount(CounterType.P1P0); - if (countersWas < 4) { - super.apply(game, source); - if (permCounters.getCount(CounterType.P1P0) > 4) { - permCounters.removeCounter(CounterType.P1P0, permCounters.getCount(CounterType.P1P0) - 4); - }//if countersWas < 4 then counter is min(current,4); there is no setCounters function tho - }//else this is a rare case of an Avian getting boosted by outside sources :) Which is the sole purpose of this if, for the benefit of this rare but not impossible case :p - return true; + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (player == null || permanent == null) { + return false; + } + int maxCounters = Integer.min( + 4 - permanent.getCounters(game).getCount(CounterType.P1P0), source.getManaCostsToPay().getX() + ); + if (maxCounters < 1) { + return false; + } + int toAdd = player.getAmount( + 0, maxCounters, "Choose how many +1/+0 counters to put on " + permanent.getName(), game + ); + return toAdd > 0 && permanent.addCounters( + CounterType.P1P0.createInstance(toAdd), source.getControllerId(), + source, game, null, true, 4 + ); } } diff --git a/Mage.Sets/src/mage/cards/c/ClockworkBeast.java b/Mage.Sets/src/mage/cards/c/ClockworkBeast.java index 48e257ada37..4d0c2b8bdff 100644 --- a/Mage.Sets/src/mage/cards/c/ClockworkBeast.java +++ b/Mage.Sets/src/mage/cards/c/ClockworkBeast.java @@ -1,7 +1,6 @@ package mage.cards.c; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EndOfCombatTriggeredAbility; @@ -12,25 +11,22 @@ import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalActivatedAbility; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.effects.common.counter.RemoveCounterSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.Counter; +import mage.constants.*; import mage.counters.CounterType; -import mage.counters.Counters; import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; import mage.watchers.common.AttackedOrBlockedThisCombatWatcher; +import java.util.UUID; + /** - * - * @author escplan9 (Derek Monturo - dmontur1 at gmail dot com) + * @author TheElk801 */ public final class ClockworkBeast extends CardImpl { @@ -48,23 +44,16 @@ public final class ClockworkBeast extends CardImpl { // At end of combat, if Clockwork Beast attacked or blocked this combat, remove a +1/+0 counter from it. this.addAbility(new ConditionalInterveningIfTriggeredAbility( - new EndOfCombatTriggeredAbility(new RemoveCounterSourceEffect(CounterType.P1P0.createInstance()), false), - AttackedOrBlockedThisCombatSourceCondition.instance, - "At end of combat, if {this} attacked or blocked this combat, remove a +1/+0 counter from it."), - new AttackedOrBlockedThisCombatWatcher() - ); + new EndOfCombatTriggeredAbility( + new RemoveCounterSourceEffect(CounterType.P1P0.createInstance()), false + ), AttackedOrBlockedThisCombatSourceCondition.instance, "At end of combat, " + + "if {this} attacked or blocked this combat, remove a +1/+0 counter from it." + ), new AttackedOrBlockedThisCombatWatcher()); // {X}, {tap}: Put up to X +1/+0 counters on Clockwork Beast. This ability can't cause the total number of +1/+0 counters on Clockwork Beast to be greater than seven. Activate this ability only during your upkeep. Ability ability = new ConditionalActivatedAbility( - Zone.BATTLEFIELD, - new BeastAddCountersSourceEffect( - CounterType.P1P0.createInstance(), - ManacostVariableValue.REGULAR, - true, true - ), - new ManaCostsImpl("{X}"), - new IsStepCondition(PhaseStep.UPKEEP), - null + Zone.BATTLEFIELD, new ClockworkBeastEffect(), + new ManaCostsImpl<>("{X}"), new IsStepCondition(PhaseStep.UPKEEP) ); ability.addCost(new TapSourceCost()); this.addAbility(ability); @@ -80,32 +69,42 @@ public final class ClockworkBeast extends CardImpl { } } -class BeastAddCountersSourceEffect extends AddCountersSourceEffect { +class ClockworkBeastEffect extends OneShotEffect { - public BeastAddCountersSourceEffect(Counter counter, DynamicValue amount, boolean informPlayers, boolean putOnCard) { - super(counter, amount, informPlayers, putOnCard); - staticText = "Put up to X +1/+0 counters on {this}. This ability can't cause the total number of +1/+0 counters on {this} to be greater than seven."; + ClockworkBeastEffect() { + super(Outcome.Benefit); + staticText = "put up to X +1/+0 counters on {this}. This ability can't cause " + + "the total number of +1/+0 counters on {this} to be greater than seven"; } - @Override - public boolean apply(Game game, Ability source) { - Counters permCounters = game.getPermanent(source.getSourceId()).getCounters(game); - int countersWas = permCounters.getCount(CounterType.P1P0); - if (countersWas < 7) { - super.apply(game, source); - if (permCounters.getCount(CounterType.P1P0) > 7) { - permCounters.removeCounter(CounterType.P1P0, permCounters.getCount(CounterType.P1P0) - 7); - }//if countersWas < 7 then counter is min(current,7); there is no setCounters function though - }//else this is a rare case of a Beast getting boosted by outside sources. Which is the sole purpose of this if, for the benefit of this rare but not impossible case - return true; - } - - public BeastAddCountersSourceEffect(final BeastAddCountersSourceEffect effect) { + private ClockworkBeastEffect(final ClockworkBeastEffect effect) { super(effect); } @Override - public BeastAddCountersSourceEffect copy() { - return new BeastAddCountersSourceEffect(this); + public ClockworkBeastEffect copy() { + return new ClockworkBeastEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (player == null || permanent == null) { + return false; + } + int maxCounters = Integer.min( + 7 - permanent.getCounters(game).getCount(CounterType.P1P0), source.getManaCostsToPay().getX() + ); + if (maxCounters < 1) { + return false; + } + int toAdd = player.getAmount( + 0, maxCounters, "Choose how many +1/+0 counters to put on " + permanent.getName(), game + ); + return toAdd > 0 && permanent.addCounters( + CounterType.P1P0.createInstance(toAdd), source.getControllerId(), + source, game, null, true, 7 + ); } } diff --git a/Mage.Sets/src/mage/cards/c/ClockworkSteed.java b/Mage.Sets/src/mage/cards/c/ClockworkSteed.java index dac9760b5e7..7ab435ef583 100644 --- a/Mage.Sets/src/mage/cards/c/ClockworkSteed.java +++ b/Mage.Sets/src/mage/cards/c/ClockworkSteed.java @@ -1,7 +1,5 @@ - package mage.cards.c; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EndOfCombatTriggeredAbility; @@ -13,28 +11,24 @@ import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalActivatedAbility; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesSourceEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.effects.common.counter.RemoveCounterSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Duration; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.Counter; +import mage.constants.*; import mage.counters.CounterType; -import mage.counters.Counters; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; import mage.watchers.common.AttackedOrBlockedThisCombatWatcher; +import java.util.UUID; + /** - * - * @author escplan9, MarcoMarin & L_J + * @author TheElk801 */ public final class ClockworkSteed extends CardImpl { @@ -58,23 +52,16 @@ public final class ClockworkSteed extends CardImpl { // At end of combat, if Clockwork Steed attacked or blocked this combat, remove a +1/+0 counter from it. this.addAbility(new ConditionalInterveningIfTriggeredAbility( - new EndOfCombatTriggeredAbility(new RemoveCounterSourceEffect(CounterType.P1P0.createInstance()), false), - AttackedOrBlockedThisCombatSourceCondition.instance, - "At end of combat, if {this} attacked or blocked this combat, remove a +1/+0 counter from it."), - new AttackedOrBlockedThisCombatWatcher() - ); + new EndOfCombatTriggeredAbility( + new RemoveCounterSourceEffect(CounterType.P1P0.createInstance()), false + ), AttackedOrBlockedThisCombatSourceCondition.instance, "At end of combat, " + + "if {this} attacked or blocked this combat, remove a +1/+0 counter from it." + ), new AttackedOrBlockedThisCombatWatcher()); // {X}, {tap}: Put up to X +1/+0 counters on Clockwork Steed. This ability can't cause the total number of +1/+0 counters on Clockwork Steed to be greater than four. Activate this ability only during your upkeep. Ability ability = new ConditionalActivatedAbility( - Zone.BATTLEFIELD, - new ClockworkSteedAddCountersSourceEffect( - CounterType.P1P0.createInstance(), - ManacostVariableValue.REGULAR, - true, true - ), - new ManaCostsImpl("{X}"), - new IsStepCondition(PhaseStep.UPKEEP), - null + Zone.BATTLEFIELD, new ClockworkSteedEffect(), + new ManaCostsImpl<>("{X}"), new IsStepCondition(PhaseStep.UPKEEP) ); ability.addCost(new TapSourceCost()); this.addAbility(ability); @@ -90,24 +77,42 @@ public final class ClockworkSteed extends CardImpl { } } -class ClockworkSteedAddCountersSourceEffect extends AddCountersSourceEffect { +class ClockworkSteedEffect extends OneShotEffect { - public ClockworkSteedAddCountersSourceEffect(Counter counter, DynamicValue amount, boolean informPlayers, boolean putOnCard) { - super(counter, amount, informPlayers, putOnCard); - staticText = "Put up to X +1/+0 counters on {this}. This ability can't cause the total number of +1/+0 counters on {this} to be greater than four."; + ClockworkSteedEffect() { + super(Outcome.Benefit); + staticText = "put up to X +1/+0 counters on {this}. This ability can't cause " + + "the total number of +1/+0 counters on {this} to be greater than four"; + } + + private ClockworkSteedEffect(final ClockworkSteedEffect effect) { + super(effect); + } + + @Override + public ClockworkSteedEffect copy() { + return new ClockworkSteedEffect(this); } @Override public boolean apply(Game game, Ability source) { - //record how many counters - Counters permCounters = game.getPermanent(source.getSourceId()).getCounters(game); - int countersWas = permCounters.getCount(CounterType.P1P0); - if (countersWas < 4) { - super.apply(game, source); - if (permCounters.getCount(CounterType.P1P0) > 4) { - permCounters.removeCounter(CounterType.P1P0, permCounters.getCount(CounterType.P1P0) - 4); - }//if countersWas < 4 then counter is min(current,4); there is no setCounters function tho - }//else this is a rare case of a Steed getting boosted by outside sources :) Which is the sole purpose of this if, for the benefit of this rare but not impossible case :p - return true; + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (player == null || permanent == null) { + return false; + } + int maxCounters = Integer.min( + 4 - permanent.getCounters(game).getCount(CounterType.P1P0), source.getManaCostsToPay().getX() + ); + if (maxCounters < 1) { + return false; + } + int toAdd = player.getAmount( + 0, maxCounters, "Choose how many +1/+0 counters to put on " + permanent.getName(), game + ); + return toAdd > 0 && permanent.addCounters( + CounterType.P1P0.createInstance(toAdd), source.getControllerId(), + source, game, null, true, 4 + ); } } diff --git a/Mage.Sets/src/mage/cards/c/ClockworkSwarm.java b/Mage.Sets/src/mage/cards/c/ClockworkSwarm.java index 02e88b6d084..2e8c15776d8 100644 --- a/Mage.Sets/src/mage/cards/c/ClockworkSwarm.java +++ b/Mage.Sets/src/mage/cards/c/ClockworkSwarm.java @@ -1,7 +1,5 @@ - package mage.cards.c; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EndOfCombatTriggeredAbility; @@ -13,27 +11,23 @@ import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalActivatedAbility; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesSourceEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.effects.common.counter.RemoveCounterSourceEffect; -import mage.constants.SubType; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.Counter; +import mage.constants.*; import mage.counters.CounterType; -import mage.counters.Counters; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; import mage.watchers.common.AttackedOrBlockedThisCombatWatcher; +import java.util.UUID; + /** - * * @author TheElk801 */ public final class ClockworkSwarm extends CardImpl { @@ -62,22 +56,16 @@ public final class ClockworkSwarm extends CardImpl { // At end of combat, if Clockwork Swarm attacked or blocked this combat, remove a +1/+0 counter from it. this.addAbility(new ConditionalInterveningIfTriggeredAbility( - new EndOfCombatTriggeredAbility(new RemoveCounterSourceEffect(CounterType.P1P0.createInstance()), false), - AttackedOrBlockedThisCombatSourceCondition.instance, - "At end of combat, if {this} attacked or blocked this combat, remove a +1/+0 counter from it."), - new AttackedOrBlockedThisCombatWatcher()); + new EndOfCombatTriggeredAbility( + new RemoveCounterSourceEffect(CounterType.P1P0.createInstance()), false + ), AttackedOrBlockedThisCombatSourceCondition.instance, "At end of combat, " + + "if {this} attacked or blocked this combat, remove a +1/+0 counter from it." + ), new AttackedOrBlockedThisCombatWatcher()); // {X}, {tap}: Put up to X +1/+0 counters on Clockwork Swarm. This ability can't cause the total number of +1/+0 counters on Clockwork Swarm to be greater than four. Activate this ability only during your upkeep. Ability ability = new ConditionalActivatedAbility( - Zone.BATTLEFIELD, - new SwarmAddCountersSourceEffect( - CounterType.P1P0.createInstance(), - ManacostVariableValue.REGULAR, - true, true - ), - new ManaCostsImpl("{X}"), - new IsStepCondition(PhaseStep.UPKEEP), - null + Zone.BATTLEFIELD, new ClockworkSwarmEffect(), + new ManaCostsImpl<>("{X}"), new IsStepCondition(PhaseStep.UPKEEP) ); ability.addCost(new TapSourceCost()); this.addAbility(ability); @@ -93,32 +81,42 @@ public final class ClockworkSwarm extends CardImpl { } } -class SwarmAddCountersSourceEffect extends AddCountersSourceEffect { +class ClockworkSwarmEffect extends OneShotEffect { - public SwarmAddCountersSourceEffect(Counter counter, DynamicValue amount, boolean informPlayers, boolean putOnCard) { - super(counter, amount, informPlayers, putOnCard); - staticText = "Put up to X +1/+0 counters on {this}. This ability can't cause the total number of +1/+0 counters on {this} to be greater than four."; + ClockworkSwarmEffect() { + super(Outcome.Benefit); + staticText = "put up to X +1/+0 counters on {this}. This ability can't cause " + + "the total number of +1/+0 counters on {this} to be greater than four"; } - @Override - public boolean apply(Game game, Ability source) { - Counters permCounters = game.getPermanent(source.getSourceId()).getCounters(game); - int countersWas = permCounters.getCount(CounterType.P1P0); - if (countersWas < 4) { - super.apply(game, source); - if (permCounters.getCount(CounterType.P1P0) > 4) { - permCounters.removeCounter(CounterType.P1P0, permCounters.getCount(CounterType.P1P0) - 4); - }//if countersWas < 4 then counter is min(current,4); there is no setCounters function though - }//else this is a rare case of a Beast getting boosted by outside sources. Which is the sole purpose of this if, for the benefit of this rare but not impossible case - return true; - } - - public SwarmAddCountersSourceEffect(final SwarmAddCountersSourceEffect effect) { + private ClockworkSwarmEffect(final ClockworkSwarmEffect effect) { super(effect); } @Override - public SwarmAddCountersSourceEffect copy() { - return new SwarmAddCountersSourceEffect(this); + public ClockworkSwarmEffect copy() { + return new ClockworkSwarmEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (player == null || permanent == null) { + return false; + } + int maxCounters = Integer.min( + 4 - permanent.getCounters(game).getCount(CounterType.P1P0), source.getManaCostsToPay().getX() + ); + if (maxCounters < 1) { + return false; + } + int toAdd = player.getAmount( + 0, maxCounters, "Choose how many +1/+0 counters to put on " + permanent.getName(), game + ); + return toAdd > 0 && permanent.addCounters( + CounterType.P1P0.createInstance(toAdd), source.getControllerId(), + source, game, null, true, 4 + ); } } diff --git a/Mage/src/main/java/mage/cards/Card.java b/Mage/src/main/java/mage/cards/Card.java index 5b99d66e858..d3b27fa0684 100644 --- a/Mage/src/main/java/mage/cards/Card.java +++ b/Mage/src/main/java/mage/cards/Card.java @@ -152,6 +152,8 @@ public interface Card extends MageObject { boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List appliedEffects, boolean isEffect); + boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List appliedEffects, boolean isEffect, int maxCounters); + void removeCounters(String name, int amount, Ability source, Game game); void removeCounters(Counter counter, Ability source, Game game); diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index 014c7400790..946710f81a2 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -704,12 +704,21 @@ public abstract class CardImpl extends MageObjectImpl implements Card { @Override public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List appliedEffects, boolean isEffect) { + return addCounters(counter, playerAddingCounters, source, game, appliedEffects, isEffect, Integer.MAX_VALUE); + } + + public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List appliedEffects, boolean isEffect, int maxCounters) { boolean returnCode = true; GameEvent addingAllEvent = GameEvent.getEvent(GameEvent.EventType.ADD_COUNTERS, objectId, source, playerAddingCounters, counter.getName(), counter.getCount()); addingAllEvent.setAppliedEffects(appliedEffects); addingAllEvent.setFlag(isEffect); if (!game.replaceEvent(addingAllEvent)) { - int amount = addingAllEvent.getAmount(); + int amount; + if (maxCounters < Integer.MAX_VALUE) { + amount = Integer.min(addingAllEvent.getAmount(), maxCounters - this.getCounters(game).getCount(counter.getName())); + } else { + amount = addingAllEvent.getAmount(); + } boolean isEffectFlag = addingAllEvent.getFlag(); int finalAmount = amount; for (int i = 0; i < amount; i++) { diff --git a/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java b/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java index 1680c05f062..db7062b9313 100644 --- a/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java +++ b/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java @@ -132,8 +132,8 @@ public abstract class ModalDoubleFacesCard extends CardImpl { } @Override - public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List appliedEffects, boolean isEffect) { - return leftHalfCard.addCounters(counter, playerAddingCounters, source, game, appliedEffects, isEffect); + public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List appliedEffects, boolean isEffect, int maxCounters) { + return leftHalfCard.addCounters(counter, playerAddingCounters, source, game, appliedEffects, isEffect, maxCounters); } @Override diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 82ea3710eae..910e19e5f27 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -1009,6 +1009,11 @@ public class Spell extends StackObjectImpl implements Card { return card.addCounters(counter, playerAddingCounters, source, game, appliedEffects, isEffect); } + @Override + public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List appliedEffects, boolean isEffect, int maxCounters) { + return card.addCounters(counter, playerAddingCounters, source, game, appliedEffects, isEffect, maxCounters); + } + @Override public void removeCounters(String name, int amount, Ability source, Game game) { card.removeCounters(name, amount, source, game); diff --git a/Mage/src/main/java/mage/watchers/common/AttackedOrBlockedThisCombatWatcher.java b/Mage/src/main/java/mage/watchers/common/AttackedOrBlockedThisCombatWatcher.java index 5f5410fca36..271c2c2d8ae 100644 --- a/Mage/src/main/java/mage/watchers/common/AttackedOrBlockedThisCombatWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/AttackedOrBlockedThisCombatWatcher.java @@ -1,15 +1,15 @@ package mage.watchers.common; -import java.util.HashSet; -import java.util.Set; import mage.MageObjectReference; import mage.constants.WatcherScope; import mage.game.Game; import mage.game.events.GameEvent; import mage.watchers.Watcher; +import java.util.HashSet; +import java.util.Set; + /** - * * @author LevelX2 */ public class AttackedOrBlockedThisCombatWatcher extends Watcher { @@ -23,14 +23,16 @@ public class AttackedOrBlockedThisCombatWatcher extends Watcher { @Override public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.BEGIN_COMBAT_STEP_PRE) { - this.getAttackedThisTurnCreatures().clear(); - } - if (event.getType() == GameEvent.EventType.ATTACKER_DECLARED) { - this.getAttackedThisTurnCreatures().add(new MageObjectReference(event.getSourceId(), game)); - } - if (event.getType() == GameEvent.EventType.BLOCKER_DECLARED) { - this.getBlockedThisTurnCreatures().add(new MageObjectReference(event.getSourceId(), game)); + switch (event.getType()) { + case BEGIN_COMBAT_STEP_PRE: + this.attackedThisTurnCreatures.clear(); + this.blockedThisTurnCreatures.clear(); + return; + case ATTACKER_DECLARED: + this.attackedThisTurnCreatures.add(new MageObjectReference(event.getSourceId(), game)); + return; + case BLOCKER_DECLARED: + this.blockedThisTurnCreatures.add(new MageObjectReference(event.getSourceId(), game)); } } From 2cf005f9712f545caeecd79ddb363fd1857f8d53 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 18 Sep 2021 14:03:54 +0400 Subject: [PATCH 135/231] Tests: improved performance in game tests; --- .../test/serverside/base/MageTestBase.java | 28 +++++---- .../serverside/base/MageTestPlayerBase.java | 16 +++-- .../base/impl/CardTestPlayerAPIImpl.java | 8 +-- Mage/src/main/java/mage/cards/decks/Deck.java | 59 +++++++++++++++---- .../java/mage/cards/decks/DeckCardInfo.java | 19 +++++- .../java/mage/cards/decks/DeckCardLayout.java | 27 ++++++++- 6 files changed, 123 insertions(+), 34 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestBase.java index 8fa74432277..996948230d4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestBase.java @@ -102,21 +102,25 @@ public abstract class MageTestBase { @BeforeClass public static void init() { Logger.getRootLogger().setLevel(Level.DEBUG); - deleteSavedGames(); - ConfigSettings config = new ConfigWrapper(ConfigFactory.loadFromFile("config/config.xml")); - config.getGameTypes().forEach((gameType) -> { - GameFactory.instance.addGameType(gameType.getName(), loadGameType(gameType), loadPlugin(gameType)); - }); - config.getTournamentTypes().forEach((tournamentType) -> { - TournamentFactory.instance.addTournamentType(tournamentType.getName(), loadTournamentType(tournamentType), loadPlugin(tournamentType)); - }); - config.getPlayerTypes().forEach((playerType) -> { - PlayerFactory.instance.addPlayerType(playerType.getName(), loadPlugin(playerType)); - }); + + // one time init for all tests + if (GameFactory.instance.getGameTypes().isEmpty()) { + deleteSavedGames(); + ConfigSettings config = new ConfigWrapper(ConfigFactory.loadFromFile("config/config.xml")); + config.getGameTypes().forEach((gameType) -> { + GameFactory.instance.addGameType(gameType.getName(), loadGameType(gameType), loadPlugin(gameType)); + }); + config.getTournamentTypes().forEach((tournamentType) -> { + TournamentFactory.instance.addTournamentType(tournamentType.getName(), loadTournamentType(tournamentType), loadPlugin(tournamentType)); + }); + config.getPlayerTypes().forEach((playerType) -> { + PlayerFactory.instance.addPlayerType(playerType.getName(), loadPlugin(playerType)); + }); // for (Plugin plugin : config.getDeckTypes()) { // DeckValidatorFactory.getInstance().addDeckType(plugin.getName(), loadPlugin(plugin)); // } - Copier.setLoader(classLoader); + Copier.setLoader(classLoader); + } } @SuppressWarnings("UseSpecificCatch") diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java index 2db1d1b67fa..864103cd90b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java @@ -81,7 +81,8 @@ public abstract class MageTestPlayerBase { protected Map> commands = new HashMap<>(); - protected static Map loadedDeckCardLists = new HashMap<>(); // test decks buffer + protected static Map loadedDecks = new HashMap<>(); // deck's cache + protected static Map loadedCardInfo = new HashMap<>(); // db card's cache protected TestPlayer playerA; protected TestPlayer playerB; @@ -131,12 +132,15 @@ public abstract class MageTestPlayerBase { logger.debug("Logging level: " + logger.getLevel()); logger.debug("Default charset: " + Charset.defaultCharset()); - deleteSavedGames(); - ConfigSettings config = new ConfigWrapper(ConfigFactory.loadFromFile("config/config.xml")); - for (GamePlugin plugin : config.getGameTypes()) { - GameFactory.instance.addGameType(plugin.getName(), loadGameType(plugin), loadPlugin(plugin)); + // one time init for all tests + if (GameFactory.instance.getGameTypes().isEmpty()) { + deleteSavedGames(); + ConfigSettings config = new ConfigWrapper(ConfigFactory.loadFromFile("config/config.xml")); + for (GamePlugin plugin : config.getGameTypes()) { + GameFactory.instance.addGameType(plugin.getName(), loadGameType(plugin), loadPlugin(plugin)); + } + Copier.setLoader(classLoader); } - Copier.setLoader(classLoader); } private static Class loadPlugin(Plugin plugin) { diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index ae6a38570e5..02e7ae5a013 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -259,13 +259,13 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement logger.debug("Loading deck..."); DeckCardLists list; - if (loadedDeckCardLists.containsKey(deckName)) { - list = loadedDeckCardLists.get(deckName); + if (loadedDecks.containsKey(deckName)) { + list = loadedDecks.get(deckName); } else { list = DeckImporter.importDeckFromFile(deckName, true); - loadedDeckCardLists.put(deckName, list); + loadedDecks.put(deckName, list); } - Deck deck = Deck.load(list, false, false); + Deck deck = Deck.load(list, false, false, loadedCardInfo); logger.debug("Done!"); if (deck.getCards().size() < 40) { throw new IllegalArgumentException("Couldn't load deck, deck size=" + deck.getCards().size()); diff --git a/Mage/src/main/java/mage/cards/decks/Deck.java b/Mage/src/main/java/mage/cards/decks/Deck.java index 1a5dd01bb22..0869c68d390 100644 --- a/Mage/src/main/java/mage/cards/decks/Deck.java +++ b/Mage/src/main/java/mage/cards/decks/Deck.java @@ -4,13 +4,17 @@ import mage.cards.Card; import mage.cards.repository.CardInfo; import mage.cards.repository.CardRepository; import mage.game.GameException; +import mage.util.Copyable; import mage.util.DeckUtil; import org.apache.log4j.Logger; import java.io.Serializable; import java.util.*; +import java.util.stream.Collectors; -public class Deck implements Serializable { +public class Deck implements Serializable, Copyable { + + static final int MAX_CARDS_PER_DECK = 1000; private String name; private final Set cards = new LinkedHashSet<>(); @@ -20,6 +24,20 @@ public class Deck implements Serializable { private long deckHashCode = 0; private long deckCompleteHashCode = 0; + public Deck() { + super(); + } + + public Deck(final Deck deck) { + this.name = deck.name; + this.cards.addAll(deck.cards.stream().map(Card::copy).collect(Collectors.toList())); + this.sideboard.addAll(deck.sideboard.stream().map(Card::copy).collect(Collectors.toList())); + this.cardsLayout = deck.cardsLayout == null ? null : deck.cardsLayout.copy(); + this.sideboardLayout = deck.sideboardLayout == null ? null : deck.sideboardLayout.copy(); + this.deckHashCode = deck.deckHashCode; + this.deckCompleteHashCode = deck.deckCompleteHashCode; + } + public static Deck load(DeckCardLists deckCardLists) throws GameException { return Deck.load(deckCardLists, false); } @@ -51,6 +69,10 @@ public class Deck implements Serializable { return currentDeck; } + public static Deck load(DeckCardLists deckCardLists, boolean ignoreErrors, boolean mockCards) throws GameException { + return load(deckCardLists, ignoreErrors, mockCards, null); + } + /** * Warning, AI can't play Mock cards, so call it with extra params in real games or tests * @@ -60,17 +82,17 @@ public class Deck implements Serializable { * @return * @throws GameException */ - public static Deck load(DeckCardLists deckCardLists, boolean ignoreErrors, boolean mockCards) throws GameException { + public static Deck load(DeckCardLists deckCardLists, boolean ignoreErrors, boolean mockCards, Map cardInfoCache) throws GameException { Deck deck = new Deck(); deck.setName(deckCardLists.getName()); - deck.cardsLayout = deckCardLists.getCardLayout(); - deck.sideboardLayout = deckCardLists.getSideboardLayout(); + deck.cardsLayout = deckCardLists.getCardLayout() == null ? null : deckCardLists.getCardLayout().copy(); + deck.sideboardLayout = deckCardLists.getSideboardLayout() == null ? null : deckCardLists.getSideboardLayout().copy(); List deckCardNames = new ArrayList<>(); int totalCards = 0; for (DeckCardInfo deckCardInfo : deckCardLists.getCards()) { - Card card = createCard(deckCardInfo, mockCards); + Card card = createCard(deckCardInfo, mockCards, cardInfoCache); if (card != null) { - if (totalCards > 1000) { + if (totalCards > MAX_CARDS_PER_DECK) { break; } deck.cards.add(card); @@ -83,9 +105,9 @@ public class Deck implements Serializable { } List sbCardNames = new ArrayList<>(); for (DeckCardInfo deckCardInfo : deckCardLists.getSideboard()) { - Card card = createCard(deckCardInfo, mockCards); + Card card = createCard(deckCardInfo, mockCards, cardInfoCache); if (card != null) { - if (totalCards > 1000) { + if (totalCards > MAX_CARDS_PER_DECK) { break; } deck.sideboard.add(card); @@ -127,8 +149,21 @@ public class Deck implements Serializable { } - private static Card createCard(DeckCardInfo deckCardInfo, boolean mockCards) { - CardInfo cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNum()); + private static Card createCard(DeckCardInfo deckCardInfo, boolean mockCards, Map cardInfoCache) { + CardInfo cardInfo; + if (cardInfoCache != null) { + // from cache + String key = String.format("%s_%s", deckCardInfo.getSetCode(), deckCardInfo.getCardNum()); + cardInfo = cardInfoCache.getOrDefault(key, null); + if (cardInfo == null) { + cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNum()); + cardInfoCache.put(key, cardInfo); + } + } else { + // from db + cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNum()); + } + if (cardInfo == null) { return null; } @@ -226,4 +261,8 @@ public class Deck implements Serializable { this.sideboardLayout = null; } + @Override + public Deck copy() { + return new Deck(this); + } } diff --git a/Mage/src/main/java/mage/cards/decks/DeckCardInfo.java b/Mage/src/main/java/mage/cards/decks/DeckCardInfo.java index 991af94fd02..6423805e317 100644 --- a/Mage/src/main/java/mage/cards/decks/DeckCardInfo.java +++ b/Mage/src/main/java/mage/cards/decks/DeckCardInfo.java @@ -2,19 +2,32 @@ package mage.cards.decks; +import mage.util.Copyable; + import java.io.Serializable; /** * * @author LevelX2 */ -public class DeckCardInfo implements Serializable { +public class DeckCardInfo implements Serializable, Copyable { private String cardName; private String setCode; private String cardNum; private int quantity; + public DeckCardInfo() { + super(); + } + + public DeckCardInfo(final DeckCardInfo info) { + this.cardName = info.cardName; + this.setCode = info.setCode; + this.cardNum = info.cardNum; + this.quantity = info.quantity; + } + public DeckCardInfo(String cardName, String cardNum, String setCode) { this(cardName, cardNum, setCode, 1); } @@ -51,4 +64,8 @@ public class DeckCardInfo implements Serializable { return setCode + cardNum; } + @Override + public DeckCardInfo copy() { + return new DeckCardInfo(this); + } } diff --git a/Mage/src/main/java/mage/cards/decks/DeckCardLayout.java b/Mage/src/main/java/mage/cards/decks/DeckCardLayout.java index eaf6dc4da95..9bf9645f199 100644 --- a/Mage/src/main/java/mage/cards/decks/DeckCardLayout.java +++ b/Mage/src/main/java/mage/cards/decks/DeckCardLayout.java @@ -1,15 +1,35 @@ package mage.cards.decks; +import mage.util.Copyable; + +import java.util.ArrayList; import java.util.List; /** * Created by stravant@gmail.com on 2016-10-03. */ -public class DeckCardLayout { +public class DeckCardLayout implements Copyable { private final List>> cards; private final String settings; + public DeckCardLayout(final DeckCardLayout layout) { + this.cards = new ArrayList<>(); + for (int i1 = 0; i1 < layout.cards.size(); i1++) { + List> list1 = new ArrayList<>(); + this.cards.add(list1); + for (int i2 = 0; i2 < layout.cards.get(i1).size(); i2++) { + List list2 = new ArrayList<>(); + list1.add(list2); + for (int i3 = 0; i3 < layout.cards.get(i1).get(i2).size(); i3++) { + DeckCardInfo info = layout.cards.get(i1).get(i2).get(i3); + list2.add(info.copy()); + } + } + } + this.settings = layout.settings; + } + public DeckCardLayout(List>> cards, String settings) { this.cards = cards; this.settings = settings; @@ -22,4 +42,9 @@ public class DeckCardLayout { public String getSettings() { return settings; } + + @Override + public DeckCardLayout copy() { + return new DeckCardLayout(this); + } } From 3d0571d5e9a75301820c4eb8818cda381bd954d1 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 18 Sep 2021 16:41:48 -0400 Subject: [PATCH 136/231] [MIC] Implemented Tomb Tyrant --- Mage.Sets/src/mage/cards/t/TombTyrant.java | 118 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + .../costs/common/SacrificeTargetCost.java | 8 +- 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/t/TombTyrant.java diff --git a/Mage.Sets/src/mage/cards/t/TombTyrant.java b/Mage.Sets/src/mage/cards/t/TombTyrant.java new file mode 100644 index 00000000000..3aadd1726d0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TombTyrant.java @@ -0,0 +1,118 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.ActivateIfConditionActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.CompoundCondition; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.CardsInControllerGraveyardCondition; +import mage.abilities.condition.common.MyTurnCondition; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.hint.common.MyTurnHint; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreatureCard; +import mage.filter.common.FilterCreaturePermanent; +import mage.game.Game; +import mage.players.Player; +import mage.util.RandomUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TombTyrant extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(SubType.ZOMBIE, "Zombies"); + private static final FilterCard filter2 = new FilterCreatureCard(); + + static { + filter2.add(SubType.ZOMBIE.getPredicate()); + } + + private static final Condition condition = new CompoundCondition( + "during your turn and only if there are at least three Zombie creature cards in your graveyard", + MyTurnCondition.instance, new CardsInControllerGraveyardCondition(3, filter2) + ); + private static final Hint hint = new ValueHint( + "Zombie creatures in your graveyard", new CardsInControllerGraveyardCount(filter2) + ); + + public TombTyrant(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.NOBLE); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Other Zombies you control get +1/+1. + this.addAbility(new SimpleStaticAbility(new BoostControlledEffect( + 1, 1, Duration.WhileOnBattlefield, filter, true + ))); + + // {2}{B}, {T}, Sacrifice a creature: Return a Zombie creature card at random from your graveyard to the battlefield. Activate only during your turn and only if there are at least three Zombie creature cards in your graveyard. + Ability ability = new ActivateIfConditionActivatedAbility( + Zone.BATTLEFIELD, new TombTyrantEffect(), + new ManaCostsImpl<>("{2}{B}"), condition + ); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeTargetCost(StaticFilters.FILTER_CONTROLLED_ANOTHER_CREATURE)); + this.addAbility(ability.addHint(MyTurnHint.instance).addHint(hint)); + } + + private TombTyrant(final TombTyrant card) { + super(card); + } + + @Override + public TombTyrant copy() { + return new TombTyrant(this); + } +} + +class TombTyrantEffect extends OneShotEffect { + + private static final FilterCard filter = new FilterCreatureCard(); + + static { + filter.add(SubType.ZOMBIE.getPredicate()); + } + + TombTyrantEffect() { + super(Outcome.Benefit); + staticText = "return a Zombie creature card at random from your graveyard to the battlefield"; + } + + private TombTyrantEffect(final TombTyrantEffect effect) { + super(effect); + } + + @Override + public TombTyrantEffect copy() { + return new TombTyrantEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Card card = RandomUtil.randomFromCollection(player.getGraveyard().getCards(filter, game)); + return card != null && player.moveCards(card, Zone.BATTLEFIELD, source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index f5fb60237b5..dd9227bf84c 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -133,6 +133,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Temple of Deceit", 184, Rarity.RARE, mage.cards.t.TempleOfDeceit.class)); cards.add(new SetCardInfo("Temple of Plenty", 185, Rarity.RARE, mage.cards.t.TempleOfPlenty.class)); cards.add(new SetCardInfo("Temple of the False God", 186, Rarity.UNCOMMON, mage.cards.t.TempleOfTheFalseGod.class)); + cards.add(new SetCardInfo("Tomb Tyrant", 23, Rarity.RARE, mage.cards.t.TombTyrant.class)); cards.add(new SetCardInfo("Trostani's Summoner", 156, Rarity.UNCOMMON, mage.cards.t.TrostanisSummoner.class)); cards.add(new SetCardInfo("Unbreakable Formation", 95, Rarity.RARE, mage.cards.u.UnbreakableFormation.class)); cards.add(new SetCardInfo("Unclaimed Territory", 187, Rarity.UNCOMMON, mage.cards.u.UnclaimedTerritory.class)); diff --git a/Mage/src/main/java/mage/abilities/costs/common/SacrificeTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/SacrificeTargetCost.java index 19a54ee2c90..08cbf858f45 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/SacrificeTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/SacrificeTargetCost.java @@ -1,4 +1,3 @@ - package mage.abilities.costs.common; import mage.abilities.Ability; @@ -7,6 +6,7 @@ import mage.abilities.costs.Cost; import mage.abilities.costs.CostImpl; import mage.constants.AbilityType; import mage.constants.Outcome; +import mage.filter.common.FilterControlledPermanent; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.common.TargetControlledPermanent; @@ -23,6 +23,10 @@ public class SacrificeTargetCost extends CostImpl { private final List permanents = new ArrayList<>(); + public SacrificeTargetCost(FilterControlledPermanent filter) { + this(new TargetControlledPermanent(filter)); + } + public SacrificeTargetCost(TargetControlledPermanent target) { this.addTarget(target); target.setNotTarget(true); // sacrifice is never targeted @@ -88,7 +92,7 @@ public class SacrificeTargetCost extends CostImpl { } } // solves issue #8097, if a sacrifice cost is optional and you don't have valid targets, then the cost can be paid - if(validTargets == 0 && targets.get(0).getMinNumberOfTargets() == 0){ + if (validTargets == 0 && targets.get(0).getMinNumberOfTargets() == 0) { return true; } return false; From affd33c23bb5691362a2e3d32d64cca1211ffc8b Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 18 Sep 2021 16:58:59 -0400 Subject: [PATCH 137/231] [MIC] Implemented Cleaver Skaab --- Mage.Sets/src/mage/cards/c/CleaverSkaab.java | 95 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + .../common/CreateTokenCopyTargetEffect.java | 6 +- Mage/src/main/java/mage/util/CardUtil.java | 6 ++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/c/CleaverSkaab.java diff --git a/Mage.Sets/src/mage/cards/c/CleaverSkaab.java b/Mage.Sets/src/mage/cards/c/CleaverSkaab.java new file mode 100644 index 00000000000..b5c78518df0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CleaverSkaab.java @@ -0,0 +1,95 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.util.CardUtil; + +import java.util.Collection; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CleaverSkaab extends CardImpl { + + private static final FilterControlledPermanent filter + = new FilterControlledPermanent(SubType.ZOMBIE, "another Zombie"); + + static { + filter.add(AnotherPredicate.instance); + } + + public CleaverSkaab(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.HORROR); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // {3}, {T}, Sacrifice another Zombie: Create two tokens that are copies of the sacrificed creature. + Ability ability = new SimpleActivatedAbility(new CleaverSkaabEffect(), new GenericManaCost(3)); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeTargetCost(filter)); + this.addAbility(ability); + } + + private CleaverSkaab(final CleaverSkaab card) { + super(card); + } + + @Override + public CleaverSkaab copy() { + return new CleaverSkaab(this); + } +} + +class CleaverSkaabEffect extends OneShotEffect { + + CleaverSkaabEffect() { + super(Outcome.Benefit); + staticText = "create two tokens that are copies of the sacrificed creature"; + } + + private CleaverSkaabEffect(final CleaverSkaabEffect effect) { + super(effect); + } + + @Override + public CleaverSkaabEffect copy() { + return new CleaverSkaabEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = CardUtil.castStream( + source.getCosts().stream(), SacrificeTargetCost.class + ) + .map(SacrificeTargetCost::getPermanents) + .flatMap(Collection::stream) + .findFirst() + .orElse(null); + if (permanent == null) { + return false; + } + CreateTokenCopyTargetEffect effect = new CreateTokenCopyTargetEffect(); + effect.setSavedPermanent(permanent); + effect.setNumber(2); + return effect.apply(game, source); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index dd9227bf84c..c11a47516c1 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -43,6 +43,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Choked Estuary", 169, Rarity.RARE, mage.cards.c.ChokedEstuary.class)); cards.add(new SetCardInfo("Citadel Siege", 81, Rarity.RARE, mage.cards.c.CitadelSiege.class)); cards.add(new SetCardInfo("Cleansing Nova", 82, Rarity.RARE, mage.cards.c.CleansingNova.class)); + cards.add(new SetCardInfo("Cleaver Skaab", 11, Rarity.RARE, mage.cards.c.CleaverSkaab.class)); cards.add(new SetCardInfo("Command Tower", 170, Rarity.COMMON, mage.cards.c.CommandTower.class)); cards.add(new SetCardInfo("Commander's Sphere", 159, Rarity.COMMON, mage.cards.c.CommandersSphere.class)); cards.add(new SetCardInfo("Corpse Augur", 109, Rarity.UNCOMMON, mage.cards.c.CorpseAugur.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java index f090a430243..2e654033f6b 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java @@ -33,7 +33,7 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { private final UUID playerId; private final CardType additionalCardType; private boolean hasHaste; - private final int number; + private int number; private final List addedTokenPermanents; private SubType additionalSubType; private SubType onlySubType; @@ -302,6 +302,10 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { this.startingLoyalty = startingLoyalty; } + public void setNumber(int number) { + this.number = number; + } + public void exileTokensCreatedAtNextEndStep(Game game, Ability source) { for (Permanent tokenPermanent : addedTokenPermanents) { ExileTargetEffect exileEffect = new ExileTargetEffect(null, "", Zone.BATTLEFIELD); diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index c5a9cffbcae..b9837f53437 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -41,6 +41,7 @@ import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author nantuko @@ -1365,4 +1366,9 @@ public final class CardUtil { } return sb.toString(); } + + public static Stream castStream(Stream stream, Class clazz) { + return stream.filter(clazz::isInstance).map(clazz::cast); + } + } From a3ca9fc03a7d4fda05095aa8508b949d43b16eca Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sun, 19 Sep 2021 04:01:01 +0400 Subject: [PATCH 138/231] Dauthi Voidwalker - correct code for #8141 and same cards (see comments in fd719ad287484a50c97378a293ece5aa93c1d7e2); --- .../mage/cards/d/DarigaazReincarnated.java | 34 ++++------- .../src/mage/cards/d/DauthiVoidwalker.java | 19 ++----- .../src/mage/cards/d/DraugrNecromancer.java | 7 +-- Mage.Sets/src/mage/cards/e/Epochrasite.java | 38 +++++++------ .../src/mage/cards/j/JhoiraOfTheGhitu.java | 2 + .../src/mage/cards/m/MairsilThePretender.java | 56 +++++++++---------- .../single/mh2/DauthiVoidwalkerTest.java | 42 ++++++++++++++ Mage/src/main/java/mage/util/CardUtil.java | 32 +++++++++++ 8 files changed, 142 insertions(+), 88 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/DauthiVoidwalkerTest.java diff --git a/Mage.Sets/src/mage/cards/d/DarigaazReincarnated.java b/Mage.Sets/src/mage/cards/d/DarigaazReincarnated.java index a4363062da4..5a7a0756ea5 100644 --- a/Mage.Sets/src/mage/cards/d/DarigaazReincarnated.java +++ b/Mage.Sets/src/mage/cards/d/DarigaazReincarnated.java @@ -1,7 +1,5 @@ - package mage.cards.d; -import java.util.UUID; import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; @@ -18,22 +16,18 @@ import mage.abilities.keyword.TrampleAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.SuperType; -import mage.constants.TargetController; -import mage.constants.Zone; +import mage.constants.*; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; import mage.players.Player; +import mage.util.CardUtil; + +import java.util.UUID; /** - * * @author TheElk801 */ public final class DarigaazReincarnated extends CardImpl { @@ -92,15 +86,11 @@ class DarigaazReincarnatedDiesEffect extends ReplacementEffectImpl { public boolean replaceEvent(GameEvent event, Ability source, Game game) { Permanent permanent = ((ZoneChangeEvent) event).getTarget(); Player controller = game.getPlayer(source.getControllerId()); - if (controller != null && permanent != null) { - Card permCard = game.getCard(permanent.getId()); - if (permCard == null) { - return false; - } - return controller.moveCardToExileWithInfo(permanent, null, null, source, game, Zone.BATTLEFIELD, true) - && permCard.addCounters(CounterType.EGG.createInstance(3), source.getControllerId(), source, game); + if (permanent == null || controller == null) { + return false; } - return false; + + return CardUtil.moveCardWithCounter(game, source, controller, permanent, Zone.EXILED, CounterType.EGG.createInstance(3)); } @Override @@ -125,7 +115,7 @@ class DarigaazReincarnatedInterveningIfTriggeredAbility extends ConditionalInter super(new BeginningOfUpkeepTriggeredAbility(Zone.EXILED, new DarigaazReincarnatedReturnEffect(), TargetController.YOU, false), DarigaazReincarnatedCondition.instance, "At the beginning of your upkeep, if {this} is exiled with an egg counter on it, " - + "remove an egg counter from it. Then if {this} has no egg counters on it, return it to the battlefield"); + + "remove an egg counter from it. Then if {this} has no egg counters on it, return it to the battlefield"); } public DarigaazReincarnatedInterveningIfTriggeredAbility(final DarigaazReincarnatedInterveningIfTriggeredAbility effect) { @@ -181,10 +171,8 @@ enum DarigaazReincarnatedCondition implements Condition { public boolean apply(Game game, Ability source) { Card card = game.getCard(source.getSourceId()); if (card != null) { - if (game.getState().getZone(card.getId()) == Zone.EXILED - && card.getCounters(game).getCount(CounterType.EGG) > 0) { - return true; - } + return game.getState().getZone(card.getId()) == Zone.EXILED + && card.getCounters(game).getCount(CounterType.EGG) > 0; } return false; } diff --git a/Mage.Sets/src/mage/cards/d/DauthiVoidwalker.java b/Mage.Sets/src/mage/cards/d/DauthiVoidwalker.java index 45e6a97c655..47f3e6acff7 100644 --- a/Mage.Sets/src/mage/cards/d/DauthiVoidwalker.java +++ b/Mage.Sets/src/mage/cards/d/DauthiVoidwalker.java @@ -19,9 +19,11 @@ import mage.filter.FilterCard; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInExile; import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; import java.util.UUID; @@ -80,21 +82,12 @@ class DauthiVoidwalkerReplacementEffect extends ReplacementEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { Player controller = game.getPlayer(source.getControllerId()); - Card card = ((ZoneChangeEvent) event).getTarget(); - if (card == null) { - card = game.getCard(event.getTargetId()); - } - if (controller == null - || card == null) { + Permanent permanent = ((ZoneChangeEvent) event).getTarget(); + if (controller == null || permanent == null) { return false; } - controller.moveCards(card, Zone.EXILED, source, game); - // okay, not sure why this needs to be done to work correctly with creature permanents - Card cardFromObject = (Card) game.getObject(card.getId()); - if (cardFromObject != null) { - return cardFromObject.addCounters(CounterType.VOID.createInstance(), source.getControllerId(), source, game); - } - return false; + CardUtil.moveCardWithCounter(game, source, controller, permanent, Zone.EXILED, CounterType.VOID.createInstance()); + return true; } @Override diff --git a/Mage.Sets/src/mage/cards/d/DraugrNecromancer.java b/Mage.Sets/src/mage/cards/d/DraugrNecromancer.java index 3e8d08a85f7..b664c9e230c 100644 --- a/Mage.Sets/src/mage/cards/d/DraugrNecromancer.java +++ b/Mage.Sets/src/mage/cards/d/DraugrNecromancer.java @@ -17,6 +17,7 @@ import mage.game.permanent.Permanent; import mage.game.permanent.PermanentToken; import mage.players.ManaPoolItem; import mage.players.Player; +import mage.util.CardUtil; import java.util.UUID; @@ -79,10 +80,8 @@ class DraugrNecromancerReplacementEffect extends ReplacementEffectImpl { || !controller.hasOpponent(permanent.getControllerId(), game)) { return false; } - Card card = game.getCard(permanent.getId()); - controller.moveCards(permanent, Zone.EXILED, source, game); - card.getMainCard().addCounters(CounterType.ICE.createInstance(), source.getControllerId(), source, game); - return true; + + return CardUtil.moveCardWithCounter(game, source, controller, permanent, Zone.EXILED, CounterType.ICE.createInstance()); } @Override diff --git a/Mage.Sets/src/mage/cards/e/Epochrasite.java b/Mage.Sets/src/mage/cards/e/Epochrasite.java index a6122b684ee..6c6b15ee5b1 100644 --- a/Mage.Sets/src/mage/cards/e/Epochrasite.java +++ b/Mage.Sets/src/mage/cards/e/Epochrasite.java @@ -1,7 +1,5 @@ - package mage.cards.e; -import java.util.UUID; import mage.MageInt; import mage.MageObjectReference; import mage.abilities.Ability; @@ -17,22 +15,23 @@ import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; import mage.players.Player; import mage.watchers.common.CastFromHandWatcher; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class Epochrasite extends CardImpl { public Epochrasite(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT,CardType.CREATURE},"{2}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{2}"); this.subtype.add(SubType.CONSTRUCT); this.power = new MageInt(1); @@ -40,9 +39,9 @@ public final class Epochrasite extends CardImpl { // Epochrasite enters the battlefield with three +1/+1 counters on it if you didn't cast it from your hand. this.addAbility(new EntersBattlefieldAbility( - new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)), - new InvertCondition(CastFromHandSourcePermanentCondition.instance), - "{this} enters the battlefield with three +1/+1 counters on it if you didn't cast it from your hand",""), + new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)), + new InvertCondition(CastFromHandSourcePermanentCondition.instance), + "{this} enters the battlefield with three +1/+1 counters on it if you didn't cast it from your hand", ""), new CastFromHandWatcher()); // When Epochrasite dies, exile it with three time counters on it and it gains suspend. @@ -79,15 +78,20 @@ class EpochrasiteEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); Card card = game.getCard(source.getSourceId()); - if (controller != null && card != null) { - if (game.getState().getZone(card.getId()) == Zone.GRAVEYARD) { - UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game); - controller.moveCardToExileWithInfo(card, exileId, "Suspended cards of " + controller.getName(), source, game, Zone.GRAVEYARD, true); - card.addCounters(CounterType.TIME.createInstance(3), source.getControllerId(), source, game); - game.addEffect(new GainSuspendEffect(new MageObjectReference(card, game)), source); - } - return true; + if (controller == null || card == null) { + return false; } - return false; + card = card.getMainCard(); + + if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) { + return false; + } + + UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game); + controller.moveCardToExileWithInfo(card, exileId, "Suspended cards of " + controller.getName(), source, game, Zone.GRAVEYARD, true); + card.addCounters(CounterType.TIME.createInstance(3), source.getControllerId(), source, game); + game.addEffect(new GainSuspendEffect(new MageObjectReference(card, game)), source); + + return true; } } diff --git a/Mage.Sets/src/mage/cards/j/JhoiraOfTheGhitu.java b/Mage.Sets/src/mage/cards/j/JhoiraOfTheGhitu.java index bfc7e937e9d..c2b5cb9ef5b 100644 --- a/Mage.Sets/src/mage/cards/j/JhoiraOfTheGhitu.java +++ b/Mage.Sets/src/mage/cards/j/JhoiraOfTheGhitu.java @@ -88,6 +88,8 @@ class JhoiraOfTheGhituSuspendEffect extends OneShotEffect { if (card == null) { return false; } + card = card.getMainCard(); + boolean hasSuspend = card.getAbilities(game).containsClass(SuspendAbility.class); UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game); diff --git a/Mage.Sets/src/mage/cards/m/MairsilThePretender.java b/Mage.Sets/src/mage/cards/m/MairsilThePretender.java index c1e0146f003..7329c29521e 100644 --- a/Mage.Sets/src/mage/cards/m/MairsilThePretender.java +++ b/Mage.Sets/src/mage/cards/m/MairsilThePretender.java @@ -1,7 +1,5 @@ package mage.cards.m; -import java.util.Objects; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.ActivatedAbility; @@ -12,14 +10,7 @@ import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; -import mage.constants.SubType; -import mage.constants.SuperType; -import mage.constants.Zone; +import mage.constants.*; import mage.counters.CounterType; import mage.filter.FilterCard; import mage.filter.predicate.Predicates; @@ -29,9 +20,12 @@ import mage.players.Player; import mage.target.Target; import mage.target.common.TargetCardInHand; import mage.target.common.TargetCardInYourGraveyard; +import mage.util.CardUtil; + +import java.util.Objects; +import java.util.UUID; /** - * * @author TheElk801 */ public final class MairsilThePretender extends CardImpl { @@ -90,27 +84,27 @@ class MairsilThePretenderExileEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - if (controller.chooseUse(Outcome.PutCardInPlay, "Exile a card from your hand? (No = from graveyard)", source, game)) { - Target target = new TargetCardInHand(0, 1, filter); - controller.choose(outcome, target, source.getSourceId(), game); - Card card = controller.getHand().get(target.getFirstTarget(), game); - if (card != null) { - controller.moveCards(card, Zone.EXILED, source, game); - card.addCounters(CounterType.CAGE.createInstance(), source.getControllerId(), source, game); - } - } else { - Target target = new TargetCardInYourGraveyard(0, 1, filter); - target.choose(Outcome.PutCardInPlay, source.getControllerId(), source.getSourceId(), game); - Card card = controller.getGraveyard().get(target.getFirstTarget(), game); - if (card != null) { - controller.moveCards(card, Zone.EXILED, source, game); - card.addCounters(CounterType.CAGE.createInstance(), source.getControllerId(), source, game); - } - } - return true; + if (controller == null) { + return false; } - return false; + + // Outcome.Detriment - AI must exile from grave only, not hand + Target target; + if (controller.chooseUse(Outcome.Detriment, "Exile a card from your hand? (No = from graveyard)", source, game)) { + // from hand + target = new TargetCardInHand(0, 1, filter); + controller.choose(outcome, target, source.getSourceId(), game); + } else { + // from graveyard + target = new TargetCardInYourGraveyard(0, 1, filter); + target.choose(outcome, source.getControllerId(), source.getSourceId(), game); + } + + Card card = controller.getHand().get(target.getFirstTarget(), game); + if (card != null) { + CardUtil.moveCardWithCounter(game, source, controller, card, Zone.EXILED, CounterType.CAGE.createInstance()); + } + return true; } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/DauthiVoidwalkerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/DauthiVoidwalkerTest.java new file mode 100644 index 00000000000..cb3aa068f82 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/DauthiVoidwalkerTest.java @@ -0,0 +1,42 @@ +package org.mage.test.cards.single.mh2; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class DauthiVoidwalkerTest extends CardTestPlayerBase { + + @Test + public void test_Play() { + // If a card would be put into an opponent's graveyard from anywhere, instead exile it with a void counter on it. + // {T}, Sacrifice Dauthi Voidwalker: Choose an exiled card an opponent owns with a void counter on it. You may play it this turn without paying its mana cost. + addCard(Zone.BATTLEFIELD, playerA, "Dauthi Voidwalker", 1); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); + // + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + + // kill B's creature and exile with void counter + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Balduvian Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkExileCount("after exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); + + // can play it for free + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice"); + setChoice(playerA, "Balduvian Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index b9837f53437..fdbae854868 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -11,12 +11,16 @@ import mage.abilities.condition.Condition; import mage.abilities.costs.VariableCost; import mage.abilities.costs.mana.*; import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.Effect; import mage.abilities.effects.common.asthought.CanPlayCardControllerEffect; import mage.abilities.effects.common.asthought.YouMaySpendManaAsAnyColorToCastTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.abilities.hint.Hint; import mage.abilities.hint.HintUtils; import mage.cards.*; import mage.constants.*; +import mage.counters.Counter; +import mage.counters.CounterType; import mage.filter.Filter; import mage.filter.predicate.mageobject.NamePredicate; import mage.game.CardState; @@ -1371,4 +1375,32 @@ public final class CardUtil { return stream.filter(clazz::isInstance).map(clazz::cast); } + /** + * Move card or permanent to dest zone and add counter to it + * + * @param game + * @param source + * @param controller + * @param card can be card or permanent + * @param toZone + * @param counter + */ + public static boolean moveCardWithCounter(Game game, Ability source, Player controller, Card card, Zone toZone, Counter counter) { + if (toZone == Zone.BATTLEFIELD) { + throw new IllegalArgumentException("Wrong code usage - method doesn't support moving to battlefield zone"); + } + + // move to zone + if (!controller.moveCards(card, toZone, source, game)) { + return false; + } + + // add counter + // after move it's a new object (not a permanent), so must work with main card + Effect effect = new AddCountersTargetEffect(counter); + effect.setTargetPointer(new FixedTarget(card.getMainCard(), game)); + effect.apply(game, source); + return true; + } + } From ef313d20adcb64fb9f1a44476dac0d9f57f7a3ef Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 18 Sep 2021 21:23:26 -0400 Subject: [PATCH 139/231] [ZEN] reworked implementation of Bold Defense (fixes #8294) --- Mage.Sets/src/mage/cards/b/BoldDefense.java | 50 ++++++++++++++++----- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/BoldDefense.java b/Mage.Sets/src/mage/cards/b/BoldDefense.java index 182b91a3c25..8cdac1833c0 100644 --- a/Mage.Sets/src/mage/cards/b/BoldDefense.java +++ b/Mage.Sets/src/mage/cards/b/BoldDefense.java @@ -1,12 +1,9 @@ - package mage.cards.b; -import java.util.UUID; -import mage.abilities.condition.LockedInCondition; +import mage.abilities.Ability; import mage.abilities.condition.common.KickedCondition; -import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.continuous.BoostControlledEffect; -import mage.abilities.effects.common.continuous.BoostTargetEffect; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; import mage.abilities.keyword.FirstStrikeAbility; import mage.abilities.keyword.KickerAbility; @@ -14,7 +11,11 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.Outcome; import mage.filter.StaticFilters; +import mage.game.Game; + +import java.util.UUID; /** * @author nantuko, Loki @@ -28,12 +29,7 @@ public final class BoldDefense extends CardImpl { this.addAbility(new KickerAbility("{3}{W}")); // Creatures you control get +1/+1 until end of turn. If Bold Defense was kicked, instead creatures you control get +2/+2 and gain first strike until end of turn. - this.getSpellAbility().addEffect(new ConditionalContinuousEffect(new BoostControlledEffect(2, 2, Duration.EndOfTurn), - new BoostTargetEffect(1, 1, Duration.EndOfTurn), new LockedInCondition(KickedCondition.instance), - "Creatures you control get +1/+1 until end of turn. If this spell was kicked, instead creatures you control get +2/+2")); - this.getSpellAbility().addEffect(new ConditionalContinuousEffect(new GainAbilityControlledEffect(FirstStrikeAbility.getInstance(), Duration.EndOfTurn, StaticFilters.FILTER_PERMANENT_CREATURE, false), - null, new LockedInCondition(KickedCondition.instance), - "and gain first strike until end of turn")); + this.getSpellAbility().addEffect(new BoldDefenseEffect()); } private BoldDefense(final BoldDefense card) { @@ -45,3 +41,35 @@ public final class BoldDefense extends CardImpl { return new BoldDefense(this); } } + +class BoldDefenseEffect extends OneShotEffect { + + BoldDefenseEffect() { + super(Outcome.Benefit); + staticText = "Creatures you control get +1/+1 until end of turn. If this spell was kicked, " + + "instead creatures you control get +2/+2 and gain first strike until end of turn."; + } + + private BoldDefenseEffect(final BoldDefenseEffect effect) { + super(effect); + } + + @Override + public BoldDefenseEffect copy() { + return new BoldDefenseEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + if (KickedCondition.instance.apply(game, source)) { + game.addEffect(new BoostControlledEffect(2, 2, Duration.EndOfTurn), source); + game.addEffect(new GainAbilityControlledEffect( + FirstStrikeAbility.getInstance(), Duration.EndOfTurn, + StaticFilters.FILTER_PERMANENT_CREATURE + ), source); + } else { + game.addEffect(new BoostControlledEffect(1, 1, Duration.EndOfTurn), source); + } + return true; + } +} From f02575ca9fd2403ca4f31f66a06b4efa054e9fd8 Mon Sep 17 00:00:00 2001 From: "Raphael \"who?!\" Kehldorfer" Date: Sun, 19 Sep 2021 18:41:29 +0200 Subject: [PATCH 140/231] [MID] Implemented Hostile Hostel // Creeping Inn (#8278) * [MID] Implemented Necrosynthesis * [MID] Refactored Hostile Hostel // Creeping Inn * [MID] Refactored Hostile Hostel // Creeping Inn --- Mage.Sets/src/mage/cards/c/CreepingInn.java | 111 ++++++++++++++++++ Mage.Sets/src/mage/cards/h/HostileHostel.java | 92 +++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 2 + 3 files changed, 205 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CreepingInn.java create mode 100644 Mage.Sets/src/mage/cards/h/HostileHostel.java diff --git a/Mage.Sets/src/mage/cards/c/CreepingInn.java b/Mage.Sets/src/mage/cards/c/CreepingInn.java new file mode 100644 index 00000000000..791bfedba22 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CreepingInn.java @@ -0,0 +1,111 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.PhaseOutSourceEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.ExileZone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCardInGraveyard; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author LePwnerer + */ +public final class CreepingInn extends CardImpl { + + public CreepingInn(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, ""); + + this.subtype.add(SubType.HORROR); + this.subtype.add(SubType.CONSTRUCT); + this.power = new MageInt(3); + this.toughness = new MageInt(7); + this.color.setBlack(true); + this.transformable = true; + this.nightCard = true; + + // Whenever Creeping Inn attacks, you may exile a creature card from your graveyard. + // If you do, each opponent loses X life and you gain X life, + // where X is the number of creature cards exiled with Creeping Inn. + this.addAbility(new AttacksTriggeredAbility(new CreepingInnEffect())); + + // {4}: Creeping Inn phases out. + this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new PhaseOutSourceEffect(), new ManaCostsImpl("{4}"))); + } + + private CreepingInn(final CreepingInn card) { + super(card); + } + + @Override + public CreepingInn copy() { + return new CreepingInn(this); + } +} + +class CreepingInnEffect extends OneShotEffect { + + public CreepingInnEffect() { + super(Outcome.Exile); + this.staticText = "you may exile a creature card from your graveyard. " + + "If you do, each opponent loses X life and you gain X life, " + + "where X is the number of creature cards exiled with Creeping Inn."; + } + + public CreepingInnEffect(final CreepingInnEffect effect) { + super(effect); + } + + @Override + public CreepingInnEffect copy() { + return new CreepingInnEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = source.getSourcePermanentOrLKI(game); + if (player != null && permanent != null) { + UUID exileId = CardUtil.getExileZoneId(game, source); + TargetCardInGraveyard target = new TargetCardInGraveyard(0, 1, StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD); + target.setNotTarget(true); + if (target.canChoose(source.getSourceId(), player.getId(), game)) { + if (player.choose(Outcome.Exile, target, source.getId(), game)) { + Card cardChosen = game.getCard(target.getFirstTarget()); + if (cardChosen != null) { + int lifeAmount = 0; + player.moveCardsToExile(cardChosen, source, game, true, exileId, permanent.getName()); + ExileZone exile = game.getExile().getExileZone(exileId); + if (exile != null) { + for (UUID cardId : exile) { + lifeAmount++; + } + } + for (UUID playerId : game.getOpponents(source.getControllerId())) { + game.getPlayer(playerId).loseLife(lifeAmount, game, source, false); + } + player.gainLife(lifeAmount, game, source); + } + } + } + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/h/HostileHostel.java b/Mage.Sets/src/mage/cards/h/HostileHostel.java new file mode 100644 index 00000000000..9156b6faebf --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HostileHostel.java @@ -0,0 +1,92 @@ +package mage.cards.h; + +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.keyword.TransformAbility; +import mage.abilities.mana.ColorlessManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +import static mage.filter.StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT; + +/** + * @author LePwnerer + */ +public final class HostileHostel extends CardImpl { + + public HostileHostel(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + this.transformable = true; + this.secondSideCardClazz = mage.cards.c.CreepingInn.class; + + // {T}: Add {C}. + this.addAbility(new ColorlessManaAbility()); + + // {1}, {T}, Sacrifice a creature: Put a soul counter on Hostile Hostel. Then if there are three or more soul counters on it, remove those counters, transform it, then untap it. Activate only as a sorcery. + this.addAbility(new TransformAbility()); + Ability ability = new ActivateAsSorceryActivatedAbility(Zone.BATTLEFIELD, new HostileHostelEffect(), new ManaCostsImpl("{1}")); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeTargetCost(new TargetControlledCreaturePermanent(FILTER_CONTROLLED_CREATURE_SHORT_TEXT))); + this.addAbility(ability); + + } + + private HostileHostel(final HostileHostel card) { + super(card); + } + + @Override + public HostileHostel copy() { + return new HostileHostel(this); + } +} + +class HostileHostelEffect extends OneShotEffect { + + HostileHostelEffect() { + super(Outcome.Benefit); + this.staticText = "Put a soul counter on {this}. " + + "Then if there are three or more soul counters on it, remove those counters, transform it, then untap it."; + } + + HostileHostelEffect(final mage.cards.h.HostileHostelEffect effect) { + super(effect); + } + + @Override + public mage.cards.h.HostileHostelEffect copy() { + return new mage.cards.h.HostileHostelEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent != null && player != null) { + permanent.addCounters(CounterType.SOUL.createInstance(), source.getControllerId(), source, game); + int counters = permanent.getCounters(game).getCount(CounterType.SOUL); + if (counters > 2) { + permanent.removeCounters(CounterType.SOUL.getName(), counters, source, game); + new TransformSourceEffect(true).apply(game, source); + permanent.untap(game); + } + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 8fb83395431..43457a77152 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -89,6 +89,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Covetous Castaway", 45, Rarity.UNCOMMON, mage.cards.c.CovetousCastaway.class)); cards.add(new SetCardInfo("Covetous Geist", 92, Rarity.UNCOMMON, mage.cards.c.CovetousGeist.class)); cards.add(new SetCardInfo("Crawl from the Cellar", 93, Rarity.COMMON, mage.cards.c.CrawlFromTheCellar.class)); + cards.add(new SetCardInfo("Creeping Inn", 264, Rarity.MYTHIC, mage.cards.c.CreepingInn.class)); cards.add(new SetCardInfo("Croaking Counterpart", 215, Rarity.RARE, mage.cards.c.CroakingCounterpart.class)); cards.add(new SetCardInfo("Crossroads Candleguide", 253, Rarity.COMMON, mage.cards.c.CrossroadsCandleguide.class)); cards.add(new SetCardInfo("Curse of Shaken Faith", 134, Rarity.RARE, mage.cards.c.CurseOfShakenFaith.class)); @@ -172,6 +173,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Hobbling Zombie", 106, Rarity.COMMON, mage.cards.h.HobblingZombie.class)); cards.add(new SetCardInfo("Homestead Courage", 24, Rarity.COMMON, mage.cards.h.HomesteadCourage.class)); cards.add(new SetCardInfo("Hook-Haunt Drifter", 42, Rarity.COMMON, mage.cards.h.HookHauntDrifter.class)); + cards.add(new SetCardInfo("Hostile Hostel", 264, Rarity.MYTHIC, mage.cards.h.HostileHostel.class)); cards.add(new SetCardInfo("Hound Tamer", 187, Rarity.UNCOMMON, mage.cards.h.HoundTamer.class)); cards.add(new SetCardInfo("Howl of the Hunt", 188, Rarity.COMMON, mage.cards.h.HowlOfTheHunt.class)); cards.add(new SetCardInfo("Hungry for More", 228, Rarity.UNCOMMON, mage.cards.h.HungryForMore.class)); From b73e38e2a6d464cc594f52d276ff71b4da2a0c84 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Sun, 19 Sep 2021 12:42:34 -0400 Subject: [PATCH 141/231] Fix Strixhaven collation (#8296) --- .../mage/sets/StrixhavenSchoolOfMages.java | 99 ++++++++++--------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java b/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java index 364aaedab82..3be9f2babb7 100644 --- a/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java +++ b/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java @@ -488,15 +488,14 @@ class StrixhavenSchoolOfMagesCollator implements BoosterCollator { private static class StrixhavenSchoolOfMagesRun extends CardRun { private static final StrixhavenSchoolOfMagesRun commonA = new StrixhavenSchoolOfMagesRun(true, "249", "206", "182", "226", "237", "255", "210", "209", "239", "226", "251", "185", "219", "243", "206", "164", "215", "238", "256", "184", "239", "208", "254", "237", "185", "223", "251", "206", "166", "182", "164", "238", "204", "233", "184", "210", "182", "252", "243", "254", "208", "194", "255", "243", "249", "201", "204", "194", "235", "239", "166", "251", "223", "252", "237", "208", "233", "241", "219", "255", "201", "194", "164", "252", "170", "223", "241", "215", "166", "249", "237", "238", "184", "210", "243", "209", "235", "252", "204", "210", "185", "233", "249", "256", "239", "235", "209", "170", "201", "226", "241", "215", "206", "256", "208", "254", "185", "251", "226", "170", "184", "256", "235", "233", "209", "238", "254", "219", "201", "241", "182", "164", "194", "223", "170", "204", "166", "219", "215", "255"); private static final StrixhavenSchoolOfMagesRun commonB = new StrixhavenSchoolOfMagesRun(true, "79", "9", "111", "136", "52", "85", "12", "118", "131", "40", "90", "34", "117", "141", "60", "69", "30", "93", "140", "36", "74", "16", "97", "143", "40", "87", "11", "99", "121", "39", "75", "30", "106", "122", "38", "79", "22", "97", "131", "51", "90", "23", "112", "145", "50", "87", "18", "103", "124", "43", "68", "34", "102", "142", "49", "77", "16", "93", "140", "52", "73", "32", "116", "144", "60", "63", "22", "109", "124", "55", "69", "9", "106", "143", "39", "73", "23", "116", "142", "61", "84", "8", "99", "122", "43", "74", "32", "117", "145", "36", "75", "19", "112", "141", "61", "77", "8", "109", "144", "50", "84", "18", "118", "137", "55", "68", "12", "111", "121", "38", "63", "19", "103", "136", "51", "85", "11", "102", "137", "49"); - private static final StrixhavenSchoolOfMagesRun commonC = new StrixhavenSchoolOfMagesRun(true, "263", "268", "270", "271", "273", "275"); + private static final StrixhavenSchoolOfMagesRun commonC = new StrixhavenSchoolOfMagesRun(false, "263", "268", "270", "271", "273", "275"); private static final StrixhavenSchoolOfMagesRun uncommonA = new StrixhavenSchoolOfMagesRun(true, "257", "65", "56", "132", "92", "125", "72", "62", "104", "89", "123", "54", "10", "135", "114", "53", "105", "188", "45", "115", "47", "70", "100", "129", "88", "260", "46", "76", "257", "110", "65", "13", "139", "78", "92", "26", "72", "15", "56", "89", "134", "28", "70", "91", "132", "105", "31", "123", "88", "104", "135", "54", "115", "25", "76", "13", "62", "125", "35", "188", "65", "260", "10", "114", "129", "47", "100", "15", "257", "53", "26", "110", "139", "28", "134", "56", "35", "45", "91", "72", "10", "31", "132", "54", "89", "15", "78", "46", "123", "25", "92", "135", "104", "70", "125", "105", "260", "62", "188", "129", "114", "88", "13", "53", "26", "115", "47", "76", "28", "139", "100", "35", "91", "45", "78", "134", "31", "46", "25", "110"); private static final StrixhavenSchoolOfMagesRun uncommonB = new StrixhavenSchoolOfMagesRun(true, "229", "218", "216", "222", "212", "198", "71", "177", "202", "138", "258", "162", "81", "175", "213", "224", "220", "176", "227", "107", "247", "186", "169", "216", "261", "197", "242", "126", "262", "198", "24", "225", "207", "229", "190", "202", "81", "178", "200", "193", "41", "162", "71", "171", "59", "212", "218", "222", "227", "138", "231", "220", "258", "175", "169", "186", "107", "193", "250", "197", "176", "171", "126", "261", "247", "227", "213", "177", "262", "207", "190", "224", "225", "24", "216", "200", "242", "71", "178", "41", "212", "59", "198", "81", "231", "220", "229", "218", "202", "169", "175", "222", "193", "197", "250", "213", "186", "126", "177", "190", "258", "138", "162", "107", "261", "224", "200", "176", "178", "24", "247", "262", "207", "171", "225", "59", "231", "242", "41", "250"); private static final StrixhavenSchoolOfMagesRun rareA = new StrixhavenSchoolOfMagesRun(false, "6", "14", "17", "20", "27", "29", "33", "37", "42", "44", "48", "58", "64", "66", "80", "82", "86", "94", "96", "98", "101", "113", "119", "127", "130", "133", "146", "147", "150", "152", "154", "155", "157", "158", "159", "160", "161", "165", "172", "173", "174", "179", "180", "181", "199", "205", "214", "217", "221", "228", "232", "234", "244", "246", "248", "253", "259", "264", "265", "266", "267", "269", "272", "274", "6", "14", "17", "20", "27", "29", "33", "37", "42", "44", "48", "58", "64", "66", "80", "82", "86", "94", "96", "98", "101", "113", "119", "127", "130", "133", "146", "147", "150", "152", "154", "155", "157", "158", "159", "160", "161", "165", "172", "173", "174", "179", "180", "181", "199", "205", "214", "217", "221", "228", "232", "234", "244", "246", "248", "253", "259", "264", "265", "266", "267", "269", "272", "274", "21", "83", "95", "128", "148", "149", "151", "153", "156", "163", "167", "168", "189", "191", "192", "196", "203", "230", "240", "245"); - private static final StrixhavenSchoolOfMagesRun commonLesson = new StrixhavenSchoolOfMagesRun(false, "1", "2", "3", "4", "183", "187", "195", "211", "236"); + private static final StrixhavenSchoolOfMagesRun commonLesson = new StrixhavenSchoolOfMagesRun(true, "4", "187", "183", "195", "211", "3", "1", "236", "2", "187", "195", "4", "183", "211", "1", "2", "236", "3", "183", "187", "4", "195", "2", "211", "3", "1", "236", "187", "183", "195", "4", "1", "211", "3", "2", "236", "187", "4", "183", "195", "3", "1", "2", "211", "236", "187", "195", "4", "183", "2", "1", "211", "3", "187", "183", "4", "236", "195", "1", "3", "211", "236", "2", "187", "4", "195", "183", "3", "211", "1", "2", "236", "187", "4", "195", "183", "211", "3", "1", "2", "236", "183", "195", "4", "187", "1", "211", "3", "2", "236", "187", "4", "195", "183", "2", "211", "3", "1", "187", "236", "4", "183", "195", "211", "1", "236", "3", "2", "195", "187", "183", "4", "2", "236", "3", "211", "1"); private static final StrixhavenSchoolOfMagesRun rareLesson = new StrixhavenSchoolOfMagesRun(false, "5", "7", "57", "67", "108", "120", "7", "57", "67", "108", "120"); private static final StrixhavenSchoolOfMagesRun uncommonArchive = new StrixhavenSchoolOfMagesRun(true, "18", "29", "19", "41", "4", "57", "46", "23", "35", "3", "20", "49", "37", "30", "41", "24", "9", "44", "4", "29", "3", "46", "35", "18", "57", "41", "9", "19", "24", "44", "51", "30", "23", "37", "18", "49", "20", "29", "4", "19", "35", "46", "51", "23", "30", "18", "57", "3", "37", "49", "41", "24", "9", "44", "20", "4", "23", "37", "30", "3", "35", "46", "57", "29", "9", "29", "49", "19", "51", "44", "4", "24", "41", "18", "46", "3", "20", "57", "19", "35", "44", "23", "24", "9", "51", "30", "49", "37", "20", "51"); - private static final StrixhavenSchoolOfMagesRun rareArchive = new StrixhavenSchoolOfMagesRun(false, "5", "6", "7", "8", "10", "13", "14", "15", "16", "21", "26", "28", "31", "32", "34", "38", "39", "42", "45", "47", "48", "52", "53", "56", "58", "59", "60", "61", "62", "63"); - private static final StrixhavenSchoolOfMagesRun mythicArchive = new StrixhavenSchoolOfMagesRun(false, "1", "2", "11", "12", "17", "22", "25", "27", "33", "36", "40", "43", "50", "54", "55"); + private static final StrixhavenSchoolOfMagesRun rareArchive = new StrixhavenSchoolOfMagesRun(false, "5", "6", "7", "8", "10", "13", "14", "15", "16", "21", "26", "28", "31", "32", "34", "38", "39", "42", "45", "47", "48", "52", "53", "56", "58", "59", "60", "61", "62", "63", "5", "6", "7", "8", "10", "13", "14", "15", "16", "21", "26", "28", "31", "32", "34", "38", "39", "42", "45", "47", "48", "52", "53", "56", "58", "59", "60", "61", "62", "63", "1", "2", "11", "12", "17", "22", "25", "27", "33", "36", "40", "43", "50", "54", "55"); private StrixhavenSchoolOfMagesRun(boolean keepOrder, String... numbers) { super(keepOrder, numbers); @@ -504,18 +503,29 @@ class StrixhavenSchoolOfMagesCollator implements BoosterCollator { } private static class StrixhavenSchoolOfMagesStructure extends BoosterStructure { - private static final StrixhavenSchoolOfMagesStructure C1 = new StrixhavenSchoolOfMagesStructure( + private static final StrixhavenSchoolOfMagesStructure AABBBBBBC = new StrixhavenSchoolOfMagesStructure( StrixhavenSchoolOfMagesRun.commonA, StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonC, StrixhavenSchoolOfMagesRun.commonB, StrixhavenSchoolOfMagesRun.commonB, StrixhavenSchoolOfMagesRun.commonB, StrixhavenSchoolOfMagesRun.commonB, StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB + StrixhavenSchoolOfMagesRun.commonB, + StrixhavenSchoolOfMagesRun.commonC ); - private static final StrixhavenSchoolOfMagesStructure C2 = new StrixhavenSchoolOfMagesStructure( + private static final StrixhavenSchoolOfMagesStructure AAABBBBBC = new StrixhavenSchoolOfMagesStructure( + StrixhavenSchoolOfMagesRun.commonA, + StrixhavenSchoolOfMagesRun.commonA, + StrixhavenSchoolOfMagesRun.commonA, + StrixhavenSchoolOfMagesRun.commonB, + StrixhavenSchoolOfMagesRun.commonB, + StrixhavenSchoolOfMagesRun.commonB, + StrixhavenSchoolOfMagesRun.commonB, + StrixhavenSchoolOfMagesRun.commonB, + StrixhavenSchoolOfMagesRun.commonC + ); + private static final StrixhavenSchoolOfMagesStructure AAABBBBBB = new StrixhavenSchoolOfMagesStructure( StrixhavenSchoolOfMagesRun.commonA, StrixhavenSchoolOfMagesRun.commonA, StrixhavenSchoolOfMagesRun.commonA, @@ -526,34 +536,12 @@ class StrixhavenSchoolOfMagesCollator implements BoosterCollator { StrixhavenSchoolOfMagesRun.commonB, StrixhavenSchoolOfMagesRun.commonB ); - private static final StrixhavenSchoolOfMagesStructure C3 = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonC, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB - ); - private static final StrixhavenSchoolOfMagesStructure C4 = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB - ); - private static final StrixhavenSchoolOfMagesStructure U1 = new StrixhavenSchoolOfMagesStructure( + private static final StrixhavenSchoolOfMagesStructure ABB = new StrixhavenSchoolOfMagesStructure( StrixhavenSchoolOfMagesRun.uncommonA, StrixhavenSchoolOfMagesRun.uncommonB, StrixhavenSchoolOfMagesRun.uncommonB ); - private static final StrixhavenSchoolOfMagesStructure U2 = new StrixhavenSchoolOfMagesStructure( + private static final StrixhavenSchoolOfMagesStructure AAB = new StrixhavenSchoolOfMagesStructure( StrixhavenSchoolOfMagesRun.uncommonA, StrixhavenSchoolOfMagesRun.uncommonA, StrixhavenSchoolOfMagesRun.uncommonB @@ -573,29 +561,45 @@ class StrixhavenSchoolOfMagesCollator implements BoosterCollator { private static final StrixhavenSchoolOfMagesStructure A2 = new StrixhavenSchoolOfMagesStructure( StrixhavenSchoolOfMagesRun.rareArchive ); - private static final StrixhavenSchoolOfMagesStructure A3 = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.mythicArchive - ); private StrixhavenSchoolOfMagesStructure(CardRun... runs) { super(runs); } } + // In order for equal numbers of each common to exist, the average booster must contain: + // 2.81 A commons (45 / 16) + // 5.63 B commons (90 / 16) + // 0.56 C commons ( 9 / 16) private final RarityConfiguration commonRuns = new RarityConfiguration( false, - StrixhavenSchoolOfMagesStructure.C1, - StrixhavenSchoolOfMagesStructure.C2, - StrixhavenSchoolOfMagesStructure.C3, - StrixhavenSchoolOfMagesStructure.C4 + StrixhavenSchoolOfMagesStructure.AABBBBBBC, + StrixhavenSchoolOfMagesStructure.AABBBBBBC, + StrixhavenSchoolOfMagesStructure.AABBBBBBC, + StrixhavenSchoolOfMagesStructure.AAABBBBBC, + StrixhavenSchoolOfMagesStructure.AAABBBBBC, + StrixhavenSchoolOfMagesStructure.AAABBBBBC, + StrixhavenSchoolOfMagesStructure.AAABBBBBC, + StrixhavenSchoolOfMagesStructure.AAABBBBBC, + StrixhavenSchoolOfMagesStructure.AAABBBBBC, + StrixhavenSchoolOfMagesStructure.AAABBBBBB, + StrixhavenSchoolOfMagesStructure.AAABBBBBB, + StrixhavenSchoolOfMagesStructure.AAABBBBBB, + StrixhavenSchoolOfMagesStructure.AAABBBBBB, + StrixhavenSchoolOfMagesStructure.AAABBBBBB, + StrixhavenSchoolOfMagesStructure.AAABBBBBB, + StrixhavenSchoolOfMagesStructure.AAABBBBBB ); private final RarityConfiguration uncommonRuns = new RarityConfiguration( - StrixhavenSchoolOfMagesStructure.U1, - StrixhavenSchoolOfMagesStructure.U2 + StrixhavenSchoolOfMagesStructure.ABB, + StrixhavenSchoolOfMagesStructure.AAB ); private final RarityConfiguration rareRuns = new RarityConfiguration( StrixhavenSchoolOfMagesStructure.R1 ); + + // The ratio of common to rare/mythic Lesson cards hasn't been officially disclosed + // rare/mythic Lessons were observed in 37 out of 468 packs, which is very close to 2/25 private final RarityConfiguration lessonRuns = new RarityConfiguration( false, StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, @@ -605,18 +609,17 @@ class StrixhavenSchoolOfMagesCollator implements BoosterCollator { StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, + StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, + StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, + StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, + StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - - StrixhavenSchoolOfMagesStructure.L2 + StrixhavenSchoolOfMagesStructure.L2, StrixhavenSchoolOfMagesStructure.L2 ); private final RarityConfiguration archiveRuns = new RarityConfiguration( false, StrixhavenSchoolOfMagesStructure.A1, StrixhavenSchoolOfMagesStructure.A1, - - StrixhavenSchoolOfMagesStructure.A2, StrixhavenSchoolOfMagesStructure.A2, - StrixhavenSchoolOfMagesStructure.A2, StrixhavenSchoolOfMagesStructure.A2, - - StrixhavenSchoolOfMagesStructure.A3 + StrixhavenSchoolOfMagesStructure.A2 ); @Override From 9767a07b3f9a13a15b8e9753c5374e11ab09db78 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Sun, 19 Sep 2021 12:42:52 -0400 Subject: [PATCH 142/231] Fix Time Spiral Remastered common print run ratios (#8260) --- .../src/mage/sets/TimeSpiralRemastered.java | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java b/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java index bd8b39451bf..7952dc33229 100644 --- a/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java +++ b/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java @@ -483,7 +483,7 @@ class TimeSpiralRemasteredCollator implements BoosterCollator { } private static class TimeSpiralRemasteredStructure extends BoosterStructure { - private static final TimeSpiralRemasteredStructure C1 = new TimeSpiralRemasteredStructure( + private static final TimeSpiralRemasteredStructure AAABBBC1C1C1C1 = new TimeSpiralRemasteredStructure( TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, @@ -495,7 +495,7 @@ class TimeSpiralRemasteredCollator implements BoosterCollator { TimeSpiralRemasteredRun.commonC1, TimeSpiralRemasteredRun.commonC1 ); - private static final TimeSpiralRemasteredStructure C2 = new TimeSpiralRemasteredStructure( + private static final TimeSpiralRemasteredStructure AAABBC1C1C1C1C1 = new TimeSpiralRemasteredStructure( TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, @@ -507,7 +507,7 @@ class TimeSpiralRemasteredCollator implements BoosterCollator { TimeSpiralRemasteredRun.commonC1, TimeSpiralRemasteredRun.commonC1 ); - private static final TimeSpiralRemasteredStructure C3 = new TimeSpiralRemasteredStructure( + private static final TimeSpiralRemasteredStructure AABBBC1C1C1C1C1 = new TimeSpiralRemasteredStructure( TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonB, @@ -519,7 +519,7 @@ class TimeSpiralRemasteredCollator implements BoosterCollator { TimeSpiralRemasteredRun.commonC1, TimeSpiralRemasteredRun.commonC1 ); - private static final TimeSpiralRemasteredStructure C4 = new TimeSpiralRemasteredStructure( + private static final TimeSpiralRemasteredStructure AAABBBC2C2C2C2 = new TimeSpiralRemasteredStructure( TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, @@ -531,7 +531,7 @@ class TimeSpiralRemasteredCollator implements BoosterCollator { TimeSpiralRemasteredRun.commonC2, TimeSpiralRemasteredRun.commonC2 ); - private static final TimeSpiralRemasteredStructure C5 = new TimeSpiralRemasteredStructure( + private static final TimeSpiralRemasteredStructure AAABBC2C2C2C2C2 = new TimeSpiralRemasteredStructure( TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, @@ -543,7 +543,7 @@ class TimeSpiralRemasteredCollator implements BoosterCollator { TimeSpiralRemasteredRun.commonC2, TimeSpiralRemasteredRun.commonC2 ); - private static final TimeSpiralRemasteredStructure C6 = new TimeSpiralRemasteredStructure( + private static final TimeSpiralRemasteredStructure AABBBC2C2C2C2C2 = new TimeSpiralRemasteredStructure( TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonA, TimeSpiralRemasteredRun.commonB, @@ -555,12 +555,12 @@ class TimeSpiralRemasteredCollator implements BoosterCollator { TimeSpiralRemasteredRun.commonC2, TimeSpiralRemasteredRun.commonC2 ); - private static final TimeSpiralRemasteredStructure U1 = new TimeSpiralRemasteredStructure( + private static final TimeSpiralRemasteredStructure AAA = new TimeSpiralRemasteredStructure( TimeSpiralRemasteredRun.uncommonA, TimeSpiralRemasteredRun.uncommonA, TimeSpiralRemasteredRun.uncommonA ); - private static final TimeSpiralRemasteredStructure U2 = new TimeSpiralRemasteredStructure( + private static final TimeSpiralRemasteredStructure BBB = new TimeSpiralRemasteredStructure( TimeSpiralRemasteredRun.uncommonB, TimeSpiralRemasteredRun.uncommonB, TimeSpiralRemasteredRun.uncommonB @@ -577,22 +577,44 @@ class TimeSpiralRemasteredCollator implements BoosterCollator { } } + // In order for equal numbers of each common to exist, the average booster must contain: + // 2.73 A commons (30 / 11) + // 2.73 B commons (30 / 11) + // 2.27 C1 commons (25 / 11, or 50 / 11 in each C1 booster) + // 2.27 C2 commons (25 / 11, or 50 / 11 in each C2 booster) private final RarityConfiguration commonRuns = new RarityConfiguration( false, - TimeSpiralRemasteredStructure.C1, - TimeSpiralRemasteredStructure.C2, - TimeSpiralRemasteredStructure.C3, - TimeSpiralRemasteredStructure.C4, - TimeSpiralRemasteredStructure.C5, - TimeSpiralRemasteredStructure.C6 + TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, + TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, + TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, + TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, + TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, + TimeSpiralRemasteredStructure.AAABBC1C1C1C1C1, + TimeSpiralRemasteredStructure.AAABBC1C1C1C1C1, + TimeSpiralRemasteredStructure.AAABBC1C1C1C1C1, + TimeSpiralRemasteredStructure.AABBBC1C1C1C1C1, + TimeSpiralRemasteredStructure.AABBBC1C1C1C1C1, + TimeSpiralRemasteredStructure.AABBBC1C1C1C1C1, + + TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, + TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, + TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, + TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, + TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, + TimeSpiralRemasteredStructure.AAABBC2C2C2C2C2, + TimeSpiralRemasteredStructure.AAABBC2C2C2C2C2, + TimeSpiralRemasteredStructure.AAABBC2C2C2C2C2, + TimeSpiralRemasteredStructure.AABBBC2C2C2C2C2, + TimeSpiralRemasteredStructure.AABBBC2C2C2C2C2, + TimeSpiralRemasteredStructure.AABBBC2C2C2C2C2 ); private final RarityConfiguration uncommonRuns = new RarityConfiguration( false, - TimeSpiralRemasteredStructure.U1, - TimeSpiralRemasteredStructure.U1, - TimeSpiralRemasteredStructure.U1, - TimeSpiralRemasteredStructure.U2, - TimeSpiralRemasteredStructure.U2 + TimeSpiralRemasteredStructure.AAA, + TimeSpiralRemasteredStructure.AAA, + TimeSpiralRemasteredStructure.AAA, + TimeSpiralRemasteredStructure.BBB, + TimeSpiralRemasteredStructure.BBB ); private final RarityConfiguration rareRuns = new RarityConfiguration( TimeSpiralRemasteredStructure.R1 From b896d205693bc2e54f8b9ceacc25a17ec6cc783f Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Sun, 19 Sep 2021 12:43:23 -0400 Subject: [PATCH 143/231] Fix AFR collator common print run ratios (#8256) --- .../sets/AdventuresInTheForgottenRealms.java | 52 ++++++++----------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java index 94bed009b6e..a2041b71c6f 100644 --- a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java +++ b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java @@ -27,6 +27,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { this.blockName = "Adventures in the Forgotten Realms"; this.hasBoosters = true; this.hasBasicLands = true; + this.numBoosterLands = 1; this.numBoosterCommon = 10; this.numBoosterUncommon = 3; this.numBoosterRare = 1; @@ -457,7 +458,7 @@ class AdventuresInTheForgottenRealmsCollator implements BoosterCollator { } private static class AdventuresInTheForgottenRealmsStructure extends BoosterStructure { - private static final AdventuresInTheForgottenRealmsStructure C1 = new AdventuresInTheForgottenRealmsStructure( + private static final AdventuresInTheForgottenRealmsStructure ABBBBBBCCC = new AdventuresInTheForgottenRealmsStructure( AdventuresInTheForgottenRealmsRun.commonA, AdventuresInTheForgottenRealmsRun.commonB, AdventuresInTheForgottenRealmsRun.commonB, @@ -469,19 +470,7 @@ class AdventuresInTheForgottenRealmsCollator implements BoosterCollator { AdventuresInTheForgottenRealmsRun.commonC, AdventuresInTheForgottenRealmsRun.commonC ); - private static final AdventuresInTheForgottenRealmsStructure C2 = new AdventuresInTheForgottenRealmsStructure( - AdventuresInTheForgottenRealmsRun.commonA, - AdventuresInTheForgottenRealmsRun.commonA, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonC, - AdventuresInTheForgottenRealmsRun.commonC, - AdventuresInTheForgottenRealmsRun.commonC - ); - private static final AdventuresInTheForgottenRealmsStructure C3 = new AdventuresInTheForgottenRealmsStructure( + private static final AdventuresInTheForgottenRealmsStructure AABBBBBBCC = new AdventuresInTheForgottenRealmsStructure( AdventuresInTheForgottenRealmsRun.commonA, AdventuresInTheForgottenRealmsRun.commonA, AdventuresInTheForgottenRealmsRun.commonB, @@ -493,12 +482,12 @@ class AdventuresInTheForgottenRealmsCollator implements BoosterCollator { AdventuresInTheForgottenRealmsRun.commonC, AdventuresInTheForgottenRealmsRun.commonC ); - private static final AdventuresInTheForgottenRealmsStructure U1 = new AdventuresInTheForgottenRealmsStructure( + private static final AdventuresInTheForgottenRealmsStructure AAA = new AdventuresInTheForgottenRealmsStructure( AdventuresInTheForgottenRealmsRun.uncommonA, AdventuresInTheForgottenRealmsRun.uncommonA, AdventuresInTheForgottenRealmsRun.uncommonA ); - private static final AdventuresInTheForgottenRealmsStructure U2 = new AdventuresInTheForgottenRealmsStructure( + private static final AdventuresInTheForgottenRealmsStructure BBB = new AdventuresInTheForgottenRealmsStructure( AdventuresInTheForgottenRealmsRun.uncommonB, AdventuresInTheForgottenRealmsRun.uncommonB, AdventuresInTheForgottenRealmsRun.uncommonB @@ -518,23 +507,28 @@ class AdventuresInTheForgottenRealmsCollator implements BoosterCollator { } } + // In order for equal numbers of each common to exist, the average booster must contain: + // 1.503 A commons (242 / 161) + // 6.012 B commons (968 / 161) + // 2.484 C commons (400 / 161) + // However, boosters with more than six B commons are not known to exist. + // This discrepancy is presumably related to foils--the above values are based on + // 10 commons per booster, but real boosters contain only 9.67 non-foil commons private final RarityConfiguration commonRuns = new RarityConfiguration( - false, - AdventuresInTheForgottenRealmsStructure.C1, - AdventuresInTheForgottenRealmsStructure.C2, - AdventuresInTheForgottenRealmsStructure.C3 + AdventuresInTheForgottenRealmsStructure.ABBBBBBCCC, + AdventuresInTheForgottenRealmsStructure.AABBBBBBCC ); private final RarityConfiguration uncommonRuns = new RarityConfiguration( false, - AdventuresInTheForgottenRealmsStructure.U1, AdventuresInTheForgottenRealmsStructure.U1, - AdventuresInTheForgottenRealmsStructure.U1, AdventuresInTheForgottenRealmsStructure.U1, - AdventuresInTheForgottenRealmsStructure.U1, AdventuresInTheForgottenRealmsStructure.U1, - AdventuresInTheForgottenRealmsStructure.U1, AdventuresInTheForgottenRealmsStructure.U1, - AdventuresInTheForgottenRealmsStructure.U1, AdventuresInTheForgottenRealmsStructure.U1, - AdventuresInTheForgottenRealmsStructure.U1, - AdventuresInTheForgottenRealmsStructure.U2, AdventuresInTheForgottenRealmsStructure.U2, - AdventuresInTheForgottenRealmsStructure.U2, AdventuresInTheForgottenRealmsStructure.U2, - AdventuresInTheForgottenRealmsStructure.U2 + AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, + AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, + AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, + AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, + AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, + AdventuresInTheForgottenRealmsStructure.AAA, + AdventuresInTheForgottenRealmsStructure.BBB, AdventuresInTheForgottenRealmsStructure.BBB, + AdventuresInTheForgottenRealmsStructure.BBB, AdventuresInTheForgottenRealmsStructure.BBB, + AdventuresInTheForgottenRealmsStructure.BBB ); private final RarityConfiguration rareRuns = new RarityConfiguration( false, From eb81ffe92b088182919575e8f57bc2807dd5e272 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Sun, 19 Sep 2021 12:44:23 -0400 Subject: [PATCH 144/231] Fix Modern Horizons 2 common and reprint print run ratios (#8245) --- Mage.Sets/src/mage/sets/ModernHorizons2.java | 78 +++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/Mage.Sets/src/mage/sets/ModernHorizons2.java b/Mage.Sets/src/mage/sets/ModernHorizons2.java index 62e7730904f..4f6386fe212 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons2.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons2.java @@ -576,9 +576,7 @@ class ModernHorizons2Collator implements BoosterCollator { private static final ModernHorizons2Run rareA = new ModernHorizons2Run(false, "12", "12", "22", "22", "23", "23", "26", "26", "27", "27", "29", "29", "30", "32", "35", "35", "39", "39", "44", "44", "47", "47", "52", "58", "58", "59", "59", "67", "68", "68", "69", "71", "71", "75", "80", "80", "81", "81", "87", "92", "92", "93", "93", "96", "96", "97", "97", "102", "106", "106", "116", "116", "117", "117", "118", "118", "120", "120", "126", "129", "129", "132", "132", "137", "137", "138", "148", "148", "151", "153", "153", "157", "162", "162", "166", "166", "171", "171", "176", "176", "178", "182", "182", "186", "186", "189", "189", "192", "197", "198", "198", "199", "202", "204", "204", "205", "205", "206", "206", "207", "207", "208", "208", "214", "214", "216", "216", "218", "218", "219", "219", "224", "224", "225", "225", "227", "231", "231", "234", "236", "236", "238", "242", "242", "243", "243", "244", "244", "248", "248", "250", "250", "254", "254", "259", "259", "260", "260", "261", "261"); private static final ModernHorizons2Run rareB = new ModernHorizons2Run(false, "328", "328", "331", "331", "332", "332", "333", "334", "334", "336", "336", "337", "340", "340", "341", "341", "342", "344", "344", "345", "345", "352", "352", "353", "353", "355", "355", "357", "357", "359", "359", "363", "365", "366", "366", "367", "368", "370", "370", "371", "371", "372", "372", "377", "377", "378", "378", "379", "380", "380", "383", "383", "385", "385", "386", "386", "388", "388", "390", "390", "391", "391", "393", "396", "396", "397", "397", "398", "398", "400", "400", "401", "401", "402", "405", "405", "407", "407", "409", "409", "410", "412", "412", "414", "414", "417", "417", "418", "418", "420", "422", "422", "425", "425", "427", "427", "431", "432", "432", "433", "435", "435", "436", "436", "437", "437", "438", "438", "439", "439", "440", "440", "441", "441"); private static final ModernHorizons2Run rareC = new ModernHorizons2Run(false, "304", "305", "306", "307", "309", "310", "311", "312", "313", "315", "316", "317", "318", "323", "324"); - private static final ModernHorizons2Run reprintUncommon = new ModernHorizons2Run(false, "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302"); - private static final ModernHorizons2Run reprintRare = new ModernHorizons2Run(false, "263", "265", "270", "271", "273", "275", "277", "279", "283", "286", "289", "290", "292", "293", "294", "295", "298", "303"); - private static final ModernHorizons2Run reprintMythic = new ModernHorizons2Run(false, "281", "287", "291", "301"); + private static final ModernHorizons2Run reprint = new ModernHorizons2Run(false, "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "263", "265", "270", "271", "273", "275", "277", "279", "283", "286", "289", "290", "292", "293", "294", "295", "298", "303", "263", "265", "270", "271", "273", "275", "277", "279", "283", "286", "289", "290", "292", "293", "294", "295", "298", "303", "281", "287", "291", "301"); private ModernHorizons2Run(boolean keepOrder, String... numbers) { super(keepOrder, numbers); @@ -586,7 +584,7 @@ class ModernHorizons2Collator implements BoosterCollator { } private static class ModernHorizons2Structure extends BoosterStructure { - private static final ModernHorizons2Structure C1 = new ModernHorizons2Structure( + private static final ModernHorizons2Structure AAABC1C1C1C1C1C1 = new ModernHorizons2Structure( ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, @@ -598,7 +596,7 @@ class ModernHorizons2Collator implements BoosterCollator { ModernHorizons2Run.commonC1, ModernHorizons2Run.commonC1 ); - private static final ModernHorizons2Structure C2 = new ModernHorizons2Structure( + private static final ModernHorizons2Structure AAABBC1C1C1C1C1 = new ModernHorizons2Structure( ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, @@ -610,7 +608,7 @@ class ModernHorizons2Collator implements BoosterCollator { ModernHorizons2Run.commonC1, ModernHorizons2Run.commonC1 ); - private static final ModernHorizons2Structure C3 = new ModernHorizons2Structure( + private static final ModernHorizons2Structure AAABBBC2C2C2C2 = new ModernHorizons2Structure( ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, @@ -622,7 +620,7 @@ class ModernHorizons2Collator implements BoosterCollator { ModernHorizons2Run.commonC2, ModernHorizons2Run.commonC2 ); - private static final ModernHorizons2Structure C4 = new ModernHorizons2Structure( + private static final ModernHorizons2Structure AAAABBC2C2C2C2 = new ModernHorizons2Structure( ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, @@ -634,7 +632,7 @@ class ModernHorizons2Collator implements BoosterCollator { ModernHorizons2Run.commonC2, ModernHorizons2Run.commonC2 ); - private static final ModernHorizons2Structure C5 = new ModernHorizons2Structure( + private static final ModernHorizons2Structure AAAABBBC2C2C2 = new ModernHorizons2Structure( ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, ModernHorizons2Run.commonA, @@ -646,12 +644,12 @@ class ModernHorizons2Collator implements BoosterCollator { ModernHorizons2Run.commonC2, ModernHorizons2Run.commonC2 ); - private static final ModernHorizons2Structure U1 = new ModernHorizons2Structure( + private static final ModernHorizons2Structure AAA = new ModernHorizons2Structure( ModernHorizons2Run.uncommonA, ModernHorizons2Run.uncommonA, ModernHorizons2Run.uncommonA ); - private static final ModernHorizons2Structure U2 = new ModernHorizons2Structure( + private static final ModernHorizons2Structure BBB = new ModernHorizons2Structure( ModernHorizons2Run.uncommonB, ModernHorizons2Run.uncommonB, ModernHorizons2Run.uncommonB @@ -666,13 +664,7 @@ class ModernHorizons2Collator implements BoosterCollator { ModernHorizons2Run.rareC ); private static final ModernHorizons2Structure RP1 = new ModernHorizons2Structure( - ModernHorizons2Run.reprintUncommon - ); - private static final ModernHorizons2Structure RP2 = new ModernHorizons2Structure( - ModernHorizons2Run.reprintRare - ); - private static final ModernHorizons2Structure RP3 = new ModernHorizons2Structure( - ModernHorizons2Run.reprintMythic + ModernHorizons2Run.reprint ); private ModernHorizons2Structure(CardRun... runs) { @@ -680,24 +672,42 @@ class ModernHorizons2Collator implements BoosterCollator { } } + // In order for equal numbers of each common to exist, the average booster must contain: + // 3.27 A commons (36 / 11) + // 2.18 B commons (24 / 11) + // 2.73 C1 commons (30 / 11, or 60 / 11 in each C1 booster) + // 1.82 C2 commons (20 / 11, or 40 / 11 in each C2 booster) + // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs + // and with 10 common slots per booster private final RarityConfiguration commonRuns = new RarityConfiguration( false, - ModernHorizons2Structure.C1, - ModernHorizons2Structure.C2, - ModernHorizons2Structure.C3, - ModernHorizons2Structure.C4, - ModernHorizons2Structure.C5, - ModernHorizons2Structure.C1, - ModernHorizons2Structure.C2, - ModernHorizons2Structure.C3, - ModernHorizons2Structure.C4, - ModernHorizons2Structure.C5, - ModernHorizons2Structure.C1, - ModernHorizons2Structure.C2 + ModernHorizons2Structure.AAABC1C1C1C1C1C1, + ModernHorizons2Structure.AAABC1C1C1C1C1C1, + ModernHorizons2Structure.AAABC1C1C1C1C1C1, + ModernHorizons2Structure.AAABC1C1C1C1C1C1, + ModernHorizons2Structure.AAABC1C1C1C1C1C1, + ModernHorizons2Structure.AAABBC1C1C1C1C1, + ModernHorizons2Structure.AAABBC1C1C1C1C1, + ModernHorizons2Structure.AAABBC1C1C1C1C1, + ModernHorizons2Structure.AAABBC1C1C1C1C1, + ModernHorizons2Structure.AAABBC1C1C1C1C1, + ModernHorizons2Structure.AAABBC1C1C1C1C1, + + ModernHorizons2Structure.AAABBBC2C2C2C2, + ModernHorizons2Structure.AAABBBC2C2C2C2, + ModernHorizons2Structure.AAABBBC2C2C2C2, + ModernHorizons2Structure.AAABBBC2C2C2C2, + ModernHorizons2Structure.AAABBBC2C2C2C2, + ModernHorizons2Structure.AAAABBC2C2C2C2, + ModernHorizons2Structure.AAAABBC2C2C2C2, + ModernHorizons2Structure.AAAABBBC2C2C2, + ModernHorizons2Structure.AAAABBBC2C2C2, + ModernHorizons2Structure.AAAABBBC2C2C2, + ModernHorizons2Structure.AAAABBBC2C2C2 ); private final RarityConfiguration uncommonRuns = new RarityConfiguration( - ModernHorizons2Structure.U1, - ModernHorizons2Structure.U2 + ModernHorizons2Structure.AAA, + ModernHorizons2Structure.BBB ); private final RarityConfiguration rareRuns = new RarityConfiguration( false, @@ -736,11 +746,7 @@ class ModernHorizons2Collator implements BoosterCollator { ModernHorizons2Structure.R3 ); private final RarityConfiguration reprintRuns = new RarityConfiguration( - false, - ModernHorizons2Structure.RP1, ModernHorizons2Structure.RP1, - ModernHorizons2Structure.RP1, ModernHorizons2Structure.RP1, - ModernHorizons2Structure.RP2, ModernHorizons2Structure.RP2, - ModernHorizons2Structure.RP3 + ModernHorizons2Structure.RP1 ); @Override From f31781e4a4cb35fc113fef860378e1cdce941f82 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 19 Sep 2021 15:27:01 -0400 Subject: [PATCH 145/231] [C21] fixed Veyran, Voice of Duality doubling triggers off of other players' spells and permanents (fixes #8287) --- .../mage/cards/v/VeyranVoiceOfDuality.java | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/Mage.Sets/src/mage/cards/v/VeyranVoiceOfDuality.java b/Mage.Sets/src/mage/cards/v/VeyranVoiceOfDuality.java index d9513a2bf44..53075230be6 100644 --- a/Mage.Sets/src/mage/cards/v/VeyranVoiceOfDuality.java +++ b/Mage.Sets/src/mage/cards/v/VeyranVoiceOfDuality.java @@ -12,7 +12,9 @@ import mage.constants.*; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.NumberOfTriggersEvent; +import mage.game.permanent.Permanent; import mage.game.stack.Spell; +import mage.util.CardUtil; import java.util.UUID; @@ -71,30 +73,23 @@ class VeyranVoiceOfDualityEffect extends ReplacementEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { - NumberOfTriggersEvent numberOfTriggersEvent = (NumberOfTriggersEvent) event; - if (!source.isControlledBy(event.getPlayerId())) { - return false; - } - GameEvent sourceEvent = numberOfTriggersEvent.getSourceEvent(); - if (sourceEvent == null) { - return false; - } + GameEvent sourceEvent = ((NumberOfTriggersEvent) event).getSourceEvent(); if (sourceEvent.getType() != GameEvent.EventType.COPIED_STACKOBJECT && sourceEvent.getType() != GameEvent.EventType.SPELL_CAST) { return false; } - // Only for entering artifacts or creatures Spell spell = game.getSpell(sourceEvent.getTargetId()); - if (spell == null || !spell.isInstantOrSorcery(game)) { - return false; - } - // Only for triggers of permanents - return game.getPermanent(numberOfTriggersEvent.getSourceId()) != null; + Permanent permanent = game.getPermanent(sourceEvent.getSourceId()); + return spell != null + && permanent != null + && spell.isInstantOrSorcery(game) + && spell.isControlledBy(source.getControllerId()) + && permanent.isControlledBy(source.getControllerId()); } @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { - event.setAmount(event.getAmount() + 1); + event.setAmount(CardUtil.overflowInc(event.getAmount(), 1)); return false; } } From 46081d9185c489eddea1ca78ef4851585e76ae9c Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Mon, 20 Sep 2021 02:17:22 +0400 Subject: [PATCH 146/231] GUI related improves: * GUI: fixed that choose triggers/piles dialog doesn't close correctly before cast mode choose (#8225); * GUI: fixed that some choose dialogs doesn't update battlefield state (example: choose amount, choose mana); * Game: fixed duplicated json logs at the game's end; --- .../components/ability/AbilityPicker.java | 2 +- .../java/mage/client/game/FeedbackPanel.java | 54 +++++--- .../main/java/mage/client/game/GamePanel.java | 131 ++++++++++++------ .../client/remote/CallbackClientImpl.java | 50 +++---- .../java/mage/view/AbilityPickerView.java | 21 ++- .../java/mage/view/GameClientMessage.java | 81 ++++------- .../java/mage/server/game/GameController.java | 4 +- .../mage/server/game/GameSessionPlayer.java | 18 +-- .../mage/server/game/GameSessionWatcher.java | 7 +- .../test/serverside/AbilityPickerTest.java | 13 +- Mage/src/main/java/mage/util/CardUtil.java | 2 +- 11 files changed, 212 insertions(+), 171 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/components/ability/AbilityPicker.java b/Mage.Client/src/main/java/mage/client/components/ability/AbilityPicker.java index 648c17bd622..855f8f13b43 100644 --- a/Mage.Client/src/main/java/mage/client/components/ability/AbilityPicker.java +++ b/Mage.Client/src/main/java/mage/client/components/ability/AbilityPicker.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.*; /** - * Dialog for choosing abilities. + * GUI: Dialog for choosing abilities (list) * * @author nantuko, JayDi85 */ diff --git a/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java b/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java index 5a2274d636d..13ff20959ac 100644 --- a/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java +++ b/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java @@ -14,6 +14,7 @@ import org.apache.log4j.Logger; import java.awt.*; import java.awt.event.ActionEvent; import java.io.Serializable; +import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.Executors; @@ -38,6 +39,7 @@ public class FeedbackPanel extends javax.swing.JPanel { private MageDialog connectedDialog; private ChatPanelBasic connectedChatPanel; private int lastMessageId; + private Map lastOptions = new HashMap<>(); private static final ScheduledExecutorService WORKER = Executors.newSingleThreadScheduledExecutor(); @@ -63,8 +65,8 @@ public class FeedbackPanel extends javax.swing.JPanel { private void setGUISize() { } - public void getFeedback(FeedbackMode mode, String message, boolean special, Map options, - int messageId, boolean gameNeedUserFeedback, TurnPhase gameTurnPhase) { + public void prepareFeedback(FeedbackMode mode, String message, boolean special, Map options, + int messageId, boolean gameNeedUserFeedback, TurnPhase gameTurnPhase) { synchronized (this) { if (messageId < this.lastMessageId) { // if too many warning messages here then look at GAME_REDRAW_GUI event logic @@ -72,13 +74,16 @@ public class FeedbackPanel extends javax.swing.JPanel { return; } this.lastMessageId = messageId; + this.lastOptions = options; + this.mode = mode; } + this.helper.setBasicMessage(message); this.helper.setOriginalId(null); // reference to the feedback causing ability String lblText = addAdditionalText(message, options); this.helper.setTextArea(lblText); - this.mode = mode; + switch (this.mode) { case INFORM: setButtonState("", "", mode); @@ -113,7 +118,7 @@ public class FeedbackPanel extends javax.swing.JPanel { } requestFocusIfPossible(); - handleOptions(options); + updateOptions(options); this.revalidate(); this.repaint(); @@ -167,29 +172,35 @@ public class FeedbackPanel extends javax.swing.JPanel { WORKER.schedule(task, 8, TimeUnit.SECONDS); } - private void handleOptions(Map options) { - // clear already opened dialog (second request) - if (connectedDialog != null) { - connectedDialog.removeDialog(); - connectedDialog = null; - } + public void updateOptions(Map options) { + this.lastOptions = options; - if (options != null) { - if (options.containsKey("UI.left.btn.text")) { - String text = (String) options.get("UI.left.btn.text"); + if (this.lastOptions != null) { + if (this.lastOptions.containsKey("UI.left.btn.text")) { + String text = (String) this.lastOptions.get("UI.left.btn.text"); this.btnLeft.setText(text); this.helper.setLeft(text, !text.isEmpty()); } - if (options.containsKey("UI.right.btn.text")) { - String text = (String) options.get("UI.right.btn.text"); + if (this.lastOptions.containsKey("UI.right.btn.text")) { + String text = (String) this.lastOptions.get("UI.right.btn.text"); this.btnRight.setText(text); this.helper.setRight(text, !text.isEmpty()); } - if (options.containsKey("dialog")) { - connectedDialog = (MageDialog) options.get("dialog"); - } - + updateConnectedDialog((MageDialog) this.lastOptions.getOrDefault("dialog", null)); this.helper.autoSizeButtonsAndFeedbackState(); + } else { + updateConnectedDialog(null); + } + } + + private void updateConnectedDialog(MageDialog newDialog) { + if (this.connectedDialog != null && this.connectedDialog != newDialog) { + // remove old + this.connectedDialog.removeDialog(); + } + this.connectedDialog = newDialog; + if (this.connectedDialog != null) { + this.connectedDialog.setVisible(true); } } @@ -244,10 +255,7 @@ public class FeedbackPanel extends javax.swing.JPanel { } private void btnRightActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnRightActionPerformed - if (connectedDialog != null) { - connectedDialog.removeDialog(); - connectedDialog = null; - } + updateConnectedDialog(null); if (mode == FeedbackMode.SELECT && (evt.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK) { SessionHandler.sendPlayerInteger(gameId, 0); } else if (mode == FeedbackMode.END) { diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 503fe93af96..83f3862b05b 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -274,8 +274,7 @@ public final class GamePanel extends javax.swing.JPanel { windowDialog.removeDialog(); } - clearPickTargetDialogs(); - clearPickPileDialogs(); + clearPickDialogs(); Plugins.instance.getActionCallback().hideOpenComponents(); try { @@ -288,18 +287,41 @@ public final class GamePanel extends javax.swing.JPanel { this.bigCard = null; } + private void hidePickDialogs() { + // temporary hide opened dialog on redraw/update + + //try { + // pick target + for (ShowCardsDialog dialog : this.pickTarget) { + dialog.setVisible(false); + } + // pick pile + for (PickPileDialog dialog : this.pickPile) { + dialog.setVisible(false); + } + //} catch (PropertyVetoException e) { + // logger.error("Couldn't close pick dialog", e); + //} + } + + private void clearPickDialogs() { + // remove dialogs forever on clean or full update + clearPickTargetDialogs(); + clearPickPileDialogs(); + } + private void clearPickTargetDialogs() { - for (ShowCardsDialog pickTargetDialog : this.pickTarget) { - pickTargetDialog.cleanUp(); - pickTargetDialog.removeDialog(); + for (ShowCardsDialog dialog : this.pickTarget) { + dialog.cleanUp(); + dialog.removeDialog(); } this.pickTarget.clear(); } private void clearPickPileDialogs() { - for (PickPileDialog pickPileDialog : this.pickPile) { - pickPileDialog.cleanUp(); - pickPileDialog.removeDialog(); + for (PickPileDialog dialog : this.pickPile) { + dialog.cleanUp(); + dialog.removeDialog(); } this.pickPile.clear(); } @@ -929,6 +951,7 @@ public final class GamePanel extends javax.swing.JPanel { } feedbackPanel.disableUndo(); + feedbackPanel.updateOptions(lastGameData.options); this.revalidate(); this.repaint(); @@ -1344,7 +1367,7 @@ public final class GamePanel extends javax.swing.JPanel { public void ask(String question, GameView gameView, int messageId, Map options) { updateGame(gameView, false, options, null); - this.feedbackPanel.getFeedback(FeedbackMode.QUESTION, question, false, options, messageId, true, gameView.getPhase()); + this.feedbackPanel.prepareFeedback(FeedbackMode.QUESTION, question, false, options, messageId, true, gameView.getPhase()); } private void keepLastGameData(GameView game, boolean showPlayable, Map options, Set targets) { @@ -1604,11 +1627,16 @@ public final class GamePanel extends javax.swing.JPanel { * @param options * @param messageId */ - public void pickTarget(String message, CardsView cardsView, GameView gameView, Set targets, boolean required, Map options, int messageId) { + public void pickTarget(GameView gameView, Map options, String message, CardsView cardsView, Set targets, boolean required, int messageId) { + updateGame(gameView, false, options, targets); + hideAll(); + DialogManager.getManager(gameId).fadeOut(); + clearPickTargetDialogs(); + PopUpMenuType popupMenuType = null; - if (options != null) { + if (lastGameData.options != null) { if (options.containsKey("queryType")) { - PlayerQueryEvent.QueryType needType = (PlayerQueryEvent.QueryType) options.get("queryType"); + PlayerQueryEvent.QueryType needType = (PlayerQueryEvent.QueryType) lastGameData.options.get("queryType"); switch (needType) { case PICK_ABILITY: popupMenuType = PopUpMenuType.TRIGGER_ORDER; @@ -1622,17 +1650,13 @@ public final class GamePanel extends javax.swing.JPanel { } } - updateGame(gameView, false, options, targets); - - Map options0 = options == null ? new HashMap<>() : options; + Map options0 = lastGameData.options == null ? new HashMap<>() : lastGameData.options; ShowCardsDialog dialog = null; if (cardsView != null && !cardsView.isEmpty()) { - // clear old dialogs before the new - clearPickTargetDialogs(); - dialog = showCards(message, cardsView, required, options0, popupMenuType); + dialog = prepareCardsDialog(message, cardsView, required, options0, popupMenuType); options0.put("dialog", dialog); } - this.feedbackPanel.getFeedback(required ? FeedbackMode.INFORM : FeedbackMode.CANCEL, message, gameView.getSpecial(), options0, messageId, true, gameView.getPhase()); + this.feedbackPanel.prepareFeedback(required ? FeedbackMode.INFORM : FeedbackMode.CANCEL, message, gameView.getSpecial(), options0, messageId, true, gameView.getPhase()); if (dialog != null) { this.pickTarget.add(dialog); } @@ -1640,15 +1664,23 @@ public final class GamePanel extends javax.swing.JPanel { public void inform(String information, GameView gameView, int messageId) { updateGame(gameView); - this.feedbackPanel.getFeedback(FeedbackMode.INFORM, information, gameView.getSpecial(), null, messageId, false, gameView.getPhase()); + this.feedbackPanel.prepareFeedback(FeedbackMode.INFORM, information, gameView.getSpecial(), null, messageId, false, gameView.getPhase()); } - public void endMessage(String message, int messageId) { - this.feedbackPanel.getFeedback(FeedbackMode.END, message, false, null, messageId, true, null); + public void endMessage(GameView gameView, Map options, String message, int messageId) { + updateGame(gameView, false, options, null); + hideAll(); + DialogManager.getManager(gameId).fadeOut(); + + this.feedbackPanel.prepareFeedback(FeedbackMode.END, message, false, null, messageId, true, null); ArrowBuilder.getBuilder().removeAllArrows(gameId); } - public void select(String message, GameView gameView, int messageId, Map options) { + public void select(GameView gameView, Map options, String message, int messageId) { + updateGame(gameView, true, options, null); + hideAll(); + DialogManager.getManager(gameId).fadeOut(); + this.abilityPicker.setVisible(false); holdingPriority = false; @@ -1659,8 +1691,6 @@ public final class GamePanel extends javax.swing.JPanel { PreferencesDialog.getCachedValue(KEY_USE_FIRST_MANA_ABILITY, "false").equals("true"), false); - updateGame(gameView, true, options, null); - boolean controllingPlayer = false; for (PlayerView playerView : gameView.getPlayers()) { if (playerView.getPlayerId().equals(playerId)) { @@ -1675,8 +1705,8 @@ public final class GamePanel extends javax.swing.JPanel { } Map panelOptions = new HashMap<>(); - if (options != null) { - panelOptions.putAll(options); + if (lastGameData.options != null) { + panelOptions.putAll(lastGameData.options); } panelOptions.put("your_turn", true); String activePlayerText; @@ -1690,39 +1720,45 @@ public final class GamePanel extends javax.swing.JPanel { priorityPlayerText = " / priority " + gameView.getPriorityPlayerName(); } String messageToDisplay = message + FeedbackPanel.getSmallText(activePlayerText + " / " + gameView.getStep().toString() + priorityPlayerText); - this.feedbackPanel.getFeedback(FeedbackMode.SELECT, messageToDisplay, gameView.getSpecial(), panelOptions, messageId, true, gameView.getPhase()); + this.feedbackPanel.prepareFeedback(FeedbackMode.SELECT, messageToDisplay, gameView.getSpecial(), panelOptions, messageId, true, gameView.getPhase()); } - public void playMana(String message, GameView gameView, Map options, int messageId) { + public void playMana(GameView gameView, Map options, String message, int messageId) { updateGame(gameView, true, options, null); + hideAll(); DialogManager.getManager(gameId).fadeOut(); - this.feedbackPanel.getFeedback(FeedbackMode.CANCEL, message, gameView.getSpecial(), options, messageId, true, gameView.getPhase()); + + this.feedbackPanel.prepareFeedback(FeedbackMode.CANCEL, message, gameView.getSpecial(), options, messageId, true, gameView.getPhase()); } - public void playXMana(String message, GameView gameView, int messageId) { - updateGame(gameView, true, null, null); + public void playXMana(GameView gameView, Map options, String message, int messageId) { + updateGame(gameView, true, options, null); + hideAll(); DialogManager.getManager(gameId).fadeOut(); - this.feedbackPanel.getFeedback(FeedbackMode.CONFIRM, message, gameView.getSpecial(), null, messageId, true, gameView.getPhase()); + + this.feedbackPanel.prepareFeedback(FeedbackMode.CONFIRM, message, gameView.getSpecial(), null, messageId, true, gameView.getPhase()); } public void replayMessage(String message) { //TODO: implement this } - public void pickAbility(AbilityPickerView choices) { + public void pickAbility(GameView gameView, Map options, AbilityPickerView choices) { + updateGame(gameView, false, options, null); hideAll(); DialogManager.getManager(gameId).fadeOut(); + this.abilityPicker.show(choices, MageFrame.getDesktop().getMousePosition()); } private void hideAll() { + hidePickDialogs(); this.abilityPicker.setVisible(false); ActionCallback callback = Plugins.instance.getActionCallback(); ((MageActionCallback) callback).hideGameUpdate(gameId); } - private ShowCardsDialog showCards(String title, CardsView cards, boolean required, Map options, PopUpMenuType popupMenuType) { - hideAll(); + private ShowCardsDialog prepareCardsDialog(String title, CardsView cards, boolean required, Map options, PopUpMenuType popupMenuType) { ShowCardsDialog showCards = new ShowCardsDialog(); JPopupMenu popupMenu = null; if (PopUpMenuType.TRIGGER_ORDER == popupMenuType) { @@ -1732,7 +1768,11 @@ public final class GamePanel extends javax.swing.JPanel { return showCards; } - public void getAmount(int min, int max, String message) { + public void getAmount(GameView gameView, Map options, int min, int max, String message) { + updateGame(gameView, false, options, null); + hideAll(); + DialogManager.getManager(gameId).fadeOut(); + pickNumber.showDialog(min, max, message); if (pickNumber.isCancel()) { SessionHandler.sendPlayerBoolean(gameId, false); @@ -1741,13 +1781,20 @@ public final class GamePanel extends javax.swing.JPanel { } } - public void getMultiAmount(List messages, int min, int max, Map options) { - pickMultiNumber.showDialog(messages, min, max, options); + public void getMultiAmount(List messages, GameView gameView, Map options, int min, int max) { + updateGame(gameView, false, options, null); + hideAll(); + DialogManager.getManager(gameId).fadeOut(); + + pickMultiNumber.showDialog(messages, min, max, lastGameData.options); SessionHandler.sendPlayerString(gameId, pickMultiNumber.getMultiAmount()); } - public void getChoice(Choice choice, UUID objectId) { + public void getChoice(GameView gameView, Map options, Choice choice, UUID objectId) { + updateGame(gameView, false, options, null); hideAll(); + DialogManager.getManager(gameId).fadeOut(); + // TODO: remember last choices and search incremental for same events? PickChoiceDialog pickChoice = new PickChoiceDialog(); pickChoice.showDialog(choice, null, objectId, choiceWindowState, bigCard); @@ -1769,8 +1816,10 @@ public final class GamePanel extends javax.swing.JPanel { pickChoice.removeDialog(); } - public void pickPile(String message, CardsView pile1, CardsView pile2) { + public void pickPile(GameView gameView, Map options, String message, CardsView pile1, CardsView pile2) { + updateGame(gameView, false, options, null); hideAll(); + DialogManager.getManager(gameId).fadeOut(); // remove old dialogs before the new clearPickPileDialogs(); diff --git a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java index e256f7307c2..1e062a2396d 100644 --- a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java +++ b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java @@ -157,7 +157,7 @@ public class CallbackClientImpl implements CallbackClient { case REPLAY_DONE: { GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { - panel.endMessage((String) callback.getData(), callback.getMessageId()); + panel.endMessage(null, null, (String) callback.getData(), callback.getMessageId()); } break; } @@ -180,16 +180,17 @@ public class CallbackClientImpl implements CallbackClient { } case GAME_OVER: { + GameClientMessage message = (GameClientMessage) callback.getData(); GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { Session session = SessionHandler.getSession(); if (session.isJsonLogActive()) { - appendJsonEvent("GAME_OVER", callback.getObjectId(), callback.getData()); - ActionData actionData = appendJsonEvent("GAME_OVER", callback.getObjectId(), callback.getData()); - String logFileName = "game-" + actionData.gameId + ".json"; - S3Uploader.upload(logFileName, actionData.gameId.toString()); + UUID gameId = callback.getObjectId(); + appendJsonEvent("GAME_OVER", callback.getObjectId(), message); + String logFileName = "game-" + gameId + ".json"; + S3Uploader.upload(logFileName, gameId.toString()); } - panel.endMessage((String) callback.getData(), callback.getMessageId()); + panel.endMessage(message.getGameView(), message.getOptions(), message.getMessage(), callback.getMessageId()); } break; } @@ -209,35 +210,34 @@ public class CallbackClientImpl implements CallbackClient { break; } - case GAME_TARGET: // e.g. Pick triggered ability - { + case GAME_TARGET: { + // e.g. Pick triggered ability GameClientMessage message = (GameClientMessage) callback.getData(); - GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { appendJsonEvent("GAME_TARGET", callback.getObjectId(), message); - panel.pickTarget(message.getMessage(), message.getCardsView(), message.getGameView(), - message.getTargets(), message.isFlag(), message.getOptions(), callback.getMessageId()); + panel.pickTarget(message.getGameView(), message.getOptions(), message.getMessage(), + message.getCardsView1(), message.getTargets(), message.isFlag(), callback.getMessageId()); } break; } case GAME_SELECT: { GameClientMessage message = (GameClientMessage) callback.getData(); - GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { appendJsonEvent("GAME_SELECT", callback.getObjectId(), message); - panel.select(message.getMessage(), message.getGameView(), callback.getMessageId(), message.getOptions()); + panel.select(message.getGameView(), message.getOptions(), message.getMessage(), callback.getMessageId()); } break; } case GAME_CHOOSE_ABILITY: { + AbilityPickerView abilityPickerView = (AbilityPickerView) callback.getData(); GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { appendJsonEvent("GAME_CHOOSE_ABILITY", callback.getObjectId(), callback.getData()); - panel.pickAbility((AbilityPickerView) callback.getData()); + panel.pickAbility(abilityPickerView.getGameView(), null, abilityPickerView); } break; } @@ -247,19 +247,17 @@ public class CallbackClientImpl implements CallbackClient { GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { appendJsonEvent("GAME_CHOOSE_PILE", callback.getObjectId(), message); - panel.pickPile(message.getMessage(), message.getPile1(), message.getPile2()); + panel.pickPile(message.getGameView(), message.getOptions(), message.getMessage(), message.getCardsView1(), message.getCardsView2()); } break; } case GAME_CHOOSE_CHOICE: { GameClientMessage message = (GameClientMessage) callback.getData(); - GamePanel panel = MageFrame.getGame(callback.getObjectId()); - if (panel != null) { appendJsonEvent("GAME_CHOOSE_CHOICE", callback.getObjectId(), message); - panel.getChoice(message.getChoice(), callback.getObjectId()); + panel.getChoice(message.getGameView(), message.getOptions(), message.getChoice(), callback.getObjectId()); } break; } @@ -269,53 +267,48 @@ public class CallbackClientImpl implements CallbackClient { GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { appendJsonEvent("GAME_PLAY_MANA", callback.getObjectId(), message); - panel.playMana(message.getMessage(), message.getGameView(), message.getOptions(), callback.getMessageId()); + panel.playMana(message.getGameView(), message.getOptions(), message.getMessage(), callback.getMessageId()); } break; } case GAME_PLAY_XMANA: { GameClientMessage message = (GameClientMessage) callback.getData(); - GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { appendJsonEvent("GAME_PLAY_XMANA", callback.getObjectId(), message); - panel.playXMana(message.getMessage(), message.getGameView(), callback.getMessageId()); + panel.playXMana(message.getGameView(), message.getOptions(), message.getMessage(), callback.getMessageId()); } break; } case GAME_GET_AMOUNT: { GameClientMessage message = (GameClientMessage) callback.getData(); - GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { appendJsonEvent("GAME_GET_AMOUNT", callback.getObjectId(), message); - panel.getAmount(message.getMin(), message.getMax(), message.getMessage()); + panel.getAmount(message.getGameView(), message.getOptions(), message.getMin(), message.getMax(), message.getMessage()); } break; } case GAME_GET_MULTI_AMOUNT: { GameClientMessage message = (GameClientMessage) callback.getData(); - GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { appendJsonEvent("GAME_GET_MULTI_AMOUNT", callback.getObjectId(), message); - panel.getMultiAmount(message.getMessages(), message.getMin(), message.getMax(), message.getOptions()); + panel.getMultiAmount(message.getMessages(), message.getGameView(), message.getOptions(), message.getMin(), message.getMax()); } break; } case GAME_UPDATE: { GamePanel panel = MageFrame.getGame(callback.getObjectId()); - if (panel != null) { appendJsonEvent("GAME_UPDATE", callback.getObjectId(), callback.getData()); - - panel.updateGame((GameView) callback.getData(), true, null, null); // update after undo + panel.updateGame((GameView) callback.getData(), true, null, null); // update after undo wtf?! } break; } @@ -325,6 +318,7 @@ public class CallbackClientImpl implements CallbackClient { // uses for client side only (example: update after scrollbars support) GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { + logger.info("redraw"); panel.updateGame(); } break; diff --git a/Mage.Common/src/main/java/mage/view/AbilityPickerView.java b/Mage.Common/src/main/java/mage/view/AbilityPickerView.java index d4ae6a0c231..7b4141294cd 100644 --- a/Mage.Common/src/main/java/mage/view/AbilityPickerView.java +++ b/Mage.Common/src/main/java/mage/view/AbilityPickerView.java @@ -17,9 +17,11 @@ public class AbilityPickerView implements Serializable { private static final long serialVersionUID = 1L; private Map choices = new LinkedHashMap<>(); - private String message = null; + private String message; + private GameView gameView; - public AbilityPickerView(String objectName, List abilities, String message) { + public AbilityPickerView(GameView gameView, String objectName, List abilities, String message) { + this.gameView = gameView; this.message = message; int num = 0; @@ -44,6 +46,12 @@ public class AbilityPickerView implements Serializable { } } + public AbilityPickerView(GameView gameView, Map modes, String message) { + this.gameView = gameView; + this.choices = modes; + this.message = message; + } + private String getAbilityRules(Ability ability, String objectName) { String rule = ability.getRule(objectName); if (rule.isEmpty()) { @@ -55,11 +63,6 @@ public class AbilityPickerView implements Serializable { return rule; } - public AbilityPickerView(Map modes, String message) { - this.choices = modes; - this.message = message; - } - public Map getChoices() { return choices; } @@ -67,4 +70,8 @@ public class AbilityPickerView implements Serializable { public String getMessage() { return message; } + + public GameView getGameView() { + return gameView; + } } diff --git a/Mage.Common/src/main/java/mage/view/GameClientMessage.java b/Mage.Common/src/main/java/mage/view/GameClientMessage.java index 092079bf7cc..64a4c43e379 100644 --- a/Mage.Common/src/main/java/mage/view/GameClientMessage.java +++ b/Mage.Common/src/main/java/mage/view/GameClientMessage.java @@ -24,7 +24,7 @@ public class GameClientMessage implements Serializable { @Expose private GameView gameView; @Expose - private CardsView cardsView; + private CardsView cardsView1; @Expose private CardsView cardsView2; @Expose @@ -32,8 +32,6 @@ public class GameClientMessage implements Serializable { @Expose private boolean flag; @Expose - private String[] strings; - @Expose private Set targets; @Expose private int min; @@ -46,64 +44,53 @@ public class GameClientMessage implements Serializable { @Expose private List messages; - public GameClientMessage(GameView gameView) { + public GameClientMessage(GameView gameView, Map options) { this.gameView = gameView; - } - - public GameClientMessage(GameView gameView, String message) { - this.gameView = gameView; - this.message = message; - } - - public GameClientMessage(GameView gameView, String message, Map options) { - this.gameView = gameView; - this.message = message; this.options = options; } - private GameClientMessage(GameView gameView, String question, CardsView cardView, Set targets, boolean required) { + public GameClientMessage(GameView gameView, Map options, String message) { this.gameView = gameView; - this.message = question; - this.cardsView = cardView; + this.options = options; + this.message = message; + } + + public GameClientMessage(GameView gameView, Map options, String message, CardsView cardsView1, Set targets, boolean required) { + this.gameView = gameView; + this.options = options; + this.message = message; + this.cardsView1 = cardsView1; this.targets = targets; this.flag = required; } - public GameClientMessage(GameView gameView, String question, CardsView cardView, Set targets, boolean required, Map options) { - this(gameView, question, cardView, targets, required); + public GameClientMessage(GameView gameView, Map options, String message, int min, int max) { + this.gameView = gameView; this.options = options; - } - - public GameClientMessage(String[] choices, String message) { - this.strings = choices; - this.message = message; - } - - public GameClientMessage(String message, int min, int max) { this.message = message; this.min = min; this.max = max; } - public GameClientMessage(String message, CardsView pile1, CardsView pile2) { + public GameClientMessage(GameView gameView, Map options, String message, CardsView pile1, CardsView pile2) { + this.gameView = gameView; + this.options = options; this.message = message; - this.cardsView = pile1; + this.cardsView1 = pile1; this.cardsView2 = pile2; } - public GameClientMessage(CardsView cardView, String name) { - this.cardsView = cardView; - this.message = name; - } - - public GameClientMessage(List messages, int min, int max, Map options) { + public GameClientMessage(GameView gameView, Map options, List messages, int min, int max) { + this.gameView = gameView; + this.options = options; this.messages = messages; this.min = min; this.max = max; - this.options = options; } - public GameClientMessage(Choice choice) { + public GameClientMessage(GameView gameView, Map options, Choice choice) { + this.gameView = gameView; + this.options = options; this.choice = choice; } @@ -111,8 +98,12 @@ public class GameClientMessage implements Serializable { return gameView; } - public CardsView getCardsView() { - return cardsView; + public CardsView getCardsView1() { + return cardsView1; + } + + public CardsView getCardsView2() { + return cardsView2; } public String getMessage() { @@ -123,22 +114,10 @@ public class GameClientMessage implements Serializable { return flag; } - public String[] getStrings() { - return strings; - } - public Set getTargets() { return targets; } - public CardsView getPile1() { - return cardsView; - } - - public CardsView getPile2() { - return cardsView2; - } - public int getMin() { return min; } diff --git a/Mage.Server/src/main/java/mage/server/game/GameController.java b/Mage.Server/src/main/java/mage/server/game/GameController.java index d2b0cbac495..e839e151864 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -810,7 +810,7 @@ public class GameController implements GameCallback { } private synchronized void chooseAbility(UUID playerId, final String objectName, final List choices, String message) throws MageException { - perform(playerId, playerId1 -> getGameSession(playerId1).chooseAbility(new AbilityPickerView(objectName, choices, message))); + perform(playerId, playerId1 -> getGameSession(playerId1).chooseAbility(new AbilityPickerView(getGameView(playerId), objectName, choices, message))); } private synchronized void choosePile(UUID playerId, final String message, final List pile1, final List pile2) throws MageException { @@ -818,7 +818,7 @@ public class GameController implements GameCallback { } private synchronized void chooseMode(UUID playerId, final Map modes, final String message) throws MageException { - perform(playerId, playerId1 -> getGameSession(playerId1).chooseAbility(new AbilityPickerView(modes, message))); + perform(playerId, playerId1 -> getGameSession(playerId1).chooseAbility(new AbilityPickerView(getGameView(playerId), modes, message))); } private synchronized void chooseChoice(UUID playerId, final Choice choice) throws MageException { diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java index dfa6bf947fa..714572fdc49 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java @@ -47,7 +47,7 @@ public class GameSessionPlayer extends GameSessionWatcher { public void ask(final String question, final Map options) { if (!killed) { - userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_ASK, game.getId(), new GameClientMessage(getGameView(), question, options))) + userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_ASK, game.getId(), new GameClientMessage(getGameView(), options, question))) ); } } @@ -55,7 +55,7 @@ public class GameSessionPlayer extends GameSessionWatcher { public void target(final String question, final CardsView cardView, final Set targets, final boolean required, final Map options) { if (!killed) { userManager.getUser(userId).ifPresent(user -> { - user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_TARGET, game.getId(), new GameClientMessage(getGameView(), question, cardView, targets, required, options))); + user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_TARGET, game.getId(), new GameClientMessage(getGameView(), options, question, cardView, targets, required))); }); } @@ -63,7 +63,7 @@ public class GameSessionPlayer extends GameSessionWatcher { public void select(final String message, final Map options) { if (!killed) { - userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_SELECT, game.getId(), new GameClientMessage(getGameView(), message, options)))); + userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_SELECT, game.getId(), new GameClientMessage(getGameView(), options, message)))); } } @@ -78,7 +78,7 @@ public class GameSessionPlayer extends GameSessionWatcher { public void choosePile(final String message, final CardsView pile1, final CardsView pile2) { if (!killed) { userManager.getUser(userId).ifPresent(user - -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_CHOOSE_PILE, game.getId(), new GameClientMessage(message, pile1, pile2)))); + -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_CHOOSE_PILE, game.getId(), new GameClientMessage(getGameView(), null, message, pile1, pile2)))); } } @@ -86,7 +86,7 @@ public class GameSessionPlayer extends GameSessionWatcher { public void chooseChoice(final Choice choice) { if (!killed) { userManager.getUser(userId).ifPresent(user - -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_CHOOSE_CHOICE, game.getId(), new GameClientMessage(choice)))); + -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_CHOOSE_CHOICE, game.getId(), new GameClientMessage(getGameView(), null, choice)))); } } @@ -94,14 +94,14 @@ public class GameSessionPlayer extends GameSessionWatcher { public void playMana(final String message, final Map options) { if (!killed) { userManager.getUser(userId).ifPresent(user - -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_PLAY_MANA, game.getId(), new GameClientMessage(getGameView(), message, options)))); + -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_PLAY_MANA, game.getId(), new GameClientMessage(getGameView(), options, message)))); } } public void playXMana(final String message) { if (!killed) { userManager.getUser(userId).ifPresent(user - -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_PLAY_XMANA, game.getId(), new GameClientMessage(getGameView(), message)))); + -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_PLAY_XMANA, game.getId(), new GameClientMessage(getGameView(), null, message)))); } } @@ -109,7 +109,7 @@ public class GameSessionPlayer extends GameSessionWatcher { public void getAmount(final String message, final int min, final int max) { if (!killed) { userManager.getUser(userId).ifPresent(user -> { - user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_AMOUNT, game.getId(), new GameClientMessage(message, min, max))); + user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_AMOUNT, game.getId(), new GameClientMessage(getGameView(), null, message, min, max))); }); } } @@ -117,7 +117,7 @@ public class GameSessionPlayer extends GameSessionWatcher { public void getMultiAmount(final List messages, final int min, final int max, final Map options) { if (!killed) { userManager.getUser(userId).ifPresent(user - -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_MULTI_AMOUNT, game.getId(), new GameClientMessage(messages, min, max, options)))); + -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_MULTI_AMOUNT, game.getId(), new GameClientMessage(getGameView(), options, messages, min, max)))); } } diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java b/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java index 4bcd69d57c9..452e948abe3 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java @@ -58,14 +58,14 @@ public class GameSessionWatcher { public void inform(final String message) { if (!killed) { - userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_INFORM, game.getId(), new GameClientMessage(getGameView(), message)))); + userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_INFORM, game.getId(), new GameClientMessage(getGameView(), null, message)))); } } public void informPersonal(final String message) { if (!killed) { - userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_INFORM_PERSONAL, game.getId(), new GameClientMessage(getGameView(), message)))); + userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_INFORM_PERSONAL, game.getId(), new GameClientMessage(getGameView(), null, message)))); } } @@ -74,7 +74,7 @@ public class GameSessionWatcher { if (!killed) { userManager.getUser(userId).ifPresent(user -> { user.removeGameWatchInfo(game.getId()); - user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_OVER, game.getId(), message)); + user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_OVER, game.getId(), new GameClientMessage(getGameView(), null, message))); }); } } @@ -89,7 +89,6 @@ public class GameSessionWatcher { public void gameError(final String message) { if (!killed) { userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_ERROR, game.getId(), message))); - } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/AbilityPickerTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/AbilityPickerTest.java index a064f280e43..100e71737d1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/AbilityPickerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/AbilityPickerTest.java @@ -7,6 +7,7 @@ import mage.cards.repository.CardRepository; import mage.game.permanent.PermanentCard; import mage.game.permanent.PermanentImpl; import mage.view.AbilityPickerView; +import mage.view.GameView; import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -16,13 +17,17 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class AbilityPickerTest extends CardTestPlayerBase { + private GameView prepareGameView() { + return new GameView(currentGame.getState(), currentGame, playerA.getId(), null); + } + @Test public void test_PickerChoices_FusedSpells() { // must be 3 spells for choices Abilities abilities = getAbilitiesFromCard("Armed // Dangerous"); Assert.assertEquals(3, abilities.size()); - AbilityPickerView view = new AbilityPickerView("test name", abilities, "test message"); + AbilityPickerView view = new AbilityPickerView(prepareGameView(), "test name", abilities, "test message"); Assert.assertEquals(3, view.getChoices().size()); view.getChoices().values().forEach(c -> { Assert.assertTrue("Must start with Cast text, but found: " + c, c.contains("Cast ")); @@ -35,7 +40,7 @@ public class AbilityPickerTest extends CardTestPlayerBase { Abilities abilities = getAbilitiesFromCard("Foulmire Knight"); Assert.assertEquals(3, abilities.size()); - AbilityPickerView view = new AbilityPickerView("test name", abilities, "test message"); + AbilityPickerView view = new AbilityPickerView(prepareGameView(), "test name", abilities, "test message"); Assert.assertEquals(3, view.getChoices().size()); view.getChoices().values().forEach(c -> { if (c.contains("Deathtouch")) { @@ -51,7 +56,7 @@ public class AbilityPickerTest extends CardTestPlayerBase { Abilities abilities = getAbilitiesFromCard("Dimir Cluestone"); Assert.assertEquals(4, abilities.size()); - AbilityPickerView view = new AbilityPickerView("test name", abilities, "test message"); + AbilityPickerView view = new AbilityPickerView(prepareGameView(), "test name", abilities, "test message"); Assert.assertEquals(4, view.getChoices().size()); int castCount = 0; int abilsCount = 0; @@ -72,7 +77,7 @@ public class AbilityPickerTest extends CardTestPlayerBase { Abilities abilities = getAbilitiesFromCard("Cling to Dust"); Assert.assertEquals(2, abilities.size()); - AbilityPickerView view = new AbilityPickerView("test name", abilities, "test message"); + AbilityPickerView view = new AbilityPickerView(prepareGameView(), "test name", abilities, "test message"); Assert.assertEquals(2, view.getChoices().size()); view.getChoices().values().forEach(c -> { Assert.assertTrue("Must start with Cast text, but found: " + c, c.contains("Cast ")); diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index fdbae854868..a5a294dc306 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1394,7 +1394,7 @@ public final class CardUtil { if (!controller.moveCards(card, toZone, source, game)) { return false; } - + // add counter // after move it's a new object (not a permanent), so must work with main card Effect effect = new AddCountersTargetEffect(counter); From 61288b643990a2b296c07dabf26109d18e82db95 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Mon, 20 Sep 2021 02:44:46 +0400 Subject: [PATCH 147/231] * GUI: fixed wrong card icons drawing in choose triggers/piles dialog (#8225); --- .../main/java/mage/client/cards/CardArea.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/cards/CardArea.java b/Mage.Client/src/main/java/mage/client/cards/CardArea.java index ad8be6e3dee..4b73077031b 100644 --- a/Mage.Client/src/main/java/mage/client/cards/CardArea.java +++ b/Mage.Client/src/main/java/mage/client/cards/CardArea.java @@ -186,14 +186,22 @@ public class CardArea extends JPanel implements CardEventProducer { card = tmp; } - CardIconRenderSettings customIconsRender = new CardIconRenderSettings() - .withDebugMode(true) - .withCustomPosition(customCardIconPosition) - .withCustomOrder(customCardIconOrder) - .withCustomColor(customCardIconColor) - .withCustomMaxVisibleCount(customCardIconsMaxVisibleCount) - .withCustomIconSizePercent(30); - MageCard cardPanel = Plugins.instance.getMageCard(card, bigCard, customIconsRender, cardDimension, gameId, true, true, + CardIconRenderSettings currentIconsRender; + if (this.customRenderMode >= 0) { + // debug + currentIconsRender = new CardIconRenderSettings() + .withDebugMode(true) + .withCustomPosition(customCardIconPosition) + .withCustomOrder(customCardIconOrder) + .withCustomColor(customCardIconColor) + .withCustomMaxVisibleCount(customCardIconsMaxVisibleCount) + .withCustomIconSizePercent(30); + } else { + // default + currentIconsRender = new CardIconRenderSettings(); + } + + MageCard cardPanel = Plugins.instance.getMageCard(card, bigCard, currentIconsRender, cardDimension, gameId, true, true, customRenderMode != -1 ? customRenderMode : PreferencesDialog.getRenderMode(), customNeedFullPermanentRender); cardPanel.setCardContainerRef(this); cardPanel.update(card); From a88427a6d9892d24aa483d0879d965019055b3ae Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 20 Sep 2021 08:48:28 -0400 Subject: [PATCH 148/231] [MIC] Implemented Visions of Dominance --- .../src/mage/cards/v/VisionsOfDominance.java | 77 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + Mage/src/main/java/mage/cards/Card.java | 2 + Mage/src/main/java/mage/cards/CardImpl.java | 5 ++ Mage/src/main/java/mage/game/stack/Spell.java | 5 ++ 5 files changed, 90 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/v/VisionsOfDominance.java diff --git a/Mage.Sets/src/mage/cards/v/VisionsOfDominance.java b/Mage.Sets/src/mage/cards/v/VisionsOfDominance.java new file mode 100644 index 00000000000..4489ab56ed8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VisionsOfDominance.java @@ -0,0 +1,77 @@ +package mage.cards.v; + +import mage.abilities.Ability; +import mage.abilities.costs.costadjusters.CommanderManaValueAdjuster; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VisionsOfDominance extends CardImpl { + + public VisionsOfDominance(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{G}"); + + // Put a +1/+1 counter on target creature, then double the number of +1/+1 counters on it. + this.getSpellAbility().addEffect(new VisionsOfDominanceEffect()); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + + // Flashback {8}{G}{G}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone. + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{8}{G}{G}")) + .setAbilityName("This spell costs {X} less to cast this way, where X is the greatest mana value " + + "of a commander you own on the battlefield or in the command zone.") + .setCostAdjuster(CommanderManaValueAdjuster.instance)); + } + + private VisionsOfDominance(final VisionsOfDominance card) { + super(card); + } + + @Override + public VisionsOfDominance copy() { + return new VisionsOfDominance(this); + } +} + +class VisionsOfDominanceEffect extends OneShotEffect { + + VisionsOfDominanceEffect() { + super(Outcome.Benefit); + staticText = "put a +1/+1 counter on target creature, then double the number of +1/+1 counters on it"; + } + + private VisionsOfDominanceEffect(final VisionsOfDominanceEffect effect) { + super(effect); + } + + @Override + public VisionsOfDominanceEffect copy() { + return new VisionsOfDominanceEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent == null) { + return false; + } + permanent.addCounters(CounterType.P1P1.createInstance(), source, game); + int counterCount = permanent.getCounters(game).getCount(CounterType.P1P1); + if (counterCount > 0) { + permanent.addCounters(CounterType.P1P1.createInstance(counterCount), source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index c11a47516c1..0ca1d956f09 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -142,6 +142,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Undead Augur", 130, Rarity.UNCOMMON, mage.cards.u.UndeadAugur.class)); cards.add(new SetCardInfo("Verdurous Gearhulk", 145, Rarity.MYTHIC, mage.cards.v.VerdurousGearhulk.class)); cards.add(new SetCardInfo("Victory's Envoy", 96, Rarity.RARE, mage.cards.v.VictorysEnvoy.class)); + cards.add(new SetCardInfo("Visions of Dominance", 37, Rarity.RARE, mage.cards.v.VisionsOfDominance.class)); cards.add(new SetCardInfo("Visions of Duplicity", 33, Rarity.RARE, mage.cards.v.VisionsOfDuplicity.class)); cards.add(new SetCardInfo("Visions of Glory", 32, Rarity.RARE, mage.cards.v.VisionsOfGlory.class)); cards.add(new SetCardInfo("Wild Beastmaster", 146, Rarity.RARE, mage.cards.w.WildBeastmaster.class)); diff --git a/Mage/src/main/java/mage/cards/Card.java b/Mage/src/main/java/mage/cards/Card.java index d3b27fa0684..c00bffaf700 100644 --- a/Mage/src/main/java/mage/cards/Card.java +++ b/Mage/src/main/java/mage/cards/Card.java @@ -144,6 +144,8 @@ public interface Card extends MageObject { void looseAllAbilities(Game game); + boolean addCounters(Counter counter, Ability source, Game game); + boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game); boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, boolean isEffect); diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index 946710f81a2..0bd9d7464e3 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -687,6 +687,11 @@ public abstract class CardImpl extends MageObjectImpl implements Card { return ownerId; } + @Override + public boolean addCounters(Counter counter, Ability source, Game game) { + return addCounters(counter, source.getControllerId(), source, game); + } + @Override public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) { return addCounters(counter, playerAddingCounters, source, game, null, true); diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 910e19e5f27..6738fd8f1a3 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -989,6 +989,11 @@ public class Spell extends StackObjectImpl implements Card { return card.getCounters(state); } + @Override + public boolean addCounters(Counter counter, Ability source, Game game) { + return card.addCounters(counter, source, game); + } + @Override public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) { return card.addCounters(counter, playerAddingCounters, source, game); From b756fd0e72c6e68c008509c1e9aefb3fa98fa48a Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 20 Sep 2021 09:12:13 -0400 Subject: [PATCH 149/231] [MIC] Implemented Visions of Ruin --- Mage.Sets/src/mage/cards/v/VisionsOfRuin.java | 94 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 95 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/v/VisionsOfRuin.java diff --git a/Mage.Sets/src/mage/cards/v/VisionsOfRuin.java b/Mage.Sets/src/mage/cards/v/VisionsOfRuin.java new file mode 100644 index 00000000000..de41c1c2d23 --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VisionsOfRuin.java @@ -0,0 +1,94 @@ +package mage.cards.v; + +import mage.abilities.Ability; +import mage.abilities.costs.costadjusters.CommanderManaValueAdjuster; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.TreasureToken; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VisionsOfRuin extends CardImpl { + + public VisionsOfRuin(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{R}"); + + // Each opponent sacrifices an artifact. For each artifact sacrificed this way, you create a Treasure token. + this.getSpellAbility().addEffect(new VisionsOfRuinEffect()); + + // Flashback {8}{R}{R}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone. + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{8}{R}{R}")) + .setAbilityName("This spell costs {X} less to cast this way, where X is the greatest mana value " + + "of a commander you own on the battlefield or in the command zone.") + .setCostAdjuster(CommanderManaValueAdjuster.instance)); + } + + private VisionsOfRuin(final VisionsOfRuin card) { + super(card); + } + + @Override + public VisionsOfRuin copy() { + return new VisionsOfRuin(this); + } +} + +class VisionsOfRuinEffect extends OneShotEffect { + + VisionsOfRuinEffect() { + super(Outcome.Benefit); + staticText = "each opponent sacrifices an artifact. For each artifact sacrificed this way, you create a Treasure token"; + } + + private VisionsOfRuinEffect(final VisionsOfRuinEffect effect) { + super(effect); + } + + @Override + public VisionsOfRuinEffect copy() { + return new VisionsOfRuinEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Set permanents = new HashSet<>(); + for (UUID playerId : game.getOpponents(source.getControllerId())) { + Player opponent = game.getPlayer(playerId); + if (opponent == null || game.getBattlefield().count( + StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT, + source.getSourceId(), playerId, game + ) < 1) { + continue; + } + TargetPermanent target = new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT); + target.setNotTarget(true); + opponent.choose(Outcome.Sacrifice, target, source.getSourceId(), game); + permanents.add(game.getPermanent(target.getFirstTarget())); + } + int sacrificed = 0; + for (Permanent permanent : permanents) { + if (permanent != null && permanent.sacrifice(source, game)) { + sacrificed++; + } + } + if (sacrificed > 0) { + new TreasureToken().putOntoBattlefield(sacrificed, game, source, source.getControllerId()); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 0ca1d956f09..a6590be0891 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -145,6 +145,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Visions of Dominance", 37, Rarity.RARE, mage.cards.v.VisionsOfDominance.class)); cards.add(new SetCardInfo("Visions of Duplicity", 33, Rarity.RARE, mage.cards.v.VisionsOfDuplicity.class)); cards.add(new SetCardInfo("Visions of Glory", 32, Rarity.RARE, mage.cards.v.VisionsOfGlory.class)); + cards.add(new SetCardInfo("Visions of Ruin", 36, Rarity.RARE, mage.cards.v.VisionsOfRuin.class)); cards.add(new SetCardInfo("Wild Beastmaster", 146, Rarity.RARE, mage.cards.w.WildBeastmaster.class)); cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); cards.add(new SetCardInfo("Yavimaya Elder", 147, Rarity.COMMON, mage.cards.y.YavimayaElder.class)); From 77845bd1df66b390b518819d69b4ea9a3ca5085e Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Mon, 20 Sep 2021 15:54:52 -0500 Subject: [PATCH 150/231] Fixed #8286. --- .../java/mage/player/ai/ComputerPlayer.java | 56 ++++++++++++++++++- .../src/mage/cards/a/AngelOfSerenity.java | 16 +++--- .../src/main/java/mage/target/TargetImpl.java | 12 +++- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 311fa30f694..c8da4b693b5 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -113,7 +113,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (hand.size() < 6 || isTestsMode() // ignore mulligan in tests || game.getClass().getName().contains("Momir") // ignore mulligan in Momir games - ) { + ) { return false; } Set lands = hand.getCards(new FilterLandCard(), game); @@ -540,6 +540,58 @@ public class ComputerPlayer extends PlayerImpl implements Player { return setTargetPlayer(outcome, target, source, sourceId, abilityControllerId, randomOpponentId, game, required); } + // Angel of Serenity trigger + if (target.getOriginalTarget() instanceof TargetCardInGraveyardOrBattlefield) { + Cards cards = new CardsImpl(possibleTargets); + List possibleCards = new ArrayList<>(cards.getCards(game)); + for (Card card : possibleCards) { + // check permanents first; they have more intrinsic worth + if (card instanceof Permanent) { + Permanent p = ((Permanent) card); + if (outcome.isGood() + && p.isControlledBy(abilityControllerId)) { + if (target.canTarget(abilityControllerId, p.getId(), source, game)) { + if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { + break; + } + target.addTarget(p.getId(), source, game); + } + } + if (!outcome.isGood() + && !p.isControlledBy(abilityControllerId)) { + if (target.canTarget(abilityControllerId, p.getId(), source, game)) { + if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { + break; + } + target.addTarget(p.getId(), source, game); + } + } + } + // check the graveyards last + if (game.getState().getZone(card.getId()) == Zone.GRAVEYARD) { + if (outcome.isGood() + && card.isOwnedBy(abilityControllerId)) { + if (target.canTarget(abilityControllerId, card.getId(), source, game)) { + if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { + break; + } + target.addTarget(card.getId(), source, game); + } + } + if (!outcome.isGood() + && !card.isOwnedBy(abilityControllerId)) { + if (target.canTarget(abilityControllerId, card.getId(), source, game)) { + if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { + break; + } + target.addTarget(card.getId(), source, game); + } + } + } + } + return target.isChosen(); + } + if (target.getOriginalTarget() instanceof TargetDiscard || target.getOriginalTarget() instanceof TargetCardInHand) { if (outcome.isGood()) { @@ -2659,7 +2711,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { } protected void findBestPermanentTargets(Outcome outcome, UUID abilityControllerId, UUID sourceId, FilterPermanent filter, Game game, Target target, - List goodList, List badList, List allList) { + List goodList, List badList, List allList) { // searching for most valuable/powerfull permanents goodList.clear(); badList.clear(); diff --git a/Mage.Sets/src/mage/cards/a/AngelOfSerenity.java b/Mage.Sets/src/mage/cards/a/AngelOfSerenity.java index 9c03356a167..57d4f570450 100644 --- a/Mage.Sets/src/mage/cards/a/AngelOfSerenity.java +++ b/Mage.Sets/src/mage/cards/a/AngelOfSerenity.java @@ -12,13 +12,12 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Zone; -import mage.filter.FilterPermanent; -import mage.filter.StaticFilters; -import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.AnotherPredicate; import mage.target.common.TargetCardInGraveyardOrBattlefield; import java.util.UUID; +import mage.filter.common.FilterCreatureCard; +import mage.filter.common.FilterCreaturePermanent; /** * @author LevelX2 @@ -27,10 +26,13 @@ public final class AngelOfSerenity extends CardImpl { private static final String rule = "you may exile up to three other target creatures " + "from the battlefield and/or creature cards from graveyards."; - private static final FilterPermanent filter = new FilterCreaturePermanent("other target creatures"); - + + private static final FilterCreatureCard filterCreatureCard = new FilterCreatureCard("creature card in a graveyard"); + + private static final FilterCreaturePermanent filterCreaturePermanent = new FilterCreaturePermanent("other target creature"); + static { - filter.add(AnotherPredicate.instance); + filterCreaturePermanent.add(AnotherPredicate.instance); } public AngelOfSerenity(UUID ownerId, CardSetInfo setInfo) { @@ -48,7 +50,7 @@ public final class AngelOfSerenity extends CardImpl { new ExileTargetForSourceEffect().setText(rule), true ); ability.addTarget(new TargetCardInGraveyardOrBattlefield( - 0, 3, StaticFilters.FILTER_CARD_CREATURE, filter + 0, 3, filterCreatureCard, filterCreaturePermanent )); this.addAbility(ability); diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java index 8d14e2f7031..0e1100e5b48 100644 --- a/Mage/src/main/java/mage/target/TargetImpl.java +++ b/Mage/src/main/java/mage/target/TargetImpl.java @@ -14,6 +14,7 @@ import mage.util.CardUtil; import mage.util.RandomUtil; import java.util.*; +import mage.game.permanent.Permanent; /** * @author BetaSteward_at_googlemail.com @@ -332,7 +333,16 @@ public abstract class TargetImpl implements Target { for (UUID targetId : targets.keySet()) { Card card = game.getCard(targetId); if (card != null) { - if (zoneChangeCounters.containsKey(targetId) && zoneChangeCounters.get(targetId) != card.getZoneChangeCounter(game)) { + // if a permanent, verify it is phased in, otherwise it is illegal + Permanent p = game.getPermanent(targetId); + if (p != null + && !p.isPhasedIn()) { + illegalTargets.add(targetId); + continue; // it's not legal so continue to have a look at other targeted objects + } + // check if the card moved to another zone + if (zoneChangeCounters.containsKey(targetId) + && zoneChangeCounters.get(targetId) != card.getZoneChangeCounter(game)) { illegalTargets.add(targetId); continue; // it's not legal so continue to have a look at other targeted objects } From b43bb44b27f81149993d5a85f677a6472ee96a1a Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 20 Sep 2021 19:53:02 -0400 Subject: [PATCH 151/231] [MIC] Implemented Curse of Clinging Webs --- .../src/mage/cards/c/CurseOfClingingWebs.java | 65 +++++++++++++++++++ .../src/mage/cards/c/CurseOfDeathsHold.java | 60 +++++------------ .../src/mage/sets/MidnightHuntCommander.java | 1 + .../EnchantPlayerControlsPredicate.java | 19 ++++++ 4 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/CurseOfClingingWebs.java create mode 100644 Mage/src/main/java/mage/filter/predicate/permanent/EnchantPlayerControlsPredicate.java diff --git a/Mage.Sets/src/mage/cards/c/CurseOfClingingWebs.java b/Mage.Sets/src/mage/cards/c/CurseOfClingingWebs.java new file mode 100644 index 00000000000..b1aff4820a1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CurseOfClingingWebs.java @@ -0,0 +1,65 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.EnchantPlayerControlsPredicate; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.game.permanent.token.SpiderToken; +import mage.target.TargetPlayer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CurseOfClingingWebs extends CardImpl { + + private static final FilterPermanent filter + = new FilterCreaturePermanent("a nontoken creature enchanted player controls"); + + static { + filter.add(TokenPredicate.FALSE); + filter.add(EnchantPlayerControlsPredicate.instance); + } + + public CurseOfClingingWebs(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); + + this.subtype.add(SubType.AURA); + this.subtype.add(SubType.CURSE); + + // Enchant player + TargetPlayer auraTarget = new TargetPlayer(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.addAbility(new EnchantAbility(auraTarget.getTargetName())); + + // Whenever a nontoken creature enchanted player controls dies, exile it and you create a 1/2 green Spider creature token with reach. + Ability ability = new DiesCreatureTriggeredAbility( + new ExileTargetEffect().setText("exile it"), + false, filter, true + ); + ability.addEffect(new CreateTokenEffect(new SpiderToken()).concatBy("and you")); + this.addAbility(ability); + } + + private CurseOfClingingWebs(final CurseOfClingingWebs card) { + super(card); + } + + @Override + public CurseOfClingingWebs copy() { + return new CurseOfClingingWebs(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/CurseOfDeathsHold.java b/Mage.Sets/src/mage/cards/c/CurseOfDeathsHold.java index 48b612e216a..d1fe0ed60d6 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfDeathsHold.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfDeathsHold.java @@ -1,28 +1,33 @@ - package mage.cards.c; -import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.continuous.BoostAllEffect; import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.filter.StaticFilters; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.players.Player; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.EnchantPlayerControlsPredicate; import mage.target.TargetPlayer; import java.util.UUID; /** - * * @author BetaSteward */ public final class CurseOfDeathsHold extends CardImpl { + private static final FilterCreaturePermanent filter + = new FilterCreaturePermanent("creatures enchanted player controls"); + + static { + filter.add(EnchantPlayerControlsPredicate.instance); + } + public CurseOfDeathsHold(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{B}{B}"); this.subtype.add(SubType.AURA, SubType.CURSE); @@ -34,7 +39,9 @@ public final class CurseOfDeathsHold extends CardImpl { this.addAbility(new EnchantAbility(auraTarget.getTargetName())); // Creatures enchanted player controls get -1/-1. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new CurseOfDeathsHoldEffect())); + this.addAbility(new SimpleStaticAbility(new BoostAllEffect( + -1, -1, Duration.WhileOnBattlefield, filter, false + ))); } private CurseOfDeathsHold(final CurseOfDeathsHold card) { @@ -46,36 +53,3 @@ public final class CurseOfDeathsHold extends CardImpl { return new CurseOfDeathsHold(this); } } - -class CurseOfDeathsHoldEffect extends ContinuousEffectImpl { - - public CurseOfDeathsHoldEffect() { - super(Duration.WhileOnBattlefield, Layer.PTChangingEffects_7, SubLayer.ModifyPT_7c, Outcome.UnboostCreature); - staticText = "Creatures enchanted player controls get -1/-1"; - } - - public CurseOfDeathsHoldEffect(final CurseOfDeathsHoldEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - Permanent enchantment = game.getPermanent(source.getSourceId()); - if (enchantment != null && enchantment.getAttachedTo() != null) { - Player player = game.getPlayer(enchantment.getAttachedTo()); - if (player != null) { - for (Permanent perm : game.getBattlefield().getAllActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, player.getId(), game)) { - perm.addPower(-1); - perm.addToughness(-1); - } - return true; - } - } - return false; - } - - @Override - public CurseOfDeathsHoldEffect copy() { - return new CurseOfDeathsHoldEffect(this); - } -} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index a6590be0891..be1a463bc9e 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -48,6 +48,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Commander's Sphere", 159, Rarity.COMMON, mage.cards.c.CommandersSphere.class)); cards.add(new SetCardInfo("Corpse Augur", 109, Rarity.UNCOMMON, mage.cards.c.CorpseAugur.class)); cards.add(new SetCardInfo("Crowded Crypt", 17, Rarity.RARE, mage.cards.c.CrowdedCrypt.class)); + cards.add(new SetCardInfo("Curse of Clinging Webs", 25, Rarity.RARE, mage.cards.c.CurseOfClingingWebs.class)); cards.add(new SetCardInfo("Custodi Soulbinders", 83, Rarity.RARE, mage.cards.c.CustodiSoulbinders.class)); cards.add(new SetCardInfo("Dark Salvation", 110, Rarity.RARE, mage.cards.d.DarkSalvation.class)); cards.add(new SetCardInfo("Darkwater Catacombs", 171, Rarity.RARE, mage.cards.d.DarkwaterCatacombs.class)); diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/EnchantPlayerControlsPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/EnchantPlayerControlsPredicate.java new file mode 100644 index 00000000000..d0086b1fbb3 --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/permanent/EnchantPlayerControlsPredicate.java @@ -0,0 +1,19 @@ +package mage.filter.predicate.permanent; + +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; + +/** + * @author TheElk801 + */ +public enum EnchantPlayerControlsPredicate implements ObjectSourcePlayerPredicate> { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + Permanent permanent = game.getPermanent(input.getSourceId()); + return permanent != null && input.getObject().isControlledBy(permanent.getAttachedTo()); + } +} From c54e9dd012d2f5575375e8d0f560a31329117143 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 20 Sep 2021 20:12:51 -0400 Subject: [PATCH 152/231] [MIC] Implemented Curse of Conformity --- .../src/mage/cards/c/CurseOfConformity.java | 104 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 105 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CurseOfConformity.java diff --git a/Mage.Sets/src/mage/cards/c/CurseOfConformity.java b/Mage.Sets/src/mage/cards/c/CurseOfConformity.java new file mode 100644 index 00000000000..6f2171b66ad --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CurseOfConformity.java @@ -0,0 +1,104 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.EnchantPlayerControlsPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPlayer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CurseOfConformity extends CardImpl { + + public CurseOfConformity(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{W}"); + + this.subtype.add(SubType.AURA); + this.subtype.add(SubType.CURSE); + + // Enchant player + TargetPlayer auraTarget = new TargetPlayer(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.addAbility(new EnchantAbility(auraTarget.getTargetName())); + + // Nonlegendary creatures enchanted player controls have base power and toughness 3/3 and lose all creature types. + this.addAbility(new SimpleStaticAbility(new CurseOfConformityEffect())); + } + + private CurseOfConformity(final CurseOfConformity card) { + super(card); + } + + @Override + public CurseOfConformity copy() { + return new CurseOfConformity(this); + } +} + +class CurseOfConformityEffect extends ContinuousEffectImpl { + + private static final FilterPermanent filter = new FilterCreaturePermanent(); + + static { + filter.add(Predicates.not(SuperType.LEGENDARY.getPredicate())); + filter.add(EnchantPlayerControlsPredicate.instance); + } + + CurseOfConformityEffect() { + super(Duration.WhileOnBattlefield, Outcome.LoseAbility); + staticText = "nonlegendary creatures enchanted player controls " + + "have base power and toughness 3/3 and lose all creature types"; + } + + private CurseOfConformityEffect(final CurseOfConformityEffect effect) { + super(effect); + } + + @Override + public CurseOfConformityEffect copy() { + return new CurseOfConformityEffect(this); + } + + @Override + public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { + for (Permanent permanent : game.getBattlefield().getActivePermanents( + filter, source.getControllerId(), source.getSourceId(), game + )) { + switch (layer) { + case TypeChangingEffects_4: + permanent.removeAllCreatureTypes(game); + break; + case PTChangingEffects_7: + if (sublayer == SubLayer.SetPT_7b) { + permanent.getPower().setValue(3); + permanent.getToughness().setValue(3); + } + } + } + return true; + } + + @Override + public boolean hasLayer(Layer layer) { + return layer == Layer.TypeChangingEffects_4 || layer == Layer.PTChangingEffects_7; + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index be1a463bc9e..c13eafb2ae3 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -49,6 +49,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Corpse Augur", 109, Rarity.UNCOMMON, mage.cards.c.CorpseAugur.class)); cards.add(new SetCardInfo("Crowded Crypt", 17, Rarity.RARE, mage.cards.c.CrowdedCrypt.class)); cards.add(new SetCardInfo("Curse of Clinging Webs", 25, Rarity.RARE, mage.cards.c.CurseOfClingingWebs.class)); + cards.add(new SetCardInfo("Curse of Conformity", 6, Rarity.RARE, mage.cards.c.CurseOfConformity.class)); cards.add(new SetCardInfo("Custodi Soulbinders", 83, Rarity.RARE, mage.cards.c.CustodiSoulbinders.class)); cards.add(new SetCardInfo("Dark Salvation", 110, Rarity.RARE, mage.cards.d.DarkSalvation.class)); cards.add(new SetCardInfo("Darkwater Catacombs", 171, Rarity.RARE, mage.cards.d.DarkwaterCatacombs.class)); From f3eef7eafd747041ddf833d1ed92b74c31def101 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 20 Sep 2021 20:34:19 -0400 Subject: [PATCH 153/231] [MIC] Implemented Curse of the Restless Dead --- .../mage/cards/c/CurseOfTheRestlessDead.java | 61 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 62 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CurseOfTheRestlessDead.java diff --git a/Mage.Sets/src/mage/cards/c/CurseOfTheRestlessDead.java b/Mage.Sets/src/mage/cards/c/CurseOfTheRestlessDead.java new file mode 100644 index 00000000000..09919c2c47f --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CurseOfTheRestlessDead.java @@ -0,0 +1,61 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAllTriggeredAbility; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterLandPermanent; +import mage.filter.predicate.permanent.EnchantPlayerControlsPredicate; +import mage.game.permanent.token.ZombieDecayedToken; +import mage.target.TargetPlayer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CurseOfTheRestlessDead extends CardImpl { + + private static final FilterPermanent filter = new FilterLandPermanent(); + + static { + filter.add(EnchantPlayerControlsPredicate.instance); + } + + public CurseOfTheRestlessDead(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}"); + + this.subtype.add(SubType.AURA); + this.subtype.add(SubType.CURSE); + + // Enchant player + TargetPlayer auraTarget = new TargetPlayer(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + Ability ability = new EnchantAbility(auraTarget.getTargetName()); + this.addAbility(ability); + + // Whenever a land enters the battlefield under enchanted player's control, you create a 2/2 black Zombie creature token with decayed. + this.addAbility(new EntersBattlefieldAllTriggeredAbility( + new CreateTokenEffect(new ZombieDecayedToken()), + filter, "Whenever a land enters the battlefield under enchanted player's control, " + + "you create a 2/2 black Zombie creature token with decayed." + )); + } + + private CurseOfTheRestlessDead(final CurseOfTheRestlessDead card) { + super(card); + } + + @Override + public CurseOfTheRestlessDead copy() { + return new CurseOfTheRestlessDead(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index c13eafb2ae3..80be93aca31 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -50,6 +50,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Crowded Crypt", 17, Rarity.RARE, mage.cards.c.CrowdedCrypt.class)); cards.add(new SetCardInfo("Curse of Clinging Webs", 25, Rarity.RARE, mage.cards.c.CurseOfClingingWebs.class)); cards.add(new SetCardInfo("Curse of Conformity", 6, Rarity.RARE, mage.cards.c.CurseOfConformity.class)); + cards.add(new SetCardInfo("Curse of the Restless Dead", 18, Rarity.RARE, mage.cards.c.CurseOfTheRestlessDead.class)); cards.add(new SetCardInfo("Custodi Soulbinders", 83, Rarity.RARE, mage.cards.c.CustodiSoulbinders.class)); cards.add(new SetCardInfo("Dark Salvation", 110, Rarity.RARE, mage.cards.d.DarkSalvation.class)); cards.add(new SetCardInfo("Darkwater Catacombs", 171, Rarity.RARE, mage.cards.d.DarkwaterCatacombs.class)); From 541511692ebfa120a6ba2ac7051dbe84960bda2e Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 20 Sep 2021 20:57:37 -0400 Subject: [PATCH 154/231] [MIC] Implemented Curse of Unbinding --- .../src/mage/cards/c/CurseOfClingingWebs.java | 4 +- .../src/mage/cards/c/CurseOfConformity.java | 3 +- .../src/mage/cards/c/CurseOfDeathsHold.java | 8 +- .../mage/cards/c/CurseOfTheRestlessDead.java | 4 +- .../src/mage/cards/c/CurseOfUnbinding.java | 92 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + .../BeginningOfUpkeepTriggeredAbility.java | 45 +++++---- .../java/mage/constants/TargetController.java | 18 ++-- .../EnchantPlayerControlsPredicate.java | 19 ---- 9 files changed, 133 insertions(+), 61 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/CurseOfUnbinding.java delete mode 100644 Mage/src/main/java/mage/filter/predicate/permanent/EnchantPlayerControlsPredicate.java diff --git a/Mage.Sets/src/mage/cards/c/CurseOfClingingWebs.java b/Mage.Sets/src/mage/cards/c/CurseOfClingingWebs.java index b1aff4820a1..42f3e572803 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfClingingWebs.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfClingingWebs.java @@ -11,9 +11,9 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.TargetController; import mage.filter.FilterPermanent; import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.permanent.EnchantPlayerControlsPredicate; import mage.filter.predicate.permanent.TokenPredicate; import mage.game.permanent.token.SpiderToken; import mage.target.TargetPlayer; @@ -30,7 +30,7 @@ public final class CurseOfClingingWebs extends CardImpl { static { filter.add(TokenPredicate.FALSE); - filter.add(EnchantPlayerControlsPredicate.instance); + filter.add(TargetController.ENCHANTED.getControllerPredicate()); } public CurseOfClingingWebs(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Sets/src/mage/cards/c/CurseOfConformity.java b/Mage.Sets/src/mage/cards/c/CurseOfConformity.java index 6f2171b66ad..41d8c9d9d19 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfConformity.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfConformity.java @@ -11,7 +11,6 @@ import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.Predicates; -import mage.filter.predicate.permanent.EnchantPlayerControlsPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPlayer; @@ -55,7 +54,7 @@ class CurseOfConformityEffect extends ContinuousEffectImpl { static { filter.add(Predicates.not(SuperType.LEGENDARY.getPredicate())); - filter.add(EnchantPlayerControlsPredicate.instance); + filter.add(TargetController.ENCHANTED.getControllerPredicate()); } CurseOfConformityEffect() { diff --git a/Mage.Sets/src/mage/cards/c/CurseOfDeathsHold.java b/Mage.Sets/src/mage/cards/c/CurseOfDeathsHold.java index d1fe0ed60d6..ab293524c59 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfDeathsHold.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfDeathsHold.java @@ -6,12 +6,8 @@ import mage.abilities.effects.common.continuous.BoostAllEffect; import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.SubType; +import mage.constants.*; import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.permanent.EnchantPlayerControlsPredicate; import mage.target.TargetPlayer; import java.util.UUID; @@ -25,7 +21,7 @@ public final class CurseOfDeathsHold extends CardImpl { = new FilterCreaturePermanent("creatures enchanted player controls"); static { - filter.add(EnchantPlayerControlsPredicate.instance); + filter.add(TargetController.ENCHANTED.getControllerPredicate()); } public CurseOfDeathsHold(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Sets/src/mage/cards/c/CurseOfTheRestlessDead.java b/Mage.Sets/src/mage/cards/c/CurseOfTheRestlessDead.java index 09919c2c47f..f1fc1cc57ef 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfTheRestlessDead.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfTheRestlessDead.java @@ -10,9 +10,9 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.TargetController; import mage.filter.FilterPermanent; import mage.filter.common.FilterLandPermanent; -import mage.filter.predicate.permanent.EnchantPlayerControlsPredicate; import mage.game.permanent.token.ZombieDecayedToken; import mage.target.TargetPlayer; @@ -26,7 +26,7 @@ public final class CurseOfTheRestlessDead extends CardImpl { private static final FilterPermanent filter = new FilterLandPermanent(); static { - filter.add(EnchantPlayerControlsPredicate.instance); + filter.add(TargetController.ENCHANTED.getControllerPredicate()); } public CurseOfTheRestlessDead(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Sets/src/mage/cards/c/CurseOfUnbinding.java b/Mage.Sets/src/mage/cards/c/CurseOfUnbinding.java new file mode 100644 index 00000000000..2cc4c2494ae --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CurseOfUnbinding.java @@ -0,0 +1,92 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.*; +import mage.constants.*; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetPlayer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CurseOfUnbinding extends CardImpl { + + public CurseOfUnbinding(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{6}{U}"); + + this.subtype.add(SubType.AURA); + this.subtype.add(SubType.CURSE); + + // Enchant player + TargetPlayer auraTarget = new TargetPlayer(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + Ability ability = new EnchantAbility(auraTarget.getTargetName()); + this.addAbility(ability); + + // At the beginning of enchanted player's upkeep, that player reveals cards from the top of their library until they reveal a creature card. Put that card onto the battlefield under your control. That player puts the rest of the revealed cards into their graveyard. + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new CurseOfConformityEffect(), TargetController.ENCHANTED, false + )); + } + + private CurseOfUnbinding(final CurseOfUnbinding card) { + super(card); + } + + @Override + public CurseOfUnbinding copy() { + return new CurseOfUnbinding(this); + } +} + +class CurseOfUnbindingEffect extends OneShotEffect { + + CurseOfUnbindingEffect() { + super(Outcome.Benefit); + staticText = "that player reveals cards from the top of their library " + + "until they reveal a creature card. Put that card onto the battlefield under your control. " + + "That player puts the rest of the revealed cards into their graveyard"; + } + + private CurseOfUnbindingEffect(final CurseOfUnbindingEffect effect) { + super(effect); + } + + @Override + public CurseOfUnbindingEffect copy() { + return new CurseOfUnbindingEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(game.getActivePlayerId()); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(); + Card toHand = null; + for (Card card : player.getLibrary().getCards(game)) { + cards.add(card); + if (card.isCreature(game)) { + toHand = card; + break; + } + } + player.revealCards(source, cards, game); + Player controller = game.getPlayer(source.getControllerId()); + if (toHand != null && controller != null) { + controller.moveCards(toHand, Zone.BATTLEFIELD, source, game); + } + cards.retainZone(Zone.LIBRARY, game); + player.moveCards(cards, Zone.GRAVEYARD, source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 80be93aca31..1a8172815c2 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -50,6 +50,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Crowded Crypt", 17, Rarity.RARE, mage.cards.c.CrowdedCrypt.class)); cards.add(new SetCardInfo("Curse of Clinging Webs", 25, Rarity.RARE, mage.cards.c.CurseOfClingingWebs.class)); cards.add(new SetCardInfo("Curse of Conformity", 6, Rarity.RARE, mage.cards.c.CurseOfConformity.class)); + cards.add(new SetCardInfo("Curse of Unbinding", 12, Rarity.RARE, mage.cards.c.CurseOfUnbinding.class)); cards.add(new SetCardInfo("Curse of the Restless Dead", 18, Rarity.RARE, mage.cards.c.CurseOfTheRestlessDead.class)); cards.add(new SetCardInfo("Custodi Soulbinders", 83, Rarity.RARE, mage.cards.c.CustodiSoulbinders.class)); cards.add(new SetCardInfo("Dark Salvation", 110, Rarity.RARE, mage.cards.d.DarkSalvation.class)); diff --git a/Mage/src/main/java/mage/abilities/common/BeginningOfUpkeepTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BeginningOfUpkeepTriggeredAbility.java index afd3a00a7ab..7f49922366e 100644 --- a/Mage/src/main/java/mage/abilities/common/BeginningOfUpkeepTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/BeginningOfUpkeepTriggeredAbility.java @@ -14,8 +14,8 @@ import mage.target.targetpointer.FixedTarget; */ public class BeginningOfUpkeepTriggeredAbility extends TriggeredAbilityImpl { - private TargetController targetController; - private boolean setTargetPointer; + private final TargetController targetController; + private final boolean setTargetPointer; protected String ruleTrigger; public BeginningOfUpkeepTriggeredAbility(Effect effect, TargetController targetController, boolean isOptional) { @@ -59,30 +59,20 @@ public class BeginningOfUpkeepTriggeredAbility extends TriggeredAbilityImpl { switch (targetController) { case YOU: boolean yours = event.getPlayerId().equals(this.controllerId); - if (yours && setTargetPointer) { - if (getTargets().isEmpty()) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } - } + if (yours && setTargetPointer && getTargets().isEmpty()) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); } return yours; case NOT_YOU: boolean notYours = !event.getPlayerId().equals(this.controllerId); - if (notYours && setTargetPointer) { - if (getTargets().isEmpty()) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } - } + if (notYours && setTargetPointer && getTargets().isEmpty()) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); } return notYours; case OPPONENT: if (game.getPlayer(this.controllerId).hasOpponent(event.getPlayerId(), game)) { if (setTargetPointer && getTargets().isEmpty()) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); } return true; } @@ -91,9 +81,7 @@ public class BeginningOfUpkeepTriggeredAbility extends TriggeredAbilityImpl { case ACTIVE: case EACH_PLAYER: if (setTargetPointer && getTargets().isEmpty()) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); } return true; case CONTROLLER_ATTACHED_TO: @@ -102,16 +90,23 @@ public class BeginningOfUpkeepTriggeredAbility extends TriggeredAbilityImpl { Permanent attachedTo = game.getPermanent(attachment.getAttachedTo()); if (attachedTo != null && attachedTo.isControlledBy(event.getPlayerId())) { if (setTargetPointer && getTargets().isEmpty()) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); } return true; } } break; + case ENCHANTED: + Permanent permanent = getSourcePermanentIfItStillExists(game); + if (permanent == null || !game.isActivePlayer(permanent.getAttachedTo())) { + break; + } + if (setTargetPointer && getTargets().isEmpty()) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + } + return true; default: - throw new UnsupportedOperationException("Value for targetController not supported: " + targetController.toString()); + throw new UnsupportedOperationException("Value for targetController not supported: " + targetController); } return false; } @@ -133,6 +128,8 @@ public class BeginningOfUpkeepTriggeredAbility extends TriggeredAbilityImpl { return "At the beginning of each upkeep, " + generateZoneString(); case CONTROLLER_ATTACHED_TO: return "At the beginning of the upkeep of enchanted creature's controller, " + generateZoneString(); + case ENCHANTED: + return "At the beginning of enchanted player's upkeep, " + generateZoneString(); } return ""; } diff --git a/Mage/src/main/java/mage/constants/TargetController.java b/Mage/src/main/java/mage/constants/TargetController.java index c827820bbdd..7bdfac067b0 100644 --- a/Mage/src/main/java/mage/constants/TargetController.java +++ b/Mage/src/main/java/mage/constants/TargetController.java @@ -1,12 +1,11 @@ package mage.constants; import mage.cards.Card; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Controllable; import mage.game.Game; +import mage.game.permanent.Permanent; import mage.players.Player; import java.util.UUID; @@ -26,6 +25,7 @@ public enum TargetController { CONTROLLER_ATTACHED_TO, NEXT, EACH_PLAYER, + ENCHANTED, SOURCE_TARGETS; private final OwnerPredicate ownerPredicate; @@ -50,7 +50,7 @@ public enum TargetController { return controllerPredicate; } - public static class OwnerPredicate implements ObjectPlayerPredicate> { + public static class OwnerPredicate implements ObjectSourcePlayerPredicate> { private final TargetController targetOwner; @@ -59,7 +59,7 @@ public enum TargetController { } @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Card card = input.getObject(); UUID playerId = input.getPlayerId(); if (card == null || playerId == null) { @@ -83,6 +83,9 @@ public enum TargetController { return true; } break; + case ENCHANTED: + Permanent permanent = game.getPermanent(input.getSourceId()); + return permanent != null && input.getObject().isOwnedBy(permanent.getAttachedTo()); case ANY: return true; } @@ -140,7 +143,7 @@ public enum TargetController { } } - public static class ControllerPredicate implements ObjectPlayerPredicate> { + public static class ControllerPredicate implements ObjectSourcePlayerPredicate> { private final TargetController controller; @@ -149,7 +152,7 @@ public enum TargetController { } @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Controllable object = input.getObject(); UUID playerId = input.getPlayerId(); @@ -180,6 +183,9 @@ public enum TargetController { return true; } break; + case ENCHANTED: + Permanent permanent = game.getPermanent(input.getSourceId()); + return permanent != null && input.getObject().isControlledBy(permanent.getAttachedTo()); case ANY: return true; } diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/EnchantPlayerControlsPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/EnchantPlayerControlsPredicate.java deleted file mode 100644 index d0086b1fbb3..00000000000 --- a/Mage/src/main/java/mage/filter/predicate/permanent/EnchantPlayerControlsPredicate.java +++ /dev/null @@ -1,19 +0,0 @@ -package mage.filter.predicate.permanent; - -import mage.filter.predicate.ObjectSourcePlayer; -import mage.filter.predicate.ObjectSourcePlayerPredicate; -import mage.game.Game; -import mage.game.permanent.Permanent; - -/** - * @author TheElk801 - */ -public enum EnchantPlayerControlsPredicate implements ObjectSourcePlayerPredicate> { - instance; - - @Override - public boolean apply(ObjectSourcePlayer input, Game game) { - Permanent permanent = game.getPermanent(input.getSourceId()); - return permanent != null && input.getObject().isControlledBy(permanent.getAttachedTo()); - } -} From bd22c4a3aec8dc8212e85f0ed9edcedaf41f3bd7 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 20 Sep 2021 21:06:41 -0400 Subject: [PATCH 155/231] [MIC] Implemented Curse of Obsession --- .../src/mage/cards/c/CurseOfObsession.java | 60 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + .../BeginningOfDrawTriggeredAbility.java | 19 ++++-- .../BeginningOfEndStepTriggeredAbility.java | 47 ++++++++------- 4 files changed, 100 insertions(+), 27 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/CurseOfObsession.java diff --git a/Mage.Sets/src/mage/cards/c/CurseOfObsession.java b/Mage.Sets/src/mage/cards/c/CurseOfObsession.java new file mode 100644 index 00000000000..38c9340b9d1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CurseOfObsession.java @@ -0,0 +1,60 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfDrawTriggeredAbility; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.DrawCardTargetEffect; +import mage.abilities.effects.common.discard.DiscardHandTargetEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.target.TargetPlayer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CurseOfObsession extends CardImpl { + + public CurseOfObsession(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{R}"); + + this.subtype.add(SubType.AURA); + this.subtype.add(SubType.CURSE); + + // Enchant player + TargetPlayer auraTarget = new TargetPlayer(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + Ability ability = new EnchantAbility(auraTarget.getTargetName()); + this.addAbility(ability); + + // At the beginning of enchanted player's draw step, that player draws two additional cards. + this.addAbility(new BeginningOfDrawTriggeredAbility( + new DrawCardTargetEffect(2) + .setText("that player draws two additional cards"), + TargetController.ENCHANTED, false + )); + + // At the beginning of enchanted player's end step, that player discards their hand. + this.addAbility(new BeginningOfEndStepTriggeredAbility( + new DiscardHandTargetEffect("that player"), + TargetController.ENCHANTED, false + )); + } + + private CurseOfObsession(final CurseOfObsession card) { + super(card); + } + + @Override + public CurseOfObsession copy() { + return new CurseOfObsession(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 1a8172815c2..d1a6e4b6e5c 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -50,6 +50,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Crowded Crypt", 17, Rarity.RARE, mage.cards.c.CrowdedCrypt.class)); cards.add(new SetCardInfo("Curse of Clinging Webs", 25, Rarity.RARE, mage.cards.c.CurseOfClingingWebs.class)); cards.add(new SetCardInfo("Curse of Conformity", 6, Rarity.RARE, mage.cards.c.CurseOfConformity.class)); + cards.add(new SetCardInfo("Curse of Obsession", 35, Rarity.RARE, mage.cards.c.CurseOfObsession.class)); cards.add(new SetCardInfo("Curse of Unbinding", 12, Rarity.RARE, mage.cards.c.CurseOfUnbinding.class)); cards.add(new SetCardInfo("Curse of the Restless Dead", 18, Rarity.RARE, mage.cards.c.CurseOfTheRestlessDead.class)); cards.add(new SetCardInfo("Custodi Soulbinders", 83, Rarity.RARE, mage.cards.c.CustodiSoulbinders.class)); diff --git a/Mage/src/main/java/mage/abilities/common/BeginningOfDrawTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BeginningOfDrawTriggeredAbility.java index 710d3c62e64..e5cd4e62640 100644 --- a/Mage/src/main/java/mage/abilities/common/BeginningOfDrawTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/BeginningOfDrawTriggeredAbility.java @@ -11,7 +11,7 @@ import mage.target.targetpointer.FixedTarget; public class BeginningOfDrawTriggeredAbility extends TriggeredAbilityImpl { - private TargetController targetController; + private final TargetController targetController; /** * The Ability sets if no target is defined the target pointer to the active @@ -50,10 +50,8 @@ public class BeginningOfDrawTriggeredAbility extends TriggeredAbilityImpl { switch (targetController) { case YOU: boolean yours = event.getPlayerId().equals(this.controllerId); - if (yours) { - if (getTargets().isEmpty()) { - this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); - } + if (yours && getTargets().isEmpty()) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); } return yours; case OPPONENT: @@ -84,6 +82,15 @@ public class BeginningOfDrawTriggeredAbility extends TriggeredAbilityImpl { } } break; + case ENCHANTED: + Permanent permanent = getSourcePermanentIfItStillExists(game); + if (permanent == null || !game.isActivePlayer(permanent.getAttachedTo())) { + break; + } + if (getTargets().isEmpty()) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + } + return true; case ANY: case ACTIVE: if (getTargets().isEmpty()) { @@ -108,6 +115,8 @@ public class BeginningOfDrawTriggeredAbility extends TriggeredAbilityImpl { return "At the beginning of each player's draw step, " + generateZoneString(); case CONTROLLER_ATTACHED_TO: return "At the beginning of the draw step of enchanted creature's controller, " + generateZoneString(); + case ENCHANTED: + return "At the beginning of enchanted player's draw step, " + generateZoneString(); } return ""; } diff --git a/Mage/src/main/java/mage/abilities/common/BeginningOfEndStepTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BeginningOfEndStepTriggeredAbility.java index fa0b18c43df..f63bd155bf2 100644 --- a/Mage/src/main/java/mage/abilities/common/BeginningOfEndStepTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/BeginningOfEndStepTriggeredAbility.java @@ -46,20 +46,14 @@ public class BeginningOfEndStepTriggeredAbility extends TriggeredAbilityImpl { switch (targetController) { case YOU: boolean yours = event.getPlayerId().equals(this.controllerId); - if (yours) { - if (getTargets().isEmpty()) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } - } + if (yours && getTargets().isEmpty()) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); } return yours; case OPPONENT: if (game.getPlayer(this.controllerId).hasOpponent(event.getPlayerId(), game)) { if (getTargets().isEmpty()) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); } return true; } @@ -68,24 +62,31 @@ public class BeginningOfEndStepTriggeredAbility extends TriggeredAbilityImpl { case EACH_PLAYER: case NEXT: if (getTargets().isEmpty()) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); } return true; case CONTROLLER_ATTACHED_TO: Permanent attachment = game.getPermanent(sourceId); - if (attachment != null && attachment.getAttachedTo() != null) { - Permanent attachedTo = game.getPermanent(attachment.getAttachedTo()); - if (attachedTo != null && attachedTo.isControlledBy(event.getPlayerId())) { - if (getTargets().isEmpty()) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } - } - return true; - } + if (attachment == null || attachment.getAttachedTo() == null) { + break; } + Permanent attachedTo = game.getPermanent(attachment.getAttachedTo()); + if (attachedTo == null || !attachedTo.isControlledBy(event.getPlayerId())) { + break; + } + if (getTargets().isEmpty()) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + } + return true; + case ENCHANTED: + Permanent permanent = getSourcePermanentIfItStillExists(game); + if (permanent == null || !game.isActivePlayer(permanent.getAttachedTo())) { + break; + } + if (getTargets().isEmpty()) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + } + return true; } return false; } @@ -113,6 +114,8 @@ public class BeginningOfEndStepTriggeredAbility extends TriggeredAbilityImpl { return "At the beginning of each player's end step, " + generateConditionString(); case CONTROLLER_ATTACHED_TO: return "At the beginning of the end step of enchanted permanent's controller, " + generateConditionString(); + case ENCHANTED: + return "At the beginning of enchanted player's draw step, " + generateConditionString(); } return ""; } From c6feab68da78d3ea24edccda6d6a53932a5e4d96 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 20 Sep 2021 21:22:17 -0400 Subject: [PATCH 156/231] refactored all instances of BeginningOfUpkeepAttachedTriggeredAbility --- Mage.Sets/src/mage/cards/c/CruelReality.java | 5 +- .../src/mage/cards/c/CurseOfOblivion.java | 11 ++-- .../src/mage/cards/c/CurseOfSurveillance.java | 31 +++++---- .../mage/cards/c/CurseOfTheBloodyTome.java | 9 ++- .../mage/cards/c/CurseOfThePiercedHeart.java | 7 ++- Mage.Sets/src/mage/cards/c/CurseOfThirst.java | 8 ++- .../src/mage/cards/i/InfectiousCurse.java | 7 ++- .../src/mage/cards/t/TormentOfScarabs.java | 7 ++- ...nningOfUpkeepAttachedTriggeredAbility.java | 63 ------------------- 9 files changed, 47 insertions(+), 101 deletions(-) delete mode 100644 Mage/src/main/java/mage/abilities/common/BeginningOfUpkeepAttachedTriggeredAbility.java diff --git a/Mage.Sets/src/mage/cards/c/CruelReality.java b/Mage.Sets/src/mage/cards/c/CruelReality.java index 7f354276433..e3249e40188 100644 --- a/Mage.Sets/src/mage/cards/c/CruelReality.java +++ b/Mage.Sets/src/mage/cards/c/CruelReality.java @@ -1,7 +1,7 @@ package mage.cards.c; import mage.abilities.Ability; -import mage.abilities.common.BeginningOfUpkeepAttachedTriggeredAbility; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.AttachEffect; import mage.abilities.keyword.EnchantAbility; @@ -10,6 +10,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.TargetController; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledPermanent; import mage.filter.predicate.Predicates; @@ -38,7 +39,7 @@ public final class CruelReality extends CardImpl { this.addAbility(new EnchantAbility(auraTarget.getTargetName())); //At the beginning of enchanted player's upkeep, that player sacrifices a creature or planeswalker. If the player can't, they lose 5 life. - this.addAbility(new BeginningOfUpkeepAttachedTriggeredAbility(new CruelRealityEffect())); + this.addAbility(new BeginningOfUpkeepTriggeredAbility(new CruelRealityEffect(), TargetController.ENCHANTED, false)); } private CruelReality(final CruelReality card) { diff --git a/Mage.Sets/src/mage/cards/c/CurseOfOblivion.java b/Mage.Sets/src/mage/cards/c/CurseOfOblivion.java index 62eec2c2e5c..fad2c782192 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfOblivion.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfOblivion.java @@ -1,16 +1,13 @@ package mage.cards.c; import mage.abilities.Ability; -import mage.abilities.common.BeginningOfUpkeepAttachedTriggeredAbility; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.ExileFromZoneTargetEffect; import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.StaticFilters; import mage.target.TargetPlayer; @@ -33,9 +30,9 @@ public final class CurseOfOblivion extends CardImpl { this.addAbility(ability); // At the beginning of enchanted player's upkeep, that player exiles two cards from their graveyard. - this.addAbility(new BeginningOfUpkeepAttachedTriggeredAbility(new ExileFromZoneTargetEffect( + this.addAbility(new BeginningOfUpkeepTriggeredAbility(new ExileFromZoneTargetEffect( Zone.GRAVEYARD, StaticFilters.FILTER_CARD_CARDS, 2, false - ).setText("that player exiles two cards from their graveyard"))); + ).setText("that player exiles two cards from their graveyard"), TargetController.ENCHANTED, false)); } private CurseOfOblivion(final CurseOfOblivion card) { diff --git a/Mage.Sets/src/mage/cards/c/CurseOfSurveillance.java b/Mage.Sets/src/mage/cards/c/CurseOfSurveillance.java index f95f7fa7ce1..0d5f1620c81 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfSurveillance.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfSurveillance.java @@ -1,19 +1,18 @@ package mage.cards.c; -import java.util.UUID; - -import mage.abilities.common.BeginningOfUpkeepAttachedTriggeredAbility; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; -import mage.abilities.effects.common.DrawCardTargetEffect; -import mage.constants.SubType; -import mage.abilities.Ability; import mage.abilities.effects.common.AttachEffect; -import mage.constants.Outcome; +import mage.abilities.effects.common.DrawCardTargetEffect; import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.TargetController; import mage.filter.FilterPlayer; import mage.filter.predicate.Predicates; import mage.filter.predicate.other.PlayerIdPredicate; @@ -22,8 +21,9 @@ import mage.game.permanent.Permanent; import mage.target.TargetPlayer; import mage.target.targetadjustment.TargetAdjuster; +import java.util.UUID; + /** - * * @author weirddan455 */ public final class CurseOfSurveillance extends CardImpl { @@ -42,11 +42,11 @@ public final class CurseOfSurveillance extends CardImpl { this.addAbility(ability); // At the beginning of enchanted player's upkeep, any number of target players other than that player each draw cards equal to the number of Curses attached to that player. - ability = new BeginningOfUpkeepAttachedTriggeredAbility( + ability = new BeginningOfUpkeepTriggeredAbility( new DrawCardTargetEffect(CurseOfSurveillanceValue.instance).setText( "any number of target players other than that player each draw cards equal to the number of Curses attached to that player" ), - false, false + TargetController.ENCHANTED, false ); ability.setTargetAdjuster(CurseOfSurveillanceTargetAdjuster.instance); ability.addTarget(new TargetPlayer(0, Integer.MAX_VALUE, false)); @@ -68,13 +68,12 @@ enum CurseOfSurveillanceValue implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - UUID enchantedPlayerId = (UUID) effect.getValue("enchantedPlayer"); int curses = 0; - if (enchantedPlayerId != null) { - for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { - if (permanent != null && permanent.hasSubtype(SubType.CURSE, game) && permanent.isAttachedTo(enchantedPlayerId)) { - curses++; - } + for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { + if (permanent != null + && permanent.hasSubtype(SubType.CURSE, game) + && permanent.isAttachedTo(game.getActivePlayerId())) { + curses++; } } return curses; diff --git a/Mage.Sets/src/mage/cards/c/CurseOfTheBloodyTome.java b/Mage.Sets/src/mage/cards/c/CurseOfTheBloodyTome.java index 9803b34953e..99b5e5b31ce 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfTheBloodyTome.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfTheBloodyTome.java @@ -1,7 +1,7 @@ package mage.cards.c; import mage.abilities.Ability; -import mage.abilities.common.BeginningOfUpkeepAttachedTriggeredAbility; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.PutLibraryIntoGraveTargetEffect; import mage.abilities.keyword.EnchantAbility; @@ -10,6 +10,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.TargetController; import mage.target.TargetPlayer; import java.util.UUID; @@ -31,8 +32,10 @@ public final class CurseOfTheBloodyTome extends CardImpl { this.addAbility(ability); // At the beginning of enchanted player's upkeep, that player puts the top two cards of their library into their graveyard. - this.addAbility(new BeginningOfUpkeepAttachedTriggeredAbility( - new PutLibraryIntoGraveTargetEffect(2).setText("that player mills two cards") + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new PutLibraryIntoGraveTargetEffect(2) + .setText("that player mills two cards"), + TargetController.ENCHANTED, false )); } diff --git a/Mage.Sets/src/mage/cards/c/CurseOfThePiercedHeart.java b/Mage.Sets/src/mage/cards/c/CurseOfThePiercedHeart.java index d12b08f91ae..1841f5cac30 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfThePiercedHeart.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfThePiercedHeart.java @@ -1,7 +1,7 @@ package mage.cards.c; import mage.abilities.Ability; -import mage.abilities.common.BeginningOfUpkeepAttachedTriggeredAbility; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.AttachEffect; import mage.abilities.keyword.EnchantAbility; @@ -10,6 +10,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.TargetController; import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.filter.common.FilterPlaneswalkerPermanent; @@ -39,7 +40,9 @@ public final class CurseOfThePiercedHeart extends CardImpl { this.addAbility(ability); // At the beginning of enchanted player's upkeep, Curse of the Pierced Heart deals 1 damage to that player. - this.addAbility(new BeginningOfUpkeepAttachedTriggeredAbility(new CurseOfThePiercedHeartEffect())); + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new CurseOfThePiercedHeartEffect(), TargetController.ENCHANTED, false + )); } private CurseOfThePiercedHeart(final CurseOfThePiercedHeart card) { diff --git a/Mage.Sets/src/mage/cards/c/CurseOfThirst.java b/Mage.Sets/src/mage/cards/c/CurseOfThirst.java index bffb0a1ea66..3ee3cc73375 100644 --- a/Mage.Sets/src/mage/cards/c/CurseOfThirst.java +++ b/Mage.Sets/src/mage/cards/c/CurseOfThirst.java @@ -1,7 +1,7 @@ package mage.cards.c; import mage.abilities.Ability; -import mage.abilities.common.BeginningOfUpkeepAttachedTriggeredAbility; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.abilities.effects.common.AttachEffect; @@ -12,6 +12,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.TargetController; import mage.game.Game; import mage.players.Player; import mage.target.TargetPlayer; @@ -35,9 +36,10 @@ public final class CurseOfThirst extends CardImpl { this.addAbility(new EnchantAbility(auraTarget.getTargetName())); // At the beginning of enchanted player's upkeep, Curse of Thirst deals damage to that player equal to the number of Curses attached to them. - this.addAbility(new BeginningOfUpkeepAttachedTriggeredAbility( + this.addAbility(new BeginningOfUpkeepTriggeredAbility( new DamageTargetEffect(CursesAttachedCount.instance) - .setText("{this} deals damage to that player equal to the number of Curses attached to them") + .setText("{this} deals damage to that player equal to the number of Curses attached to them"), + TargetController.ENCHANTED, false )); } diff --git a/Mage.Sets/src/mage/cards/i/InfectiousCurse.java b/Mage.Sets/src/mage/cards/i/InfectiousCurse.java index cc822992d5b..4d5aec75422 100644 --- a/Mage.Sets/src/mage/cards/i/InfectiousCurse.java +++ b/Mage.Sets/src/mage/cards/i/InfectiousCurse.java @@ -2,7 +2,7 @@ package mage.cards.i; import mage.abilities.Ability; import mage.abilities.SpellAbility; -import mage.abilities.common.BeginningOfUpkeepAttachedTriggeredAbility; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.GainLifeEffect; @@ -45,8 +45,9 @@ public final class InfectiousCurse extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new InfectiousCurseCostReductionEffect())); // At the beginning of enchanted player's upkeep, that player loses 1 life and you gain 1 life. - Ability ability = new BeginningOfUpkeepAttachedTriggeredAbility( - new LoseLifeTargetEffect(1).setText("that player loses 1 life") + Ability ability = new BeginningOfUpkeepTriggeredAbility( + new LoseLifeTargetEffect(1).setText("that player loses 1 life"), + TargetController.ENCHANTED, false ); ability.addEffect(new GainLifeEffect(1).concatBy("and")); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/t/TormentOfScarabs.java b/Mage.Sets/src/mage/cards/t/TormentOfScarabs.java index ac2d734da37..78f81a776b3 100644 --- a/Mage.Sets/src/mage/cards/t/TormentOfScarabs.java +++ b/Mage.Sets/src/mage/cards/t/TormentOfScarabs.java @@ -1,7 +1,7 @@ package mage.cards.t; import mage.abilities.Ability; -import mage.abilities.common.BeginningOfUpkeepAttachedTriggeredAbility; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.AttachEffect; import mage.abilities.keyword.EnchantAbility; @@ -10,6 +10,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.TargetController; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; @@ -38,7 +39,9 @@ public final class TormentOfScarabs extends CardImpl { this.addAbility(ability); // At the beginning of enchanted player's upkeep, that player loses 3 life unless they sacrifice a nonland permanent or discards a card. - this.addAbility(new BeginningOfUpkeepAttachedTriggeredAbility(new TormentOfScarabsEffect())); + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new TormentOfScarabsEffect(), TargetController.ENCHANTED, false + )); } private TormentOfScarabs(final TormentOfScarabs card) { diff --git a/Mage/src/main/java/mage/abilities/common/BeginningOfUpkeepAttachedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BeginningOfUpkeepAttachedTriggeredAbility.java deleted file mode 100644 index ed24f7e994a..00000000000 --- a/Mage/src/main/java/mage/abilities/common/BeginningOfUpkeepAttachedTriggeredAbility.java +++ /dev/null @@ -1,63 +0,0 @@ -package mage.abilities.common; - -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.target.targetpointer.FixedTarget; - -/** - * @author TheElk801 - */ -public class BeginningOfUpkeepAttachedTriggeredAbility extends TriggeredAbilityImpl { - - private final boolean setTargetPointer; - - public BeginningOfUpkeepAttachedTriggeredAbility(Effect effect) { - this(effect, false); - } - - public BeginningOfUpkeepAttachedTriggeredAbility(Effect effect, boolean optional) { - this(effect, optional, true); - } - - public BeginningOfUpkeepAttachedTriggeredAbility(Effect effect, boolean optional, boolean setTargetPointer) { - super(Zone.BATTLEFIELD, effect, optional); - this.setTargetPointer = setTargetPointer; - } - - private BeginningOfUpkeepAttachedTriggeredAbility(final BeginningOfUpkeepAttachedTriggeredAbility ability) { - super(ability); - this.setTargetPointer = ability.setTargetPointer; - } - - @Override - public BeginningOfUpkeepAttachedTriggeredAbility copy() { - return new BeginningOfUpkeepAttachedTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.UPKEEP_STEP_PRE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Permanent enchantment = getSourcePermanentOrLKI(game); - if (enchantment == null || !game.isActivePlayer(enchantment.getAttachedTo())) { - return false; - } - if (setTargetPointer) { - this.getEffects().setTargetPointer(new FixedTarget(enchantment.getAttachedTo())); - } - this.getEffects().setValue("enchantedPlayer", enchantment.getAttachedTo()); - return true; - } - - @Override - public String getTriggerPhrase() { - return "At the beginning of enchanted player's upkeep, " ; - } -} From 658ae06e6fd0ab6c88f6d1d135ae7a042c36a7ff Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 20 Sep 2021 23:38:31 -0400 Subject: [PATCH 157/231] removed ObjectPlayer and ObjectPlayerPredicate --- .../mage/cards/a/AkiriFearlessVoyager.java | 8 ++-- Mage.Sets/src/mage/cards/a/AuraGraft.java | 12 ++--- Mage.Sets/src/mage/cards/c/CeruleanDrake.java | 8 ++-- Mage.Sets/src/mage/cards/c/ConduitOfRuin.java | 8 ++-- Mage.Sets/src/mage/cards/d/DawnCharm.java | 8 ++-- Mage.Sets/src/mage/cards/d/DevoutHarpist.java | 8 ++-- Mage.Sets/src/mage/cards/f/FalseOrders.java | 8 ++-- Mage.Sets/src/mage/cards/f/FlameSweep.java | 8 ++-- .../src/mage/cards/f/FrostpyreArcanist.java | 8 ++-- .../src/mage/cards/h/HinderingLight.java | 8 ++-- Mage.Sets/src/mage/cards/m/MaceWindu.java | 3 +- Mage.Sets/src/mage/cards/m/MirrorSheen.java | 8 ++-- Mage.Sets/src/mage/cards/m/MuckDrubb.java | 8 ++-- .../src/mage/cards/o/OldManOfTheSea.java | 8 ++-- .../mage/cards/p/PersonalEnergyShield.java | 8 ++-- .../src/mage/cards/p/PsychicRebuttal.java | 8 ++-- Mage.Sets/src/mage/cards/p/Pyramids.java | 8 ++-- Mage.Sets/src/mage/cards/r/Radiate.java | 12 ++--- .../src/mage/cards/r/RemoveEnchantments.java | 8 ++-- Mage.Sets/src/mage/cards/r/Ricochet.java | 8 ++-- .../src/mage/cards/s/SageOfTheBeyond.java | 4 +- Mage.Sets/src/mage/cards/s/SavaenElves.java | 8 ++-- .../src/mage/cards/s/ShellOfTheLastKappa.java | 8 ++-- Mage.Sets/src/mage/cards/s/SoulShatter.java | 8 ++-- .../src/mage/cards/t/TelimTorsEdict.java | 8 ++-- .../src/mage/cards/v/VedalkenShackles.java | 8 ++-- Mage.Sets/src/mage/cards/v/VineGecko.java | 8 ++-- .../src/main/java/mage/filter/FilterCard.java | 17 ++++--- .../java/mage/filter/FilterPermanent.java | 10 ++-- .../main/java/mage/filter/FilterPlayer.java | 9 ++-- .../java/mage/filter/FilterStackObject.java | 10 ++-- .../common/FilterPermanentOrPlayer.java | 5 +- .../filter/common/FilterSpellOrPermanent.java | 47 +++---------------- .../mage/filter/predicate/ObjectPlayer.java | 28 ----------- .../predicate/ObjectPlayerPredicate.java | 11 ----- .../filter/predicate/ObjectSourcePlayer.java | 19 ++++++-- .../ObjectSourcePlayerPredicate.java | 6 +-- .../card/CardOnTopOfLibraryPredicate.java | 8 ++-- .../DefendingPlayerOwnsCardPredicate.java | 8 ++-- .../other/DamagedPlayerThisTurnPredicate.java | 8 ++-- ...ttachedToControlledPermanentPredicate.java | 8 ++-- 41 files changed, 172 insertions(+), 237 deletions(-) delete mode 100644 Mage/src/main/java/mage/filter/predicate/ObjectPlayer.java delete mode 100644 Mage/src/main/java/mage/filter/predicate/ObjectPlayerPredicate.java diff --git a/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java b/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java index cf08f48cc2c..26d165568c4 100644 --- a/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java +++ b/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java @@ -14,8 +14,8 @@ import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.common.FilterEquipmentPermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.events.DefenderAttackedEvent; import mage.game.events.GameEvent; @@ -103,11 +103,11 @@ class AkiriFearlessVoyagerTriggeredAbility extends TriggeredAbilityImpl { class AkiriFearlessVoyagerEffect extends OneShotEffect { - private static enum AkiriFearlessVoyagerPredicate implements ObjectPlayerPredicate> { + private static enum AkiriFearlessVoyagerPredicate implements ObjectSourcePlayerPredicate> { instance; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { return game.getPermanent(input.getObject().getAttachedTo()) != null && game.getControllerId(input.getObject().getAttachedTo()).equals(input.getPlayerId()); } diff --git a/Mage.Sets/src/mage/cards/a/AuraGraft.java b/Mage.Sets/src/mage/cards/a/AuraGraft.java index 26fdd13056d..519b2d25dc3 100644 --- a/Mage.Sets/src/mage/cards/a/AuraGraft.java +++ b/Mage.Sets/src/mage/cards/a/AuraGraft.java @@ -14,8 +14,8 @@ import mage.constants.Outcome; import mage.constants.SubType; import mage.filter.Filter; import mage.filter.FilterPermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.mageobject.AnotherPredicate; import mage.game.Game; import mage.game.permanent.Permanent; @@ -54,19 +54,19 @@ public final class AuraGraft extends CardImpl { } } -class AttachedToPermanentPredicate implements ObjectPlayerPredicate> { +class AttachedToPermanentPredicate implements ObjectSourcePlayerPredicate> { public AttachedToPermanentPredicate() { super(); } - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent attached = input.getObject(); return attached != null && game.getPermanent(attached.getAttachedTo()) != null; } } -class PermanentCanBeAttachedToPredicate implements ObjectPlayerPredicate> { +class PermanentCanBeAttachedToPredicate implements ObjectSourcePlayerPredicate> { protected Permanent aura; @@ -76,7 +76,7 @@ class PermanentCanBeAttachedToPredicate implements ObjectPlayerPredicate input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent potentialAttachment = input.getObject(); for (TargetAddress addr : TargetAddress.walk(aura)) { Target target = addr.getTarget(aura); diff --git a/Mage.Sets/src/mage/cards/c/CeruleanDrake.java b/Mage.Sets/src/mage/cards/c/CeruleanDrake.java index 00cbafc41fa..f4c831dce91 100644 --- a/Mage.Sets/src/mage/cards/c/CeruleanDrake.java +++ b/Mage.Sets/src/mage/cards/c/CeruleanDrake.java @@ -13,8 +13,8 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.stack.StackObject; import mage.target.TargetSpell; @@ -61,11 +61,11 @@ public final class CeruleanDrake extends CardImpl { } } -enum CeruleanDrakePredicate implements ObjectPlayerPredicate> { +enum CeruleanDrakePredicate implements ObjectSourcePlayerPredicate> { instance; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { if (input.getPlayerId() == null) { return false; } diff --git a/Mage.Sets/src/mage/cards/c/ConduitOfRuin.java b/Mage.Sets/src/mage/cards/c/ConduitOfRuin.java index b08ee2cff9b..dfd4a39e22a 100644 --- a/Mage.Sets/src/mage/cards/c/ConduitOfRuin.java +++ b/Mage.Sets/src/mage/cards/c/ConduitOfRuin.java @@ -19,8 +19,8 @@ import mage.constants.ComparisonType; import mage.constants.WatcherScope; import mage.constants.Zone; import mage.filter.common.FilterCreatureCard; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.mageobject.ColorlessPredicate; import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.game.Controllable; @@ -100,10 +100,10 @@ class ConduitOfRuinWatcher extends Watcher { } } -class FirstCastCreatureSpellPredicate implements ObjectPlayerPredicate> { +class FirstCastCreatureSpellPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { if (input.getObject() instanceof Card && ((Card) input.getObject()).isCreature(game)) { ConduitOfRuinWatcher watcher = game.getState().getWatcher(ConduitOfRuinWatcher.class); diff --git a/Mage.Sets/src/mage/cards/d/DawnCharm.java b/Mage.Sets/src/mage/cards/d/DawnCharm.java index 25e194a01b2..db5b065bd31 100644 --- a/Mage.Sets/src/mage/cards/d/DawnCharm.java +++ b/Mage.Sets/src/mage/cards/d/DawnCharm.java @@ -11,8 +11,8 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.stack.StackObject; import mage.target.Target; @@ -59,10 +59,10 @@ public final class DawnCharm extends CardImpl { } } -class DawnCharmPredicate implements ObjectPlayerPredicate> { +class DawnCharmPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { UUID controllerId = input.getPlayerId(); if (controllerId == null) { return false; diff --git a/Mage.Sets/src/mage/cards/d/DevoutHarpist.java b/Mage.Sets/src/mage/cards/d/DevoutHarpist.java index 20a3a427220..1dbd8aebcc3 100644 --- a/Mage.Sets/src/mage/cards/d/DevoutHarpist.java +++ b/Mage.Sets/src/mage/cards/d/DevoutHarpist.java @@ -13,8 +13,8 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Zone; import mage.filter.common.FilterEnchantmentPermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; @@ -56,9 +56,9 @@ public final class DevoutHarpist extends CardImpl { } -class DevoutHarpistPredicate implements ObjectPlayerPredicate> { +class DevoutHarpistPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent attachment = input.getObject(); if (attachment != null) { Permanent permanent = game.getPermanent(attachment.getAttachedTo()); diff --git a/Mage.Sets/src/mage/cards/f/FalseOrders.java b/Mage.Sets/src/mage/cards/f/FalseOrders.java index ba4101ff11d..f788ead8b07 100644 --- a/Mage.Sets/src/mage/cards/f/FalseOrders.java +++ b/Mage.Sets/src/mage/cards/f/FalseOrders.java @@ -13,8 +13,8 @@ import mage.constants.Outcome; import mage.constants.PhaseStep; import mage.filter.FilterPermanent; import mage.filter.common.FilterAttackingCreature; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.permanent.PermanentInListPredicate; import mage.game.Controllable; import mage.game.Game; @@ -64,11 +64,11 @@ public final class FalseOrders extends CardImpl { } -enum FalseOrdersDefendingPlayerControlsPredicate implements ObjectPlayerPredicate> { +enum FalseOrdersDefendingPlayerControlsPredicate implements ObjectSourcePlayerPredicate> { instance; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { return game.getCombat().getPlayerDefenders(game).contains(input.getObject().getControllerId()); } } diff --git a/Mage.Sets/src/mage/cards/f/FlameSweep.java b/Mage.Sets/src/mage/cards/f/FlameSweep.java index 6b27948005b..e40d1c9762e 100644 --- a/Mage.Sets/src/mage/cards/f/FlameSweep.java +++ b/Mage.Sets/src/mage/cards/f/FlameSweep.java @@ -7,8 +7,8 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterPermanent; import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; @@ -43,11 +43,11 @@ public final class FlameSweep extends CardImpl { } } -enum FlameSweepPredicate implements ObjectPlayerPredicate> { +enum FlameSweepPredicate implements ObjectSourcePlayerPredicate> { instance; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent object = input.getObject(); UUID playerId = input.getPlayerId(); return !(object.isControlledBy(playerId) diff --git a/Mage.Sets/src/mage/cards/f/FrostpyreArcanist.java b/Mage.Sets/src/mage/cards/f/FrostpyreArcanist.java index 7d636885db1..f17f0b31a17 100644 --- a/Mage.Sets/src/mage/cards/f/FrostpyreArcanist.java +++ b/Mage.Sets/src/mage/cards/f/FrostpyreArcanist.java @@ -20,8 +20,8 @@ import mage.filter.FilterCard; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterInstantOrSorceryCard; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.players.Player; @@ -79,11 +79,11 @@ public final class FrostpyreArcanist extends CardImpl { } } -enum FrostpyreArcanistPredicate implements ObjectPlayerPredicate> { +enum FrostpyreArcanistPredicate implements ObjectSourcePlayerPredicate> { instance; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Player player = game.getPlayer(input.getPlayerId()); if (player == null || player.getGraveyard().isEmpty()) { return false; diff --git a/Mage.Sets/src/mage/cards/h/HinderingLight.java b/Mage.Sets/src/mage/cards/h/HinderingLight.java index 3fda3c40dbe..21838f6aa40 100644 --- a/Mage.Sets/src/mage/cards/h/HinderingLight.java +++ b/Mage.Sets/src/mage/cards/h/HinderingLight.java @@ -9,8 +9,8 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.stack.StackObject; @@ -49,10 +49,10 @@ public final class HinderingLight extends CardImpl { } } -class HinderingLightPredicate implements ObjectPlayerPredicate> { +class HinderingLightPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { UUID controllerId = input.getPlayerId(); if (controllerId == null) { return false; diff --git a/Mage.Sets/src/mage/cards/m/MaceWindu.java b/Mage.Sets/src/mage/cards/m/MaceWindu.java index 94b36eb6da7..6749bb9c43b 100644 --- a/Mage.Sets/src/mage/cards/m/MaceWindu.java +++ b/Mage.Sets/src/mage/cards/m/MaceWindu.java @@ -26,7 +26,8 @@ public final class MaceWindu extends CardImpl { private static final FilterSpellOrPermanent filter = new FilterSpellOrPermanent("spell or creature you don't control"); static { - filter.add(TargetController.NOT_YOU.getControllerPredicate()); + filter.getPermanentFilter().add(TargetController.NOT_YOU.getControllerPredicate()); + filter.getSpellFilter().add(TargetController.NOT_YOU.getControllerPredicate()); } public MaceWindu(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Sets/src/mage/cards/m/MirrorSheen.java b/Mage.Sets/src/mage/cards/m/MirrorSheen.java index 4f59ebf5c6d..e658e9a7fe0 100644 --- a/Mage.Sets/src/mage/cards/m/MirrorSheen.java +++ b/Mage.Sets/src/mage/cards/m/MirrorSheen.java @@ -11,8 +11,8 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Zone; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.game.stack.StackObject; @@ -53,10 +53,10 @@ public final class MirrorSheen extends CardImpl { } } -class TargetYouPredicate implements ObjectPlayerPredicate> { +class TargetYouPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { UUID controllerId = input.getPlayerId(); if (controllerId == null) { return false; diff --git a/Mage.Sets/src/mage/cards/m/MuckDrubb.java b/Mage.Sets/src/mage/cards/m/MuckDrubb.java index adecc34727c..c892e9bf5e0 100644 --- a/Mage.Sets/src/mage/cards/m/MuckDrubb.java +++ b/Mage.Sets/src/mage/cards/m/MuckDrubb.java @@ -16,8 +16,8 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.filter.FilterSpell; import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.mageobject.TargetsPermanentPredicate; import mage.game.Game; import mage.game.stack.Spell; @@ -69,10 +69,10 @@ public final class MuckDrubb extends CardImpl { } } -class SpellWithOnlySingleTargetPredicate implements ObjectPlayerPredicate> { +class SpellWithOnlySingleTargetPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Spell spell = input.getObject(); if (spell == null) { return false; diff --git a/Mage.Sets/src/mage/cards/o/OldManOfTheSea.java b/Mage.Sets/src/mage/cards/o/OldManOfTheSea.java index 7a64a2a896d..6e5ee08db4f 100644 --- a/Mage.Sets/src/mage/cards/o/OldManOfTheSea.java +++ b/Mage.Sets/src/mage/cards/o/OldManOfTheSea.java @@ -21,8 +21,8 @@ import mage.constants.SubType; import mage.constants.Duration; import mage.constants.Zone; import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; @@ -148,7 +148,7 @@ class SourcePowerGreaterEqualTargetCondition implements Condition { } } -class PowerLowerEqualSourcePredicate implements ObjectPlayerPredicate> { +class PowerLowerEqualSourcePredicate implements ObjectSourcePlayerPredicate> { UUID sourceId; @@ -157,7 +157,7 @@ class PowerLowerEqualSourcePredicate implements ObjectPlayerPredicate input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent sourcePermanent = game.getPermanent(sourceId); Permanent permanent = input.getObject(); if (permanent != null && sourcePermanent != null) { diff --git a/Mage.Sets/src/mage/cards/p/PersonalEnergyShield.java b/Mage.Sets/src/mage/cards/p/PersonalEnergyShield.java index 89c9313d7a7..a7294fdf46d 100644 --- a/Mage.Sets/src/mage/cards/p/PersonalEnergyShield.java +++ b/Mage.Sets/src/mage/cards/p/PersonalEnergyShield.java @@ -8,8 +8,8 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.stack.StackObject; @@ -46,10 +46,10 @@ public final class PersonalEnergyShield extends CardImpl { } } -class PersonalEnergyFieldPredicate implements ObjectPlayerPredicate> { +class PersonalEnergyFieldPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { UUID controllerId = input.getPlayerId(); if (controllerId == null) { return false; diff --git a/Mage.Sets/src/mage/cards/p/PsychicRebuttal.java b/Mage.Sets/src/mage/cards/p/PsychicRebuttal.java index 960db894b79..2fcc52ca173 100644 --- a/Mage.Sets/src/mage/cards/p/PsychicRebuttal.java +++ b/Mage.Sets/src/mage/cards/p/PsychicRebuttal.java @@ -10,8 +10,8 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.game.stack.Spell; @@ -91,10 +91,10 @@ class PsychicRebuttalEffect extends OneShotEffect { } } -class PsychicRebuttalPredicate implements ObjectPlayerPredicate> { +class PsychicRebuttalPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { UUID controllerId = input.getPlayerId(); if (controllerId == null) { return false; diff --git a/Mage.Sets/src/mage/cards/p/Pyramids.java b/Mage.Sets/src/mage/cards/p/Pyramids.java index d0281ba7f56..8f14c637860 100644 --- a/Mage.Sets/src/mage/cards/p/Pyramids.java +++ b/Mage.Sets/src/mage/cards/p/Pyramids.java @@ -16,8 +16,8 @@ import mage.constants.Duration; import mage.constants.SubType; import mage.constants.Zone; import mage.filter.common.FilterEnchantmentPermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.common.TargetLandPermanent; @@ -60,9 +60,9 @@ public final class Pyramids extends CardImpl { return new Pyramids(this); } } -class PyramidsPredicate implements ObjectPlayerPredicate> { +class PyramidsPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent attachment = input.getObject(); if (attachment != null) { Permanent permanent = game.getPermanent(attachment.getAttachedTo()); diff --git a/Mage.Sets/src/mage/cards/r/Radiate.java b/Mage.Sets/src/mage/cards/r/Radiate.java index c50204dd57d..ad83f123481 100644 --- a/Mage.Sets/src/mage/cards/r/Radiate.java +++ b/Mage.Sets/src/mage/cards/r/Radiate.java @@ -10,8 +10,8 @@ import mage.constants.CardType; import mage.filter.FilterSpell; import mage.filter.StaticFilters; import mage.filter.common.FilterInstantOrSorcerySpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.mageobject.MageObjectReferencePredicate; import mage.game.Game; import mage.game.stack.Spell; @@ -55,11 +55,11 @@ public final class Radiate extends CardImpl { } } -enum SpellWithOnlySingleTargetPredicate implements ObjectPlayerPredicate> { +enum SpellWithOnlySingleTargetPredicate implements ObjectSourcePlayerPredicate> { instance; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Spell spell = input.getObject(); if (spell == null) { return false; @@ -79,11 +79,11 @@ enum SpellWithOnlySingleTargetPredicate implements ObjectPlayerPredicate> { +enum SpellWithOnlyPermanentOrPlayerTargetsPredicate implements ObjectSourcePlayerPredicate> { instance; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Spell spell = input.getObject(); if (spell == null) { return false; diff --git a/Mage.Sets/src/mage/cards/r/RemoveEnchantments.java b/Mage.Sets/src/mage/cards/r/RemoveEnchantments.java index 89a4b0a853d..991a19f7d9c 100644 --- a/Mage.Sets/src/mage/cards/r/RemoveEnchantments.java +++ b/Mage.Sets/src/mage/cards/r/RemoveEnchantments.java @@ -11,8 +11,8 @@ import mage.constants.SubType; import mage.constants.TargetController; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledEnchantmentPermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.permanent.AttachedToControlledPermanentPredicate; import mage.game.Game; import mage.game.permanent.Permanent; @@ -74,10 +74,10 @@ public final class RemoveEnchantments extends CardImpl { } } -class AttachedToOpponentControlledAttackingCreaturePredicate implements ObjectPlayerPredicate> { +class AttachedToOpponentControlledAttackingCreaturePredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent attachement = input.getObject(); if (attachement != null) { Permanent permanent = game.getPermanent(attachement.getAttachedTo()); diff --git a/Mage.Sets/src/mage/cards/r/Ricochet.java b/Mage.Sets/src/mage/cards/r/Ricochet.java index 365d5b78a39..99f3b66f719 100644 --- a/Mage.Sets/src/mage/cards/r/Ricochet.java +++ b/Mage.Sets/src/mage/cards/r/Ricochet.java @@ -17,8 +17,8 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SetTargetPointer; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.other.NumberOfTargetsPredicate; import mage.game.Game; import mage.game.stack.Spell; @@ -57,10 +57,10 @@ public final class Ricochet extends CardImpl { } } -class SpellWithOnlyPlayerTargetsPredicate implements ObjectPlayerPredicate> { +class SpellWithOnlyPlayerTargetsPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Spell spell = input.getObject(); if (spell == null) { return false; diff --git a/Mage.Sets/src/mage/cards/s/SageOfTheBeyond.java b/Mage.Sets/src/mage/cards/s/SageOfTheBeyond.java index 8c8fad4a209..0f3de5243bc 100644 --- a/Mage.Sets/src/mage/cards/s/SageOfTheBeyond.java +++ b/Mage.Sets/src/mage/cards/s/SageOfTheBeyond.java @@ -12,8 +12,8 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Zone; import mage.filter.FilterCard; -import mage.filter.predicate.ObjectPlayerPredicate; import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.stack.Spell; @@ -59,7 +59,7 @@ public final class SageOfTheBeyond extends CardImpl { } } -enum SageOfTheBeyondPredicate implements ObjectPlayerPredicate> { +enum SageOfTheBeyondPredicate implements ObjectSourcePlayerPredicate> { instance; @Override diff --git a/Mage.Sets/src/mage/cards/s/SavaenElves.java b/Mage.Sets/src/mage/cards/s/SavaenElves.java index 2c48c386efe..bc2837ff4ea 100644 --- a/Mage.Sets/src/mage/cards/s/SavaenElves.java +++ b/Mage.Sets/src/mage/cards/s/SavaenElves.java @@ -15,8 +15,8 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Zone; import mage.filter.common.FilterEnchantmentPermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; @@ -59,9 +59,9 @@ public final class SavaenElves extends CardImpl { } } -class SavaenElvesPredicate implements ObjectPlayerPredicate> { +class SavaenElvesPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent attachment = input.getObject(); if (attachment != null) { Permanent permanent = game.getPermanent(attachment.getAttachedTo()); diff --git a/Mage.Sets/src/mage/cards/s/ShellOfTheLastKappa.java b/Mage.Sets/src/mage/cards/s/ShellOfTheLastKappa.java index f7f3022ab0f..34809f201ae 100644 --- a/Mage.Sets/src/mage/cards/s/ShellOfTheLastKappa.java +++ b/Mage.Sets/src/mage/cards/s/ShellOfTheLastKappa.java @@ -17,8 +17,8 @@ import mage.constants.SuperType; import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.game.permanent.Permanent; @@ -156,10 +156,10 @@ class ShellOfTheLastKappaCastEffect extends OneShotEffect { } } -class TargetYouPredicate implements ObjectPlayerPredicate> { +class TargetYouPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { UUID controllerId = input.getPlayerId(); if (controllerId == null) { return false; diff --git a/Mage.Sets/src/mage/cards/s/SoulShatter.java b/Mage.Sets/src/mage/cards/s/SoulShatter.java index 60ec94f3a53..cdb4429db05 100644 --- a/Mage.Sets/src/mage/cards/s/SoulShatter.java +++ b/Mage.Sets/src/mage/cards/s/SoulShatter.java @@ -8,8 +8,8 @@ import mage.constants.CardType; import mage.constants.TargetController; import mage.filter.FilterPermanent; import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; @@ -47,7 +47,7 @@ public final class SoulShatter extends CardImpl { } } -enum SoulShatterPredicate implements ObjectPlayerPredicate> { +enum SoulShatterPredicate implements ObjectSourcePlayerPredicate> { instance; private static final FilterPermanent filter = new FilterCreatureOrPlaneswalkerPermanent(); @@ -57,7 +57,7 @@ enum SoulShatterPredicate implements ObjectPlayerPredicate input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { int cmc = game.getBattlefield() .getActivePermanents(filter, input.getPlayerId(), game) .stream() diff --git a/Mage.Sets/src/mage/cards/t/TelimTorsEdict.java b/Mage.Sets/src/mage/cards/t/TelimTorsEdict.java index 5ee311dc8c6..c4145bdbeb3 100644 --- a/Mage.Sets/src/mage/cards/t/TelimTorsEdict.java +++ b/Mage.Sets/src/mage/cards/t/TelimTorsEdict.java @@ -10,8 +10,8 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterPermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; @@ -49,13 +49,13 @@ public final class TelimTorsEdict extends CardImpl { } } -class TelimTorsEdictPredicate implements ObjectPlayerPredicate> { +class TelimTorsEdictPredicate implements ObjectSourcePlayerPredicate> { public TelimTorsEdictPredicate() { } @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent permanent = input.getObject(); UUID playerId = input.getPlayerId(); if (permanent.isControlledBy(playerId) || permanent.isOwnedBy(playerId)) { diff --git a/Mage.Sets/src/mage/cards/v/VedalkenShackles.java b/Mage.Sets/src/mage/cards/v/VedalkenShackles.java index 470a36f907b..2049a078224 100644 --- a/Mage.Sets/src/mage/cards/v/VedalkenShackles.java +++ b/Mage.Sets/src/mage/cards/v/VedalkenShackles.java @@ -18,8 +18,8 @@ import mage.constants.SubType; import mage.constants.Zone; import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterLandPermanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.common.TargetCreaturePermanent; @@ -62,7 +62,7 @@ public final class VedalkenShackles extends CardImpl { } } -class PowerIslandPredicate implements ObjectPlayerPredicate> { +class PowerIslandPredicate implements ObjectSourcePlayerPredicate> { static final FilterLandPermanent filter = new FilterLandPermanent("Island"); static { @@ -70,7 +70,7 @@ class PowerIslandPredicate implements ObjectPlayerPredicate input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent permanent = input.getObject(); if (permanent != null) { int islands = game.getBattlefield().countAll(filter, input.getPlayerId(), game); diff --git a/Mage.Sets/src/mage/cards/v/VineGecko.java b/Mage.Sets/src/mage/cards/v/VineGecko.java index 3b0fc089a30..fbdf5006061 100644 --- a/Mage.Sets/src/mage/cards/v/VineGecko.java +++ b/Mage.Sets/src/mage/cards/v/VineGecko.java @@ -16,8 +16,8 @@ import mage.constants.WatcherScope; import mage.counters.CounterType; import mage.filter.FilterCard; import mage.filter.StaticFilters; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.stack.Spell; @@ -67,11 +67,11 @@ public final class VineGecko extends CardImpl { } } -enum VineGeckoPredicate implements ObjectPlayerPredicate> { +enum VineGeckoPredicate implements ObjectSourcePlayerPredicate> { instance; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { VineGeckoWatcher watcher = game.getState().getWatcher(VineGeckoWatcher.class); if (watcher == null || watcher.checkPlayer(input.getPlayerId())) { return false; diff --git a/Mage/src/main/java/mage/filter/FilterCard.java b/Mage/src/main/java/mage/filter/FilterCard.java index cd1a0bb2cfe..331558337dd 100644 --- a/Mage/src/main/java/mage/filter/FilterCard.java +++ b/Mage/src/main/java/mage/filter/FilterCard.java @@ -2,7 +2,10 @@ package mage.filter; import mage.cards.Card; import mage.constants.TargetController; -import mage.filter.predicate.*; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.filter.predicate.Predicate; +import mage.filter.predicate.Predicates; import mage.game.Game; import java.util.ArrayList; @@ -20,7 +23,7 @@ import java.util.stream.Collectors; public class FilterCard extends FilterObject { private static final long serialVersionUID = 1L; - protected List>> extraPredicates = new ArrayList<>(); + protected List>> extraPredicates = new ArrayList<>(); public FilterCard() { super("card"); @@ -53,21 +56,17 @@ public class FilterCard extends FilterObject { } public boolean match(Card card, UUID playerId, Game game) { - if (!this.match(card, game)) { - return false; - } - - return Predicates.and(extraPredicates).apply(new ObjectPlayer(card, playerId), game); + return match(card, null, playerId, game); } public boolean match(Card card, UUID sourceId, UUID playerId, Game game) { if (!this.match(card, game)) { return false; } - return Predicates.and(extraPredicates).apply(new ObjectSourcePlayer(card, sourceId, playerId), game); + return Predicates.and(extraPredicates).apply(new ObjectSourcePlayer(card, sourceId, playerId), game); } - public final void add(ObjectPlayerPredicate predicate) { + public final void add(ObjectSourcePlayerPredicate predicate) { if (isLockedFilter()) { throw new UnsupportedOperationException("You may not modify a locked filter"); } diff --git a/Mage/src/main/java/mage/filter/FilterPermanent.java b/Mage/src/main/java/mage/filter/FilterPermanent.java index f7d4e9759a7..02248373d57 100644 --- a/Mage/src/main/java/mage/filter/FilterPermanent.java +++ b/Mage/src/main/java/mage/filter/FilterPermanent.java @@ -1,8 +1,8 @@ package mage.filter; import mage.constants.SubType; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.Predicates; import mage.game.Game; @@ -18,7 +18,7 @@ import java.util.UUID; */ public class FilterPermanent extends FilterObject implements FilterInPlay { - protected List>> extraPredicates = new ArrayList<>(); + protected List>> extraPredicates = new ArrayList<>(); public FilterPermanent() { super("permanent"); @@ -56,10 +56,10 @@ public class FilterPermanent extends FilterObject implements FilterIn return false; } - return Predicates.and(extraPredicates).apply(new ObjectSourcePlayer(permanent, sourceId, playerId), game); + return Predicates.and(extraPredicates).apply(new ObjectSourcePlayer(permanent, sourceId, playerId), game); } - public final void add(ObjectPlayerPredicate predicate) { + public final void add(ObjectSourcePlayerPredicate predicate) { if (isLockedFilter()) { throw new UnsupportedOperationException("You may not modify a locked filter"); } diff --git a/Mage/src/main/java/mage/filter/FilterPlayer.java b/Mage/src/main/java/mage/filter/FilterPlayer.java index 85e26cff854..d01aa89994b 100644 --- a/Mage/src/main/java/mage/filter/FilterPlayer.java +++ b/Mage/src/main/java/mage/filter/FilterPlayer.java @@ -1,8 +1,7 @@ package mage.filter; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.players.Player; @@ -17,7 +16,7 @@ import java.util.UUID; */ public class FilterPlayer extends FilterImpl { - protected List>> extraPredicates = new ArrayList<>(); + protected List>> extraPredicates = new ArrayList<>(); public FilterPlayer() { this("player"); @@ -32,7 +31,7 @@ public class FilterPlayer extends FilterImpl { this.extraPredicates = new ArrayList<>(filter.extraPredicates); } - public void add(ObjectPlayerPredicate predicate) { + public void add(ObjectSourcePlayerPredicate predicate) { if (isLockedFilter()) { throw new UnsupportedOperationException("You may not modify a locked filter"); } @@ -49,7 +48,7 @@ public class FilterPlayer extends FilterImpl { return false; } - return Predicates.and(extraPredicates).apply(new ObjectSourcePlayer(checkPlayer, sourceId, sourceControllerId), game); + return Predicates.and(extraPredicates).apply(new ObjectSourcePlayer(checkPlayer, sourceId, sourceControllerId), game); } @Override diff --git a/Mage/src/main/java/mage/filter/FilterStackObject.java b/Mage/src/main/java/mage/filter/FilterStackObject.java index 789c2d3267e..d3c38ecd332 100644 --- a/Mage/src/main/java/mage/filter/FilterStackObject.java +++ b/Mage/src/main/java/mage/filter/FilterStackObject.java @@ -1,7 +1,7 @@ package mage.filter; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.Predicates; import mage.game.Game; @@ -17,7 +17,7 @@ import java.util.UUID; */ public class FilterStackObject extends FilterObject { - protected List>> extraPredicates = new ArrayList<>(); + protected List>> extraPredicates = new ArrayList<>(); public FilterStackObject() { this("spell or ability"); @@ -37,10 +37,10 @@ public class FilterStackObject extends FilterObject { return false; } - return Predicates.and(extraPredicates).apply(new ObjectSourcePlayer(stackObject, sourceId, playerId), game); + return Predicates.and(extraPredicates).apply(new ObjectSourcePlayer(stackObject, sourceId, playerId), game); } - public final void add(ObjectPlayerPredicate predicate) { + public final void add(ObjectSourcePlayerPredicate predicate) { if (isLockedFilter()) { throw new UnsupportedOperationException("You may not modify a locked filter"); } diff --git a/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java b/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java index c885b03522d..f6fc17d2481 100644 --- a/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java +++ b/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java @@ -5,7 +5,8 @@ import mage.filter.FilterImpl; import mage.filter.FilterInPlay; import mage.filter.FilterPermanent; import mage.filter.FilterPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.Predicate; import mage.game.Game; import mage.game.permanent.Permanent; @@ -46,7 +47,7 @@ public class FilterPermanentOrPlayer extends FilterImpl implements Fil return true; } - public void add(ObjectPlayerPredicate predicate) { + public void add(ObjectSourcePlayerPredicate predicate) { playerFilter.add((Predicate) predicate); permanentFilter.add((Predicate) predicate); } diff --git a/Mage/src/main/java/mage/filter/common/FilterSpellOrPermanent.java b/Mage/src/main/java/mage/filter/common/FilterSpellOrPermanent.java index 5a8c08ec3ae..e15fa68b20f 100644 --- a/Mage/src/main/java/mage/filter/common/FilterSpellOrPermanent.java +++ b/Mage/src/main/java/mage/filter/common/FilterSpellOrPermanent.java @@ -1,49 +1,17 @@ -/* - * - * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation are those of the - * authors and should not be interpreted as representing official policies, either expressed - * or implied, of BetaSteward_at_googlemail.com. - * - */ package mage.filter.common; -import java.util.UUID; - import mage.MageObject; import mage.filter.FilterImpl; import mage.filter.FilterInPlay; import mage.filter.FilterPermanent; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.stack.Spell; +import java.util.UUID; + /** - * * @author LevelX */ public class FilterSpellOrPermanent extends FilterImpl implements FilterInPlay { @@ -92,12 +60,11 @@ public class FilterSpellOrPermanent extends FilterImpl implements Fi return false; } - public final void add(ObjectPlayerPredicate predicate) { - if (isLockedFilter()) { - throw new UnsupportedOperationException("You may not modify a locked filter"); - } - spellFilter.add(predicate); - permanentFilter.add(predicate); + @Override + public void setLockedFilter(boolean lockedFilter) { + super.setLockedFilter(lockedFilter); + spellFilter.setLockedFilter(lockedFilter); + permanentFilter.setLockedFilter(lockedFilter); } public FilterPermanent getPermanentFilter() { diff --git a/Mage/src/main/java/mage/filter/predicate/ObjectPlayer.java b/Mage/src/main/java/mage/filter/predicate/ObjectPlayer.java deleted file mode 100644 index d59c0fffb5e..00000000000 --- a/Mage/src/main/java/mage/filter/predicate/ObjectPlayer.java +++ /dev/null @@ -1,28 +0,0 @@ - -package mage.filter.predicate; - -import java.util.UUID; - -/** - * - * @author North - * @param - */ -public class ObjectPlayer { - - protected final T object; - protected final UUID playerId; - - public ObjectPlayer(T object, UUID playerId) { - this.object = object; - this.playerId = playerId; - } - - public T getObject() { - return object; - } - - public UUID getPlayerId() { - return playerId; - } -} diff --git a/Mage/src/main/java/mage/filter/predicate/ObjectPlayerPredicate.java b/Mage/src/main/java/mage/filter/predicate/ObjectPlayerPredicate.java deleted file mode 100644 index 6f664d00ba7..00000000000 --- a/Mage/src/main/java/mage/filter/predicate/ObjectPlayerPredicate.java +++ /dev/null @@ -1,11 +0,0 @@ - -package mage.filter.predicate; - -/** - * - * @author North - * @param - */ -@FunctionalInterface -public interface ObjectPlayerPredicate extends Predicate { -} diff --git a/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayer.java b/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayer.java index 671c8abf0ca..b5019768bdd 100644 --- a/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayer.java +++ b/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayer.java @@ -1,22 +1,31 @@ - package mage.filter.predicate; import java.util.UUID; /** - * - * @author North * @param + * @author North */ -public class ObjectSourcePlayer extends ObjectPlayer { +public class ObjectSourcePlayer { + protected final T object; + protected final UUID playerId; protected final UUID sourceId; public ObjectSourcePlayer(T object, UUID sourceId, UUID sourceControllerId) { - super(object, sourceControllerId); + this.object = object; + this.playerId = sourceControllerId; this.sourceId = sourceId; } + public T getObject() { + return object; + } + + public UUID getPlayerId() { + return playerId; + } + public UUID getSourceId() { return sourceId; } diff --git a/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayerPredicate.java b/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayerPredicate.java index 92bfbddee80..73b3679062e 100644 --- a/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayerPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayerPredicate.java @@ -1,11 +1,9 @@ - package mage.filter.predicate; /** - * - * @author North * @param + * @author North */ @FunctionalInterface -public interface ObjectSourcePlayerPredicate extends ObjectPlayerPredicate { +public interface ObjectSourcePlayerPredicate> extends Predicate { } diff --git a/Mage/src/main/java/mage/filter/predicate/card/CardOnTopOfLibraryPredicate.java b/Mage/src/main/java/mage/filter/predicate/card/CardOnTopOfLibraryPredicate.java index f21aa550852..46661a3d88b 100644 --- a/Mage/src/main/java/mage/filter/predicate/card/CardOnTopOfLibraryPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/card/CardOnTopOfLibraryPredicate.java @@ -1,8 +1,8 @@ package mage.filter.predicate.card; import mage.cards.Card; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.players.Player; @@ -10,12 +10,12 @@ import mage.players.Player; * @author JayDi85 */ -public enum CardOnTopOfLibraryPredicate implements ObjectPlayerPredicate> { +public enum CardOnTopOfLibraryPredicate implements ObjectSourcePlayerPredicate> { YOUR, ANY; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Player player; switch (this) { diff --git a/Mage/src/main/java/mage/filter/predicate/card/DefendingPlayerOwnsCardPredicate.java b/Mage/src/main/java/mage/filter/predicate/card/DefendingPlayerOwnsCardPredicate.java index b4eeb609e5b..a667d02abdf 100644 --- a/Mage/src/main/java/mage/filter/predicate/card/DefendingPlayerOwnsCardPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/card/DefendingPlayerOwnsCardPredicate.java @@ -1,18 +1,18 @@ package mage.filter.predicate.card; import mage.cards.Card; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; /** * @author TheElk801 */ -public enum DefendingPlayerOwnsCardPredicate implements ObjectPlayerPredicate> { +public enum DefendingPlayerOwnsCardPredicate implements ObjectSourcePlayerPredicate> { instance; @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { return game.getCombat().getPlayerDefenders(game, false).contains(input.getObject().getOwnerId()); } diff --git a/Mage/src/main/java/mage/filter/predicate/other/DamagedPlayerThisTurnPredicate.java b/Mage/src/main/java/mage/filter/predicate/other/DamagedPlayerThisTurnPredicate.java index c45dc5815c3..a7935bd77cd 100644 --- a/Mage/src/main/java/mage/filter/predicate/other/DamagedPlayerThisTurnPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/other/DamagedPlayerThisTurnPredicate.java @@ -1,8 +1,8 @@ package mage.filter.predicate.other; import mage.constants.TargetController; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Controllable; import mage.game.Game; import mage.watchers.common.PlayerDamagedBySourceWatcher; @@ -12,7 +12,7 @@ import java.util.UUID; /** * @author LevelX2 */ -public class DamagedPlayerThisTurnPredicate implements ObjectPlayerPredicate> { +public class DamagedPlayerThisTurnPredicate implements ObjectSourcePlayerPredicate> { private final TargetController controller; @@ -21,7 +21,7 @@ public class DamagedPlayerThisTurnPredicate implements ObjectPlayerPredicate input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Controllable object = input.getObject(); UUID playerId = input.getPlayerId(); diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/AttachedToControlledPermanentPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/AttachedToControlledPermanentPredicate.java index c8257e2c2dd..58b8aa6fe96 100644 --- a/Mage/src/main/java/mage/filter/predicate/permanent/AttachedToControlledPermanentPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/permanent/AttachedToControlledPermanentPredicate.java @@ -1,8 +1,8 @@ package mage.filter.predicate.permanent; -import mage.filter.predicate.ObjectPlayer; -import mage.filter.predicate.ObjectPlayerPredicate; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; @@ -10,10 +10,10 @@ import mage.game.permanent.Permanent; * * @author North & L_J */ -public class AttachedToControlledPermanentPredicate implements ObjectPlayerPredicate> { +public class AttachedToControlledPermanentPredicate implements ObjectSourcePlayerPredicate> { @Override - public boolean apply(ObjectPlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { Permanent attachement = input.getObject(); if (attachement != null) { Permanent permanent = game.getPermanent(attachement.getAttachedTo()); From c49ca90b84e1a6c2a91838dd65671f5925a21968 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 21 Sep 2021 12:14:22 +0400 Subject: [PATCH 158/231] Tests: execute time stats in logs are disabled by default; --- .../test/serverside/base/impl/CardTestPlayerAPIImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 02e7ae5a013..0dd3cdc2c81 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -55,6 +55,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement // DEBUG only, enable it to fast startup tests without database create (delete \db\ folder to force db recreate) private static final boolean FAST_SCAN_WITHOUT_DATABASE_CREATE = false; + private static final boolean SHOW_EXECUTE_TIME_PER_TEST = false; + public static final String ALIAS_PREFIX = "@"; // don't change -- it uses in user's tests public static final String CHECK_PARAM_DELIMETER = "#"; public static final String CHECK_PREFIX = "check:"; // prefix for all check commands @@ -332,7 +334,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement currentGame.setGameStopped(true); // used for rollback handling long t2 = System.nanoTime(); logger.debug("Winner: " + currentGame.getWinner()); - logger.info(Thread.currentThread().getStackTrace()[2].getMethodName() + " has been executed. Execution time: " + (t2 - t1) / 1000000 + " ms"); + if (SHOW_EXECUTE_TIME_PER_TEST) { + logger.info(Thread.currentThread().getStackTrace()[2].getMethodName() + " has been executed. Execution time: " + (t2 - t1) / 1000000 + " ms"); + } // TODO: 01.12.2018, JayDi85 - uncomment and fix MANY broken tests with wrong commands //assertAllCommandsUsed(); From 6aaf4613626913ad9ddc724b5a7353a57fef64ed Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 21 Sep 2021 13:01:11 +0400 Subject: [PATCH 159/231] Accursed Witch, Vengeful Strangler - fixed rollback error on some usage; --- Mage.Sets/src/mage/cards/a/AccursedWitch.java | 18 ++++++++++-------- .../src/mage/cards/v/VengefulStrangler.java | 15 +++++++-------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AccursedWitch.java b/Mage.Sets/src/mage/cards/a/AccursedWitch.java index 85743de74eb..d157b752a25 100644 --- a/Mage.Sets/src/mage/cards/a/AccursedWitch.java +++ b/Mage.Sets/src/mage/cards/a/AccursedWitch.java @@ -78,15 +78,17 @@ class AccursedWitchReturnTransformedEffect extends OneShotEffect { if (controller == null || !(game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) || attachTo == null) { return false; } - game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + source.getSourceId(), Boolean.TRUE); - UUID secondFaceId = game.getCard(source.getSourceId()).getSecondCardFace().getId(); - game.getState().setValue("attachTo:" + secondFaceId, attachTo.getId()); - //note: should check for null after game.getCard + Card card = game.getCard(source.getSourceId()); - if (card != null) { - if (controller.moveCards(card, Zone.BATTLEFIELD, source, game)) { - attachTo.addAttachment(card.getId(), source, game); - } + if (card == null) { + return false; + } + + game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + source.getSourceId(), Boolean.TRUE); + UUID secondFaceId = card.getSecondCardFace().getId(); + game.getState().setValue("attachTo:" + secondFaceId, attachTo.getId()); + if (controller.moveCards(card, Zone.BATTLEFIELD, source, game)) { + attachTo.addAttachment(card.getId(), source, game); } return true; } diff --git a/Mage.Sets/src/mage/cards/v/VengefulStrangler.java b/Mage.Sets/src/mage/cards/v/VengefulStrangler.java index b7d56d257f6..f1ca4ac0437 100644 --- a/Mage.Sets/src/mage/cards/v/VengefulStrangler.java +++ b/Mage.Sets/src/mage/cards/v/VengefulStrangler.java @@ -86,19 +86,18 @@ class VengefulStranglerEffect extends OneShotEffect { || game.getState().getZone(source.getSourceId()) != Zone.GRAVEYARD) { return false; } - game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + source.getSourceId(), Boolean.TRUE); - UUID secondFaceId = game.getCard(source.getSourceId()).getSecondCardFace().getId(); - game.getState().setValue("attachTo:" + secondFaceId, permanent.getId()); + Card card = game.getCard(source.getSourceId()); if (card == null) { return false; } - controller.moveCards(card, Zone.BATTLEFIELD, source, game); - Permanent sourcePermanent = game.getPermanent(card.getId()); - if (sourcePermanent == null) { - return false; + + game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + source.getSourceId(), Boolean.TRUE); + UUID secondFaceId = game.getCard(source.getSourceId()).getSecondCardFace().getId(); + game.getState().setValue("attachTo:" + secondFaceId, permanent.getId()); + if (controller.moveCards(card, Zone.BATTLEFIELD, source, game)) { + permanent.addAttachment(card.getId(), source, game); } - permanent.addAttachment(card.getId(), source, game); return true; } } From 6bc5a00e8ac604bd98e927bd88f03486830126a8 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 21 Sep 2021 14:22:46 +0400 Subject: [PATCH 160/231] * Alternative costs - fixed that it can be activated on free cast (example: cascade with overload, #6925, #7410, #7741, #6342); --- .../main/java/mage/client/game/GamePanel.java | 19 +-- .../java/mage/player/ai/ComputerPlayer.java | 2 +- .../src/mage/player/human/HumanPlayer.java | 6 +- .../src/mage/cards/i/IsochronScepter.java | 6 +- .../abilities/keywords/OverloadTest.java | 126 ++++++++++++++++-- .../java/org/mage/test/player/TestPlayer.java | 2 +- .../main/java/mage/players/PlayerImpl.java | 34 +++-- 7 files changed, 151 insertions(+), 44 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 83f3862b05b..f84b1c75270 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -289,19 +289,12 @@ public final class GamePanel extends javax.swing.JPanel { private void hidePickDialogs() { // temporary hide opened dialog on redraw/update - - //try { - // pick target - for (ShowCardsDialog dialog : this.pickTarget) { - dialog.setVisible(false); - } - // pick pile - for (PickPileDialog dialog : this.pickPile) { - dialog.setVisible(false); - } - //} catch (PropertyVetoException e) { - // logger.error("Couldn't close pick dialog", e); - //} + for (ShowCardsDialog dialog : this.pickTarget) { + dialog.setVisible(false); + } + for (PickPileDialog dialog : this.pickPile) { + dialog.setVisible(false); + } } private void clearPickDialogs() { diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index c8da4b693b5..59d2553e7d0 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -3002,7 +3002,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { @Override public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { - Map useable = PlayerImpl.getSpellAbilities(this.getId(), card, game.getState().getZone(card.getId()), game); + Map useable = PlayerImpl.getCastableSpellAbilities(game, this.getId(), card, game.getState().getZone(card.getId()), noMana); return (SpellAbility) useable.values().stream().findFirst().orElse(null); } diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 6312f27ab0a..352147acc40 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -2174,7 +2174,7 @@ public class HumanPlayer extends PlayerImpl { } @Override - public SpellAbility chooseAbilityForCast(Card card, Game game, boolean nonMana) { + public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { if (gameInCheckPlayableState(game)) { return null; } @@ -2186,8 +2186,8 @@ public class HumanPlayer extends PlayerImpl { MageObject object = game.getObject(card.getId()); // must be object to find real abilities (example: commander) if (object != null) { - String message = "Choose ability to cast" + (nonMana ? " for FREE" : "") + "
" + object.getLogName(); - LinkedHashMap useableAbilities = getSpellAbilities(playerId, object, game.getState().getZone(object.getId()), game); + String message = "Choose ability to cast" + (noMana ? " for FREE" : "") + "
" + object.getLogName(); + LinkedHashMap useableAbilities = PlayerImpl.getCastableSpellAbilities(game, playerId, object, game.getState().getZone(object.getId()), noMana); if (useableAbilities != null && useableAbilities.size() == 1) { return (SpellAbility) useableAbilities.values().iterator().next(); diff --git a/Mage.Sets/src/mage/cards/i/IsochronScepter.java b/Mage.Sets/src/mage/cards/i/IsochronScepter.java index d2ab47d3751..b52f68e325d 100644 --- a/Mage.Sets/src/mage/cards/i/IsochronScepter.java +++ b/Mage.Sets/src/mage/cards/i/IsochronScepter.java @@ -33,15 +33,13 @@ public final class IsochronScepter extends CardImpl { public IsochronScepter(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}"); - // Imprint - When Isochron Scepter enters the battlefield, you may exile an - // instant card with converted mana cost 2 or less from your hand. + // Imprint - When Isochron Scepter enters the battlefield, you may exile an instant card with converted mana cost 2 or less from your hand. this.addAbility(new EntersBattlefieldTriggeredAbility( new IsochronScepterImprintEffect(),true) .withFlavorWord("Imprint") ); - // {2}, {tap}: You may copy the exiled card. If you do, you may cast the - // copy without paying its mana cost. + // {2}, {T}: You may copy the exiled card. If you do, you may cast the copy without paying its mana cost. Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new IsochronScepterCopyEffect(), new GenericManaCost(2)); ability.addCost(new TapSourceCost()); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OverloadTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OverloadTest.java index 7e74f4420ee..101fe368ca1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OverloadTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OverloadTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; @@ -7,44 +6,147 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * - * @author LevelX2 + * @author LevelX2, JayDi85 */ public class OverloadTest extends CardTestPlayerBase { /** * My opponent cast an overloaded Vandalblast, and Xmage would not let me * cast Mental Misstep on it. - * + *

* The CMC of a card never changes, and Vandalblast's CMC is always 1. - * + *

* 4/15/2013 Casting a spell with overload doesn't change that spell's mana * cost. You just pay the overload cost instead. */ @Test - public void testCastByOverloadDoesNotChangeCMC() { - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + public void test_CastByOverloadDoesNotChangeCMC() { // Destroy target artifact you don't control. // Overload {4}{R} (You may cast this spell for its overload cost. If you do, change its text by replacing all instances of "target" with "each.") addCard(Zone.HAND, playerA, "Vandalblast"); - - addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + // // Counter target spell with converted mana cost 1. addCard(Zone.HAND, playerB, "Mental Misstep", 1); + addCard(Zone.BATTLEFIELD, playerB, "Island", 1); addCard(Zone.BATTLEFIELD, playerB, "War Horn", 2); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Vandalblast with overload"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Mental Misstep", "Vandalblast"); + setChoice(playerB, false); // pay mana instead life + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Vandalblast", 1); assertGraveyardCount(playerB, "Mental Misstep", 1); - assertPermanentCount(playerB, "War Horn", 2); - } + @Test + public void test_CyclonicRift_NormalPlay() { + // Return target nonland permanent you don't control to its owner's hand. + // Overload {6}{U} (You may cast this spell for its overload cost. If you do, change its text + // by replacing all instances of "target" with "each.") + addCard(Zone.HAND, playerA, "Cyclonic Rift"); // {1}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 7); + // + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); + addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); + + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Cyclonic Rift", true); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Cyclonic Rift with overload", true); + + // cast and remove 1 target + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cyclonic Rift"); + addTarget(playerA, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Cyclonic Rift", 1); + assertPermanentCount(playerB, "Swamp", 1); + assertHandCount(playerB, "Balduvian Bears", 1); + assertPermanentCount(playerB, "Spectral Bears", 1); + } + + @Test + public void test_CyclonicRift_OverloadPlay() { + // Return target nonland permanent you don't control to its owner's hand. + // Overload {6}{U} (You may cast this spell for its overload cost. If you do, change its text + // by replacing all instances of "target" with "each.") + addCard(Zone.HAND, playerA, "Cyclonic Rift"); // {1}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 7); + // + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); + addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); + + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Cyclonic Rift", true); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Cyclonic Rift with overload", true); + + // cast and remove all possible targets (all bears) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cyclonic Rift with overload"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Cyclonic Rift", 1); + assertPermanentCount(playerB, "Swamp", 1); + assertHandCount(playerB, "Balduvian Bears", 1); + assertHandCount(playerB, "Spectral Bears", 1); + } + + @Test + public void test_CyclonicRift_CantUseAlternativeSpellOnFreeCast() { + // bug: https://github.com/magefree/mage/issues/6925 + // Casting Overloaded Cyclonic Rift with Isochron Scepter - This should not be possible, + // You can't pay two alternate costs for the same thing. + + // Return target nonland permanent you don't control to its owner's hand. + // Overload {6}{U} (You may cast this spell for its overload cost. If you do, change its text + // by replacing all instances of "target" with "each.") + addCard(Zone.HAND, playerA, "Cyclonic Rift"); // {1}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 7); + // + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); + addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); + // + // Imprint - When Isochron Scepter enters the battlefield, you may exile an instant card with converted mana cost 2 or less from your hand. + // {2}, {T}: You may copy the exiled card. If you do, you may cast the copy without paying its mana cost. + addCard(Zone.HAND, playerA, "Isochron Scepter"); // {2} + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + + // prepare scepter for imprint + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Isochron Scepter"); + setChoice(playerA, true); // use imprint + setChoice(playerA, "Cyclonic Rift"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // cast rift for free + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}:"); + setChoice(playerA, true); // create copy + setChoice(playerA, true); // use free cast + //setChoice(playerA, "Cast Cyclonic Rift with overload"); // must be NO choices, cause only normal cast allows here + addTarget(playerA, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertExileCount(playerA, "Cyclonic Rift", 1); + assertGraveyardCount(playerA, "Cyclonic Rift", 0); // imprinted copy discarded + assertPermanentCount(playerB, "Swamp", 1); + assertHandCount(playerB, "Balduvian Bears", 1); + assertPermanentCount(playerB, "Spectral Bears", 1); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index ed258018eb6..d7ff1c66a5d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -4351,7 +4351,7 @@ public class TestPlayer implements Player { assertAliasSupportInChoices(false); MageObject object = game.getObject(card.getId()); // must be object to find real abilities (example: commander) - Map useable = PlayerImpl.getSpellAbilities(this.getId(), object, game.getState().getZone(object.getId()), game); + Map useable = PlayerImpl.getCastableSpellAbilities(game, this.getId(), object, game.getState().getZone(object.getId()), noMana); String allInfo = useable.values().stream().map(Object::toString).collect(Collectors.joining("\n")); if (useable.size() == 1) { return (SpellAbility) useable.values().iterator().next(); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 89140dc946f..d66ad138d08 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1544,13 +1544,14 @@ public abstract class PlayerImpl implements Player, Serializable { * for choosing from the card (example: effect allow to cast card and player * must choose the spell ability) * + * @param game * @param playerId * @param object * @param zone - * @param game + * @param noMana * @return */ - public static LinkedHashMap getSpellAbilities(UUID playerId, MageObject object, Zone zone, Game game) { + public static LinkedHashMap getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) { // it uses simple check from spellCanBeActivatedRegularlyNow // reason: no approved info here (e.g. forced to choose spell ability from cast card) LinkedHashMap useable = new LinkedHashMap<>(); @@ -1563,16 +1564,29 @@ public abstract class PlayerImpl implements Player, Serializable { for (Ability ability : allAbilities) { if (ability instanceof SpellAbility) { - switch (((SpellAbility) ability).getSpellAbilityType()) { + SpellAbility spellAbility = (SpellAbility) ability; + + switch (spellAbility.getSpellAbilityType()) { case BASE_ALTERNATE: - if (((SpellAbility) ability).spellCanBeActivatedRegularlyNow(playerId, game)) { - useable.put(ability.getId(), (SpellAbility) ability); // example: Chandra, Torch of Defiance +1 loyal ability + // rules: + // If you cast a spell “without paying its mana cost,” you can’t choose to cast it for + // any alternative costs. You can, however, pay additional costs, such as kicker costs. + // If the card has any mandatory additional costs, those must be paid to cast the spell. + // (2021-02-05) + if (!noMana) { + if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability + } + return useable; } - return useable; + break; case SPLIT_FUSED: + // rules: + // If you cast a split card with fuse from your hand without paying its mana cost, + // you can choose to use its fuse ability and cast both halves without paying their mana costs. if (zone == Zone.HAND) { - if (ability.canChooseTarget(game, playerId)) { - useable.put(ability.getId(), (SpellAbility) ability); + if (spellAbility.canChooseTarget(game, playerId)) { + useable.put(spellAbility.getId(), spellAbility); } } case SPLIT: @@ -1599,8 +1613,8 @@ public abstract class PlayerImpl implements Player, Serializable { } return useable; default: - if (((SpellAbility) ability).spellCanBeActivatedRegularlyNow(playerId, game)) { - useable.put(ability.getId(), (SpellAbility) ability); + if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + useable.put(spellAbility.getId(), spellAbility); } } } From d35e1fbfb19da4b24bf97e9e58221a0983f73966 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 21 Sep 2021 15:30:40 +0400 Subject: [PATCH 161/231] * Play card without mana - fixed that some cards did not allow to choose a casting spell from split/mdfc cards (#7410); --- Mage.Sets/src/mage/cards/b/BrilliantUltimatum.java | 3 +-- Mage.Sets/src/mage/cards/c/ChandraAblaze.java | 5 ++++- Mage.Sets/src/mage/cards/d/DjinnOfWishes.java | 2 +- Mage.Sets/src/mage/cards/e/EyeOfTheStorm.java | 4 +++- Mage.Sets/src/mage/cards/g/Guile.java | 2 +- Mage.Sets/src/mage/cards/h/HordeOfNotions.java | 2 +- Mage.Sets/src/mage/cards/k/KnowledgePool.java | 4 +++- Mage.Sets/src/mage/cards/l/LeafCrownedElder.java | 2 +- Mage.Sets/src/mage/cards/o/OmenMachine.java | 4 +++- Mage.Sets/src/mage/cards/p/PossibilityStorm.java | 4 +++- Mage.Sets/src/mage/cards/s/SpellQueller.java | 4 +++- Mage.Sets/src/mage/cards/s/Spellshift.java | 4 +++- Mage.Sets/src/mage/cards/w/WordOfCommand.java | 2 +- .../src/mage/cards/y/YennettCrypticSovereign.java | 4 +++- .../test/java/org/mage/test/player/TestPlayer.java | 4 ++-- .../src/test/java/org/mage/test/stub/PlayerStub.java | 2 +- .../common/CastCardFromOutsideTheGameEffect.java | 4 +++- .../abilities/effects/common/HideawayPlayEffect.java | 2 +- .../java/mage/abilities/keyword/RippleAbility.java | 7 +++++-- Mage/src/main/java/mage/players/Player.java | 10 +++------- Mage/src/main/java/mage/players/PlayerImpl.java | 11 ++++++++--- 21 files changed, 54 insertions(+), 32 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/BrilliantUltimatum.java b/Mage.Sets/src/mage/cards/b/BrilliantUltimatum.java index aa914cfbe47..158bfc6d391 100644 --- a/Mage.Sets/src/mage/cards/b/BrilliantUltimatum.java +++ b/Mage.Sets/src/mage/cards/b/BrilliantUltimatum.java @@ -110,8 +110,7 @@ class BrilliantUltimatumEffect extends OneShotEffect { TargetCard targetExiledCard = new TargetCard(Zone.EXILED, new FilterCard()); if (controller.chooseTarget(Outcome.PlayForFree, selectedPile, targetExiledCard, source, game)) { Card card = selectedPile.get(targetExiledCard.getFirstTarget(), game); - controller.canPlayLand(); - if (controller.playCard(card, game, true, true, new ApprovingObject(source, game))) { + if (controller.playCard(card, game, true, new ApprovingObject(source, game))) { selectedPileCards.remove(card); selectedPile.remove(card); } diff --git a/Mage.Sets/src/mage/cards/c/ChandraAblaze.java b/Mage.Sets/src/mage/cards/c/ChandraAblaze.java index 20518c65b7b..2e665564dff 100644 --- a/Mage.Sets/src/mage/cards/c/ChandraAblaze.java +++ b/Mage.Sets/src/mage/cards/c/ChandraAblaze.java @@ -171,7 +171,10 @@ class ChandraAblazeEffect5 extends OneShotEffect { if (player.choose(outcome, target, source.getSourceId(), game)) { Card card = game.getCard(target.getFirstTarget()); if (card != null) { - player.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + player.cast(player.chooseAbilityForCast(card, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + cards.remove(card); } } diff --git a/Mage.Sets/src/mage/cards/d/DjinnOfWishes.java b/Mage.Sets/src/mage/cards/d/DjinnOfWishes.java index b51e75ef4d6..3269631dec2 100644 --- a/Mage.Sets/src/mage/cards/d/DjinnOfWishes.java +++ b/Mage.Sets/src/mage/cards/d/DjinnOfWishes.java @@ -78,7 +78,7 @@ class DjinnOfWishesEffect extends OneShotEffect { Cards cards = new CardsImpl(card); controller.revealCards(sourceObject.getIdName(), cards, game); if (!controller.chooseUse(Outcome.PlayForFree, "Play " + card.getName() + " without paying its mana cost?", source, game) - || !controller.playCard(card, game, true, true, new ApprovingObject(source, game))) { + || !controller.playCard(card, game, true, new ApprovingObject(source, game))) { controller.moveCards(card, Zone.EXILED, source, game); } return true; diff --git a/Mage.Sets/src/mage/cards/e/EyeOfTheStorm.java b/Mage.Sets/src/mage/cards/e/EyeOfTheStorm.java index b38d643aa17..36273b7f0c7 100644 --- a/Mage.Sets/src/mage/cards/e/EyeOfTheStorm.java +++ b/Mage.Sets/src/mage/cards/e/EyeOfTheStorm.java @@ -154,7 +154,9 @@ class EyeOfTheStormEffect1 extends OneShotEffect { if (cardToCopy != null) { Card copy = game.copyCard(cardToCopy, source, source.getControllerId()); if (spellController.chooseUse(outcome, "Cast " + copy.getIdName() + " without paying mana cost?", source, game)) { - spellController.cast(copy.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + copy.getId(), Boolean.TRUE); + spellController.cast(spellController.chooseAbilityForCast(copy, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + copy.getId(), null); } } } diff --git a/Mage.Sets/src/mage/cards/g/Guile.java b/Mage.Sets/src/mage/cards/g/Guile.java index d4ddadcd01c..057c1cec99b 100644 --- a/Mage.Sets/src/mage/cards/g/Guile.java +++ b/Mage.Sets/src/mage/cards/g/Guile.java @@ -86,7 +86,7 @@ class GuileReplacementEffect extends ReplacementEffectImpl { Card spellCard = spell.getCard(); if (spellCard != null && controller.chooseUse(Outcome.PlayForFree, "Play " + spellCard.getIdName() + " for free?", source, game)) { - controller.playCard(spellCard, game, true, true, new ApprovingObject(source, game)); + controller.playCard(spellCard, game, true, new ApprovingObject(source, game)); } return true; } diff --git a/Mage.Sets/src/mage/cards/h/HordeOfNotions.java b/Mage.Sets/src/mage/cards/h/HordeOfNotions.java index 0fbbdfe19b4..eaf2125caf2 100644 --- a/Mage.Sets/src/mage/cards/h/HordeOfNotions.java +++ b/Mage.Sets/src/mage/cards/h/HordeOfNotions.java @@ -84,7 +84,7 @@ class HordeOfNotionsEffect extends OneShotEffect { if (controller != null) { Card card = game.getCard(getTargetPointer().getFirst(game, source)); if (card != null && controller.chooseUse(outcome, "Play " + card.getName() + " from your graveyard for free?", source, game)) { - controller.playCard(card, game, true, true, new ApprovingObject(source, game)); + controller.playCard(card, game, true, new ApprovingObject(source, game)); } return true; } diff --git a/Mage.Sets/src/mage/cards/k/KnowledgePool.java b/Mage.Sets/src/mage/cards/k/KnowledgePool.java index c4af8384624..c813eea3cb1 100644 --- a/Mage.Sets/src/mage/cards/k/KnowledgePool.java +++ b/Mage.Sets/src/mage/cards/k/KnowledgePool.java @@ -157,7 +157,9 @@ class KnowledgePoolEffect2 extends OneShotEffect { if (player.choose(Outcome.PlayForFree, game.getExile().getExileZone(exileZoneId), target, game)) { Card card = game.getCard(target.getFirstTarget()); if (card != null && !card.getId().equals(spell.getSourceId())) { - player.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + player.cast(player.chooseAbilityForCast(card, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); } } } diff --git a/Mage.Sets/src/mage/cards/l/LeafCrownedElder.java b/Mage.Sets/src/mage/cards/l/LeafCrownedElder.java index 4369e88b393..370a1db7352 100644 --- a/Mage.Sets/src/mage/cards/l/LeafCrownedElder.java +++ b/Mage.Sets/src/mage/cards/l/LeafCrownedElder.java @@ -63,7 +63,7 @@ class LeafCrownedElderPlayEffect extends OneShotEffect { Card card = game.getCard(getTargetPointer().getFirst(game, source)); if (controller != null && card != null) { if (controller.chooseUse(Outcome.PlayForFree, "Play " + card.getIdName() + " without paying its mana cost?", source, game)) { - controller.playCard(card, game, true, true, new ApprovingObject(source, game)); + controller.playCard(card, game, true, new ApprovingObject(source, game)); } return true; } diff --git a/Mage.Sets/src/mage/cards/o/OmenMachine.java b/Mage.Sets/src/mage/cards/o/OmenMachine.java index b0c7a5bf1de..dc23dc54aec 100644 --- a/Mage.Sets/src/mage/cards/o/OmenMachine.java +++ b/Mage.Sets/src/mage/cards/o/OmenMachine.java @@ -100,7 +100,9 @@ class OmenMachineEffect2 extends OneShotEffect { if (card.isLand(game)) { player.moveCards(card, Zone.BATTLEFIELD, source, game); } else { - player.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + player.cast(player.chooseAbilityForCast(card, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); } } return true; diff --git a/Mage.Sets/src/mage/cards/p/PossibilityStorm.java b/Mage.Sets/src/mage/cards/p/PossibilityStorm.java index 273fff0ad1a..10dafb8feac 100644 --- a/Mage.Sets/src/mage/cards/p/PossibilityStorm.java +++ b/Mage.Sets/src/mage/cards/p/PossibilityStorm.java @@ -128,7 +128,9 @@ class PossibilityStormEffect extends OneShotEffect { && !card.isLand(game) && card.getSpellAbility().canChooseTarget(game, spellController.getId())) { if (spellController.chooseUse(Outcome.PlayForFree, "Cast " + card.getLogName() + " without paying cost?", source, game)) { - spellController.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + spellController.cast(spellController.chooseAbilityForCast(card, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); } } diff --git a/Mage.Sets/src/mage/cards/s/SpellQueller.java b/Mage.Sets/src/mage/cards/s/SpellQueller.java index 70a2a9e74a5..219a71856f1 100644 --- a/Mage.Sets/src/mage/cards/s/SpellQueller.java +++ b/Mage.Sets/src/mage/cards/s/SpellQueller.java @@ -141,7 +141,9 @@ class SpellQuellerLeavesEffect extends OneShotEffect { Player cardOwner = game.getPlayer(card.getOwnerId()); if (cardOwner != null) { if (cardOwner.chooseUse(Outcome.PlayForFree, "Cast " + card.getLogName() + " without paying cost?", source, game)) { - cardOwner.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + cardOwner.cast(cardOwner.chooseAbilityForCast(card, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); } } } diff --git a/Mage.Sets/src/mage/cards/s/Spellshift.java b/Mage.Sets/src/mage/cards/s/Spellshift.java index 274482596d2..2d0f5622767 100644 --- a/Mage.Sets/src/mage/cards/s/Spellshift.java +++ b/Mage.Sets/src/mage/cards/s/Spellshift.java @@ -78,7 +78,9 @@ class SpellshiftEffect extends OneShotEffect { } spellController.revealCards(source, cardsToReveal, game); if (toCast != null && spellController.chooseUse(outcome, "Cast " + toCast.getLogName() + " without paying its mana cost?", source, game)) { - spellController.cast(toCast.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + toCast.getId(), Boolean.TRUE); + spellController.cast(spellController.chooseAbilityForCast(toCast, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + toCast.getId(), null); } spellController.shuffleLibrary(source, game); return true; diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java index ca70b2d3079..0bb6b35bbd3 100644 --- a/Mage.Sets/src/mage/cards/w/WordOfCommand.java +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -115,7 +115,7 @@ class WordOfCommandEffect extends OneShotEffect { boolean canPlay = checkPlayability(card, targetPlayer, game, source); while (canPlay && targetPlayer.canRespond() - && !targetPlayer.playCard(card, game, false, true, new ApprovingObject(source, game))) { + && !targetPlayer.playCard(card, game, false, new ApprovingObject(source, game))) { SpellAbility spellAbility = card.getSpellAbility(); if (spellAbility != null) { spellAbility.getManaCostsToPay().clear(); diff --git a/Mage.Sets/src/mage/cards/y/YennettCrypticSovereign.java b/Mage.Sets/src/mage/cards/y/YennettCrypticSovereign.java index 0caca766001..c6e65bb05b1 100644 --- a/Mage.Sets/src/mage/cards/y/YennettCrypticSovereign.java +++ b/Mage.Sets/src/mage/cards/y/YennettCrypticSovereign.java @@ -92,7 +92,9 @@ class YennettCrypticSovereignEffect extends OneShotEffect { player.revealCards(source, new CardsImpl(card), game); if (card.getManaValue() % 2 == 1) { if (player.chooseUse(outcome, "Cast " + card.getLogName() + " without paying its mana cost?", source, game)) { - player.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + player.cast(player.chooseAbilityForCast(card, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); } else { /* 7/13/2018 | If the revealed card doesn’t have an odd converted mana cost or if that card does but you diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index d7ff1c66a5d..da125caed00 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -3092,8 +3092,8 @@ public class TestPlayer implements Player { } @Override - public boolean playCard(Card card, Game game, boolean noMana, boolean ignoreTiming, ApprovingObject approvingObject) { - return computerPlayer.playCard(card, game, noMana, ignoreTiming, approvingObject); + public boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject) { + return computerPlayer.playCard(card, game, noMana, approvingObject); } @Override diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index 8277b202bc8..8a408f0c736 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -601,7 +601,7 @@ public class PlayerStub implements Player { } @Override - public boolean playCard(Card card, Game game, boolean noMana, boolean checkTiming, ApprovingObject approvingObject) { + public boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject) { return false; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/CastCardFromOutsideTheGameEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CastCardFromOutsideTheGameEffect.java index a566afcc6ac..4b032cfc69c 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CastCardFromOutsideTheGameEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CastCardFromOutsideTheGameEffect.java @@ -72,7 +72,9 @@ public class CastCardFromOutsideTheGameEffect extends OneShotEffect { if (player.choose(Outcome.Benefit, filteredCards, target, game)) { Card card = player.getSideboard().get(target.getFirstTarget(), game); if (card != null) { - player.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + player.cast(player.chooseAbilityForCast(card, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); } } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/HideawayPlayEffect.java b/Mage/src/main/java/mage/abilities/effects/common/HideawayPlayEffect.java index 63772bab379..ec54ab19a98 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/HideawayPlayEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/HideawayPlayEffect.java @@ -67,7 +67,7 @@ public class HideawayPlayEffect extends OneShotEffect { } } - if (!controller.playCard(card, game, true, true, new ApprovingObject(source, game))) { + if (!controller.playCard(card, game, true, new ApprovingObject(source, game))) { if (card.getZoneChangeCounter(game) == zcc) { card.setFaceDown(true, game); } diff --git a/Mage/src/main/java/mage/abilities/keyword/RippleAbility.java b/Mage/src/main/java/mage/abilities/keyword/RippleAbility.java index 35d959a2ee4..d1c9ef37cc5 100644 --- a/Mage/src/main/java/mage/abilities/keyword/RippleAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/RippleAbility.java @@ -88,7 +88,7 @@ class RippleEffect extends OneShotEffect { if (!player.chooseUse(Outcome.Neutral, "Reveal " + rippleNumber + " cards from the top of your library?", source, game)) { return true; //fizzle } - // reveal to/**/p cards from library + // reveal top cards from library Cards cards = new CardsImpl(); cards.addAll(player.getLibrary().getTopCards(game, rippleNumber)); player.revealCards(sourceObject.getIdName(), cards, game); @@ -104,7 +104,10 @@ class RippleEffect extends OneShotEffect { while (player.canRespond() && cards.count(sameNameFilter, game) > 0 && player.choose(Outcome.PlayForFree, cards, target1, game)) { Card card = cards.get(target1.getFirstTarget(), game); if (card != null) { - player.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + player.cast(player.chooseAbilityForCast(card, game, true), game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + cards.remove(card); } target1.clearChosen(); diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 35adef2c317..abd1239872f 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -449,19 +449,15 @@ public interface Player extends MageItem, Copyable { boolean canPlayLand(); /** - * Plays a card if possible + * Plays a card (play land or cast spell). Works from any zones without timing restriction * * @param card the card that can be cast * @param game - * @param noMana if it's a spell i can be cast without paying mana - * @param ignoreTiming if it's cast during the resolution of another - * spell no sorcery or play land timing restriction - * are checked. For a land it has to be the turn of - * the player playing that card. + * @param noMana if it's a spell it can be cast without paying mana * @param approvingObject reference to the ability that allows to play the card * @return */ - boolean playCard(Card card, Game game, boolean noMana, boolean ignoreTiming, ApprovingObject approvingObject); + boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject); /** * @param card the land card to play diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index d66ad138d08..d5bfc7ef5b6 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1128,16 +1128,21 @@ public abstract class PlayerImpl implements Player, Serializable { } @Override - public boolean playCard(Card card, Game game, boolean noMana, boolean ignoreTiming, ApprovingObject approvingObject) { + public boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject) { if (card == null) { return false; } + + // play without timing and from any zone boolean result; if (card.isLand(game)) { - result = playLand(card, game, ignoreTiming); + result = playLand(card, game, true); } else { - result = cast(card.getSpellAbility(), game, noMana, approvingObject); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + result = cast(this.chooseAbilityForCast(card, game, noMana), game, noMana, approvingObject); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); } + if (!result) { game.informPlayer(this, "You can't play " + card.getIdName() + '.'); } From b81b02b6c34b53d7bfac3aef837cb780474ca72d Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 21 Sep 2021 09:14:46 -0400 Subject: [PATCH 162/231] [MIC] Implemented Empty the Laboratory --- .../src/mage/cards/e/EmptyTheLaboratory.java | 110 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 111 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/e/EmptyTheLaboratory.java diff --git a/Mage.Sets/src/mage/cards/e/EmptyTheLaboratory.java b/Mage.Sets/src/mage/cards/e/EmptyTheLaboratory.java new file mode 100644 index 00000000000..74e0868a828 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EmptyTheLaboratory.java @@ -0,0 +1,110 @@ +package mage.cards.e; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EmptyTheLaboratory extends CardImpl { + + public EmptyTheLaboratory(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{U}{U}"); + + // Sacrifice X Zombies, then reveal cards from the top of your library until you reveal a number of Zombie creature cards equal to the number of Zombies sacrificed this way. Put those cards onto the battlefield and the rest on the bottom of your library in a random order. + this.getSpellAbility().addEffect(new EmptyTheLaboratoryEffect()); + } + + private EmptyTheLaboratory(final EmptyTheLaboratory card) { + super(card); + } + + @Override + public EmptyTheLaboratory copy() { + return new EmptyTheLaboratory(this); + } +} + +class EmptyTheLaboratoryEffect extends OneShotEffect { + + private static final FilterPermanent filter = new FilterControlledPermanent(SubType.ZOMBIE, "Zombies"); + private static final FilterCard filter2 = new FilterCreatureCard(); + + static { + filter2.add(SubType.ZOMBIE.getPredicate()); + } + + EmptyTheLaboratoryEffect() { + super(Outcome.Benefit); + staticText = "sacrifice X Zombies, then reveal cards from the top of your library until you reveal " + + "a number of Zombie creature cards equal to the number of Zombies sacrificed this way. " + + "Put those cards onto the battlefield and the rest on the bottom of your library in a random order"; + } + + private EmptyTheLaboratoryEffect(final EmptyTheLaboratoryEffect effect) { + super(effect); + } + + @Override + public EmptyTheLaboratoryEffect copy() { + return new EmptyTheLaboratoryEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + int toSacrifice = Math.min( + source.getManaCostsToPay().getX(), + game.getBattlefield().count( + filter, source.getSourceId(), source.getControllerId(), game + ) + ); + if (toSacrifice < 1) { + return false; + } + TargetPermanent target = new TargetPermanent(toSacrifice, filter); + target.setNotTarget(true); + player.choose(Outcome.Sacrifice, target, source.getSourceId(), game); + int sacrificed = 0; + for (UUID permanentId : target.getTargets()) { + Permanent permanent = game.getPermanent(permanentId); + if (permanent != null && permanent.sacrifice(source, game)) { + sacrificed++; + } + } + Cards toReveal = new CardsImpl(); + int zombies = 0; + for (Card card : player.getLibrary().getCards(game)) { + toReveal.add(card); + if (card.isCreature(game) && card.hasSubtype(SubType.ZOMBIE, game)) { + zombies++; + } + if (zombies >= sacrificed) { + break; + } + } + player.revealCards(source, toReveal, game); + player.moveCards(toReveal.getCards(filter2, game), Zone.BATTLEFIELD, source, game); + toReveal.retainZone(Zone.LIBRARY, game); + player.putCardsOnBottomOfLibrary(toReveal, game, source, false); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index d1a6e4b6e5c..95457715b7a 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -69,6 +69,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Eater of Hope", 115, Rarity.RARE, mage.cards.e.EaterOfHope.class)); cards.add(new SetCardInfo("Elite Scaleguard", 85, Rarity.UNCOMMON, mage.cards.e.EliteScaleguard.class)); cards.add(new SetCardInfo("Eloise, Nephalia Sleuth", 3, Rarity.MYTHIC, mage.cards.e.EloiseNephaliaSleuth.class)); + cards.add(new SetCardInfo("Empty the Laboratory", 14, Rarity.RARE, mage.cards.e.EmptyTheLaboratory.class)); cards.add(new SetCardInfo("Endless Ranks of the Dead", 116, Rarity.RARE, mage.cards.e.EndlessRanksOfTheDead.class)); cards.add(new SetCardInfo("Enduring Scalelord", 149, Rarity.UNCOMMON, mage.cards.e.EnduringScalelord.class)); cards.add(new SetCardInfo("Eternal Skylord", 99, Rarity.UNCOMMON, mage.cards.e.EternalSkylord.class)); From 7a869ff79198069ace79f1fbd8ab64db384cf789 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 21 Sep 2021 09:20:33 -0400 Subject: [PATCH 163/231] [MIC] Implemented Visions of Dread --- .../src/mage/cards/v/VisionsOfDread.java | 82 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 83 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/v/VisionsOfDread.java diff --git a/Mage.Sets/src/mage/cards/v/VisionsOfDread.java b/Mage.Sets/src/mage/cards/v/VisionsOfDread.java new file mode 100644 index 00000000000..f86f6534086 --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VisionsOfDread.java @@ -0,0 +1,82 @@ +package mage.cards.v; + +import mage.abilities.Ability; +import mage.abilities.costs.costadjusters.CommanderManaValueAdjuster; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VisionsOfDread extends CardImpl { + + public VisionsOfDread(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}"); + + // Target opponent puts a creature card of their choice from their graveyard onto the battlefield under your control. + this.getSpellAbility().addEffect(new VisionsOfDreadEffect()); + this.getSpellAbility().addTarget(new TargetOpponent()); + + // Flashback {8}{B}{B}. This spell costs {X} less to cast this way, where X is the greatest mana value of a commander you own on the battlefield or in the command zone. + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{8}{B}{B}")) + .setAbilityName("This spell costs {X} less to cast this way, where X is the greatest mana value " + + "of a commander you own on the battlefield or in the command zone.") + .setCostAdjuster(CommanderManaValueAdjuster.instance)); + } + + private VisionsOfDread(final VisionsOfDread card) { + super(card); + } + + @Override + public VisionsOfDread copy() { + return new VisionsOfDread(this); + } +} + +class VisionsOfDreadEffect extends OneShotEffect { + + VisionsOfDreadEffect() { + super(Outcome.Benefit); + staticText = "target opponent puts a creature card of their choice " + + "from their graveyard onto the battlefield under your control"; + } + + private VisionsOfDreadEffect(final VisionsOfDreadEffect effect) { + super(effect); + } + + @Override + public VisionsOfDreadEffect copy() { + return new VisionsOfDreadEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Player opponent = game.getPlayer(source.getFirstTarget()); + if (controller == null || opponent == null + || opponent.getGraveyard().count(StaticFilters.FILTER_CARD_CREATURE, game) < 1) { + return false; + } + TargetCardInYourGraveyard target = new TargetCardInYourGraveyard( + StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD + ); + target.setNotTarget(true); + opponent.choose(Outcome.Detriment, target, source.getSourceId(), game); + return controller.moveCards(game.getCard(target.getFirstTarget()), Zone.BATTLEFIELD, source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 95457715b7a..fa6b06d2fcd 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -149,6 +149,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Verdurous Gearhulk", 145, Rarity.MYTHIC, mage.cards.v.VerdurousGearhulk.class)); cards.add(new SetCardInfo("Victory's Envoy", 96, Rarity.RARE, mage.cards.v.VictorysEnvoy.class)); cards.add(new SetCardInfo("Visions of Dominance", 37, Rarity.RARE, mage.cards.v.VisionsOfDominance.class)); + cards.add(new SetCardInfo("Visions of Dread", 34, Rarity.RARE, mage.cards.v.VisionsOfDread.class)); cards.add(new SetCardInfo("Visions of Duplicity", 33, Rarity.RARE, mage.cards.v.VisionsOfDuplicity.class)); cards.add(new SetCardInfo("Visions of Glory", 32, Rarity.RARE, mage.cards.v.VisionsOfGlory.class)); cards.add(new SetCardInfo("Visions of Ruin", 36, Rarity.RARE, mage.cards.v.VisionsOfRuin.class)); From 2653b806cbbb5e49f2623d4c9285b2678c6c8383 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 21 Sep 2021 19:56:32 -0400 Subject: [PATCH 164/231] [MIC] fixed Eloise, Nephalia Sleuth triggering off of opposing creatures (fixes #8307) --- .../src/mage/cards/e/EloiseNephaliaSleuth.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Mage.Sets/src/mage/cards/e/EloiseNephaliaSleuth.java b/Mage.Sets/src/mage/cards/e/EloiseNephaliaSleuth.java index 17f13811056..f5f8f03f758 100644 --- a/Mage.Sets/src/mage/cards/e/EloiseNephaliaSleuth.java +++ b/Mage.Sets/src/mage/cards/e/EloiseNephaliaSleuth.java @@ -11,6 +11,8 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; import mage.filter.predicate.permanent.TokenPredicate; import java.util.UUID; @@ -20,10 +22,12 @@ import java.util.UUID; */ public final class EloiseNephaliaSleuth extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("a token"); + private static final FilterPermanent filter = new FilterControlledCreaturePermanent("another creature you control"); + private static final FilterPermanent filter2 = new FilterPermanent("a token"); static { - filter.add(TokenPredicate.TRUE); + filter.add(AnotherPredicate.instance); + filter2.add(TokenPredicate.TRUE); } public EloiseNephaliaSleuth(UUID ownerId, CardSetInfo setInfo) { @@ -36,12 +40,10 @@ public final class EloiseNephaliaSleuth extends CardImpl { this.toughness = new MageInt(4); // Whenever another creature you control dies, investigate. - this.addAbility(new DiesCreatureTriggeredAbility( - new InvestigateEffect(1), false, true - )); + this.addAbility(new DiesCreatureTriggeredAbility(new InvestigateEffect(1), false, filter)); // Whenever you sacrifice a token, surveil 1. - this.addAbility(new SacrificePermanentTriggeredAbility(new SurveilEffect(1), filter)); + this.addAbility(new SacrificePermanentTriggeredAbility(new SurveilEffect(1), filter2)); } private EloiseNephaliaSleuth(final EloiseNephaliaSleuth card) { From 3d1d56270c83380d3bfc12e91e57a9f963d7cf4d Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 21 Sep 2021 20:05:54 -0400 Subject: [PATCH 165/231] [MIC] Implemented Heronblade Elite --- .../src/mage/cards/h/HeronbladeElite.java | 57 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 58 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HeronbladeElite.java diff --git a/Mage.Sets/src/mage/cards/h/HeronbladeElite.java b/Mage.Sets/src/mage/cards/h/HeronbladeElite.java new file mode 100644 index 00000000000..e4b4ec2a3da --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HeronbladeElite.java @@ -0,0 +1,57 @@ +package mage.cards.h; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.SourcePermanentPowerCount; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.VigilanceAbility; +import mage.abilities.mana.AnyColorManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HeronbladeElite extends CardImpl { + + private static final FilterPermanent filter = new FilterCreaturePermanent(SubType.HUMAN, "another Human"); + private static final DynamicValue xValue = new SourcePermanentPowerCount(); + + public HeronbladeElite(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Whenever another Human enters the battlefield under your control, put a +1/+1 counter on Heronblade Elite. + this.addAbility(new EntersBattlefieldControlledTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), filter + )); + + // {T}: Add X mana of any one color, where X is Heronblade Elite's power. + this.addAbility(new AnyColorManaAbility(new TapSourceCost(), xValue, false)); + } + + private HeronbladeElite(final HeronbladeElite card) { + super(card); + } + + @Override + public HeronbladeElite copy() { + return new HeronbladeElite(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index fa6b06d2fcd..5460a3e54ff 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -88,6 +88,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Havengul Runebinder", 101, Rarity.RARE, mage.cards.h.HavengulRunebinder.class)); cards.add(new SetCardInfo("Herald of War", 86, Rarity.RARE, mage.cards.h.HeraldOfWar.class)); cards.add(new SetCardInfo("Heron's Grace Champion", 152, Rarity.RARE, mage.cards.h.HeronsGraceChampion.class)); + cards.add(new SetCardInfo("Heronblade Elite", 26, Rarity.RARE, mage.cards.h.HeronbladeElite.class)); cards.add(new SetCardInfo("Hour of Eternity", 102, Rarity.RARE, mage.cards.h.HourOfEternity.class)); cards.add(new SetCardInfo("Hour of Reckoning", 87, Rarity.RARE, mage.cards.h.HourOfReckoning.class)); cards.add(new SetCardInfo("Inspiring Call", 141, Rarity.UNCOMMON, mage.cards.i.InspiringCall.class)); From aa0fe5528d09760bf817f871086e4a15251ff134 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Tue, 21 Sep 2021 20:32:39 -0400 Subject: [PATCH 166/231] Implement booster collation for Rise of the Eldrazi (#8302) --- Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java | 235 +++++++++++++++++- 1 file changed, 233 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java b/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java index 9e7f6cb6f2c..7a4ee5bea6d 100644 --- a/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java +++ b/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java @@ -2,9 +2,16 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + /** * * @author BetaSteward_at_googlemail.com @@ -18,7 +25,7 @@ public final class RiseOfTheEldrazi extends ExpansionSet { } private RiseOfTheEldrazi() { - super("Rise of the Eldrazi", "ROE", ExpansionSet.buildDate(2010, 3, 17), SetType.EXPANSION); + super("Rise of the Eldrazi", "ROE", ExpansionSet.buildDate(2010, 3, 17), SetType.EXPANSION, new RiseOfTheEldraziCollator()); this.blockName = "Zendikar"; this.parentSet = Zendikar.getInstance(); this.hasBoosters = true; @@ -276,5 +283,229 @@ public final class RiseOfTheEldrazi extends ExpansionSet { cards.add(new SetCardInfo("Zof Shade", 132, Rarity.COMMON, mage.cards.z.ZofShade.class)); cards.add(new SetCardInfo("Zulaport Enforcer", 133, Rarity.COMMON, mage.cards.z.ZulaportEnforcer.class)); } - +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/roe.html +// Using USA collation +class RiseOfTheEldraziCollator implements BoosterCollator { + private static class RiseOfTheEldraziRun extends CardRun { + private static final RiseOfTheEldraziRun commonA = new RiseOfTheEldraziRun(true, "153", "16", "186", "60", "228", "148", "111", "145", "29", "213", "72", "110", "138", "36", "174", "133", "88", "222", "56", "138", "42", "203", "142", "121", "223", "88", "213", "43", "97", "56", "145", "222", "111", "187", "29", "149", "60", "16", "148", "74", "186", "130", "41", "133", "166", "223", "65", "36", "203", "149", "110", "142", "41", "65", "228", "187", "97", "72", "153", "42", "130", "174", "43", "166", "74", "121"); + private static final RiseOfTheEldraziRun commonB = new RiseOfTheEldraziRun(true, "144", "30", "196", "83", "98", "22", "195", "135", "59", "106", "209", "30", "147", "83", "118", "195", "106", "201", "22", "144", "76", "123", "154", "44", "199", "95", "59", "76", "209", "144", "44", "19", "123", "54", "98", "201", "135", "76", "199", "106", "147", "22", "59", "196", "19", "135", "118", "30", "201", "54", "147", "95", "44", "196", "118", "154", "83", "199", "98", "19", "54", "209", "95", "154", "195", "123"); + private static final RiseOfTheEldraziRun commonC1 = new RiseOfTheEldraziRun(true, "132", "50", "85", "207", "136", "116", "175", "26", "5", "85", "99", "155", "78", "207", "15", "150", "108", "86", "27", "210", "164", "5", "202", "99", "79", "23", "182", "136", "114", "155", "175", "78", "23", "132", "182", "15", "164", "114", "68", "210", "171", "193", "50", "86", "150", "27", "68", "202", "108", "26", "79", "171", "116", "193", "5"); + private static final RiseOfTheEldraziRun commonC2 = new RiseOfTheEldraziRun(true, "87", "173", "73", "102", "34", "161", "208", "18", "200", "93", "105", "34", "159", "87", "24", "102", "208", "93", "18", "161", "13", "67", "46", "159", "208", "73", "200", "34", "67", "161", "126", "18", "105", "13", "194", "73", "24", "173", "102", "200", "13", "87", "194", "24", "159", "126", "67", "46", "93", "173", "194", "105", "126", "13", "46"); + private static final RiseOfTheEldraziRun uncommonA = new RiseOfTheEldraziRun(true, "168", "8", "189", "63", "28", "127", "190", "168", "10", "40", "162", "226", "119", "204", "77", "8", "53", "103", "216", "163", "63", "48", "9", "204", "122", "28", "221", "146", "61", "189", "143", "122", "53", "226", "2", "71", "178", "143", "103", "77", "48", "216", "61", "178", "2", "127", "146", "37", "221", "71", "179", "162", "94", "9", "66", "40", "119", "179", "220", "163", "10", "190", "37", "94", "66", "220"); + private static final RiseOfTheEldraziRun uncommonB = new RiseOfTheEldraziRun(true, "137", "80", "224", "49", "115", "217", "205", "137", "92", "45", "185", "115", "70", "49", "134", "217", "104", "181", "20", "81", "180", "170", "92", "14", "128", "157", "109", "181", "35", "58", "167", "104", "157", "224", "180", "35", "81", "131", "80", "170", "128", "188", "45", "167", "109", "20", "185", "58", "134", "205", "14", "70", "131", "188"); + private static final RiseOfTheEldraziRun rareA = new RiseOfTheEldraziRun(true, "158", "47", "198", "12", "129", "7", "52", "191", "215", "64", "160", "113", "219", "39", "84", "3", "218", "31", "125", "212", "158", "184", "25", "112", "75", "156", "84", "11", "225", "31", "211", "219", "107", "172", "25", "191", "227", "62", "214", "3", "160", "7", "57", "52", "107", "156", "125", "184", "6", "225", "64", "47", "211", "215", "152", "39", "198", "129", "11", "112", "62", "172", "21", "218", "227", "57"); + private static final RiseOfTheEldraziRun rareB = new RiseOfTheEldraziRun(true, "38", "176", "91", "139", "140", "183", "117", "51", "90", "169", "124", "177", "100", "55", "38", "141", "197", "89", "206", "117", "151", "17", "1", "82", "96", "140", "69", "183", "100", "169", "192", "197", "165", "90", "17", "91", "101", "139", "4", "124", "32", "176", "141", "69", "177", "82", "33", "151", "96", "206", "101", "89", "165", "32", "120"); + private static final RiseOfTheEldraziRun land = new RiseOfTheEldraziRun(false, "229", "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", "240", "241", "242", "243", "244", "245", "246", "247", "248"); + private RiseOfTheEldraziRun(boolean keepOrder, String... numbers) { + super(keepOrder, numbers); + } + } + + private static class RiseOfTheEldraziStructure extends BoosterStructure { + private static final RiseOfTheEldraziStructure AAABC1C1C1C1C1C1 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonC1, + RiseOfTheEldraziRun.commonC1, + RiseOfTheEldraziRun.commonC1, + RiseOfTheEldraziRun.commonC1, + RiseOfTheEldraziRun.commonC1, + RiseOfTheEldraziRun.commonC1 + ); + private static final RiseOfTheEldraziStructure AAABBC1C1C1C1C1 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonC1, + RiseOfTheEldraziRun.commonC1, + RiseOfTheEldraziRun.commonC1, + RiseOfTheEldraziRun.commonC1, + RiseOfTheEldraziRun.commonC1 + ); + private static final RiseOfTheEldraziStructure AAABC2C2C2C2C2C2 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2 + ); + private static final RiseOfTheEldraziStructure AAABBC2C2C2C2C2 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2 + ); + private static final RiseOfTheEldraziStructure AAABBBC2C2C2C2 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2 + ); + private static final RiseOfTheEldraziStructure AAAABBBC2C2C2 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2 + ); + private static final RiseOfTheEldraziStructure AAAABBBBC2C2 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonA, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonB, + RiseOfTheEldraziRun.commonC2, + RiseOfTheEldraziRun.commonC2 + ); + private static final RiseOfTheEldraziStructure AAB = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.uncommonA, + RiseOfTheEldraziRun.uncommonA, + RiseOfTheEldraziRun.uncommonB + ); + private static final RiseOfTheEldraziStructure ABB = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.uncommonA, + RiseOfTheEldraziRun.uncommonB, + RiseOfTheEldraziRun.uncommonB + ); + private static final RiseOfTheEldraziStructure R1 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.rareA + ); + private static final RiseOfTheEldraziStructure R2 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.rareB + ); + private static final RiseOfTheEldraziStructure L1 = new RiseOfTheEldraziStructure( + RiseOfTheEldraziRun.land + ); + + private RiseOfTheEldraziStructure(CardRun... runs) { + super(runs); + } + } + + // In order for equal numbers of each common to exist, the average booster must contain: + // 3.27 A commons (36 / 11) + // 2.18 B commons (24 / 11) + // 2.73 C1 commons (30 / 11, or 60 / 11 in each C1 booster) + // 1.82 C2 commons (20 / 11, or 40 / 11 in each C2 booster) + // These numbers are the same as all sets with 101 commons in A/B/C1/C2 print runs + // The only difference is ROE has two overprinted commons instead of one short-printed + private final RarityConfiguration commonRuns = new RarityConfiguration( + false, + RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, + RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, + RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, + RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, + RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, + RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, + RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, + RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, + RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, + RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, + RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, + + RiseOfTheEldraziStructure.AAABC2C2C2C2C2C2, + RiseOfTheEldraziStructure.AAABBC2C2C2C2C2, + RiseOfTheEldraziStructure.AAABBBC2C2C2C2, + RiseOfTheEldraziStructure.AAABBBC2C2C2C2, + RiseOfTheEldraziStructure.AAABBBC2C2C2C2, + RiseOfTheEldraziStructure.AAAABBBC2C2C2, + RiseOfTheEldraziStructure.AAAABBBC2C2C2, + RiseOfTheEldraziStructure.AAAABBBC2C2C2, + RiseOfTheEldraziStructure.AAAABBBC2C2C2, + RiseOfTheEldraziStructure.AAAABBBC2C2C2, + RiseOfTheEldraziStructure.AAAABBBBC2C2 + ); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 1.65 A uncommons (33 / 20) + // 1.35 B uncommons (27 / 20) + // These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + false, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.AAB, + RiseOfTheEldraziStructure.ABB, + RiseOfTheEldraziStructure.ABB, + RiseOfTheEldraziStructure.ABB, + RiseOfTheEldraziStructure.ABB, + RiseOfTheEldraziStructure.ABB, + RiseOfTheEldraziStructure.ABB, + RiseOfTheEldraziStructure.ABB + ); + private final RarityConfiguration rareRuns = new RarityConfiguration( + false, + RiseOfTheEldraziStructure.R1, + RiseOfTheEldraziStructure.R1, + RiseOfTheEldraziStructure.R1, + RiseOfTheEldraziStructure.R1, + RiseOfTheEldraziStructure.R1, + RiseOfTheEldraziStructure.R1, + RiseOfTheEldraziStructure.R2, + RiseOfTheEldraziStructure.R2, + RiseOfTheEldraziStructure.R2, + RiseOfTheEldraziStructure.R2, + RiseOfTheEldraziStructure.R2 + ); + private final RarityConfiguration landRuns = new RarityConfiguration( + RiseOfTheEldraziStructure.L1 + ); + + @Override + public void shuffle() { + commonRuns.shuffle(); + uncommonRuns.shuffle(); + rareRuns.shuffle(); + landRuns.shuffle(); + } + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(rareRuns.getNext().makeRun()); + booster.addAll(landRuns.getNext().makeRun()); + return booster; + } } From 863d8166f1d06af5bbf17480142b724890abec13 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 22 Sep 2021 07:52:32 -0400 Subject: [PATCH 167/231] [MID] fixed Foul Play only targeting creatures with power 1 or less (fixes #8308) --- Mage.Sets/src/mage/cards/f/FoulPlay.java | 2 +- Mage/src/main/java/mage/constants/ComparisonType.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/f/FoulPlay.java b/Mage.Sets/src/mage/cards/f/FoulPlay.java index e13f2ec8e10..cceb6ba97c5 100644 --- a/Mage.Sets/src/mage/cards/f/FoulPlay.java +++ b/Mage.Sets/src/mage/cards/f/FoulPlay.java @@ -21,7 +21,7 @@ public final class FoulPlay extends CardImpl { private static final FilterPermanent filter = new FilterCreaturePermanent("creature with power 2 or less"); static { - filter.add(new PowerPredicate(ComparisonType.FEWER_THAN, 2)); + filter.add(new PowerPredicate(ComparisonType.FEWER_THAN, 3)); } public FoulPlay(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage/src/main/java/mage/constants/ComparisonType.java b/Mage/src/main/java/mage/constants/ComparisonType.java index f47bd1839c4..cbbeb739114 100644 --- a/Mage/src/main/java/mage/constants/ComparisonType.java +++ b/Mage/src/main/java/mage/constants/ComparisonType.java @@ -4,7 +4,9 @@ package mage.constants; * Created by IGOUDT on 5-3-2017. */ public enum ComparisonType { - MORE_THAN(">", "more", "than"), FEWER_THAN("<", "fewer", "than"), EQUAL_TO("==", "equal", "to"); + FEWER_THAN("<", "fewer", "than"), + EQUAL_TO("==", "equal", "to"), + MORE_THAN(">", "more", "than"); String operator; String text1; From de4afab118ed5704399f454f189fffe062c7f474 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 22 Sep 2021 08:08:14 -0400 Subject: [PATCH 168/231] [MID] Implemented Siphon Insight --- .../src/mage/cards/g/GontiLordOfLuxury.java | 79 +++--- Mage.Sets/src/mage/cards/s/SiphonInsight.java | 244 ++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + 3 files changed, 284 insertions(+), 40 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/s/SiphonInsight.java diff --git a/Mage.Sets/src/mage/cards/g/GontiLordOfLuxury.java b/Mage.Sets/src/mage/cards/g/GontiLordOfLuxury.java index ffd0b7369a2..669690eb3ee 100644 --- a/Mage.Sets/src/mage/cards/g/GontiLordOfLuxury.java +++ b/Mage.Sets/src/mage/cards/g/GontiLordOfLuxury.java @@ -29,8 +29,6 @@ import java.util.UUID; */ public final class GontiLordOfLuxury extends CardImpl { - protected static final String VALUE_PREFIX = "ExileZones"; - public GontiLordOfLuxury(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{B}"); addSuperType(SuperType.LEGENDARY); @@ -62,6 +60,8 @@ public final class GontiLordOfLuxury extends CardImpl { class GontiLordOfLuxuryEffect extends OneShotEffect { + private static final String VALUE_PREFIX = "ExileZones"; + public GontiLordOfLuxuryEffect() { super(Outcome.Benefit); this.staticText = "look at the top four cards of target opponent's library, exile one of them face down, then put the rest on the bottom of that library in a random order. You may look at and cast that card for as long as it remains exiled, and you may spend mana as though it were mana of any type to cast that spell"; @@ -81,48 +81,47 @@ class GontiLordOfLuxuryEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source)); MageObject sourceObject = source.getSourceObject(game); - if (controller != null && opponent != null && sourceObject != null) { - Cards topCards = new CardsImpl(); - topCards.addAll(opponent.getLibrary().getTopCards(game, 4)); - TargetCard target = new TargetCard(Zone.LIBRARY, new FilterCard("card to exile")); - if (controller.choose(outcome, topCards, target, game)) { - Card card = game.getCard(target.getFirstTarget()); - if (card != null) { - topCards.remove(card); - // move card to exile - UUID exileZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter()); - card.setFaceDown(true, game); - if (controller.moveCardsToExile(card, source, game, false, exileZoneId, sourceObject.getIdName())) { - card.setFaceDown(true, game); - Set exileZones = (Set) game.getState().getValue(GontiLordOfLuxury.VALUE_PREFIX + source.getSourceId().toString()); - if (exileZones == null) { - exileZones = new HashSet<>(); - game.getState().setValue(GontiLordOfLuxury.VALUE_PREFIX + source.getSourceId().toString(), exileZones); - } - exileZones.add(exileZoneId); - // allow to cast the card - ContinuousEffect effect = new GontiLordOfLuxuryCastFromExileEffect(); - effect.setTargetPointer(new FixedTarget(card.getId(), game)); - game.addEffect(effect, source); - // and you may spend mana as though it were mana of any color to cast it - effect = new GontiLordOfLuxurySpendAnyManaEffect(); - effect.setTargetPointer(new FixedTarget(card.getId(), game)); - game.addEffect(effect, source); - // For as long as that card remains exiled, you may look at it - effect = new GontiLordOfLuxuryLookEffect(controller.getId()); - effect.setTargetPointer(new FixedTarget(card.getId(), game)); - game.addEffect(effect, source); - } - } - } - // then put the rest on the bottom of that library in a random order + if (controller == null || opponent == null || sourceObject == null) { + return false; + } + Cards topCards = new CardsImpl(); + topCards.addAll(opponent.getLibrary().getTopCards(game, 4)); + TargetCard target = new TargetCard(Zone.LIBRARY, new FilterCard("card to exile")); + controller.choose(outcome, topCards, target, game); + Card card = game.getCard(target.getFirstTarget()); + if (card == null) { controller.putCardsOnBottomOfLibrary(topCards, game, source, false); return true; } - - return false; + topCards.remove(card); + // move card to exile + UUID exileZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter()); + card.setFaceDown(true, game); + if (controller.moveCardsToExile(card, source, game, false, exileZoneId, sourceObject.getIdName())) { + card.setFaceDown(true, game); + Set exileZones = (Set) game.getState().getValue(VALUE_PREFIX + source.getSourceId().toString()); + if (exileZones == null) { + exileZones = new HashSet<>(); + game.getState().setValue(VALUE_PREFIX + source.getSourceId().toString(), exileZones); + } + exileZones.add(exileZoneId); + // allow to cast the card + ContinuousEffect effect = new GontiLordOfLuxuryCastFromExileEffect(); + effect.setTargetPointer(new FixedTarget(card.getId(), game)); + game.addEffect(effect, source); + // and you may spend mana as though it were mana of any color to cast it + effect = new GontiLordOfLuxurySpendAnyManaEffect(); + effect.setTargetPointer(new FixedTarget(card.getId(), game)); + game.addEffect(effect, source); + // For as long as that card remains exiled, you may look at it + effect = new GontiLordOfLuxuryLookEffect(controller.getId()); + effect.setTargetPointer(new FixedTarget(card.getId(), game)); + game.addEffect(effect, source); + } + // then put the rest on the bottom of that library in a random order + controller.putCardsOnBottomOfLibrary(topCards, game, source, false); + return true; } - } class GontiLordOfLuxuryCastFromExileEffect extends AsThoughEffectImpl { diff --git a/Mage.Sets/src/mage/cards/s/SiphonInsight.java b/Mage.Sets/src/mage/cards/s/SiphonInsight.java new file mode 100644 index 00000000000..b3603389046 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SiphonInsight.java @@ -0,0 +1,244 @@ +package mage.cards.s; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.AsThoughManaEffect; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.*; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.players.ManaPoolItem; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetOpponent; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 plus everyone who worked on Gonti + */ +public final class SiphonInsight extends CardImpl { + + public SiphonInsight(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}{B}"); + + // Look at the top two cards of target opponent's library. Exile one of them face down and put the other on the bottom of that library. You may look at and play the exiled card for as long as it remains exiled, and you may spend mana as though it were mana of any color to cast that spell. + this.getSpellAbility().addEffect(new SiphonInsightEffect()); + this.getSpellAbility().addTarget(new TargetOpponent()); + + // Flashback {1}{U}{B} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{U}{B}"))); + } + + private SiphonInsight(final SiphonInsight card) { + super(card); + } + + @Override + public SiphonInsight copy() { + return new SiphonInsight(this); + } +} + +class SiphonInsightEffect extends OneShotEffect { + + private static final String VALUE_PREFIX = "ExileZones"; + + public SiphonInsightEffect() { + super(Outcome.Benefit); + this.staticText = "look at the top two cards of target opponent's library. " + + "Exile one of them face down and put the other on the bottom of that library. " + + "You may look at and play the exiled card for as long as it remains exiled, " + + "and you may spend mana as though it were mana of any color to cast that spell"; + } + + private SiphonInsightEffect(final SiphonInsightEffect effect) { + super(effect); + } + + @Override + public SiphonInsightEffect copy() { + return new SiphonInsightEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source)); + MageObject sourceObject = source.getSourceObject(game); + if (controller == null || opponent == null || sourceObject == null) { + return false; + } + Cards topCards = new CardsImpl(); + topCards.addAll(opponent.getLibrary().getTopCards(game, 2)); + TargetCard target = new TargetCard(Zone.LIBRARY, new FilterCard("card to exile")); + controller.choose(outcome, topCards, target, game); + Card card = game.getCard(target.getFirstTarget()); + if (card == null) { + controller.putCardsOnBottomOfLibrary(topCards, game, source, false); + return true; + } + topCards.remove(card); + // move card to exile + UUID exileZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter()); + card.setFaceDown(true, game); + if (controller.moveCardsToExile(card, source, game, false, exileZoneId, sourceObject.getIdName())) { + card.setFaceDown(true, game); + Set exileZones = (Set) game.getState().getValue(VALUE_PREFIX + source.getSourceId().toString()); + if (exileZones == null) { + exileZones = new HashSet<>(); + game.getState().setValue(VALUE_PREFIX + source.getSourceId().toString(), exileZones); + } + exileZones.add(exileZoneId); + // allow to cast the card + ContinuousEffect effect = new SiphonInsightCastFromExileEffect(); + effect.setTargetPointer(new FixedTarget(card.getId(), game)); + game.addEffect(effect, source); + // and you may spend mana as though it were mana of any color to cast it + effect = new SiphonInsightSpendAnyManaEffect(); + effect.setTargetPointer(new FixedTarget(card.getId(), game)); + game.addEffect(effect, source); + // For as long as that card remains exiled, you may look at it + effect = new SiphonInsightLookEffect(controller.getId()); + effect.setTargetPointer(new FixedTarget(card.getId(), game)); + game.addEffect(effect, source); + } + // then put the rest on the bottom of that library in a random order + controller.putCardsOnBottomOfLibrary(topCards, game, source, false); + return true; + } +} + +class SiphonInsightCastFromExileEffect extends AsThoughEffectImpl { + + public SiphonInsightCastFromExileEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.Custom, Outcome.Benefit); + staticText = "You may cast that card for as long as it remains exiled, and you may spend mana as though it were mana of any color to cast that spell"; + } + + private SiphonInsightCastFromExileEffect(final SiphonInsightCastFromExileEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public SiphonInsightCastFromExileEffect copy() { + return new SiphonInsightCastFromExileEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + UUID targetId = getTargetPointer().getFirst(game, source); + if (targetId == null) { + this.discard(); // card is no longer in the origin zone, effect can be discarded + return false; + } + Card theCard = game.getCard(objectId); + if (theCard == null || theCard.isLand(game)) { + return false; + } + objectId = theCard.getMainCard().getId(); // for split cards + + if (objectId.equals(targetId) + && affectedControllerId.equals(source.getControllerId())) { + Card card = game.getCard(objectId); + // TODO: Allow to cast Zoetic Cavern face down + return card != null; + } + return false; + } +} + +class SiphonInsightSpendAnyManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { + + public SiphonInsightSpendAnyManaEffect() { + super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.Custom, Outcome.Benefit); + staticText = "you may spend mana as though it were mana of any color to cast it"; + } + + private SiphonInsightSpendAnyManaEffect(final SiphonInsightSpendAnyManaEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public SiphonInsightSpendAnyManaEffect copy() { + return new SiphonInsightSpendAnyManaEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + Card theCard = game.getCard(objectId); + if (theCard == null) { + return false; + } + objectId = theCard.getMainCard().getId(); // for split cards + if (objectId.equals(((FixedTarget) getTargetPointer()).getTarget()) + && game.getState().getZoneChangeCounter(objectId) <= ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1) { + // if the card moved from exile to spell the zone change counter is increased by 1 (effect must applies before and on stack, use isCheckPlayableMode?) + return source.isControlledBy(affectedControllerId); + } else if (((FixedTarget) getTargetPointer()).getTarget().equals(objectId)) { + // object has moved zone so effect can be discarded + this.discard(); + } + return false; + } + + @Override + public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { + return mana.getFirstAvailable(); + } +} + +class SiphonInsightLookEffect extends AsThoughEffectImpl { + + private final UUID authorizedPlayerId; + + public SiphonInsightLookEffect(UUID authorizedPlayerId) { + super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.Benefit); + this.authorizedPlayerId = authorizedPlayerId; + staticText = "You may look at the cards exiled with {this}"; + } + + private SiphonInsightLookEffect(final SiphonInsightLookEffect effect) { + super(effect); + this.authorizedPlayerId = effect.authorizedPlayerId; + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public SiphonInsightLookEffect copy() { + return new SiphonInsightLookEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + UUID cardId = getTargetPointer().getFirst(game, source); + if (cardId == null) { + this.discard(); // card is no longer in the origin zone, effect can be discarded + } + return affectedControllerId.equals(authorizedPlayerId) + && objectId.equals(cardId); + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 43457a77152..e8484c9a1ab 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -276,6 +276,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Sigarda, Champion of Light", 240, Rarity.MYTHIC, mage.cards.s.SigardaChampionOfLight.class)); cards.add(new SetCardInfo("Sigardian Savior", 34, Rarity.MYTHIC, mage.cards.s.SigardianSavior.class)); cards.add(new SetCardInfo("Silver Bolt", 258, Rarity.COMMON, mage.cards.s.SilverBolt.class)); + cards.add(new SetCardInfo("Siphon Insight", 241, Rarity.RARE, mage.cards.s.SiphonInsight.class)); cards.add(new SetCardInfo("Skaab Wrangler", 75, Rarity.UNCOMMON, mage.cards.s.SkaabWrangler.class)); cards.add(new SetCardInfo("Slaughter Specialist", 122, Rarity.RARE, mage.cards.s.SlaughterSpecialist.class)); cards.add(new SetCardInfo("Slogurk, the Overslime", 242, Rarity.RARE, mage.cards.s.SlogurkTheOverslime.class)); From aec68ac9d5666340ed0d2ae7d10c20fcaaca906a Mon Sep 17 00:00:00 2001 From: "Raphael \"who?!\" Kehldorfer" Date: Wed, 22 Sep 2021 14:54:13 +0200 Subject: [PATCH 169/231] [MID] Implemented Teferi, Who Slows the Sunset (#8267) * [MID] Implemented Teferi, Who Slows the Sunset * [MID] Refactored Teferi, Who Slows the Sunset: - Add Targets during activation ? Don't know where / how to properly implement the Emblem effect yet * [MID] Refactored Teferi, Who Slows the Sunset: - Add Targets during activation ? Don't know where / how to properly implement the Emblem effect yet --- .../mage/cards/t/TeferiWhoSlowsTheSunset.java | 99 +++++++++++++++++++ .../src/mage/sets/InnistradMidnightHunt.java | 1 + .../TeferiWhoSlowsTheSunsetEmblem.java | 19 ++++ 3 files changed, 119 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TeferiWhoSlowsTheSunset.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/TeferiWhoSlowsTheSunsetEmblem.java diff --git a/Mage.Sets/src/mage/cards/t/TeferiWhoSlowsTheSunset.java b/Mage.Sets/src/mage/cards/t/TeferiWhoSlowsTheSunset.java new file mode 100644 index 00000000000..bcbbc31e51c --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TeferiWhoSlowsTheSunset.java @@ -0,0 +1,99 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.PlaneswalkerEntersWithLoyaltyCountersAbility; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.GetEmblemEffect; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.game.command.emblems.TeferiWhoSlowsTheSunsetEmblem; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.Target; +import mage.target.common.TargetArtifactPermanent; +import mage.target.common.TargetCreaturePermanent; +import mage.target.common.TargetLandPermanent; + +import java.util.UUID; + +/** + * @author LePwnerer + */ +public final class TeferiWhoSlowsTheSunset extends CardImpl { + + public TeferiWhoSlowsTheSunset(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{W}{U}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.TEFERI); + this.addAbility(new PlaneswalkerEntersWithLoyaltyCountersAbility(4)); + + // +1: Choose up to one target artifact, up to one target creature, and up to one target land. Untap the chosen permanents you control. Tap the chosen permanents you don't control. You gain 2 life. + Ability ability = new LoyaltyAbility(new TeferiWhoSlowsTheSunsetEffect(), 1); + ability.addTarget(new TargetArtifactPermanent()); + ability.addTarget(new TargetCreaturePermanent()); + ability.addTarget(new TargetLandPermanent()); + this.addAbility(ability); + + // −2: Look at the top three cards of your library. Put one of them into your hand and the rest on the bottom of your library in any order. + this.addAbility(new LoyaltyAbility(new LookLibraryAndPickControllerEffect(StaticValue.get(3), false, StaticValue.get(1), new FilterCard("card"), false, false), -2)); + + // −7: You get an emblem with "Untap all permanents you control during each opponent's untap step" and "You draw a card during each opponent's draw step." + this.addAbility(new LoyaltyAbility(new GetEmblemEffect(new TeferiWhoSlowsTheSunsetEmblem()), -7)); + } + + private TeferiWhoSlowsTheSunset(final TeferiWhoSlowsTheSunset card) { + super(card); + } + + @Override + public TeferiWhoSlowsTheSunset copy() { + return new TeferiWhoSlowsTheSunset(this); + } +} + +class TeferiWhoSlowsTheSunsetEffect extends OneShotEffect { + + TeferiWhoSlowsTheSunsetEffect() { + super(Outcome.Benefit); + staticText = "Choose up to one target artifact, up to one target creature, and up to one target land. " + + "Untap the chosen permanents you control. " + + "Tap the chosen permanents you don't control. "; + } + + private TeferiWhoSlowsTheSunsetEffect(final TeferiWhoSlowsTheSunsetEffect effect) { + super(effect); + } + + @Override + public TeferiWhoSlowsTheSunsetEffect copy() { + return new TeferiWhoSlowsTheSunsetEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + for (Target target : source.getTargets()) { + Permanent targetPermanent = game.getPermanent(target.getFirstTarget()); + if (targetPermanent != null) { + if (targetPermanent.getControllerId() == player.getId()) { + targetPermanent.untap(game); + } else { + targetPermanent.tap(source, game); + } + } + } + + player.gainLife(2, game, source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index e8484c9a1ab..0361ced3dd4 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -308,6 +308,7 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.add(new SetCardInfo("Tapping at the Window", 201, Rarity.COMMON, mage.cards.t.TappingAtTheWindow.class)); cards.add(new SetCardInfo("Tavern Ruffian", 163, Rarity.COMMON, mage.cards.t.TavernRuffian.class)); cards.add(new SetCardInfo("Tavern Smasher", 163, Rarity.COMMON, mage.cards.t.TavernSmasher.class)); + cards.add(new SetCardInfo("Teferi, Who Slows the Sunset", 245, Rarity.MYTHIC, mage.cards.t.TeferiWhoSlowsTheSunset.class)); cards.add(new SetCardInfo("The Meathook Massacre", 112, Rarity.MYTHIC, mage.cards.t.TheMeathookMassacre.class)); cards.add(new SetCardInfo("Thermo-Alchemist", 164, Rarity.UNCOMMON, mage.cards.t.ThermoAlchemist.class)); cards.add(new SetCardInfo("Thraben Exorcism", 39, Rarity.COMMON, mage.cards.t.ThrabenExorcism.class)); diff --git a/Mage/src/main/java/mage/game/command/emblems/TeferiWhoSlowsTheSunsetEmblem.java b/Mage/src/main/java/mage/game/command/emblems/TeferiWhoSlowsTheSunsetEmblem.java new file mode 100644 index 00000000000..99ba626e571 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/TeferiWhoSlowsTheSunsetEmblem.java @@ -0,0 +1,19 @@ +package mage.game.command.emblems; + +import mage.abilities.common.BeginningOfDrawTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.UntapAllDuringEachOtherPlayersUntapStepEffect; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.filter.common.FilterControlledPermanent; +import mage.game.command.Emblem; + +public class TeferiWhoSlowsTheSunsetEmblem extends Emblem { + // You get an emblem with "Untap all permanents you control during each opponent's untap step" and "You draw a card during each opponent's draw step." + public TeferiWhoSlowsTheSunsetEmblem() { + this.setName("Emblem Teferi"); + this.getAbilities().add(new SimpleStaticAbility(Zone.COMMAND, new UntapAllDuringEachOtherPlayersUntapStepEffect(new FilterControlledPermanent("permanents you control")))); + this.getAbilities().add(new BeginningOfDrawTriggeredAbility(Zone.COMMAND, new DrawCardSourceControllerEffect(1), TargetController.OPPONENT, false)); + } +} From 4ab41a5b12efb32ed416e362839cb81db3afc23a Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 22 Sep 2021 09:39:21 -0400 Subject: [PATCH 170/231] [MID] updated Teferi, Who Slows the Sunset emblem --- .../java/org/mage/test/player/TestPlayer.java | 12 ++- .../java/org/mage/test/stub/PlayerStub.java | 10 +++ .../main/java/mage/filter/StaticFilters.java | 6 ++ .../TeferiWhoSlowsTheSunsetEmblem.java | 44 +++++++++-- .../main/java/mage/game/turn/DrawStep.java | 8 ++ Mage/src/main/java/mage/players/Player.java | 4 + .../main/java/mage/players/PlayerImpl.java | 74 +++++++++++-------- 7 files changed, 120 insertions(+), 38 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index da125caed00..b700d8363f1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -2571,7 +2571,7 @@ public class TestPlayer implements Player { // library if (target.getOriginalTarget() instanceof TargetCardInLibrary - || (target.getOriginalTarget() instanceof TargetCard && target.getOriginalTarget().getZone() == Zone.LIBRARY)) { + || (target.getOriginalTarget() instanceof TargetCard && target.getOriginalTarget().getZone() == Zone.LIBRARY)) { // user don't have access to library, so it must be targeted through list/revealed cards Assert.fail("Library zone is private, you must target through cards list, e.g. revealed: " + target.getOriginalTarget().getClass().getCanonicalName()); } @@ -3714,6 +3714,16 @@ public class TestPlayer implements Player { return computerPlayer.canPlayCardsFromGraveyard(); } + @Override + public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) { + computerPlayer.setDrawsOnOpponentsTurn(drawsOnOpponentsTurn); + } + + @Override + public boolean isDrawsOnOpponentsTurn() { + return computerPlayer.isDrawsOnOpponentsTurn(); + } + @Override public void setPayManaMode(boolean payManaMode) { computerPlayer.setPayManaMode(payManaMode); diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index 8a408f0c736..91a4b116bd0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -236,6 +236,16 @@ public class PlayerStub implements Player { return false; } + @Override + public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) { + + } + + @Override + public boolean isDrawsOnOpponentsTurn() { + return false; + } + @Override public List getAlternativeSourceCosts() { return null; diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index 2ee42a1a61a..4b66c81727b 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -317,6 +317,12 @@ public final class StaticFilters { FILTER_CONTROLLED_A_PERMANENT.setLockedFilter(true); } + public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENTS = new FilterControlledPermanent("permanents you control"); + + static { + FILTER_CONTROLLED_PERMANENTS.setLockedFilter(true); + } + public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_SHORT_TEXT = new FilterControlledPermanent("permanent"); static { diff --git a/Mage/src/main/java/mage/game/command/emblems/TeferiWhoSlowsTheSunsetEmblem.java b/Mage/src/main/java/mage/game/command/emblems/TeferiWhoSlowsTheSunsetEmblem.java index 99ba626e571..f2d3bd7981b 100644 --- a/Mage/src/main/java/mage/game/command/emblems/TeferiWhoSlowsTheSunsetEmblem.java +++ b/Mage/src/main/java/mage/game/command/emblems/TeferiWhoSlowsTheSunsetEmblem.java @@ -1,19 +1,49 @@ package mage.game.command.emblems; -import mage.abilities.common.BeginningOfDrawTriggeredAbility; +import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.continuous.UntapAllDuringEachOtherPlayersUntapStepEffect; -import mage.constants.TargetController; -import mage.constants.Zone; -import mage.filter.common.FilterControlledPermanent; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; import mage.game.command.Emblem; +import mage.players.Player; public class TeferiWhoSlowsTheSunsetEmblem extends Emblem { // You get an emblem with "Untap all permanents you control during each opponent's untap step" and "You draw a card during each opponent's draw step." public TeferiWhoSlowsTheSunsetEmblem() { this.setName("Emblem Teferi"); - this.getAbilities().add(new SimpleStaticAbility(Zone.COMMAND, new UntapAllDuringEachOtherPlayersUntapStepEffect(new FilterControlledPermanent("permanents you control")))); - this.getAbilities().add(new BeginningOfDrawTriggeredAbility(Zone.COMMAND, new DrawCardSourceControllerEffect(1), TargetController.OPPONENT, false)); + this.getAbilities().add(new SimpleStaticAbility( + Zone.COMMAND, new UntapAllDuringEachOtherPlayersUntapStepEffect(StaticFilters.FILTER_CONTROLLED_PERMANENTS) + )); + this.getAbilities().add(new SimpleStaticAbility(new TeferiWhoSlowsTheSunsetEmblemEffect())); + } +} + +class TeferiWhoSlowsTheSunsetEmblemEffect extends ContinuousEffectImpl { + + TeferiWhoSlowsTheSunsetEmblemEffect() { + super(Duration.EndOfGame, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit); + staticText = "you draw a card during each opponent's draw step"; + } + + private TeferiWhoSlowsTheSunsetEmblemEffect(final TeferiWhoSlowsTheSunsetEmblemEffect effect) { + super(effect); + } + + @Override + public TeferiWhoSlowsTheSunsetEmblemEffect copy() { + return new TeferiWhoSlowsTheSunsetEmblemEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + player.setDrawsOnOpponentsTurn(true); + return true; } } diff --git a/Mage/src/main/java/mage/game/turn/DrawStep.java b/Mage/src/main/java/mage/game/turn/DrawStep.java index ffa7d26f3b7..df07bf4c85b 100644 --- a/Mage/src/main/java/mage/game/turn/DrawStep.java +++ b/Mage/src/main/java/mage/game/turn/DrawStep.java @@ -29,6 +29,14 @@ public class DrawStep extends Step { //20091005 - 504.1/703.4c activePlayer.drawCards(1, null, game); game.applyEffects(); + for (UUID playerId : game.getState().getPlayersInRange(activePlayerId, game)) { + Player player = game.getPlayer(playerId); + if (player != null + && player.isDrawsOnOpponentsTurn() + && player.hasOpponent(activePlayerId, game)) { + player.drawCards(1, null, game); + } + } super.beginStep(game, activePlayerId); } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index abd1239872f..1a1f9111da2 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -178,6 +178,10 @@ public interface Player extends MageItem, Copyable { boolean canPlayCardsFromGraveyard(); + void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn); + + boolean isDrawsOnOpponentsTurn(); + /** * Returns alternative casting costs a player can cast spells for * diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index d5bfc7ef5b6..bc5662a357e 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -138,6 +138,7 @@ public abstract class PlayerImpl implements Player, Serializable { protected boolean canPayLifeCost = true; protected boolean loseByZeroOrLessLife = true; protected boolean canPlayCardsFromGraveyard = true; + protected boolean drawsOnOpponentsTurn = false; protected FilterPermanent sacrificeCostFilter; @@ -239,6 +240,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.canLoseLife = player.canLoseLife; this.loseByZeroOrLessLife = player.loseByZeroOrLessLife; this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard; + this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn; this.attachments.addAll(player.attachments); @@ -347,6 +349,7 @@ public abstract class PlayerImpl implements Player, Serializable { ? player.getSacrificeCostFilter().copy() : null; this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard(); + this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn(); this.alternativeSourceCosts.clear(); this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); @@ -470,6 +473,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.sacrificeCostFilter = null; this.loseByZeroOrLessLife = true; this.canPlayCardsFromGraveyard = false; + this.drawsOnOpponentsTurn = false; this.topCardRevealed = false; this.alternativeSourceCosts.clear(); this.clearCastSourceIdManaCosts(); @@ -637,9 +641,9 @@ public abstract class PlayerImpl implements Player, Serializable { && this.hasOpponent(sourceControllerId, game) && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null && abilities.stream() - .filter(HexproofBaseAbility.class::isInstance) - .map(HexproofBaseAbility.class::cast) - .anyMatch(ability -> ability.checkObject(source, game))) { + .filter(HexproofBaseAbility.class::isInstance) + .map(HexproofBaseAbility.class::cast) + .anyMatch(ability -> ability.checkObject(source, game))) { return false; } @@ -679,7 +683,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(getLogName() + " discards down to " + this.maxHandSize + (this.maxHandSize == 1 - ? " hand card" : " hand cards")); + ? " hand card" : " hand cards")); } discard(hand.size() - this.maxHandSize, false, false, null, game); } @@ -1152,7 +1156,7 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param originalAbility * @param game - * @param noMana cast it without paying mana costs + * @param noMana cast it without paying mana costs * @param approvingObject which object approved the cast * @return */ @@ -2914,7 +2918,7 @@ public abstract class PlayerImpl implements Player, Serializable { * @return */ private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { if (rollsAmount == 1) { return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount); } @@ -3010,8 +3014,8 @@ public abstract class PlayerImpl implements Player, Serializable { * @param outcome * @param source * @param game - * @param sidesAmount number of sides the dice has - * @param rollsAmount number of tries to roll the dice + * @param sidesAmount number of sides the dice has + * @param rollsAmount number of tries to roll the dice * @param ignoreLowestAmount remove the lowest rolls from the results * @return the number that the player rolled */ @@ -3029,18 +3033,18 @@ public abstract class PlayerImpl implements Player, Serializable { * @param outcome * @param source * @param game - * @param rollDieType die type to roll, e.g. planar or numerical - * @param sidesAmount sides per die - * @param chaosSidesAmount for planar die: chaos sides - * @param planarSidesAmount for planar die: planar sides - * @param rollsAmount rolls + * @param rollDieType die type to roll, e.g. planar or numerical + * @param sidesAmount sides per die + * @param chaosSidesAmount for planar die: chaos sides + * @param planarSidesAmount for planar die: planar sides + * @param rollsAmount rolls * @param ignoreLowestAmount for numerical die: ignore multiple rolls with - * the lowest values + * the lowest values * @return */ private List rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, - int rollsAmount, int ignoreLowestAmount) { + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, + int rollsAmount, int ignoreLowestAmount) { RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount); if (ignoreLowestAmount > 0) { rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount); @@ -3200,10 +3204,10 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param source * @param game - * @param chaosSidesAmount The number of chaos sides the planar die - * currently has (normally 1 but can be 5) + * @param chaosSidesAmount The number of chaos sides the planar die + * currently has (normally 1 but can be 5) * @param planarSidesAmount The number of chaos sides the planar die - * currently has (normally 1) + * currently has (normally 1) * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll * or BlankRoll */ @@ -3261,7 +3265,7 @@ public abstract class PlayerImpl implements Player, Serializable { for (Card card : getHand().getCards(game)) { Abilities manaAbilities = card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game); - for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + for (Iterator it = manaAbilities.iterator(); it.hasNext(); ) { ActivatedManaAbilityImpl ability = it.next(); Abilities noTapAbilities = new AbilitiesImpl<>(ability); if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { @@ -3278,7 +3282,7 @@ public abstract class PlayerImpl implements Player, Serializable { boolean useLater = false; // sources with mana costs or mana pool dependency Abilities manaAbilities = permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true - for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + for (Iterator it = manaAbilities.iterator(); it.hasNext(); ) { ActivatedManaAbilityImpl ability = it.next(); if (canUse == null) { canUse = permanent.canUseActivatedAbilities(game); @@ -3320,7 +3324,7 @@ public abstract class PlayerImpl implements Player, Serializable { boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) { anAbilityWasUsed = false; - for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext();) { + for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext(); ) { Abilities manaAbilities = iterator.next(); if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) { boolean used; @@ -3356,7 +3360,7 @@ public abstract class PlayerImpl implements Player, Serializable { * and cleared thereafter * * @param netManaAvailable the net mana produced by the triggered mana - * abaility + * abaility */ @Override public void addAvailableTriggeredMana(List netManaAvailable @@ -3438,7 +3442,7 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param ability * @param availableMana if null, it won't be checked if enough mana is - * available + * available * @param sourceObject * @param game * @return @@ -3873,10 +3877,10 @@ public abstract class PlayerImpl implements Player, Serializable { * currently cast/activate with his available resources * * @param game - * @param hidden also from hidden objects (e.g. turned face down cards ?) - * @param fromZone of objects from which zone (ALL = from all zones) + * @param hidden also from hidden objects (e.g. turned face down cards ?) + * @param fromZone of objects from which zone (ALL = from all zones) * @param hideDuplicatedAbilities if equal abilities exist return only the - * first instance + * first instance * @return */ public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { @@ -4335,6 +4339,16 @@ public abstract class PlayerImpl implements Player, Serializable { this.canPlayCardsFromGraveyard = playCardsFromGraveyard; } + @Override + public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) { + this.drawsOnOpponentsTurn = drawsOnOpponentsTurn; + } + + @Override + public boolean isDrawsOnOpponentsTurn() { + return drawsOnOpponentsTurn; + } + @Override public boolean autoLoseGame() { return false; @@ -4462,7 +4476,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards, toZone, source, game, false, false, false, null); } @@ -4621,7 +4635,7 @@ public abstract class PlayerImpl implements Player, Serializable { // identify cards from one owner Cards cards = new CardsImpl(); UUID ownerId = null; - for (Iterator it = allCards.iterator(); it.hasNext();) { + for (Iterator it = allCards.iterator(); it.hasNext(); ) { Card card = it.next(); if (cards.isEmpty()) { ownerId = card.getOwnerId(); @@ -4799,7 +4813,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) - + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); + + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); } } From d6e7ff4d77327c2d5212473d4d696d0e2847060d Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Wed, 22 Sep 2021 14:09:45 -0500 Subject: [PATCH 171/231] Fixed #8295 --- .../src/mage/cards/a/ArchmagesCharm.java | 2 +- Mage.Sets/src/mage/cards/o/OutOfTime.java | 53 +++++++++++-------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/ArchmagesCharm.java b/Mage.Sets/src/mage/cards/a/ArchmagesCharm.java index d74e8e6f61a..51509613010 100644 --- a/Mage.Sets/src/mage/cards/a/ArchmagesCharm.java +++ b/Mage.Sets/src/mage/cards/a/ArchmagesCharm.java @@ -44,7 +44,7 @@ public final class ArchmagesCharm extends CardImpl { this.getSpellAbility().addMode(mode); // • Gain control of target nonland permanent with converted mana cost 1 or less. - mode = new Mode(new GainControlTargetEffect(Duration.Custom, true)); + mode = new Mode(new GainControlTargetEffect(Duration.EndOfGame, true)); mode.addTarget(new TargetPermanent(filter)); this.getSpellAbility().addMode(mode); } diff --git a/Mage.Sets/src/mage/cards/o/OutOfTime.java b/Mage.Sets/src/mage/cards/o/OutOfTime.java index 02a4c1188ca..bf8907d190c 100644 --- a/Mage.Sets/src/mage/cards/o/OutOfTime.java +++ b/Mage.Sets/src/mage/cards/o/OutOfTime.java @@ -5,8 +5,8 @@ import java.util.*; import mage.abilities.Ability; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.PhaseOutAllEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.keyword.VanishingSacrificeAbility; @@ -23,7 +23,6 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; -import mage.util.CardUtil; /** * @@ -56,7 +55,7 @@ public final class OutOfTime extends CardImpl { class OutOfTimePhaseOutEffect extends OneShotEffect { public OutOfTimePhaseOutEffect() { - super(Outcome.Detriment); + super(Outcome.AIDontUseIt); this.staticText = "untap all creatures, then phase them out until {this} leaves the battlefield. " + "Put a time counter on {this} for each creature phased out this way"; } @@ -83,13 +82,15 @@ class OutOfTimePhaseOutEffect extends OneShotEffect { } // https://magic.wizards.com/en/articles/archive/feature/modern-horizons-2-release-notes-2021-06-04 // If Out of Time leaves the battlefield before its enter the battlefield trigger resolves, creatures will untap, but they won't phase out. - Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent != null) { + Permanent outOfTime = game.getPermanent(source.getSourceId()); + if (outOfTime != null) { new PhaseOutAllEffect(new ArrayList<>(creatureIds)).apply(game, source); new AddCountersSourceEffect(CounterType.TIME.createInstance(numCreatures)).apply(game, source); - game.getState().setValue(CardUtil.getCardZoneString("phasedOutCreatures", source.getSourceId(), game), creatureIds); + game.getState().setValue("phasedOutCreatures" + + source.getId().toString(), creatureIds); + game.getState().setValue("phasedOutBySourceId" + source.getSourceId(), source.getId()); game.addDelayedTriggeredAbility(new OutOfTimeDelayedTriggeredAbility(), source); - game.addEffect(new OutOfTimeReplcementEffect(), source); + game.addEffect(new OutOfTimeReplacementEffect(), source); } } return true; @@ -130,7 +131,7 @@ class OutOfTimeDelayedTriggeredAbility extends DelayedTriggeredAbility { class OutOfTimeLeavesBattlefieldEffect extends OneShotEffect { public OutOfTimeLeavesBattlefieldEffect() { - super(Outcome.Benefit); + super(Outcome.Neutral); } private OutOfTimeLeavesBattlefieldEffect(final OutOfTimeLeavesBattlefieldEffect effect) { @@ -144,15 +145,18 @@ class OutOfTimeLeavesBattlefieldEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Set creatureIds = (Set) game.getState().getValue(CardUtil.getCardZoneString( - "phasedOutCreatures", source.getSourceId(), game, true)); + UUID sourceId = (UUID) game.getState().getValue("phasedOutBySourceId" + source.getSourceId()); + Set creatureIds = (Set) game.getState().getValue("phasedOutCreatures" + + sourceId.toString()); if (creatureIds != null) { for (UUID creatureId : creatureIds) { Permanent creature = game.getPermanent(creatureId); - if (creature != null && !creature.isPhasedIn()) { + if (creature != null + && !creature.isPhasedIn()) { creature.phaseIn(game); } } + game.getState().processAction(game); return true; } return false; @@ -160,19 +164,19 @@ class OutOfTimeLeavesBattlefieldEffect extends OneShotEffect { } // Stops creatures from phasing back in on their controller's next turn -class OutOfTimeReplcementEffect extends ReplacementEffectImpl { +class OutOfTimeReplacementEffect extends ContinuousRuleModifyingEffectImpl { - public OutOfTimeReplcementEffect() { - super(Duration.WhileOnBattlefield, Outcome.Detriment); + public OutOfTimeReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Neutral, false, false); } - private OutOfTimeReplcementEffect(final OutOfTimeReplcementEffect effect) { + private OutOfTimeReplacementEffect(final OutOfTimeReplacementEffect effect) { super(effect); } @Override - public OutOfTimeReplcementEffect copy() { - return new OutOfTimeReplcementEffect(this); + public OutOfTimeReplacementEffect copy() { + return new OutOfTimeReplacementEffect(this); } @Override @@ -181,14 +185,17 @@ class OutOfTimeReplcementEffect extends ReplacementEffectImpl { } @Override - public boolean applies(GameEvent event, Ability source, Game game) { - Set creatureIds = (Set) game.getState().getValue(CardUtil.getCardZoneString( - "phasedOutCreatures", source.getSourceId(), game)); - return creatureIds != null && creatureIds.contains(event.getTargetId()); + public boolean apply(Game game, Ability source) { + return true; } @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - return true; + public boolean applies(GameEvent event, Ability source, Game game) { + game.getState().processAction(game); + Set creatureIds = (Set) game.getState().getValue("phasedOutCreatures" + + source.getId().toString()); + return source.getSourceObjectZoneChangeCounter() == game.getState().getZoneChangeCounter(source.getSourceId()) // blinked + && creatureIds != null + && creatureIds.contains(event.getTargetId()); } } From b0094331b4fdf1a1e1028a6b72ceaea764b7908b Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Wed, 22 Sep 2021 16:46:52 -0500 Subject: [PATCH 172/231] Fixed #5630 --- .../main/java/mage/players/PlayerImpl.java | 10162 ++++++++-------- 1 file changed, 5078 insertions(+), 5084 deletions(-) diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index bc5662a357e..08e3127b259 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1,5084 +1,5078 @@ -package mage.players; - -import com.google.common.collect.ImmutableMap; -import mage.*; -import mage.abilities.*; -import mage.abilities.ActivatedAbility.ActivationStatus; -import mage.abilities.common.PassAbility; -import mage.abilities.common.PlayLandAsCommanderAbility; -import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; -import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; -import mage.abilities.costs.*; -import mage.abilities.costs.mana.AlternateManaPaymentAbility; -import mage.abilities.costs.mana.ManaCost; -import mage.abilities.costs.mana.ManaCosts; -import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.RestrictionEffect; -import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; -import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; -import mage.abilities.keyword.*; -import mage.abilities.mana.ActivatedManaAbilityImpl; -import mage.abilities.mana.ManaOptions; -import mage.actions.MageDrawAction; -import mage.cards.*; -import mage.cards.decks.Deck; -import mage.choices.Choice; -import mage.choices.ChoiceImpl; -import mage.constants.*; -import mage.counters.Counter; -import mage.counters.CounterType; -import mage.counters.Counters; -import mage.designations.Designation; -import mage.designations.DesignationType; -import mage.filter.FilterCard; -import mage.filter.FilterMana; -import mage.filter.FilterPermanent; -import mage.filter.StaticFilters; -import mage.filter.common.FilterControlledPermanent; -import mage.filter.common.FilterCreatureForCombat; -import mage.filter.common.FilterCreatureForCombatBlock; -import mage.filter.predicate.Predicates; -import mage.filter.predicate.permanent.PermanentIdPredicate; -import mage.game.*; -import mage.game.combat.CombatGroup; -import mage.game.command.CommandObject; -import mage.game.events.*; -import mage.game.match.MatchPlayer; -import mage.game.permanent.Permanent; -import mage.game.permanent.PermanentCard; -import mage.game.permanent.PermanentToken; -import mage.game.permanent.token.SquirrelToken; -import mage.game.stack.Spell; -import mage.game.stack.StackAbility; -import mage.game.stack.StackObject; -import mage.game.turn.Step; -import mage.players.net.UserData; -import mage.target.Target; -import mage.target.TargetAmount; -import mage.target.TargetCard; -import mage.target.TargetPermanent; -import mage.target.common.TargetCardInLibrary; -import mage.target.common.TargetDiscard; -import mage.util.CardUtil; -import mage.util.GameLog; -import mage.util.RandomUtil; -import org.apache.log4j.Logger; - -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -public abstract class PlayerImpl implements Player, Serializable { - - private static final Logger logger = Logger.getLogger(PlayerImpl.class); - - /** - * Used to cancel waiting requests send to the player - */ - protected boolean abort; - - protected final UUID playerId; - protected String name; - protected boolean human; - protected int life; - protected boolean wins; - protected boolean draws; - protected boolean loses; - protected Library library; - protected Cards sideboard; - protected Cards hand; - protected Graveyard graveyard; - protected Set commandersIds = new HashSet<>(0); - protected Abilities abilities; - protected Counters counters; - protected int landsPlayed; - protected int landsPerTurn = 1; - protected int loyaltyUsePerTurn = 1; - protected int maxHandSize = 7; - protected int maxAttackedBy = Integer.MAX_VALUE; - protected ManaPool manaPool; - // priority control - protected boolean passed; // player passed priority - protected boolean passedTurn; // F4 - protected boolean passedTurnSkipStack; // F6 // TODO: research - protected boolean passedUntilEndOfTurn; // F5 - protected boolean passedUntilNextMain; // F7 - protected boolean passedUntilStackResolved; // F10 - protected Date dateLastAddedToStack; - protected boolean passedUntilEndStepBeforeMyTurn; // F11 - protected boolean skippedAtLeastOnce; // used to track if passed started in specific phase - /** - * This indicates that player passed all turns until their own turn starts - * (F9). Note! This differs from passedTurn as it doesn't care about spells - * and abilities in the stack and will pass them as well. - */ - protected boolean passedAllTurns; // F9 - protected AbilityType justActivatedType; // used to check if priority can be passed automatically - - protected int turns; - protected int storedBookmark = -1; - protected int priorityTimeLeft = Integer.MAX_VALUE; - - // conceded or connection lost game - protected boolean left; - // set if the player quits the complete match - protected boolean quit; - // set if the player lost match because of priority timeout - protected boolean timerTimeout; - // set if the player lost match because of idle timeout - protected boolean idleTimeout; - - protected RangeOfInfluence range; - protected Set inRange = new HashSet<>(); // players list in current range of influence (updates each turn) - - protected boolean isTestMode = false; - protected boolean canGainLife = true; - protected boolean canLoseLife = true; - protected boolean canPayLifeCost = true; - protected boolean loseByZeroOrLessLife = true; - protected boolean canPlayCardsFromGraveyard = true; - protected boolean drawsOnOpponentsTurn = false; - - protected FilterPermanent sacrificeCostFilter; - - protected final List alternativeSourceCosts = new ArrayList<>(); - - protected boolean isGameUnderControl = true; - protected UUID turnController; - protected List turnControllers = new ArrayList<>(); - protected Set playersUnderYourControl = new HashSet<>(); - - protected Set usersAllowedToSeeHandCards = new HashSet<>(); - - protected List attachments = new ArrayList<>(); - - protected boolean topCardRevealed = false; - - // 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn - // or until a specific point in that turn will last until that turn would have begun. - // They neither expire immediately nor last indefinitely. - protected boolean reachedNextTurnAfterLeaving = false; - - // indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs) - // support multiple cards with alternative mana cost - protected Set castSourceIdWithAlternateMana = new HashSet<>(); - protected Map> castSourceIdManaCosts = new HashMap<>(); - protected Map> castSourceIdCosts = new HashMap<>(); - - // indicates that the player is in mana payment phase - protected boolean payManaMode = false; - - protected UserData userData; - protected MatchPlayer matchPlayer; - - protected List designations = new ArrayList<>(); - - // mana colors the player can handle like Phyrexian mana - protected FilterMana phyrexianColors; - - // Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy) - protected final List> availableTriggeredManaList = new ArrayList<>(); - - /** - * During some steps we can't play anything - */ - protected final Map silentPhaseSteps = ImmutableMap.builder(). - put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build(); - - public PlayerImpl(String name, RangeOfInfluence range) { - this(UUID.randomUUID()); - this.name = name; - this.range = range; - hand = new CardsImpl(); - graveyard = new Graveyard(); - abilities = new AbilitiesImpl<>(); - counters = new Counters(); - manaPool = new ManaPool(playerId); - library = new Library(playerId); - sideboard = new CardsImpl(); - phyrexianColors = null; - } - - protected PlayerImpl(UUID id) { - this.playerId = id; - } - - public PlayerImpl(final PlayerImpl player) { - this.abort = player.abort; - this.playerId = player.playerId; - - this.name = player.name; - this.human = player.human; - this.life = player.life; - this.wins = player.wins; - this.draws = player.draws; - this.loses = player.loses; - - this.library = player.library.copy(); - this.sideboard = player.sideboard.copy(); - this.hand = player.hand.copy(); - this.graveyard = player.graveyard.copy(); - this.commandersIds = player.commandersIds; - this.abilities = player.abilities.copy(); - this.counters = player.counters.copy(); - - this.landsPlayed = player.landsPlayed; - this.landsPerTurn = player.landsPerTurn; - this.loyaltyUsePerTurn = player.loyaltyUsePerTurn; - this.maxHandSize = player.maxHandSize; - this.maxAttackedBy = player.maxAttackedBy; - this.manaPool = player.manaPool.copy(); - this.turns = player.turns; - - this.left = player.left; - this.quit = player.quit; - this.timerTimeout = player.timerTimeout; - this.idleTimeout = player.idleTimeout; - this.range = player.range; - this.canGainLife = player.canGainLife; - this.canLoseLife = player.canLoseLife; - this.loseByZeroOrLessLife = player.loseByZeroOrLessLife; - this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard; - this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn; - - this.attachments.addAll(player.attachments); - - this.inRange.addAll(player.inRange); - this.userData = player.userData; - this.matchPlayer = player.matchPlayer; - - this.canPayLifeCost = player.canPayLifeCost; - this.sacrificeCostFilter = player.sacrificeCostFilter; - this.alternativeSourceCosts.addAll(player.alternativeSourceCosts); - this.storedBookmark = player.storedBookmark; - - this.topCardRevealed = player.topCardRevealed; - this.playersUnderYourControl.addAll(player.playersUnderYourControl); - this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); - - this.isTestMode = player.isTestMode; - this.isGameUnderControl = player.isGameUnderControl; - - this.turnController = player.turnController; - this.turnControllers.addAll(player.turnControllers); - - this.passed = player.passed; - this.passedTurn = player.passedTurn; - this.passedTurnSkipStack = player.passedTurnSkipStack; - this.passedUntilEndOfTurn = player.passedUntilEndOfTurn; - this.passedUntilNextMain = player.passedUntilNextMain; - this.passedUntilStackResolved = player.passedUntilStackResolved; - this.dateLastAddedToStack = player.dateLastAddedToStack; - this.passedUntilEndStepBeforeMyTurn = player.passedUntilEndStepBeforeMyTurn; - this.skippedAtLeastOnce = player.skippedAtLeastOnce; - this.passedAllTurns = player.passedAllTurns; - this.justActivatedType = player.justActivatedType; - - this.priorityTimeLeft = player.getPriorityTimeLeft(); - this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; - - this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); - for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { - this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { - this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - this.payManaMode = player.payManaMode; - this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; - for (Designation object : player.designations) { - this.designations.add(object.copy()); - } - } - - @Override - public void restore(Player player) { - this.name = player.getName(); - this.human = player.isHuman(); - this.life = player.getLife(); - - this.passed = player.isPassed(); - - // Don't restore more global states. If restored they are probably cause for unintended draws (https://github.com/magefree/mage/issues/1205). -// this.wins = player.hasWon(); -// this.loses = player.hasLost(); -// this.left = player.hasLeft(); -// this.quit = player.hasQuit(); - // Makes no sense to restore -// this.priorityTimeLeft = player.getPriorityTimeLeft(); -// this.idleTimeout = player.hasIdleTimeout(); -// this.timerTimeout = player.hasTimerTimeout(); - // can't change so no need to restore -// this.isTestMode = player.isTestMode(); - // This is meta data and should'nt be restored by rollback -// this.userData = player.getUserData(); - this.library = player.getLibrary().copy(); - this.sideboard = player.getSideboard().copy(); - this.hand = player.getHand().copy(); - this.graveyard = player.getGraveyard().copy(); - - //noinspection deprecation - it's ok to use it in inner methods - this.commandersIds = new HashSet<>(player.getCommandersIds()); - - this.abilities = player.getAbilities().copy(); - this.counters = player.getCounters().copy(); - - this.landsPlayed = player.getLandsPlayed(); - this.landsPerTurn = player.getLandsPerTurn(); - this.loyaltyUsePerTurn = player.getLoyaltyUsePerTurn(); - this.maxHandSize = player.getMaxHandSize(); - this.maxAttackedBy = player.getMaxAttackedBy(); - this.manaPool = player.getManaPool().copy(); - // Restore user specific settings in case changed since state save - this.manaPool.setAutoPayment(this.getUserData().isManaPoolAutomatic()); - this.manaPool.setAutoPaymentRestricted(this.getUserData().isManaPoolAutomaticRestricted()); - - this.turns = player.getTurns(); - - this.range = player.getRange(); - this.canGainLife = player.isCanGainLife(); - this.canLoseLife = player.isCanLoseLife(); - this.attachments.clear(); - this.attachments.addAll(player.getAttachments()); - - this.inRange.clear(); - this.inRange.addAll(player.getInRange()); - this.canPayLifeCost = player.getCanPayLifeCost(); - this.sacrificeCostFilter = player.getSacrificeCostFilter() != null - ? player.getSacrificeCostFilter().copy() : null; - this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); - this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard(); - this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn(); - this.alternativeSourceCosts.clear(); - this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); - - this.topCardRevealed = player.isTopCardRevealed(); - this.playersUnderYourControl.clear(); - this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl()); - this.isGameUnderControl = player.isGameUnderControl(); - - this.turnController = player.getTurnControlledBy(); - this.turnControllers.clear(); - this.turnControllers.addAll(player.getTurnControllers()); - this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); - - this.clearCastSourceIdManaCosts(); - this.castSourceIdWithAlternateMana.clear(); - this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); - for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { - this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { - this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - - this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null; - - this.designations.clear(); - for (Designation object : player.getDesignations()) { - this.designations.add(object.copy()); - } - - // Don't restore! - // this.storedBookmark - // this.usersAllowedToSeeHandCards - } - - @Override - public void useDeck(Deck deck, Game game) { - library.clear(); - library.addAll(deck.getCards(), game); - sideboard.clear(); - for (Card card : deck.getSideboard()) { - sideboard.add(card); - } - } - - /** - * Cast e.g. from Karn Liberated to restart the current game - * - * @param game - */ - @Override - public void init(Game game) { - init(game, false); - } - - @Override - public void init(Game game, boolean testMode) { - this.abort = false; - if (!testMode) { - this.hand.clear(); - this.graveyard.clear(); - } - this.library.reset(); - this.abilities.clear(); - this.counters.clear(); - this.wins = false; - this.draws = false; - this.loses = false; - this.left = false; - // reset is necessary because in tournament player will be used for each round - this.quit = false; - this.timerTimeout = false; - this.idleTimeout = false; - - this.turns = 0; - this.isGameUnderControl = true; - this.turnController = this.getId(); - this.turnControllers.clear(); - this.playersUnderYourControl.clear(); - - this.passed = false; - this.passedTurn = false; - this.passedTurnSkipStack = false; - this.passedUntilEndOfTurn = false; - this.passedUntilNextMain = false; - this.passedUntilStackResolved = false; - this.dateLastAddedToStack = null; - this.passedUntilEndStepBeforeMyTurn = false; - this.skippedAtLeastOnce = false; - this.passedAllTurns = false; - this.justActivatedType = null; - - this.canGainLife = true; - this.canLoseLife = true; - this.topCardRevealed = false; - this.payManaMode = false; - this.setLife(game.getStartingLife(), game, null); - this.setReachedNextTurnAfterLeaving(false); - - this.clearCastSourceIdManaCosts(); - - this.getManaPool().init(); // needed to remove mana that not empties on step change from previous game if left - this.phyrexianColors = null; - - this.designations.clear(); - } - - /** - * called before apply effects - */ - @Override - public void reset() { - this.abilities.clear(); - this.landsPerTurn = 1; - this.loyaltyUsePerTurn = 1; - this.maxHandSize = 7; - this.maxAttackedBy = Integer.MAX_VALUE; - this.canGainLife = true; - this.canLoseLife = true; - this.canPayLifeCost = true; - this.sacrificeCostFilter = null; - this.loseByZeroOrLessLife = true; - this.canPlayCardsFromGraveyard = false; - this.drawsOnOpponentsTurn = false; - this.topCardRevealed = false; - this.alternativeSourceCosts.clear(); - this.clearCastSourceIdManaCosts(); - this.getManaPool().clearEmptyManaPoolRules(); - this.phyrexianColors = null; - } - - @Override - public Counters getCounters() { - return counters; - } - - @Override - public void beginTurn(Game game) { - this.landsPlayed = 0; - updateRange(game); - } - - @Override - public RangeOfInfluence getRange() { - return range; - } - - @Override - public void updateRange(Game game) { - // 20100423 - 801.2c - // 801.2c The particular players within each player’s range of influence are determined as each turn begins. - // BUT it also uses before game start to fill game and card data in starting game events - inRange.clear(); - inRange.add(this.playerId); - inRange.addAll(getAllNearPlayers(game, true)); - inRange.addAll(getAllNearPlayers(game, false)); - } - - private Set getAllNearPlayers(Game game, boolean needPrevious) { - // find all near players (search from current player position) - Set foundedList = new HashSet<>(); - PlayerList players = game.getState().getPlayerList(this.playerId); - int needAmount = this.getRange().getRange(); // distance to search (0 - ALL range) - int foundedAmount = 0; - while (needAmount == 0 || foundedAmount < needAmount) { - Player foundedPlayer = needPrevious ? players.getPrevious(game) : players.getNext(game, false); - - // PlayerList is inifine, so stops on repeats - if (foundedPlayer == null || foundedPlayer.getId().equals(this.playerId) || foundedList.contains(foundedPlayer.getId())) { - break; - } - // skip leaved player (no needs cause next/previous code already checks it) - - foundedList.add(foundedPlayer.getId()); - foundedAmount++; - } - return foundedList; - } - - @Override - public Set getInRange() { - if (inRange.isEmpty()) { - // runtime check: inRange filled on beginTurn, but unit tests adds cards by cheat engine before game starting, - // so inRange will be empty and some ETB effects can be broken (example: Spark Double puts direct to battlefield). - // Cheat engine already have a workaround, so that error must not be visible in normal situation. - throw new IllegalStateException("Wrong code usage (game is not started, but you call getInRange in some effects)."); - } - - return inRange; - } - - @Override - public Set getPlayersUnderYourControl() { - return this.playersUnderYourControl; - } - - @Override - public void controlPlayersTurn(Game game, UUID playerId) { - Player player = game.getPlayer(playerId); - player.setTurnControlledBy(this.getId()); - game.informPlayers(getLogName() + " controls the turn of " + player.getLogName()); - if (!playerId.equals(this.getId())) { - this.playersUnderYourControl.add(playerId); - if (!player.hasLeft() && !player.hasLost()) { - player.setGameUnderYourControl(false); - } - DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility( - new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), player.getLogName())); - ability.setSourceId(getId()); - ability.setControllerId(getId()); - game.addDelayedTriggeredAbility(ability, null); - } - } - - @Override - public void setTurnControlledBy(UUID playerId) { - this.turnController = playerId; - this.turnControllers.add(playerId); - } - - @Override - public List getTurnControllers() { - return this.turnControllers; - } - - @Override - public UUID getTurnControlledBy() { - return this.turnController; - } - - @Override - public void resetOtherTurnsControlled() { - playersUnderYourControl.clear(); - } - - /** - * returns true if the player has the control itself - false if the player - * is controlled by another player - * - * @return - */ - @Override - public boolean isGameUnderControl() { - return isGameUnderControl; - } - - @Override - public void setGameUnderYourControl(boolean value) { - setGameUnderYourControl(value, true); - } - - @Override - public void setGameUnderYourControl(boolean value, boolean fullRestore) { - this.isGameUnderControl = value; - if (isGameUnderControl) { - if (fullRestore) { - this.turnControllers.clear(); - this.turnController = getId(); - } else { - if (turnControllers.size() > 0) { - this.turnControllers.remove(turnControllers.size() - 1); - } - if (turnControllers.isEmpty()) { - this.turnController = getId(); - } else { - this.turnController = turnControllers.get(turnControllers.size() - 1); - isGameUnderControl = false; - } - } - } - } - - @Override - public void endOfTurn(Game game) { - this.passedTurn = false; - this.passedTurnSkipStack = false; - } - - @Override - public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) { - if (this.hasLost() || this.hasLeft()) { - return false; - } - if (source != null) { - if (abilities.containsKey(ShroudAbility.getInstance().getId())) { - return false; - } - if (sourceControllerId != null - && this.hasOpponent(sourceControllerId, game) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null - && abilities.stream() - .filter(HexproofBaseAbility.class::isInstance) - .map(HexproofBaseAbility.class::cast) - .anyMatch(ability -> ability.checkObject(source, game))) { - return false; - } - - return !hasProtectionFrom(source, game); - } - - return true; - } - - @Override - public boolean hasProtectionFrom(MageObject source, Game game) { - for (ProtectionAbility ability : abilities.getProtectionAbilities()) { - if (!ability.canTarget(source, game)) { - return true; - } - } - return false; - } - - @Override - public int drawCards(int num, Ability source, Game game) { - if (num > 0) { - return game.doAction(source, new MageDrawAction(this, num, null)); - } - return 0; - } - - @Override - public int drawCards(int num, Ability source, Game game, GameEvent event) { - return game.doAction(source, new MageDrawAction(this, num, event)); - } - - @Override - public void discardToMax(Game game) { - if (hand.size() > this.maxHandSize) { - if (!game.isSimulation()) { - game.informPlayers(getLogName() + " discards down to " - + this.maxHandSize - + (this.maxHandSize == 1 - ? " hand card" : " hand cards")); - } - discard(hand.size() - this.maxHandSize, false, false, null, game); - } - } - - /** - * Don't use this in normal card code, it's for more internal use. Always - * use the [Player].moveCards methods if possible for card movement of card - * code. - * - * @param card - * @param game - * @return - */ - @Override - public boolean putInHand(Card card, Game game) { - if (card.isOwnedBy(playerId)) { - card.setZone(Zone.HAND, game); - this.hand.add(card); - } else { - return game.getPlayer(card.getOwnerId()).putInHand(card, game); - } - return true; - } - - @Override - public boolean removeFromHand(Card card, Game game) { - return hand.remove(card.getId()); - } - - @Override - public boolean removeFromLibrary(Card card, Game game) { - if (card == null) { - return false; - } - library.remove(card.getId(), game); - // must return true all the time (some cards can be removed directly from library, see getLibrary().removeFromTop) - // TODO: replace removeFromTop logic to normal with moveToZone - return true; - } - - @Override - public Card discardOne(boolean random, boolean payForCost, Ability source, Game game) { - return discard(1, random, payForCost, source, game).getRandom(game); - } - - @Override - public Cards discard(int amount, boolean random, boolean payForCost, Ability source, Game game) { - if (random) { - return discard(getRandomToDiscard(amount, source, game), payForCost, source, game); - } - return discard(amount, amount, payForCost, source, game); - } - - @Override - public Cards discard(int minAmount, int maxAmount, boolean payForCost, Ability source, Game game) { - return discard(getToDiscard(minAmount, maxAmount, source, game), payForCost, source, game); - } - - @Override - public Cards discard(Cards cards, boolean payForCost, Ability source, Game game) { - Cards discardedCards = new CardsImpl(); - if (cards == null) { - return discardedCards; - } - for (Card card : cards.getCards(game)) { - if (doDiscard(card, source, game, payForCost, false)) { - discardedCards.add(card); - } - } - if (!discardedCards.isEmpty()) { - game.fireEvent(new DiscardedCardsEvent(source, playerId, discardedCards.size(), discardedCards)); - } - return discardedCards; - } - - @Override - public boolean discard(Card card, boolean payForCost, Ability source, Game game) { - return doDiscard(card, source, game, payForCost, true); - } - - private Cards getToDiscard(int minAmount, int maxAmount, Ability source, Game game) { - Cards toDiscard = new CardsImpl(); - if (minAmount > maxAmount) { - return getToDiscard(maxAmount, minAmount, source, game); - } - if (maxAmount < 1) { - return toDiscard; - } - if (getHand().size() <= minAmount) { - toDiscard.addAll(getHand()); - return toDiscard; - } - TargetDiscard target = new TargetDiscard(minAmount, maxAmount, StaticFilters.FILTER_CARD, getId()); - choose(Outcome.Discard, target, source != null ? source.getSourceId() : null, game); - toDiscard.addAll(target.getTargets()); - return toDiscard; - } - - private Cards getRandomToDiscard(int amount, Ability source, Game game) { - Cards toDiscard = new CardsImpl(); - Cards hand = getHand().copy(); - for (int i = 0; i < amount; i++) { - if (hand.isEmpty()) { - break; - } - Card card = hand.getRandom(game); - hand.remove(card); - toDiscard.add(card); - } - return toDiscard; - } - - private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) { - //20100716 - 701.7 - /* 701.7. Discard # - 701.7a To discard a card, move it from its owner’s hand to that player’s graveyard. - 701.7b By default, effects that cause a player to discard a card allow the affected - player to choose which card to discard. Some effects, however, require a random - discard or allow another player to choose which card is discarded. - 701.7c If a card is discarded, but an effect causes it to be put into a hidden zone - instead of into its owner’s graveyard without being revealed, all values of that - card’s characteristics are considered to be undefined. - TODO: - If a card is discarded this way to pay a cost that specifies a characteristic - about the discarded card, that cost payment is illegal; the game returns to - the moment before the cost was paid (see rule 717, "Handling Illegal Actions"). - */ - if (card == null) { - return false; - } - GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, card.getId(), source, playerId); - gameEvent.setFlag(!payForCost); // event from effect (1) or from cost (0) - if (game.replaceEvent(gameEvent, source)) { - return false; - } - // write info to game log first so game log infos from triggered or replacement effects follow in the game log - if (!game.isSimulation()) { - game.informPlayers(getLogName() + " discards " + card.getLogName() + CardUtil.getSourceLogName(game, source)); - } - /* If a card is discarded while Rest in Peace is on the battlefield, abilities that function - * when a card is discarded (such as madness) still work, even though that card never reaches - * a graveyard. In addition, spells or abilities that check the characteristics of a discarded - * card (such as Chandra Ablaze's first ability) can find that card in exile. */ - card.moveToZone(Zone.GRAVEYARD, source, game, false); - // So discard is also successful if card is moved to another zone by replacement effect! - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DISCARDED_CARD, card.getId(), source, playerId)); - - if (fireFinalEvent) { - game.fireEvent(new DiscardedCardsEvent(source, playerId, 1, new CardsImpl(card))); - } - return true; - } - - @Override - public List getAttachments() { - return attachments; - } - - @Override - public boolean addAttachment(UUID permanentId, Ability source, Game game) { - if (!this.attachments.contains(permanentId)) { - Permanent aura = game.getPermanent(permanentId); - if (aura == null) { - aura = game.getPermanentEntering(permanentId); - } - if (aura != null) { - if (!game.replaceEvent(new EnchantPlayerEvent(playerId, aura, source))) { - this.attachments.add(permanentId); - aura.attachTo(playerId, source, game); - game.fireEvent(new EnchantedPlayerEvent(playerId, aura, source)); - return true; - } - } - } - return false; - } - - @Override - public boolean removeAttachment(Permanent attachment, Ability source, Game game) { - if (this.attachments.contains(attachment.getId())) { - if (!game.replaceEvent(new UnattachEvent(playerId, attachment.getId(), attachment, source))) { - this.attachments.remove(attachment.getId()); - attachment.attachTo(null, source, game); - game.fireEvent(new UnattachedEvent(playerId, attachment.getId(), attachment, source)); - return true; - } - } - return false; - } - - @Override - public boolean removeFromBattlefield(Permanent permanent, Ability source, Game game) { - permanent.removeFromCombat(game, false); - game.getBattlefield().removePermanent(permanent.getId()); - if (permanent.getAttachedTo() != null) { - Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); - if (attachedTo != null) { - attachedTo.removeAttachment(permanent.getId(), source, game); - } else { - Player attachedToPlayer = game.getPlayer(permanent.getAttachedTo()); - if (attachedToPlayer != null) { - attachedToPlayer.removeAttachment(permanent, source, game); - } else { - Card attachedToCard = game.getCard(permanent.getAttachedTo()); - if (attachedToCard != null) { - attachedToCard.removeAttachment(permanent.getId(), source, game); - } - } - } - - } - if (permanent.getPairedCard() != null) { - Permanent pairedCard = permanent.getPairedCard().getPermanent(game); - if (pairedCard != null) { - pairedCard.clearPairedCard(); - } - } - if (permanent.getBandedCards() != null && !permanent.getBandedCards().isEmpty()) { - for (UUID bandedId : permanent.getBandedCards()) { - Permanent banded = game.getPermanent(bandedId); - if (banded != null) { - banded.removeBandedCard(permanent.getId()); - } - } - } - return true; - } - - @Override - public boolean putInGraveyard(Card card, Game game) { - if (card.isOwnedBy(playerId)) { - this.graveyard.add(card); - } else { - return game.getPlayer(card.getOwnerId()).putInGraveyard(card, game); - } - return true; - } - - @Override - public boolean removeFromGraveyard(Card card, Game game) { - return this.graveyard.remove(card); - } - - @Override - public boolean putCardsOnBottomOfLibrary(Card card, Game game, Ability source, boolean anyOrder) { - return putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, anyOrder); - } - - @Override - public boolean putCardsOnBottomOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { - if (!cardsToLibrary.isEmpty()) { - Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException - if (!anyOrder) { - // random order - List ids = new ArrayList<>(cards); - Collections.shuffle(ids); - for (UUID id : ids) { - moveObjectToLibrary(id, source, game, false, false); - } - } else { - // user defined order - TargetCard target = new TargetCard(Zone.ALL, - new FilterCard("card ORDER to put on the BOTTOM of your library (last one chosen will be bottommost)")); - target.setRequired(true); - while (cards.size() > 1 && this.canRespond() - && this.choose(Outcome.Neutral, cards, target, game)) { - UUID targetObjectId = target.getFirstTarget(); - if (targetObjectId == null) { - break; - } - cards.remove(targetObjectId); - moveObjectToLibrary(targetObjectId, source, game, false, false); - target.clearChosen(); - } - for (UUID c : cards) { - moveObjectToLibrary(c, source, game, false, false); - } - } - } - return true; - } - - @Override - public boolean shuffleCardsToLibrary(Cards cards, Game game, Ability source) { - if (cards.isEmpty()) { - return true; - } - game.informPlayers(getLogName() + " shuffles " + CardUtil.numberToText(cards.size(), "a") - + " card" + (cards.size() == 1 ? "" : "s") - + " into their library" + CardUtil.getSourceLogName(game, source)); - boolean status = moveCards(cards, Zone.LIBRARY, source, game); - shuffleLibrary(source, game); - return status; - } - - @Override - public boolean shuffleCardsToLibrary(Card card, Game game, Ability source) { - if (card == null) { - return true; - } - return shuffleCardsToLibrary(new CardsImpl(card), game, source); - } - - @Override - public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int xFromTheTop, boolean withName) { - if (card.isOwnedBy(getId())) { - if (library.size() + 1 < xFromTheTop) { - putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, true); - } else { - if (card.moveToZone(Zone.LIBRARY, source, game, true) - && !(card instanceof PermanentToken) && !card.isCopy()) { - Card cardInLib = getLibrary().getFromTop(game); - if (cardInLib != null && cardInLib.getId().equals(card.getId())) { // check needed because e.g. commander can go to command zone - cardInLib = getLibrary().removeFromTop(game); - getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game); - game.informPlayers(withName ? cardInLib.getLogName() : "A card" - + " is put into " - + getLogName() - + "'s library " - + CardUtil.numberToOrdinalText(xFromTheTop) - + " from the top" + CardUtil.getSourceLogName(game, source, cardInLib.getId())); - } - } else { - return false; - } - } - } else { - return game.getPlayer(card.getOwnerId()).putCardOnTopXOfLibrary(card, game, source, xFromTheTop, withName); - } - return true; - } - - /** - * Can be cards or permanents that go to library - * - * @param cardsToLibrary - * @param game - * @param source - * @param anyOrder - * @return - */ - @Override - public boolean putCardsOnTopOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { - if (cardsToLibrary != null && !cardsToLibrary.isEmpty()) { - Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException - if (!anyOrder) { - // random order - List ids = new ArrayList<>(cards); - Collections.shuffle(ids); - for (UUID id : ids) { - moveObjectToLibrary(id, source, game, true, false); - } - } else { - // user defined order - TargetCard target = new TargetCard(Zone.ALL, - new FilterCard("card ORDER to put on the TOP of your library (last one chosen will be topmost)")); - target.setRequired(true); - while (cards.size() > 1 - && this.canRespond() - && this.choose(Outcome.Neutral, cards, target, game)) { - UUID targetObjectId = target.getFirstTarget(); - if (targetObjectId == null) { - break; - } - cards.remove(targetObjectId); - moveObjectToLibrary(targetObjectId, source, game, true, false); - target.clearChosen(); - } - for (UUID c : cards) { - moveObjectToLibrary(c, source, game, true, false); - } - } - } - return true; - } - - @Override - public boolean putCardsOnTopOfLibrary(Card cardToLibrary, Game game, Ability source, boolean anyOrder) { - if (cardToLibrary != null) { - return putCardsOnTopOfLibrary(new CardsImpl(cardToLibrary), game, source, anyOrder); - } - return true; - } - - private boolean moveObjectToLibrary(UUID objectId, Ability source, Game game, boolean toTop, boolean withName) { - MageObject mageObject = game.getObject(objectId); - if (mageObject instanceof Spell && mageObject.isCopy()) { - // Spell copies are not moved as cards, so here the no copy spell has to be selected to move - // (but because copy and original have the same objectId the wrong sepell can be selected from stack). - // So let's check if the original spell is on the stack and has to be selected. // TODO: Better handling so each spell could be selected by a unique id - Spell spellNoCopy = game.getStack().getSpell(source.getSourceId(), false); - if (spellNoCopy != null) { - mageObject = spellNoCopy; - } - } - if (mageObject != null) { - Zone fromZone = game.getState().getZone(objectId); - if ((mageObject instanceof Permanent)) { - return this.moveCardToLibraryWithInfo((Permanent) mageObject, source, game, fromZone, toTop, withName); - } else if (mageObject instanceof Card) { - return this.moveCardToLibraryWithInfo((Card) mageObject, source, game, fromZone, toTop, withName); - } - } - return false; - } - - @Override - public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { - // cost must be copied for data consistence between game simulations - castSourceIdWithAlternateMana.add(sourceId); - castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null); - castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null); - } - - @Override - public Set getCastSourceIdWithAlternateMana() { - return castSourceIdWithAlternateMana; - } - - @Override - public Map> getCastSourceIdCosts() { - return castSourceIdCosts; - } - - @Override - public Map> getCastSourceIdManaCosts() { - return castSourceIdManaCosts; - } - - @Override - public void clearCastSourceIdManaCosts() { - this.castSourceIdCosts.clear(); - this.castSourceIdManaCosts.clear(); - this.castSourceIdWithAlternateMana.clear(); - } - - @Override - public void setPayManaMode(boolean payManaMode) { - this.payManaMode = payManaMode; - } - - @Override - public boolean isInPayManaMode() { - return payManaMode; - } - - @Override - public boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject) { - if (card == null) { - return false; - } - - // play without timing and from any zone - boolean result; - if (card.isLand(game)) { - result = playLand(card, game, true); - } else { - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); - result = cast(this.chooseAbilityForCast(card, game, noMana), game, noMana, approvingObject); - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); - } - - if (!result) { - game.informPlayer(this, "You can't play " + card.getIdName() + '.'); - } - return result; - } - - /** - * @param originalAbility - * @param game - * @param noMana cast it without paying mana costs - * @param approvingObject which object approved the cast - * @return - */ - @Override - public boolean cast(SpellAbility originalAbility, Game game, boolean noMana, ApprovingObject approvingObject) { - if (game == null || originalAbility == null) { - return false; - } - - // Use ability copy to avoid problems with targets and costs on recast (issue https://github.com/magefree/mage/issues/5189). - SpellAbility ability = originalAbility.copy(); - ability.setControllerId(getId()); - ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); - - //20091005 - 601.2a - if (ability.getSourceId() == null) { - logger.error("Ability without sourceId turn " + game.getTurnNum() + ". Ability: " + ability.getRule()); - return false; - } - Card card = game.getCard(ability.getSourceId()); - if (card != null) { - Zone fromZone = game.getState().getZone(card.getMainCard().getId()); - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, - ability.getId(), ability, playerId, approvingObject); - castEvent.setZone(fromZone); - if (!game.replaceEvent(castEvent, ability)) { - int bookmark = game.bookmarkState(); - setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) - card.cast(game, fromZone, ability, playerId); - Spell spell = game.getStack().getSpell(ability.getId()); - if (spell == null) { - logger.error("Got no spell from stack. ability: " + ability.getRule()); - return false; - } - if (card.isCopy()) { - spell.setCopy(true, null); - } - // Update the zcc to the stack - ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); - - // ALTERNATIVE COST from dynamic effects - // some effects set sourceId to cast without paying mana costs or other costs - if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) { - Ability spellAbility = spell.getSpellAbility(); - ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()); - Costs costs = getCastSourceIdCosts().get(ability.getSourceId()); - if (alternateCosts == null) { - noMana = true; - } else { - spellAbility.getManaCosts().clear(); - spellAbility.getManaCostsToPay().clear(); - spellAbility.getManaCosts().add(alternateCosts.copy()); - spellAbility.getManaCostsToPay().add(alternateCosts.copy()); - } - spellAbility.getCosts().clear(); - if (costs != null) { - spellAbility.getCosts().addAll(costs); - } - } - clearCastSourceIdManaCosts(); // TODO: test multiple alternative cost for different cards as same time - - castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, - spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); - castEvent.setZone(fromZone); - game.fireEvent(castEvent); - if (spell.activate(game, noMana)) { - GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, - spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); - castedEvent.setZone(fromZone); - game.fireEvent(castedEvent); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + spell.getActivatedMessage(game)); - } - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - restoreState(bookmark, ability.getRule(), game); - } - } - return false; - } - - @Override - public boolean playLand(Card card, Game game, boolean ignoreTiming) { - // Check for alternate casting possibilities: e.g. land with Morph - if (card == null) { - return false; - } - ActivatedAbility playLandAbility = null; - boolean foundAlternative = false; - for (Ability ability : card.getAbilities(game)) { - // if cast for noMana no Alternative costs are allowed - if ((ability instanceof AlternativeSourceCosts) - || (ability instanceof OptionalAdditionalSourceCosts)) { - foundAlternative = true; - } - if (ability instanceof PlayLandAbility) { - playLandAbility = (ActivatedAbility) ability; - } - } - - // try alternative cast (face down) - if (foundAlternative) { - SpellAbility spellAbility = new SpellAbility(null, "", - game.getState().getZone(card.getId()), SpellAbilityType.FACE_DOWN_CREATURE); - spellAbility.setControllerId(this.getId()); - spellAbility.setSourceId(card.getId()); - if (cast(spellAbility, game, false, null)) { - return true; - } - } - - if (playLandAbility == null) { - return false; - } - - //20091005 - 114.2a - ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game); - if (ignoreTiming) { - if (!canPlayLand()) { - return false; // ignore timing does not mean that more lands than normal can be played - } - } else { - if (!activationStatus.canActivate()) { - return false; - } - } - - //20091005 - 305.1 - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()))) { - // int bookmark = game.bookmarkState(); - // land events must return original zone (uses for commander watcher) - Zone cardZoneBefore = game.getState().getZone(card.getId()); - GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); - landEventBefore.setZone(cardZoneBefore); - game.fireEvent(landEventBefore); - - if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { - landsPlayed++; - GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); - landEventAfter.setZone(cardZoneBefore); - game.fireEvent(landEventAfter); - - String playText = getLogName() + " plays " + card.getLogName(); - if (card instanceof ModalDoubleFacesCardHalf) { - ModalDoubleFacesCard mdfCard = (ModalDoubleFacesCard) card.getMainCard(); - playText = getLogName() + " plays " + GameLog.replaceNameByColoredName(card, card.getName(), mdfCard) - + " as MDF side of " + GameLog.getColoredObjectIdName(mdfCard); - } - game.fireInformEvent(playText); - // game.removeBookmark(bookmark); - resetStoredBookmark(game); // prevent undo after playing a land - return true; - } - // putOntoBattlefield returned false if putOntoBattlefield was replaced by replacement effect (e.g. Kjeldoran Outpost). - // But that would undo the effect completely, - // what makes no real sense. So it makes no sense to generally do a restoreState here. - // restoreState(bookmark, card.getName(), game); - } - // if the to play the land is replaced (e.g. Kjeldoran Outpost and don't sacrificing a Plains) it's a valid state so returning true here - return true; - } - - protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) { - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, - ability.getId(), ability, playerId))) { - int bookmark = game.bookmarkState(); - if (ability.activate(game, false)) { - if (ability.resolve(game)) { - if (ability.isUndoPossible()) { - if (storedBookmark == -1 || storedBookmark > bookmark) { // e.g. useful for undo Nykthos, Shrine to Nyx - setStoredBookmark(bookmark); - } - } else { - resetStoredBookmark(game); - } - return true; - } - } - restoreState(bookmark, ability.getRule(), game); - } - return false; - } - - protected boolean playAbility(ActivatedAbility ability, Game game) { - //20091005 - 602.2a - if (ability.isUsesStack()) { - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, - ability.getId(), ability, playerId))) { - int bookmark = game.bookmarkState(); - setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) - ability.newId(); - ability.setControllerId(playerId); - game.getStack().push(new StackAbility(ability, playerId)); - if (ability.activate(game, false)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATED_ABILITY, - ability.getId(), ability, playerId)); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + ability.getGameLogMessage(game)); - } - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - restoreState(bookmark, ability.getRule(), game); - } - } else { - int bookmark = game.bookmarkState(); - if (ability.activate(game, false)) { - ability.resolve(game); - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - restoreState(bookmark, ability.getRule(), game); - } - return false; - } - - protected boolean specialAction(SpecialAction action, Game game) { - //20091005 - 114 - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_ACTION, - action.getId(), action, getId()))) { - int bookmark = game.bookmarkState(); - if (action.activate(game, false)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_ACTION, - action.getId(), action, getId())); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + action.getGameLogMessage(game)); - } - if (action.resolve(game)) { - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - } - restoreState(bookmark, action.getRule(), game); - } - return false; - } - - protected boolean specialManaPayment(SpecialAction action, Game game) { - //20091005 - 114 - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_MANA_PAYMENT, - action.getId(), action, getId()))) { - int bookmark = game.bookmarkState(); - if (action.activate(game, false)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_MANA_PAYMENT, - action.getId(), action, getId())); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + action.getGameLogMessage(game)); - } - if (action.resolve(game)) { - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - } - restoreState(bookmark, action.getRule(), game); - } - return false; - } - - @Override - public boolean activateAbility(ActivatedAbility ability, Game game) { - if (ability == null) { - return false; - } - boolean result; - if (ability instanceof PassAbility) { - pass(game); - return true; - } - Card card = game.getCard(ability.getSourceId()); - if (ability instanceof PlayLandAsCommanderAbility) { - - // LAND as commander: play land with cost, but without stack - ActivationStatus activationStatus = ability.canActivate(this.playerId, game); - if (!activationStatus.canActivate() || !this.canPlayLand()) { - return false; - } - if (card == null) { - return false; - } - - // as copy, tries to applie cost effects and pays - Ability activatingAbility = ability.copy(); - if (activatingAbility.activate(game, false)) { - result = playLand(card, game, false); - } else { - result = false; - } - - } else if (ability instanceof PlayLandAbility) { - - // LAND as normal card: without cost and stack - result = playLand(card, game, false); - - } else { - - // ABILITY - ActivationStatus activationStatus = ability.canActivate(this.playerId, game); - if (!activationStatus.canActivate()) { - return false; - } - - switch (ability.getAbilityType()) { - case SPECIAL_ACTION: - result = specialAction((SpecialAction) ability.copy(), game); - break; - case SPECIAL_MANA_PAYMENT: - result = specialManaPayment((SpecialAction) ability.copy(), game); - break; - case MANA: - result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game); - break; - case SPELL: - result = cast((SpellAbility) ability, game, false, activationStatus.getApprovingObject()); - break; - default: - result = playAbility(ability.copy(), game); - break; - } - } - - //if player has taken an action then reset all player passed flags - justActivatedType = null; - if (result) { - if (isHuman() - && (ability.getAbilityType() == AbilityType.SPELL - || ability.getAbilityType() == AbilityType.ACTIVATED)) { - if (ability.isUsesStack()) { // if the ability does not use the stack (e.g. Suspend) auto pass would go to next phase unintended - setJustActivatedType(ability.getAbilityType()); - } - } - game.getPlayers().resetPassed(); - } - return result; - } - - @Override - public boolean triggerAbility(TriggeredAbility triggeredAbility, Game game) { - if (triggeredAbility == null) { - logger.warn("Null source in triggerAbility method"); - throw new IllegalArgumentException("source TriggeredAbility must not be null"); - } - //20091005 - 603.3c, 603.3d - int bookmark = game.bookmarkState(); - TriggeredAbility ability = triggeredAbility.copy(); - MageObject sourceObject = ability.getSourceObject(game); - if (sourceObject != null) { - sourceObject.adjustTargets(ability, game); - } - UUID triggerId = null; - if (ability.canChooseTarget(game, playerId)) { - if (ability.isUsesStack()) { - game.getStack().push(new StackAbility(ability, playerId)); - } - if (ability.activate(game, false)) { - if ((ability.isUsesStack() - || ability.getRuleVisible()) - && !game.isSimulation()) { - game.informPlayers(getLogName() + " - " + ability.getGameLogMessage(game)); - } - if (!ability.isUsesStack()) { - ability.resolve(game); - } else { - game.fireEvent(new GameEvent( - GameEvent.EventType.TRIGGERED_ABILITY, - ability.getId(), ability, ability.getControllerId() - )); - triggerId = ability.getId(); - } - game.removeBookmark(bookmark); - return true; - } - } - restoreState(bookmark, triggeredAbility.getRule(), game); // why restore is needed here? (to remove the triggered ability from the stack because of no possible targets) - GameEvent event = new GameEvent( - GameEvent.EventType.ABILITY_TRIGGERED, - triggerId, ability, ability.getControllerId() - ); - game.getState().setValue(event.getId().toString(), ability.getTriggerEvent()); - game.fireEvent(event); - return false; - } - - /** - * Return spells for possible cast Uses in GUI to show only playable spells - * for choosing from the card (example: effect allow to cast card and player - * must choose the spell ability) - * - * @param game - * @param playerId - * @param object - * @param zone - * @param noMana - * @return - */ - public static LinkedHashMap getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) { - // it uses simple check from spellCanBeActivatedRegularlyNow - // reason: no approved info here (e.g. forced to choose spell ability from cast card) - LinkedHashMap useable = new LinkedHashMap<>(); - Abilities allAbilities; - if (object instanceof Card) { - allAbilities = ((Card) object).getAbilities(game); - } else { - allAbilities = object.getAbilities(); - } - - for (Ability ability : allAbilities) { - if (ability instanceof SpellAbility) { - SpellAbility spellAbility = (SpellAbility) ability; - - switch (spellAbility.getSpellAbilityType()) { - case BASE_ALTERNATE: - // rules: - // If you cast a spell “without paying its mana cost,” you can’t choose to cast it for - // any alternative costs. You can, however, pay additional costs, such as kicker costs. - // If the card has any mandatory additional costs, those must be paid to cast the spell. - // (2021-02-05) - if (!noMana) { - if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { - useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability - } - return useable; - } - break; - case SPLIT_FUSED: - // rules: - // If you cast a split card with fuse from your hand without paying its mana cost, - // you can choose to use its fuse ability and cast both halves without paying their mana costs. - if (zone == Zone.HAND) { - if (spellAbility.canChooseTarget(game, playerId)) { - useable.put(spellAbility.getId(), spellAbility); - } - } - case SPLIT: - if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getLeftHalfCard().getSpellAbility()); - } - if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getRightHalfCard().getSpellAbility()); - } - return useable; - case SPLIT_AFTERMATH: - if (zone == Zone.GRAVEYARD) { - if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getRightHalfCard().getSpellAbility()); - } - } else { - if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getLeftHalfCard().getSpellAbility()); - } - } - return useable; - default: - if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { - useable.put(spellAbility.getId(), spellAbility); - } - } - } - } - return useable; - } - - @Override - public LinkedHashMap getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) { - LinkedHashMap useable = new LinkedHashMap<>(); - // stack abilities - can't activate anything - // spell ability - can activate additional abilities (example: "Lightning Storm") - if (object instanceof StackAbility || object == null) { - return useable; - } - boolean previousState = game.inCheckPlayableState(); - game.setCheckPlayableState(true); - try { - // collect and filter playable activated abilities - // GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right) - Set needIds = CardUtil.getObjectParts(object); - - // workaround to find all abilities first and filter it for one object - List allPlayable = getPlayable(game, true, zone, false); - for (ActivatedAbility ability : allPlayable) { - if (needIds.contains(ability.getSourceId())) { - useable.putIfAbsent(ability.getId(), ability); - } - } - } finally { - game.setCheckPlayableState(previousState); - } - return useable; - } - - protected LinkedHashMap getUseableManaAbilities(MageObject object, Zone zone, Game game) { - LinkedHashMap useable = new LinkedHashMap<>(); - boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); - for (ActivatedManaAbilityImpl ability : object.getAbilities().getActivatedManaAbilities(zone)) { - if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { - if (ability.canActivate(playerId, game).canActivate()) { - useable.put(ability.getId(), ability); - } - } - } - return useable; - } - - @Override - public int getLandsPlayed() { - return landsPlayed; - } - - @Override - public boolean canPlayLand() { - //20091005 - 114.2a - return landsPlayed < landsPerTurn; - } - - protected boolean isActivePlayer(Game game) { - return game.isActivePlayer(this.playerId); - } - - @Override - public void shuffleLibrary(Ability source, Game game) { - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SHUFFLE_LIBRARY, playerId, source, playerId))) { - this.library.shuffle(); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + "'s library is shuffled" + CardUtil.getSourceLogName(game, source)); - } - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SHUFFLED, playerId, source, playerId)); - } - } - - @Override - public void revealCards(Ability source, Cards cards, Game game) { - revealCards(source, null, cards, game, true); - } - - @Override - public void revealCards(String titleSuffix, Cards cards, Game game) { - revealCards(titleSuffix, cards, game, true); - } - - @Override - public void revealCards(String titleSuffix, Cards cards, Game game, boolean postToLog) { - revealCards(null, titleSuffix, cards, game, postToLog); - } - - @Override - public void revealCards(Ability source, String titleSuffix, Cards cards, Game game) { - revealCards(source, titleSuffix, cards, game, true); - } - - @Override - public void revealCards(Ability source, String titleSuffix, Cards cards, Game game, boolean postToLog) { - if (cards == null || cards.isEmpty()) { - return; - } - if (postToLog) { - game.getState().getRevealed().add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); - } else { - game.getState().getRevealed().update(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); - } - if (postToLog && !game.isSimulation()) { - StringBuilder sb = new StringBuilder(getLogName()).append(" reveals "); - int current = 0, last = cards.size(); - for (Card card : cards.getCards(game)) { - current++; - sb.append(GameLog.getColoredObjectName(card)); - if (current < last) { - sb.append(", "); - } - } - sb.append(CardUtil.getSourceLogName(game, source)); - game.informPlayers(sb.toString()); - } - } - - @Override - public void lookAtCards(String titleSuffix, Card card, Game game) { - game.getState().getLookedAt(this.playerId).add(titleSuffix, card); - game.fireUpdatePlayersEvent(); - } - - @Override - public void lookAtCards(String titleSuffix, Cards cards, Game game) { - this.lookAtCards(null, titleSuffix, cards, game); - } - - @Override - public void lookAtCards(Ability source, String titleSuffix, Cards cards, Game game) { - game.getState().getLookedAt(this.playerId).add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); - game.fireUpdatePlayersEvent(); - } - - @Override - public void phasing(Game game) { - //20091005 - 502.1 - List phasedOut = game.getBattlefield().getPhasedOut(game, playerId); - for (Permanent permanent : game.getBattlefield().getPhasedIn(game, playerId)) { - // 502.15i When a permanent phases out, any local enchantments or Equipment - // attached to that permanent phase out at the same time. This alternate way of - // phasing out is known as phasing out "indirectly." An enchantment or Equipment - // that phased out indirectly won't phase in by itself, but instead phases in - // along with the card it's attached to. - Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); - if (!(attachedTo != null && attachedTo.isControlledBy(this.getId()))) { - permanent.phaseOut(game, false); - } - } - for (Permanent permanent : phasedOut) { - if (!permanent.isPhasedOutIndirectly()) { - permanent.phaseIn(game); - } - } - } - - @Override - public void untap(Game game) { - // create list of all "notMoreThan" effects to track which one are consumed - Map>, Integer> notMoreThanEffectsUsage = new HashMap<>(); - for (Entry> restrictionEffect - : game.getContinuousEffects().getApplicableRestrictionUntapNotMoreThanEffects(this, game).entrySet()) { - notMoreThanEffectsUsage.put(restrictionEffect, restrictionEffect.getKey().getNumber()); - } - - if (!notMoreThanEffectsUsage.isEmpty()) { - // create list of all permanents that can be untapped generally - List canBeUntapped = new ArrayList<>(); - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { - boolean untap = true; - for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { - untap &= effect.canBeUntapped(permanent, null, game, true); - } - if (untap) { - canBeUntapped.add(permanent); - } - } - // selected permanents to untap - List selectedToUntap = new ArrayList<>(); - - // player can cancel the selection of an effect to use a preferred order of restriction effects - boolean playerCanceledSelection; - do { - playerCanceledSelection = false; - // select permanents to untap to consume the "notMoreThan" effects - for (Map.Entry>, Integer> handledEntry : notMoreThanEffectsUsage.entrySet()) { - // select a permanent to untap for this entry - int numberToUntap = handledEntry.getValue(); - if (numberToUntap > 0) { - - List leftForUntap = getPermanentsThatCanBeUntapped(game, - canBeUntapped, - handledEntry.getKey().getKey(), - notMoreThanEffectsUsage); - - FilterControlledPermanent filter = handledEntry.getKey().getKey().getFilter().copy(); - String message = filter.getMessage(); - // omit already from other untap effects selected permanents - for (Permanent permanent : selectedToUntap) { - filter.add(Predicates.not(new PermanentIdPredicate(permanent.getId()))); - } - // while targets left and there is still allowed to untap - while (canRespond() && !leftForUntap.isEmpty() && numberToUntap > 0) { - // player has to select the permanent they want to untap for this restriction - Ability ability = handledEntry.getKey().getValue().iterator().next(); - if (ability != null) { - StringBuilder sb = new StringBuilder(message).append(" to untap").append(" (").append(Math.min(leftForUntap.size(), - numberToUntap)).append(" in total"); - MageObject effectSource = game.getObject(ability.getSourceId()); - if (effectSource != null) { - sb.append(" from ").append(effectSource.getLogName()); - } - sb.append(')'); - filter.setMessage(sb.toString()); - Target target = new TargetPermanent(1, 1, filter, true); - if (!this.chooseTarget(Outcome.Untap, target, ability, game)) { - // player canceled, go on with the next effect (if no other effect available, this effect will be active again) - playerCanceledSelection = true; - break; - } - Permanent selectedPermanent = game.getPermanent(target.getFirstTarget()); - if (leftForUntap.contains(selectedPermanent)) { - selectedToUntap.add(selectedPermanent); - numberToUntap--; - // don't allow to select same permanent twice - filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); - // reduce available untap numbers from other "UntapNotMoreThan" effects if selected permanent applies to their filter too - for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { - if (notMoreThanEffect.getValue() > 0 - && notMoreThanEffect.getKey().getKey().getFilter().match(selectedPermanent, game)) { - notMoreThanEffect.setValue(notMoreThanEffect.getValue() - 1); - } - } - // update the left for untap list - leftForUntap = getPermanentsThatCanBeUntapped(game, - canBeUntapped, - handledEntry.getKey().getKey(), - notMoreThanEffectsUsage); - // remove already selected permanents - for (Permanent permanent : selectedToUntap) { - leftForUntap.remove(permanent); - } - - } else { - // player selected an permanent that is restricted by another effect, disallow it (so AI can select another one) - filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); - if (this.isHuman() && !game.isSimulation()) { - game.informPlayer(this, "This permanent can't be untapped because of other restricting effect."); - } - } - } - } - } - } - - } while (canRespond() && playerCanceledSelection); - - if (!game.isSimulation()) { - // show in log which permanents were selected to untap - for (Permanent permanent : selectedToUntap) { - game.informPlayers(this.getLogName() + " untapped " + permanent.getLogName()); - } - } - // untap if permanent is not concerned by notMoreThan effects or is included in the selectedToUntapList - for (Permanent permanent : canBeUntapped) { - boolean doUntap = true; - if (!selectedToUntap.contains(permanent)) { - // if the permanent is covered by one of the restriction effects, don't untap it - for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { - if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game)) { - doUntap = false; - break; - } - } - } - if (permanent != null && doUntap) { - permanent.untap(game); - } - - } - - } else { - //20091005 - 502.2 - - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { - boolean untap = true; - for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { - untap &= effect.canBeUntapped(permanent, null, game, true); - } - if (untap) { - permanent.untap(game); - } - } - } - } - - private List getPermanentsThatCanBeUntapped(Game game, List canBeUntapped, RestrictionUntapNotMoreThanEffect handledEffect, Map>, Integer> notMoreThanEffectsUsage) { - List leftForUntap = new ArrayList<>(); - // select permanents that can still be untapped - for (Permanent permanent : canBeUntapped) { - if (handledEffect.getFilter().match(permanent, game)) { // matches the restricted permanents of handled entry - boolean canBeSelected = true; - // check if the permanent is restricted by another restriction that has left no permanent - for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { - if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game) - && notMoreThanEffect.getValue() == 0) { - canBeSelected = false; - break; - } - } - if (canBeSelected) { - leftForUntap.add(permanent); - } - } - } - return leftForUntap; - } - - @Override - public UUID getId() { - return playerId; - } - - @Override - public Cards getHand() { - return hand; - } - - @Override - public Graveyard getGraveyard() { - return graveyard; - } - - @Override - public ManaPool getManaPool() { - return this.manaPool; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getLogName() { - return GameLog.getColoredPlayerName(name); - } - - @Override - public boolean isHuman() { - return human; - } - - @Override - public Library getLibrary() { - return library; - } - - @Override - public Cards getSideboard() { - return sideboard; - } - - @Override - public int getLife() { - return life; - } - - @Override - public void initLife(int life) { - this.life = life; - } - - @Override - public void setLife(int life, Game game, Ability source) { - // rule 118.5 - if (life > this.life) { - gainLife(life - this.life, game, source); - } else if (life < this.life) { - loseLife(this.life - life, game, source, false); - } - } - - @Override - public void setLifeTotalCanChange(boolean lifeTotalCanChange) { - this.canGainLife = lifeTotalCanChange; - this.canLoseLife = lifeTotalCanChange; - } - - @Override - public boolean isLifeTotalCanChange() { - return canGainLife || canLoseLife; - } - - @Override - public List getAlternativeSourceCosts() { - return alternativeSourceCosts; - } - - @Override - public boolean isCanLoseLife() { - return canLoseLife; - } - - @Override - public void setCanLoseLife(boolean canLoseLife) { - this.canLoseLife = canLoseLife; - } - - @Override - public int loseLife(int amount, Game game, Ability source, boolean atCombat, UUID attackerId) { - if (!canLoseLife || !this.isInGame()) { - return 0; - } - GameEvent event = new GameEvent(GameEvent.EventType.LOSE_LIFE, - playerId, source, playerId, amount, atCombat); - if (!game.replaceEvent(event)) { - this.life = CardUtil.overflowDec(this.life, event.getAmount()); - if (!game.isSimulation()) { - UUID needId = attackerId; - if (needId == null) { - needId = source == null ? null : source.getSourceId(); - } - game.informPlayers(this.getLogName() + " loses " + event.getAmount() + " life" - + (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", "")); - } - if (amount > 0) { - game.fireEvent(new GameEvent(GameEvent.EventType.LOST_LIFE, - playerId, source, playerId, amount, atCombat)); - } - return amount; - } - return 0; - } - - @Override - public int loseLife(int amount, Game game, Ability source, boolean atCombat) { - return loseLife(amount, game, source, atCombat, null); - } - - @Override - public boolean isCanGainLife() { - return canGainLife; - } - - @Override - public void setCanGainLife(boolean canGainLife) { - this.canGainLife = canGainLife; - } - - @Override - public int gainLife(int amount, Game game, Ability source) { - if (!canGainLife || amount <= 0) { - return 0; - } - GameEvent event = new GameEvent(GameEvent.EventType.GAIN_LIFE, - playerId, source, playerId, amount, false); - if (!game.replaceEvent(event)) { - // TODO: lock life at Integer.MAX_VALUE if reached, until it's set to a different amount - // (https://magic.wizards.com/en/articles/archive/news/unstable-faqawaslfaqpaftidawabiajtbt-2017-12-06 - "infinite" life total stays infinite no matter how much is gained or lost) - // this.life += event.getAmount(); - this.life = CardUtil.overflowInc(this.life, event.getAmount()); - if (!game.isSimulation()) { - game.informPlayers(this.getLogName() + " gains " + event.getAmount() + " life" + CardUtil.getSourceLogName(game, source)); - } - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.GAINED_LIFE, - playerId, source, playerId, event.getAmount())); - return event.getAmount(); - } - return 0; - } - - @Override - public void exchangeLife(Player player, Ability source, Game game) { - int lifePlayer1 = getLife(); - int lifePlayer2 = player.getLife(); - if ((lifePlayer1 != lifePlayer2 && this.isLifeTotalCanChange() && player.isLifeTotalCanChange()) - && (lifePlayer1 >= lifePlayer2 || (this.isCanGainLife() && player.isCanLoseLife())) - && (lifePlayer1 <= lifePlayer2 || (this.isCanLoseLife() && player.isCanGainLife()))) { - this.setLife(lifePlayer2, game, source); - player.setLife(lifePlayer1, game, source); - } - } - - @Override - public int damage(int damage, UUID attackerId, Ability source, Game game) { - return doDamage(damage, attackerId, source, game, false, true, null); - } - - @Override - public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable) { - return doDamage(damage, attackerId, source, game, combatDamage, preventable, null); - } - - @Override - public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { - return doDamage(damage, attackerId, source, game, combatDamage, preventable, appliedEffects); - } - - private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { - if (!this.isInGame()) { - return 0; - } - - if (damage < 1) { - return 0; - } - if (!canDamage(game.getObject(attackerId), game)) { - MageObject sourceObject = game.getObject(attackerId); - game.informPlayers(damage + " damage " - + (sourceObject == null ? "" : "from " + sourceObject.getLogName()) - + " to " + getLogName() - + (damage > 1 ? " were" : "was") + " prevented because of protection"); - return 0; - } - DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combatDamage); - event.setAppliedEffects(appliedEffects); - if (game.replaceEvent(event)) { - return 0; - } - int actualDamage = event.getAmount(); - if (actualDamage < 1) { - return 0; - } - UUID sourceControllerId = null; - Abilities sourceAbilities = null; - MageObject attacker = game.getPermanentOrLKIBattlefield(attackerId); - if (attacker == null) { - StackObject stackObject = game.getStack().getStackObject(attackerId); - if (stackObject != null) { - attacker = stackObject.getStackAbility().getSourceObject(game); - } else { - attacker = game.getObject(attackerId); - } - if (attacker instanceof Spell) { - sourceAbilities = ((Spell) attacker).getAbilities(game); - sourceControllerId = ((Spell) attacker).getControllerId(); - } else if (attacker instanceof Card) { - sourceAbilities = ((Card) attacker).getAbilities(game); - sourceControllerId = ((Card) attacker).getOwnerId(); - } else if (attacker instanceof CommandObject) { - sourceControllerId = ((CommandObject) attacker).getControllerId(); - sourceAbilities = attacker.getAbilities(); - } - } else { - sourceAbilities = ((Permanent) attacker).getAbilities(game); - sourceControllerId = ((Permanent) attacker).getControllerId(); - } - if (event.isAsThoughInfect() || (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId()))) { - addCounters(CounterType.POISON.createInstance(actualDamage), sourceControllerId, source, game); - } else { - GameEvent damageToLifeLossEvent = new GameEvent(GameEvent.EventType.DAMAGE_CAUSES_LIFE_LOSS, - playerId, source, playerId, actualDamage, combatDamage); - if (!game.replaceEvent(damageToLifeLossEvent)) { - this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combatDamage, attackerId); - } - } - if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) { - if (combatDamage) { - game.getPermanent(attackerId).markLifelink(actualDamage); - } else { - Player player = game.getPlayer(sourceControllerId); - player.gainLife(actualDamage, game, source); - } - } - // Unstable ability - Earl of Squirrel - if (sourceAbilities != null && sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) { - Player player = game.getPlayer(sourceControllerId); - new SquirrelToken().putOntoBattlefield(actualDamage, game, source, player.getId()); - } - DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combatDamage); - game.fireEvent(damagedEvent); - game.getState().addSimultaneousDamage(damagedEvent, game); - return actualDamage; - } - - @Override - public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) { - boolean returnCode = true; - GameEvent addingAllEvent = GameEvent.getEvent( - GameEvent.EventType.ADD_COUNTERS, playerId, source, - playerAddingCounters, counter.getName(), counter.getCount() - ); - if (!game.replaceEvent(addingAllEvent)) { - int amount = addingAllEvent.getAmount(); - int finalAmount = amount; - boolean isEffectFlag = addingAllEvent.getFlag(); - for (int i = 0; i < amount; i++) { - Counter eventCounter = counter.copy(); - eventCounter.remove(eventCounter.getCount() - 1); - GameEvent addingOneEvent = GameEvent.getEvent( - GameEvent.EventType.ADD_COUNTER, playerId, source, - playerAddingCounters, counter.getName(), 1 - ); - addingOneEvent.setFlag(isEffectFlag); - if (!game.replaceEvent(addingOneEvent)) { - getCounters().addCounter(eventCounter); - GameEvent addedOneEvent = GameEvent.getEvent( - GameEvent.EventType.COUNTER_ADDED, playerId, source, - playerAddingCounters, counter.getName(), 1 - ); - addedOneEvent.setFlag(addingOneEvent.getFlag()); - game.fireEvent(addedOneEvent); - } else { - finalAmount--; - returnCode = false; - } - } - if (finalAmount > 0) { - GameEvent addedAllEvent = GameEvent.getEvent( - GameEvent.EventType.COUNTERS_ADDED, playerId, source, - playerAddingCounters, counter.getName(), amount - ); - addedAllEvent.setFlag(addingAllEvent.getFlag()); - game.fireEvent(addedAllEvent); - } - } else { - returnCode = false; - } - return returnCode; - } - - @Override - public void removeCounters(String name, int amount, Ability source, Game game) { - int finalAmount = 0; - for (int i = 0; i < amount; i++) { - if (!counters.removeCounter(name, 1)) { - break; - } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, - getId(), source, (source == null ? null : source.getControllerId())); - event.setData(name); - event.setAmount(1); - game.fireEvent(event); - finalAmount++; - } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, - getId(), source, (source == null ? null : source.getControllerId())); - event.setData(name); - event.setAmount(finalAmount); - game.fireEvent(event); - } - - protected boolean canDamage(MageObject source, Game game) { - for (ProtectionAbility ability : abilities.getProtectionAbilities()) { - if (!ability.canTarget(source, game)) { - return false; - } - } - return true; - } - - @Override - public Abilities getAbilities() { - return this.abilities; - } - - @Override - public void addAbility(Ability ability) { - ability.setSourceId(playerId); - this.abilities.add(ability); - } - - @Override - public int getLandsPerTurn() { - return this.landsPerTurn; - } - - @Override - public void setLandsPerTurn(int landsPerTurn) { - this.landsPerTurn = landsPerTurn; - } - - @Override - public int getLoyaltyUsePerTurn() { - return this.loyaltyUsePerTurn; - } - - @Override - public void setLoyaltyUsePerTurn(int loyaltyUsePerTurn) { - this.loyaltyUsePerTurn = loyaltyUsePerTurn; - } - - @Override - public int getMaxHandSize() { - return maxHandSize; - } - - @Override - public void setMaxHandSize(int maxHandSize) { - this.maxHandSize = maxHandSize; - } - - @Override - public void setMaxAttackedBy(int maxAttackedBy) { - this.maxAttackedBy = maxAttackedBy; - } - - @Override - public int getMaxAttackedBy() { - return maxAttackedBy; - } - - @Override - public void setResponseString(String responseString) { - } - - @Override - public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) { - } - - @Override - public void setResponseUUID(UUID responseUUID) { - } - - @Override - public void setResponseBoolean(Boolean responseBoolean) { - } - - @Override - public void setResponseInteger(Integer responseInteger) { - } - - @Override - public boolean isPassed() { - return passed; - } - - @Override - public void pass(Game game) { - this.passed = true; - resetStoredBookmark(game); - } - - @Override - public void resetPassed() { - this.passed = this.loses || this.hasLeft(); - } - - @Override - public void resetPlayerPassedActions() { - this.passed = false; - this.passedTurn = false; - this.passedTurnSkipStack = false; - this.passedUntilEndOfTurn = false; - this.passedUntilNextMain = false; - this.passedUntilStackResolved = false; - this.dateLastAddedToStack = null; - this.passedUntilEndStepBeforeMyTurn = false; - this.skippedAtLeastOnce = false; - this.passedAllTurns = false; - this.justActivatedType = null; - } - - @Override - public void quit(Game game) { - quit = true; - this.concede(game); - logger.debug(getName() + " quits the match."); - game.informPlayers(getLogName() + " quits the match."); - } - - @Override - public void timerTimeout(Game game) { - quit = true; - timerTimeout = true; - this.concede(game); - game.informPlayers(getLogName() + " has run out of time, losing the match."); - } - - @Override - public void idleTimeout(Game game) { - quit = true; - idleTimeout = true; - this.concede(game); - game.informPlayers(getLogName() + " was idle for too long, losing the Match."); - } - - @Override - public void concede(Game game) { - game.setConcedingPlayer(playerId); - lost(game); -// this.left = true; - } - - @Override - public void sendPlayerAction(PlayerAction playerAction, Game game, Object data) { - switch (playerAction) { - case PASS_PRIORITY_UNTIL_MY_NEXT_TURN: // F9 - resetPlayerPassedActions(); - passedAllTurns = true; - this.skip(); - break; - case PASS_PRIORITY_UNTIL_TURN_END_STEP: // F5 - resetPlayerPassedActions(); - passedUntilEndOfTurn = true; - skippedAtLeastOnce = PhaseStep.END_TURN != game.getTurn().getStepType(); - this.skip(); - break; - case PASS_PRIORITY_UNTIL_NEXT_TURN: // F4 - resetPlayerPassedActions(); - passedTurn = true; - this.skip(); - break; - case PASS_PRIORITY_UNTIL_NEXT_TURN_SKIP_STACK: // F6 - resetPlayerPassedActions(); - passedTurnSkipStack = true; - this.skip(); - break; - case PASS_PRIORITY_UNTIL_NEXT_MAIN_PHASE: //F7 - resetPlayerPassedActions(); - passedUntilNextMain = true; - skippedAtLeastOnce = !(game.getTurn().getStepType() == PhaseStep.POSTCOMBAT_MAIN - || game.getTurn().getStepType() == PhaseStep.PRECOMBAT_MAIN); - this.skip(); - break; - case PASS_PRIORITY_UNTIL_STACK_RESOLVED: // Default F10 - Skips until the current stack is resolved - if (!game.getStack().isEmpty()) { // If stack is empty do nothing - resetPlayerPassedActions(); - passedUntilStackResolved = true; - dateLastAddedToStack = game.getStack().getDateLastAdded(); - this.skip(); - } - break; - case PASS_PRIORITY_UNTIL_END_STEP_BEFORE_MY_NEXT_TURN: //F11 - resetPlayerPassedActions(); - passedUntilEndStepBeforeMyTurn = true; - this.skip(); - break; - case PASS_PRIORITY_CANCEL_ALL_ACTIONS: - resetPlayerPassedActions(); - break; - case PERMISSION_REQUESTS_ALLOWED_OFF: - userData.setAllowRequestShowHandCards(false); - break; - case PERMISSION_REQUESTS_ALLOWED_ON: - userData.setAllowRequestShowHandCards(true); - userData.resetRequestedHandPlayersList(game.getId()); // users can send request again - break; - } - logger.trace("PASS Priority: " + playerAction); - } - - @Override - public void leave() { - this.passed = true; - this.loses = true; - this.left = true; - this.abort(); - //20100423 - 800.4a - this.hand.clear(); - this.graveyard.clear(); - this.library.clear(); - } - - @Override - public boolean hasLeft() { - return this.left; - } - - @Override - public void lost(Game game) { - if (canLose(game)) { - lostForced(game); - } - } - - @Override - public void lostForced(Game game) { - logger.debug(this.getName() + " has lost gameId: " + game.getId()); - //20100423 - 603.9 - if (!this.wins) { - this.loses = true; - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LOST, null, null, playerId)); - game.informPlayers(this.getLogName() + " has lost the game."); - } else { - logger.debug(this.getName() + " has already won - stop lost"); - } - // for draw - first all players that have lost have to be set to lost - if (!hasLeft()) { - logger.debug("Game over playerId: " + playerId); - game.setConcedingPlayer(playerId); - } - } - - @Override - public boolean canLose(Game game) { - return hasLeft() // If a player concedes or has left the match they lose also if effect would say otherwise - || !game.replaceEvent(new GameEvent(GameEvent.EventType.LOSES, null, null, playerId)); - } - - @Override - public void won(Game game) { - boolean opponentInGame = false; - for (UUID opponentId : game.getOpponents(playerId)) { - Player opponent = game.getPlayer(opponentId); - - if (opponent != null && opponent.isInGame()) { - opponentInGame = true; - break; - } - } - if (!opponentInGame - || // if no more opponent is in game the wins event may no longer be replaced - !game.replaceEvent(new GameEvent(GameEvent.EventType.WINS, null, null, playerId))) { - logger.debug("player won -> start: " + this.getName()); - if (!this.loses) { - //20130501 - 800.7, 801.16 - // all opponents in range loose the game - for (UUID opponentId : game.getOpponents(playerId)) { - Player opponent = game.getPlayer(opponentId); - if (opponent != null && !opponent.hasLost()) { - logger.debug("player won -> calling opponent lost: " - + this.getName() + " opponent: " + opponent.getName()); - opponent.lostForced(game); - } - } - // if no more opponents alive (independant from range), you win and the game ends - int opponentsAlive = 0; - for (UUID playerIdToCheck : game.getPlayerList()) { - if (game.isOpponent(this, playerIdToCheck)) { // Check without range - Player opponent = game.getPlayer(playerIdToCheck); - if (opponent != null && !opponent.hasLost()) { - opponentsAlive++; - } - } - } - if (opponentsAlive == 0 && !hasWon()) { - logger.debug("player won -> No more opponents alive game won: " + this.getName()); - game.informPlayers(this.getLogName() + " has won the game"); - this.wins = true; - game.end(); - } - } else { - logger.debug("player won -> but already lost before or other players still alive: " + this.getName()); - } - } - } - - @Override - public void drew(Game game) { - if (!hasLost()) { - this.draws = true; - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DRAW_PLAYER, null, null, playerId)); - game.informPlayers("For " + this.getLogName() + " the game is a draw."); - game.setConcedingPlayer(playerId); - } - } - - @Override - public boolean hasLost() { - return this.loses; - } - - @Override - public boolean isInGame() { - return !hasQuit() && !hasLost() && !hasWon() && !hasDrew() && !hasLeft(); - } - - @Override - public boolean canRespond() { // abort is checked here to get out of player requests (as example: after disconnect) - return isInGame() && !abort; - } - - @Override - public boolean hasWon() { - return !this.loses && this.wins; - } - - @Override - public boolean hasDrew() { - return this.draws; - } - - @Override - public void declareAttacker(UUID attackerId, UUID defenderId, Game game, boolean allowUndo) { - if (allowUndo) { - setStoredBookmark(game.bookmarkState()); // makes it possible to UNDO a declared attacker with costs from e.g. Propaganda - } - Permanent attacker = game.getPermanent(attackerId); - if (attacker != null - && attacker.canAttack(defenderId, game) - && attacker.isControlledBy(playerId)) { - if (!game.getCombat().declareAttacker(attackerId, defenderId, playerId, game)) { - game.undo(playerId); - } - } - } - - @Override - public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game) { - declareBlocker(defenderId, blockerId, attackerId, game, true); - } - - @Override - public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game, boolean allowUndo) { - if (isHuman() && allowUndo) { - setStoredBookmark(game.bookmarkState()); - } - Permanent blocker = game.getPermanent(blockerId); - CombatGroup group = game.getCombat().findGroup(attackerId); - if (blocker != null && group != null && group.canBlock(blocker, game)) { - group.addBlocker(blockerId, playerId, game); - game.getCombat().addBlockingGroup(blockerId, attackerId, playerId, game); - } else if (this.isHuman() && !game.isSimulation()) { - game.informPlayer(this, "You can't block this creature."); - } - } - - @Override - public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game) { - return searchLibrary(target, source, game, playerId); - } - - @Override - public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) { - //20091005 - 701.14c - - // searching control can be intercepted by another player, see Opposition Agent - SearchLibraryEvent searchEvent = new SearchLibraryEvent(targetPlayerId, source, playerId, Integer.MAX_VALUE); - if (game.replaceEvent(searchEvent)) { - return false; - } - - Player targetPlayer = game.getPlayer(targetPlayerId); - Player searchingPlayer = this; - Player searchingController = game.getPlayer(searchEvent.getSearchingControllerId()); - if (targetPlayer == null || searchingController == null) { - return false; - } - - String searchInfo = searchingPlayer.getLogName(); - if (!searchingPlayer.getId().equals(searchingController.getId())) { - searchInfo = searchInfo + " under control of " + searchingPlayer.getLogName(); - } - if (targetPlayer.getId().equals(searchingPlayer.getId())) { - searchInfo = searchInfo + " searches their library"; - } else { - searchInfo = searchInfo + " searches the library of " + targetPlayer.getLogName(); - } - - if (!game.isSimulation()) { - game.informPlayers(searchInfo + CardUtil.getSourceLogName(game, source)); - } - - // https://www.reddit.com/r/magicTCG/comments/jj8gh9/opposition_agent_and_panglacial_wurm_interaction/ - // You must take full player control while searching, e.g. you can cast opponent's cards by Panglacial Wurm effect: - // * While you’re searching your library, you may cast Panglacial Wurm from your library. - // So use here same code as Word of Command - // P.S. no needs in searchingController, but it helps with unit tests, see TakeControlWhileSearchingLibraryTest - boolean takeControl = false; - if (!searchingPlayer.getId().equals(searchingController.getId())) { - CardUtil.takeControlUnderPlayerStart(game, searchingController, searchingPlayer, true); - takeControl = true; - } - - Library searchingLibrary = targetPlayer.getLibrary(); - TargetCardInLibrary newTarget = target.copy(); - int count; - int librarySearchLimit = searchEvent.getAmount(); - List cardsFromTop = null; - do { - // TODO: prevent shuffling from moving the visualized cards - if (librarySearchLimit == Integer.MAX_VALUE) { - count = searchingLibrary.count(target.getFilter(), game); - } else { - if (cardsFromTop == null) { - cardsFromTop = new ArrayList<>(searchingLibrary.getTopCards(game, librarySearchLimit)); - } else { - cardsFromTop.retainAll(searchingLibrary.getCards(game)); - } - newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size())); - count = Math.min(searchingLibrary.count(target.getFilter(), game), librarySearchLimit); - } - - if (count < target.getNumberOfTargets()) { - newTarget.setMinNumberOfTargets(count); - } - - // handling Panglacial Wurm - cast cards while searching from own library - if (targetPlayer.getId().equals(searchingPlayer.getId())) { - if (handleCastableCardsWhileLibrarySearching(library, game, targetPlayer)) { - // clear all choices to start from scratch (casted cards must be removed from library) - newTarget.clearChosen(); - continue; - } - } - - if (newTarget.choose(Outcome.Neutral, searchingController.getId(), targetPlayer.getId(), game)) { - target.getTargets().clear(); - for (UUID targetId : newTarget.getTargets()) { - target.add(targetId, game); - } - } - - // END SEARCH - if (takeControl) { - CardUtil.takeControlUnderPlayerEnd(game, searchingController, searchingPlayer); - game.informPlayers("Control of " + searchingPlayer.getLogName() + " is back" + CardUtil.getSourceLogName(game, source)); - } - - LibrarySearchedEvent searchedEvent = new LibrarySearchedEvent(targetPlayer.getId(), source, searchingPlayer.getId(), target); - if (!game.replaceEvent(searchedEvent)) { - game.fireEvent(searchedEvent); - } - break; - } while (true); - - return true; - } - - @Override - public boolean seekCard(FilterCard filter, Ability source, Game game) { - Set cards = this.getLibrary() - .getCards(game) - .stream() - .filter(card -> filter.match(card, source.getSourceId(), getId(), game)) - .collect(Collectors.toSet()); - Card card = RandomUtil.randomFromCollection(cards); - if (card == null) { - return false; - } - game.informPlayers(this.getLogName() + " seeks a card from their library"); - this.moveCards(card, Zone.HAND, source, game); - return true; - } - - @Override - public void lookAtAllLibraries(Ability source, Game game) { - for (UUID playerId : game.getState().getPlayersInRange(this.getId(), game)) { - Player player = game.getPlayer(playerId); - String playerName = this.getName().equals(player.getName()) ? "Your " : player.getName() + "'s "; - playerName += "library"; - Cards cardsInLibrary = new CardsImpl(player.getLibrary().getTopCards(game, player.getLibrary().size())); - lookAtCards(playerName, cardsInLibrary, game); - } - } - - private boolean handleCastableCardsWhileLibrarySearching(Library library, Game game, Player targetPlayer) { - // must return true after cast try (to restart searching process without casted cards) - // uses for handling Panglacial Wurm: - // * While you're searching your library, you may cast Panglacial Wurm from your library. - - List castableCards = library.getCards(game).stream() - .filter(card -> card.getAbilities(game).containsClass(WhileSearchingPlayFromLibraryAbility.class)) - .map(MageItem::getId) - .collect(Collectors.toList()); - if (castableCards.size() == 0) { - return false; - } - - // only humans can use it - if (targetPlayer.isComputer()) { - return false; - } - - if (!targetPlayer.chooseUse(Outcome.AIDontUseIt, "There are " + castableCards.size() + " cards you can cast while searching your library. Cast any of them?", null, game)) { - return false; - } - - boolean casted = false; - TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, StaticFilters.FILTER_CARD); - targetCard.setTargetName("card to cast from library"); - targetCard.setNotTarget(true); - while (castableCards.size() > 0) { - targetCard.clearChosen(); - if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, game)) { - break; - } - - Card card = game.getCard(targetCard.getFirstTarget()); - if (card == null) { - break; - } - - // AI NOTICE: if you want AI implement here then remove selected card from castable after each - // choice (otherwise you catch infinite freeze on uncastable use case) - // casting selected card - // TODO: fix costs (why is Panglacial Wurm automatically accepting payment?) - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); - targetPlayer.cast(targetPlayer.chooseAbilityForCast(card, game, false), game, false, null); - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); - castableCards.remove(card.getId()); - casted = true; - } - return casted; - } - - /** - * @param source - * @param game - * @param winnable - * @return if winnable, true if player won the toss, if not winnable, true - * for heads and false for tails - */ - @Override - public boolean flipCoin(Ability source, Game game, boolean winnable) { - boolean chosen = false; - if (winnable) { - chosen = this.chooseUse(Outcome.Benefit, "Heads or tails?", "", "Heads", "Tails", source, game); - game.informPlayers(getLogName() + " chose " + CardUtil.booleanToFlipName(chosen)); - } - boolean result = this.flipCoinResult(game); - FlipCoinEvent event = new FlipCoinEvent(playerId, source, result, chosen, winnable); - game.replaceEvent(event); - game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult()) - + CardUtil.getSourceLogName(game, source)); - if (event.getFlipCount() > 1) { - boolean canChooseHeads = event.getResult(); - boolean canChooseTails = !event.getResult(); - for (int i = 1; i < event.getFlipCount(); i++) { - boolean tempFlip = this.flipCoinResult(game); - canChooseHeads = canChooseHeads || tempFlip; - canChooseTails = canChooseTails || !tempFlip; - game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(tempFlip)); - } - if (canChooseHeads && canChooseTails) { - event.setResult(chooseUse(Outcome.Benefit, "Choose which flip to keep", - (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), - "Heads", "Tails", source, game - )); - } else { - event.setResult(canChooseHeads); - } - game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); - } - if (event.isWinnable()) { - game.informPlayers(getLogName() + " " + (event.getResult() == event.getChosen() ? "won" : "lost") + " the flip" - + CardUtil.getSourceLogName(game, source)); - } - game.fireEvent(event.createFlippedEvent()); - if (event.isWinnable()) { - return event.getResult() == event.getChosen(); - } - return event.getResult(); - } - - /** - * Return result for next flip coint try (can be contolled in tests) - * - * @return - */ - @Override - public boolean flipCoinResult(Game game) { - return RandomUtil.nextBoolean(); - } - - private static final class RollDieResult { - - // 706.2. - // After the roll, the number indicated on the top face of the die before any modifiers is - // the natural result. The instruction may include modifiers to the roll which add to or - // subtract from the natural result. Modifiers may also come from other sources. After - // considering all applicable modifiers, the final number is the result of the die roll. - private final int naturalResult; - private final int modifier; - private final PlanarDieRollResult planarResult; - - RollDieResult(int naturalResult, int modifier, PlanarDieRollResult planarResult) { - this.naturalResult = naturalResult; - this.modifier = modifier; - this.planarResult = planarResult; - } - - public int getResult() { - return this.naturalResult + this.modifier; - } - - public PlanarDieRollResult getPlanarResult() { - return this.planarResult; - } - } - - @Override - public int rollDieResult(int sides, Game game) { - return RandomUtil.nextInt(sides) + 1; - } - - /** - * Roll single die. Support both die types: planar and numerical. - * - * @param outcome - * @param game - * @param source - * @param rollDieType - * @param sidesAmount - * @param chaosSidesAmount - * @param planarSidesAmount - * @param rollsAmount - * @return - */ - private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { - if (rollsAmount == 1) { - return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount); - } - Set choices = new HashSet<>(); - for (int j = 0; j < rollsAmount; j++) { - choices.add(rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount)); - } - if (choices.size() == 1) { - return choices.stream().findFirst().orElse(0); - } - - // AI hint - use max/min values - if (this.isComputer()) { - if (rollDieType == RollDieType.NUMERICAL) { - // numerical - if (outcome.isGood()) { - return choices.stream() - .map(Integer.class::cast) - .max(Comparator.naturalOrder()) - .orElse(null); - } else { - return choices.stream() - .map(Integer.class::cast) - .min(Comparator.naturalOrder()) - .orElse(null); - } - } else { - // planar - // priority: chaos -> planar -> blank - return choices.stream() - .map(PlanarDieRollResult.class::cast) - .max(Comparator.comparingInt(PlanarDieRollResult::getAIPriority)) - .orElse(null); - } - } - - Choice choice = new ChoiceImpl(true); - choice.setMessage("Choose which die roll result to keep (the rest will be ignored)"); - choice.setChoices(choices.stream().sorted().map(Object::toString).collect(Collectors.toSet())); - - this.choose(Outcome.Neutral, choice, game); - Object defaultChoice = choices.iterator().next(); - return choices.stream() - .filter(o -> o.toString().equals(choice.getChoice())) - .findFirst() - .orElse(defaultChoice); - } - - private Object rollDieInnerWithReplacement(Game game, Ability source, RollDieType rollDieType, int numSides, int numChaosSides, int numPlanarSides) { - switch (rollDieType) { - - case NUMERICAL: { - int result = rollDieResult(numSides, game); - // Clam-I-Am workaround: - // If you roll a 3 on a six-sided die, you may reroll that die. - if (numSides == 6 - && result == 3 - && game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.REPLACE_ROLLED_DIE, source.getControllerId(), source, source.getControllerId())) - && chooseUse(Outcome.Neutral, "Re-roll the 3?", source, game)) { - result = rollDieResult(numSides, game); - } - return result; - } - - case PLANAR: { - if (numChaosSides + numPlanarSides > numSides) { - numChaosSides = GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES; - numPlanarSides = GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES; - } - // for 9 sides: - // 1..2 - chaos - // 3..7 - blank - // 8..9 - planar - int result = this.rollDieResult(numSides, game); - PlanarDieRollResult roll; - if (result <= numChaosSides) { - roll = PlanarDieRollResult.CHAOS_ROLL; - } else if (result > numSides - numPlanarSides) { - roll = PlanarDieRollResult.PLANAR_ROLL; - } else { - roll = PlanarDieRollResult.BLANK_ROLL; - } - return roll; - } - - default: { - throw new IllegalArgumentException("Unknown roll die type " + rollDieType); - } - } - } - - /** - * @param outcome - * @param source - * @param game - * @param sidesAmount number of sides the dice has - * @param rollsAmount number of tries to roll the dice - * @param ignoreLowestAmount remove the lowest rolls from the results - * @return the number that the player rolled - */ - @Override - public List rollDice(Outcome outcome, Ability source, Game game, int sidesAmount, int rollsAmount, int ignoreLowestAmount) { - return rollDiceInner(outcome, source, game, RollDieType.NUMERICAL, sidesAmount, 0, 0, rollsAmount, ignoreLowestAmount) - .stream() - .map(Integer.class::cast) - .collect(Collectors.toList()); - } - - /** - * Inner code to roll a dice. Support normal and planar types. - * - * @param outcome - * @param source - * @param game - * @param rollDieType die type to roll, e.g. planar or numerical - * @param sidesAmount sides per die - * @param chaosSidesAmount for planar die: chaos sides - * @param planarSidesAmount for planar die: planar sides - * @param rollsAmount rolls - * @param ignoreLowestAmount for numerical die: ignore multiple rolls with - * the lowest values - * @return - */ - private List rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, - int rollsAmount, int ignoreLowestAmount) { - RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount); - if (ignoreLowestAmount > 0) { - rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount); - } - game.replaceEvent(rollDiceEvent); - - // 706.6. - // In a Planechase game, rolling the planar die will cause any ability that triggers whenever a - // player rolls one or more dice to trigger. However, any effect that refers to a numerical - // result of a die roll, including ones that compare the results of that roll to other rolls - // or to a given number, ignores the rolling of the planar die. See rule 901, “Planechase.” - // ROLL MULTIPLE dies - // results amount can be less than a rolls amount (example: The Big Idea allows rolling 2x instead 1x) - List dieResults = new ArrayList<>(); - List dieRolls = new ArrayList<>(); - for (int i = 0; i < rollDiceEvent.getAmount(); i++) { - // ROLL SINGLE die - RollDieEvent rollDieEvent = new RollDieEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides()); - game.replaceEvent(rollDieEvent); - - Object rollResult; - // big idea logic for numerical rolls only - if (rollDieEvent.getRollDieType() == RollDieType.NUMERICAL && rollDieEvent.getBigIdeaRollsAmount() > 0) { - // rolls 2x + sum results - // The Big Idea: roll two six-sided dice and use the total of those results - int totalSum = 0; - for (int j = 0; j < rollDieEvent.getBigIdeaRollsAmount() + 1; j++) { - int singleResult = (Integer) rollDieInner( - outcome, - game, - source, - rollDieEvent.getRollDieType(), - rollDieEvent.getSides(), - chaosSidesAmount, - planarSidesAmount, - rollDieEvent.getRollsAmount()); - totalSum += singleResult; - dieRolls.add(new RollDieResult(singleResult, rollDieEvent.getResultModifier(), null)); - } - rollResult = totalSum; - } else { - // rolls 1x - switch (rollDieEvent.getRollDieType()) { - default: - case NUMERICAL: { - int naturalResult = (Integer) rollDieInner( - outcome, - game, - source, - rollDieEvent.getRollDieType(), - rollDieEvent.getSides(), - chaosSidesAmount, - planarSidesAmount, - rollDieEvent.getRollsAmount() - ); - dieRolls.add(new RollDieResult(naturalResult, rollDieEvent.getResultModifier(), null)); - rollResult = naturalResult; - break; - } - - case PLANAR: { - PlanarDieRollResult planarResult = (PlanarDieRollResult) rollDieInner( - outcome, - game, - source, - rollDieEvent.getRollDieType(), - rollDieEvent.getSides(), - chaosSidesAmount, - planarSidesAmount, - rollDieEvent.getRollsAmount() - ); - dieRolls.add(new RollDieResult(0, 0, planarResult)); - rollResult = planarResult; - break; - } - } - } - dieResults.add(rollResult); - } - - // ignore the lowest results - // planar dies: due to 706.6. planar die results must be fully ignored - // - // 706.5. - // If a player is instructed to roll two or more dice and ignore the lowest roll, the roll - // that yielded the lowest result is considered to have never happened. No abilities trigger - // because of the ignored roll, and no effects apply to that roll. If multiple results are tied - // for the lowest, the player chooses one of those rolls to be ignored. - int diceRolledTotal = dieRolls.size(); - String ignoreMessage; - if (rollDiceEvent.getRollDieType() == RollDieType.NUMERICAL && rollDiceEvent.getIgnoreLowestAmount() > 0) { - // find ignored values - List ignoredResults = new ArrayList<>(); - for (int i = 0; i < rollDiceEvent.getIgnoreLowestAmount(); i++) { - int min = dieResults.stream().map(Integer.class::cast).mapToInt(Integer::intValue).min().orElse(0); - dieResults.remove(Integer.valueOf(min)); - ignoredResults.add(min); - } - ignoreMessage = String.format( - ignoredResults.size() > 1 ? ", ignoring [%s]" : ", ignoring %s", - ignoredResults - .stream() - .map(x -> "" + x) - .collect(Collectors.joining(", ")) - ); - // remove ignored rolls (they not exist anymore) - List newRolls = new ArrayList<>(); - for (RollDieResult rollDieResult : dieRolls) { - if (ignoredResults.contains(rollDieResult.getResult())) { - ignoredResults.remove((Integer) rollDieResult.getResult()); - } else { - newRolls.add(rollDieResult); - } - } - dieRolls.clear(); - dieRolls.addAll(newRolls); - } else { - ignoreMessage = ""; - } - - // raise affected roll events - for (RollDieResult result : dieRolls) { - game.fireEvent(new DieRolledEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides(), result.naturalResult, result.modifier, result.planarResult)); - } - game.fireEvent(new DiceRolledEvent(rollDiceEvent.getSides(), dieResults, source)); - - String resultString = dieResults - .stream() - .map(Object::toString) - .collect(Collectors.joining(", ")); - String message; - switch (rollDiceEvent.getRollDieType()) { - default: - case NUMERICAL: - // [Roll a die] user rolled 4d6, results: [4, 6], ignoring [1, 3] (source: xxx) - message = String.format("[Roll a die] %s rolled %sd%s, result%s: %s%s%s", - getLogName(), - diceRolledTotal > 1 ? diceRolledTotal : "a ", - rollDiceEvent.getSides(), - dieResults.size() > 1 ? 's' : "", - dieResults.size() > 1 ? '[' + resultString + ']' : resultString, - ignoreMessage, - CardUtil.getSourceLogName(game, source)); - break; - case PLANAR: - // [Roll a planar die] user rolled CHAOS (source: xxx) - message = String.format("[Roll a planar die] %s rolled %s%s", - getLogName(), - dieResults.size() > 1 ? '[' + resultString + ']' : resultString, - CardUtil.getSourceLogName(game, source)); - break; - } - game.informPlayers(message); - return dieResults; - } - - /** - * @param source - * @param game - * @param chaosSidesAmount The number of chaos sides the planar die - * currently has (normally 1 but can be 5) - * @param planarSidesAmount The number of chaos sides the planar die - * currently has (normally 1) - * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll - * or BlankRoll - */ - @Override - public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int chaosSidesAmount, int planarSidesAmount) { - return rollDiceInner(outcome, source, game, RollDieType.PLANAR, GameOptions.PLANECHASE_PLANAR_DIE_TOTAL_SIDES, chaosSidesAmount, planarSidesAmount, 1, 0) - .stream() - .map(o -> (PlanarDieRollResult) o) - .findFirst() - .orElse(PlanarDieRollResult.BLANK_ROLL); - } - - @Override - public List getAvailableAttackers(Game game) { - // TODO: get available opponents and their planeswalkers, check for each if permanent can attack one - return getAvailableAttackers(null, game); - } - - @Override - public List getAvailableAttackers(UUID defenderId, Game game) { - FilterCreatureForCombat filter = new FilterCreatureForCombat(); - List attackers = game.getBattlefield().getAllActivePermanents(filter, playerId, game); - attackers.removeIf(entry -> !entry.canAttack(defenderId, game)); - return attackers; - } - - @Override - public List getAvailableBlockers(Game game) { - FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock(); - return game.getBattlefield().getAllActivePermanents(blockFilter, playerId, game); - } - - /** - * Returns the mana options the player currently has. That means which - * combinations of mana are available to cast spells or activate abilities - * etc. - * - * @param game - * @return - */ - @Override - public ManaOptions getManaAvailable(Game game) { - boolean oldState = game.inCheckPlayableState(); - game.setCheckPlayableState(true); - - ManaOptions availableMana = new ManaOptions(); - availableMana.addMana(manaPool.getMana()); - // conditional mana - for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { - availableMana.addMana(conditionalMana); - } - - List> sourceWithoutManaCosts = new ArrayList<>(); - List> sourceWithCosts = new ArrayList<>(); - for (Card card : getHand().getCards(game)) { - Abilities manaAbilities - = card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game); - for (Iterator it = manaAbilities.iterator(); it.hasNext(); ) { - ActivatedManaAbilityImpl ability = it.next(); - Abilities noTapAbilities = new AbilitiesImpl<>(ability); - if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { - sourceWithoutManaCosts.add(noTapAbilities); - } else { - sourceWithCosts.add(noTapAbilities); - } - } - } - - for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range - Boolean canUse = null; - boolean canAdd = false; - boolean useLater = false; // sources with mana costs or mana pool dependency - Abilities manaAbilities - = permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true - for (Iterator it = manaAbilities.iterator(); it.hasNext(); ) { - ActivatedManaAbilityImpl ability = it.next(); - if (canUse == null) { - canUse = permanent.canUseActivatedAbilities(game); - } - if (canUse) { - // abilities without Tap costs have to be handled as separate sources, because they can be used also - if (!ability.hasTapCost()) { - it.remove(); - Abilities noTapAbilities = new AbilitiesImpl<>(ability); - if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { - sourceWithoutManaCosts.add(noTapAbilities); - } else { - sourceWithCosts.add(noTapAbilities); - } - continue; - } - - canAdd = true; - if (!ability.getManaCosts().isEmpty() || ability.isPoolDependant()) { - useLater = true; - break; - } - } - } - if (canAdd) { - if (useLater) { - sourceWithCosts.add(manaAbilities); - } else { - sourceWithoutManaCosts.add(manaAbilities); - } - } - } - - for (Abilities manaAbilities : sourceWithoutManaCosts) { - availableMana.addMana(manaAbilities, game); - } - - boolean anAbilityWasUsed = true; - boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production - while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) { - anAbilityWasUsed = false; - for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext(); ) { - Abilities manaAbilities = iterator.next(); - if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) { - boolean used; - if (manaAbilities.hasPoolDependantAbilities()) { - used = availableMana.addManaPoolDependant(manaAbilities, game); - } else { - used = availableMana.addManaWithCost(manaAbilities, game); - } - if (used) { - iterator.remove(); - availableMana.removeDuplicated(); - anAbilityWasUsed = true; - } - } - } - if (!anAbilityWasUsed && !usePoolDependantAbilities) { - usePoolDependantAbilities = true; - anAbilityWasUsed = true; - } - } - - // remove duplicated variants (see ManaOptionsTest for info - when that rises) - availableMana.removeDuplicated(); - - game.setCheckPlayableState(oldState); - return availableMana; - } - - /** - * Used during calculation of available mana to gather the amount of - * producable triggered mana caused by using mana sources. So the set value - * is only used during the calculation of the mana produced by one source - * and cleared thereafter - * - * @param netManaAvailable the net mana produced by the triggered mana - * abaility - */ - @Override - public void addAvailableTriggeredMana(List netManaAvailable - ) { - this.availableTriggeredManaList.add(netManaAvailable); - } - - /** - * Used during calculation of available mana to get the amount of producable - * triggered mana caused by using mana sources. The list is cleared as soon - * the value is retrieved during available mana calculation. - * - * @return - */ - @Override - public List> getAvailableTriggeredMana() { - return availableTriggeredManaList; - } - // returns only mana producers that don't require mana payment - - protected List getAvailableManaProducers(Game game) { - List result = new ArrayList<>(); - for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range - Boolean canUse = null; - boolean canAdd = false; - for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { - if (!ability.getManaCosts().isEmpty()) { - canAdd = false; - break; - } - if (canUse == null) { - canUse = permanent.canUseActivatedAbilities(game); - } - if (canUse && ability.canActivate(playerId, game).canActivate()) { - canAdd = true; - } - } - if (canAdd) { - result.add(permanent); - } - } - for (Card card : getHand().getCards(game)) { - boolean canAdd = false; - for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.HAND)) { - if (!ability.getManaCosts().isEmpty()) { - canAdd = false; - break; - } - if (ability.canActivate(playerId, game).canActivate()) { - canAdd = true; - } - } - if (canAdd) { - result.add(card); - } - } - return result; - } - - // returns only mana producers that require mana payment - public List getAvailableManaProducersWithCost(Game game) { - List result = new ArrayList<>(); - for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { - Boolean canUse = null; - for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { - if (canUse == null) { - canUse = permanent.canUseActivatedAbilities(game); - } - if (canUse && ability.canActivate(playerId, game).canActivate() - && !ability.getManaCosts().isEmpty()) { - result.add(permanent); - break; - } - } - } - return result; - } - - /** - * @param ability - * @param availableMana if null, it won't be checked if enough mana is - * available - * @param sourceObject - * @param game - * @return - */ - protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { - if (!(ability instanceof ActivatedManaAbilityImpl)) { - ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability - if (!copy.canActivate(playerId, game).canActivate()) { - return false; - } - if (availableMana != null) { - sourceObject.adjustCosts(copy, game); - game.getContinuousEffects().costModification(copy, game); - } - boolean canBeCastRegularly = true; - if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { - // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... - // 117.6a (...) If an alternative cost is applied to an unpayable cost, - // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. - canBeCastRegularly = false; - } - if (canBeCastRegularly) { - if (canPayMinimumManaCost(copy, availableMana, game)) { - return true; - } - } - - // ALTERNATIVE COST FROM dynamic effects - if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) { - ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()); - Costs costs = getCastSourceIdCosts().get(copy.getSourceId()); - - boolean canPutToPlay = true; - if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { - canPutToPlay = false; - } - if (costs != null && !costs.canPay(copy, copy, playerId, game)) { - canPutToPlay = false; - } - - if (canPutToPlay) { - return true; - } - } - - // ALTERNATIVE COST from source card (any AlternativeSourceCosts) - if (AbilityType.SPELL.equals(ability.getAbilityType())) { - return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); - } - } - return false; - } - - protected boolean canPayMinimumManaCost(ActivatedAbility ability, ManaOptions availableMana, Game game) { - ManaOptions abilityOptions = ability.getMinimumCostToActivate(playerId, game); - if (abilityOptions.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - // Check for pay option with like phyrexian mana - if (getPhyrexianColors() != null) { - addPhyrexianLikePayOptions(abilityOptions, availableMana, game); - } - - ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(), - AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); - for (Mana mana : abilityOptions) { - if (mana.count() == 0) { - return true; - } - for (Mana avail : availableMana) { - // TODO: SPEND_OTHER_MANA effects with getAsThoughManaType can change mana type to pay, - // but that code processing it as any color, need to test and fix another use cases - // (example: Sunglasses of Urza - may spend white mana as though it were red mana) - - // - // add tests for non any color like Sunglasses of Urza - if (approvingObject != null && mana.count() <= avail.count()) { - return true; - } - if (avail instanceof ConditionalMana && !((ConditionalMana) avail).apply(ability, game, getId(), ability.getManaCosts())) { - continue; - } - if (mana.enough(avail)) { // here we need to check if spend mana as though allow to pay the mana cost - return true; - } - } - } - } - return false; - } - - private void addPhyrexianLikePayOptions(ManaOptions abilityOptions, ManaOptions availableMana, Game game) { - int maxLifeMana = getLife() / 2; - if (maxLifeMana > 0) { - Set phyrexianOptions = new HashSet<>(); - for (Mana mana : abilityOptions) { - int availableLifeMana = maxLifeMana; - if (getPhyrexianColors().isBlack()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLACK); - } - if (getPhyrexianColors().isBlue()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLUE); - } - if (getPhyrexianColors().isRed()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.RED); - } - if (getPhyrexianColors().isGreen()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.GREEN); - } - if (getPhyrexianColors().isWhite()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.WHITE); - } - } - abilityOptions.addAll(phyrexianOptions); - } - } - - private int createReducedManaPayOption(int availableLifeMana, Mana oldPayOption, Set phyrexianOptions, ManaType manaType) { - if (oldPayOption.get(manaType) > 0) { - Mana manaCopy = oldPayOption.copy(); - int restVal; - if (availableLifeMana > oldPayOption.get(manaType)) { - restVal = 0; - availableLifeMana -= oldPayOption.get(manaType); - } else { - restVal = CardUtil.overflowDec(oldPayOption.get(manaType), availableLifeMana); - availableLifeMana = 0; - } - manaCopy.set(manaType, restVal); - phyrexianOptions.add(manaCopy); - } - return availableLifeMana; - } - - protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) { - if (sourceObject != null && !(sourceObject instanceof Permanent)) { - Ability copyAbility; // for alternative cost and reduce tries - for (Ability alternateSourceCostsAbility : sourceObject.getAbilities()) { - // if cast for noMana no Alternative costs are allowed - if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) { - if (((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) { - if (alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl(); - for (Cost cost : alternateSourceCostsAbility.getCosts()) { - // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost2) { - if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); - } - } else { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - } - - if (manaCosts.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - - // alternative cost reduce - copyAbility = ability.copy(); - copyAbility.getManaCostsToPay().clear(); - copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); - sourceObject.adjustCosts(copyAbility, game); - game.getContinuousEffects().costModification(copyAbility, game); - - // reduced all cost - if (copyAbility.getManaCostsToPay().isEmpty()) { - return true; - } - - for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - if (availableMana.enough(mana)) { - return true; - } - } - } - } - } - } - } - - // controller specific alternate spell costs - for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) { - if (alternateSourceCosts instanceof Ability) { - if (alternateSourceCosts.isAvailable(ability, game)) { - if (((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl(); - for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { - // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost2) { - if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); - } - } else { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - } - - if (manaCosts.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - - // alternative cost reduce - copyAbility = ability.copy(); - copyAbility.getManaCostsToPay().clear(); - copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); - sourceObject.adjustCosts(copyAbility, game); - game.getContinuousEffects().costModification(copyAbility, game); - - // reduced all cost - if (copyAbility.getManaCostsToPay().isEmpty()) { - return true; - } - - for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - if (availableMana.enough(mana)) { - return true; - } - } - } - } - } - } - } - } - return false; - } - - protected ActivatedAbility findActivatedAbilityFromPlayable(MageObject object, ManaOptions availableMana, Ability ability, Game game) { - - // special mana to pay spell cost - ManaOptions manaFull = availableMana.copy(); - if (ability instanceof SpellAbility) { - for (AlternateManaPaymentAbility altAbility : CardUtil.getAbilities(object, game).stream() - .filter(a -> a instanceof AlternateManaPaymentAbility) - .map(a -> (AlternateManaPaymentAbility) a) - .collect(Collectors.toList())) { - ManaOptions manaSpecial = altAbility.getManaOptions(ability, game, ability.getManaCostsToPay()); - manaFull.addMana(manaSpecial); - } - } - - // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) - if (ability instanceof ActivatedManaAbilityImpl) { - // mana ability - if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) { - return (ActivatedManaAbilityImpl) ability; - } - } else if (ability instanceof AlternativeSourceCosts) { - // alternative cost must be replaced by real play ability - return findActivatedAbilityFromAlternativeSourceCost(object, manaFull, ability, game); - } else if (ability instanceof ActivatedAbility) { - // all other activated ability - if (canPlay((ActivatedAbility) ability, manaFull, object, game)) { - return (ActivatedAbility) ability; - } - } - - // non playable abilities like static - return null; - } - - protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions availableMana, Ability ability, Game game) { - // return play ability that can activate AlternativeSourceCosts - if (ability instanceof AlternativeSourceCosts && object != null && !(object instanceof Permanent)) { - ActivatedAbility playAbility = null; - if (object.isLand(game)) { - playAbility = (PlayLandAbility) CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); - } else if (object instanceof Card) { - playAbility = ((Card) object).getSpellAbility(); - } - if (playAbility == null) { - return null; - } - - // 707.4.Objects that are cast face down are turned face down before they are put onto the stack - // E.g. no lands per turn limit, no cast restrictions, cost reduce, etc - // Even mana cost can't be checked here without lookahead - // So make it available all the time - boolean canUse; - if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId()) - || (null != game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game)))) { - canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game); - } else { - canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions - } - - if (canUse) { - return playAbility; - } - } - return null; - } - - private void getPlayableFromObjectAll(Game game, Zone fromZone, MageObject object, ManaOptions availableMana, List output) { - if (fromZone == null || object == null) { - return; - } - - // BASIC abilities - if (object instanceof SplitCard) { - SplitCard mainCard = (SplitCard) object; - getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof ModalDoubleFacesCard) { - ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) object; - getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof AdventureCard) { - // adventure must use different card characteristics for different spells (main or adventure) - AdventureCard adventureCard = (AdventureCard) object; - getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof Card) { - getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output); - } else if (object instanceof StackObject) { - // spells on stack are processing by Card above, other stack objects must be ignored - } else { - // other things like CommandObject - getPlayableFromObjectSingle(game, fromZone, object, object.getAbilities(), availableMana, output); - } - - // DYNAMIC ADDED abilities are adds in getAbilities(game) - } - - private void getPlayableFromObjectSingle(Game game, Zone fromZone, MageObject object, Abilities candidateAbilities, ManaOptions availableMana, List output) { - // check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller) - // must check all abilities, not activated only - for (Ability ability : candidateAbilities) { - if (!(ability instanceof ActivatedAbility)) { - continue; - } - boolean isPlaySpell = (ability instanceof SpellAbility); - boolean isPlayLand = (ability instanceof PlayLandAbility); - - // as original controller - // play land restrictions - if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( - GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), - ability, this.getId()), ability, game, true)) { - continue; - } - // cast spell restrictions 1 - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability, this.getId()); - castEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castEvent, ability, game, true)) { - continue; - } - // cast spell restrictions 2 - GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, - ability.getId(), ability, this.getId()); - castLateEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castLateEvent, ability, game, true)) { - continue; - } - - ApprovingObject approvingObject; - if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) { - // play hand from non hand zone (except battlefield - you can't play already played permanents) - approvingObject = game.getContinuousEffects().asThough(object.getId(), - AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); - } else { - // other abilities from direct zones - approvingObject = null; - } - - boolean canActivateAsHandZone = approvingObject != null - || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); - boolean possibleToPlay = canActivateAsHandZone - && ability.getZone().match(Zone.HAND) - && (isPlaySpell || isPlayLand); - - // spell/hand abilities (play from all zones) - // need permitingObject or canPlayCardsFromGraveyard - // zone's abilities (play from specific zone) - // no need in permitingObject - if (fromZone != Zone.ALL && ability.getZone().match(fromZone)) { - possibleToPlay = true; - } - - if (!possibleToPlay) { - continue; - } - - // direct mode (with original controller) - ActivatedAbility playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); - if (playAbility != null && !output.contains(playAbility)) { - output.add(playAbility); - continue; - } - - // from non hand mode (with affected controller) - if (canActivateAsHandZone && ability.getControllerId() != this.getId()) { - UUID savedControllerId = ability.getControllerId(); - ability.setControllerId(this.getId()); - try { - playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); - if (playAbility != null && !output.contains(playAbility)) { - output.add(playAbility); - } - } finally { - ability.setControllerId(savedControllerId); - } - } - } - } - - @Override - public List getPlayable(Game game, boolean hidden) { - return getPlayable(game, hidden, Zone.ALL, true); - } - - /** - * Returns a list of all available spells and abilities the player can - * currently cast/activate with his available resources - * - * @param game - * @param hidden also from hidden objects (e.g. turned face down cards ?) - * @param fromZone of objects from which zone (ALL = from all zones) - * @param hideDuplicatedAbilities if equal abilities exist return only the - * first instance - * @return - */ - public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { - List playable = new ArrayList<>(); - if (shouldSkipGettingPlayable(game)) { - return playable; - } - - boolean previousState = game.inCheckPlayableState(); - game.setCheckPlayableState(true); - try { - ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition)) - boolean fromAll = fromZone.equals(Zone.ALL); - if (hidden && (fromAll || fromZone == Zone.HAND)) { - for (Card card : hand.getCards(game)) { - for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) - if (ability.getZone().match(Zone.HAND)) { - boolean isPlaySpell = (ability instanceof SpellAbility); - boolean isPlayLand = (ability instanceof PlayLandAbility); - - // play land restrictions - if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( - GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), - ability, this.getId()), ability, game, true)) { - continue; - } - // cast spell restrictions 1 - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, - ability.getId(), ability, this.getId()); - castEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castEvent, ability, game, true)) { - continue; - } - // cast spell restrictions 2 - GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, - ability.getId(), ability, this.getId()); - castLateEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castLateEvent, ability, game, true)) { - continue; - } - - ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); - if (playAbility != null && !playable.contains(playAbility)) { - playable.add(playAbility); - } - } - } - } - } - - if (fromAll || fromZone == Zone.GRAVEYARD) { - for (UUID playerId : game.getState().getPlayersInRange(getId(), game)) { - Player player = game.getPlayer(playerId); - if (player == null) { - continue; - } - for (Card card : player.getGraveyard().getCards(game)) { - getPlayableFromObjectAll(game, Zone.GRAVEYARD, card, availableMana, playable); - } - } - } - - if (fromAll || fromZone == Zone.EXILED) { - for (ExileZone exile : game.getExile().getExileZones()) { - for (Card card : exile.getCards(game)) { - getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable); - } - } - } - - // check to play revealed cards - if (fromAll) { - for (Cards revealedCards : game.getState().getRevealed().values()) { - for (Card card : revealedCards.getCards(game)) { - // revealed cards can be from any zones - getPlayableFromObjectAll(game, game.getState().getZone(card.getId()), card, availableMana, playable); - } - } - } - - // outside cards - if (fromAll || fromZone == Zone.OUTSIDE) { - // companion cards - for (Cards companionCards : game.getState().getCompanion().values()) { - for (Card card : companionCards.getCards(game)) { - getPlayableFromObjectAll(game, Zone.OUTSIDE, card, availableMana, playable); - } - } - - // sideboard cards (example: Wish) - for (UUID sideboardCardId : this.getSideboard()) { - Card sideboardCard = game.getCard(sideboardCardId); - if (sideboardCard != null) { - getPlayableFromObjectAll(game, Zone.OUTSIDE, sideboardCard, availableMana, playable); - } - } - } - - // check if it's possible to play the top card of a library - if (fromAll || fromZone == Zone.LIBRARY) { - for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { - Player player = game.getPlayer(playerInRangeId); - if (player != null && player.getLibrary().hasCards()) { - Card card = player.getLibrary().getFromTop(game); - if (card != null) { - getPlayableFromObjectAll(game, Zone.LIBRARY, card, availableMana, playable); - } - } - } - } - - // check the hand zone (Sen Triplets) - // TODO: remove direct hand check (reveal fix in Sen Triplets)? - // human games: cards from opponent's hand must be revealed before play - // AI games: computer can see and play cards from opponent's hand without reveal - if (fromAll || fromZone == Zone.HAND) { - for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { - Player player = game.getPlayer(playerInRangeId); - if (player != null && !player.getHand().isEmpty()) { - for (Card card : player.getHand().getCards(game)) { - if (card != null) { - getPlayableFromObjectAll(game, Zone.HAND, card, availableMana, playable); - } - } - } - } - } - - // eliminate duplicate activated abilities (uses for AI plays) - Map activatedUnique = new HashMap<>(); - List activatedAll = new ArrayList<>(); - - // activated abilities from battlefield objects - if (fromAll || fromZone == Zone.BATTLEFIELD) { - for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { - boolean canUseActivated = permanent.canUseActivatedAbilities(game); - List currentPlayable = new ArrayList<>(); - getPlayableFromObjectAll(game, Zone.BATTLEFIELD, permanent, availableMana, currentPlayable); - for (ActivatedAbility ability : currentPlayable) { - if (ability instanceof SpecialAction || canUseActivated) { - activatedUnique.putIfAbsent(ability.toString(), ability); - activatedAll.add(ability); - } - } - } - } - - // activated abilities from stack objects - if (fromAll || fromZone == Zone.STACK) { - for (StackObject stackObject : game.getState().getStack()) { - List currentPlayable = new ArrayList<>(); - getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable); - for (ActivatedAbility ability : currentPlayable) { - activatedUnique.put(ability.toString(), ability); - activatedAll.add(ability); - } - } - } - - // activated abilities from objects in the command zone (emblems or commanders) - if (fromAll || fromZone == Zone.COMMAND) { - for (CommandObject commandObject : game.getState().getCommand()) { - List currentPlayable = new ArrayList<>(); - getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable); - for (ActivatedAbility ability : currentPlayable) { - activatedUnique.put(ability.toString(), ability); - activatedAll.add(ability); - } - } - } - - if (hideDuplicatedAbilities) { - playable.addAll(activatedUnique.values()); - } else { - playable.addAll(activatedAll); - } - } finally { - game.setCheckPlayableState(previousState); - } - - return playable; - } - - /** - * Creates a list of card ids that are currently playable.
- * Used to mark the playable cards in GameView Also contains number of - * playable abilities for that object (it's just info, server decides to - * show choose dialog or not) - * - * @param game - * @return A Set of cardIds that are playable and amount of playable - * abilities - */ - @Override - public PlayableObjectsList getPlayableObjects(Game game, Zone zone) { - // collect abilities per object - List playableAbilities = getPlayable(game, true, zone, false); // do not hide duplicated abilities/cards - Map> playableObjects = new HashMap<>(); - for (ActivatedAbility ability : playableAbilities) { - if (ability.getSourceId() != null) { - - // normal card - putToPlayableObjects(playableObjects, ability.getSourceId(), ability); - - // main card - must be marked playable in GUI - Card card = game.getCard(ability.getSourceId()); - if (card != null && card.getMainCard().getId() != card.getId()) { - putToPlayableObjects(playableObjects, card.getMainCard().getId(), ability); - } - - // spell on stack - can have activated abilities, - // so mark it as playable too (users must able to clicks on stack objects) - // example: Lightning Storm - Spell spell = game.getSpell(ability.getSourceId()); - if (spell != null) { - putToPlayableObjects(playableObjects, spell.getId(), ability); - } - } - } - - // collect stats - PlayableObjectsList playableObjectsList = new PlayableObjectsList(playableObjects); - return playableObjectsList; - } - - private void putToPlayableObjects(Map> playableObjects, UUID objectId, ActivatedAbility ability) { - if (!playableObjects.containsKey(objectId)) { - playableObjects.put(objectId, new ArrayList<>()); - } - playableObjects.get(objectId).add(ability); - } - - /** - * Skip "silent" phase step when players are not allowed to cast anything. - * E.g. players can't play or cast anything during declaring attackers. - * - * @param game - * @return - */ - private boolean shouldSkipGettingPlayable(Game game) { - if (game.getStep() == null) { // happens at the start of the game - return true; - } - for (Entry phaseStep : silentPhaseSteps.entrySet()) { - if (game.getPhase() != null - && game.getPhase().getStep() != null - && phaseStep.getKey() == game.getPhase().getStep().getType()) { - if (phaseStep.getValue() == null - || phaseStep.getValue() == game.getPhase().getStep().getStepPart()) { - return true; - } - } - } - return false; - } - - /** - * Only used for AIs - * - * @param ability - * @param game - * @return - */ - @Override - public List getPlayableOptions(Ability ability, Game game) { - List options = new ArrayList<>(); - if (ability.isModal()) { - addModeOptions(options, ability, game); - } else if (!ability.getTargets().getUnchosen().isEmpty()) { - // TODO: Handle other variable costs than mana costs - if (!ability.getManaCosts().getVariableCosts().isEmpty()) { - addVariableXOptions(options, ability, 0, game); - } else { - addTargetOptions(options, ability, 0, game); - } - } else if (!ability.getCosts().getTargets().getUnchosen().isEmpty()) { - addCostTargetOptions(options, ability, 0, game); - } - - return options; - } - - private void addModeOptions(List options, Ability option, Game game) { - // TODO: Support modal spells with more than one selectable mode - for (Mode mode : option.getModes().values()) { - Ability newOption = option.copy(); - newOption.getModes().clearSelectedModes(); - newOption.getModes().addSelectedMode(mode.getId()); - newOption.getModes().setActiveMode(mode); - if (!newOption.getTargets().getUnchosen().isEmpty()) { - if (!newOption.getManaCosts().getVariableCosts().isEmpty()) { - addVariableXOptions(options, newOption, 0, game); - } else { - addTargetOptions(options, newOption, 0, game); - } - } else if (!newOption.getCosts().getTargets().getUnchosen().isEmpty()) { - addCostTargetOptions(options, newOption, 0, game); - } else { - options.add(newOption); - } - } - } - - protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) { - addTargetOptions(options, option, targetNum, game); - } - - protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { - for (Target target : option.getTargets().getUnchosen().get(targetNum).getTargetOptions(option, game)) { - Ability newOption = option.copy(); - if (target instanceof TargetAmount) { - for (UUID targetId : target.getTargets()) { - int amount = target.getTargetAmount(targetId); - newOption.getTargets().get(targetNum).addTarget(targetId, amount, newOption, game, true); - } - } else { - for (UUID targetId : target.getTargets()) { - newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); - } - } - if (targetNum < option.getTargets().size() - 2) { - addTargetOptions(options, newOption, targetNum + 1, game); - } else if (!option.getCosts().getTargets().isEmpty()) { - addCostTargetOptions(options, newOption, 0, game); - } else { - options.add(newOption); - } - } - } - - private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) { - for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(option.getSourceId(), playerId, game)) { - Ability newOption = option.copy(); - newOption.getCosts().getTargets().get(targetNum).addTarget(targetId, option, game, true); - if (targetNum < option.getCosts().getTargets().size() - 1) { - addCostTargetOptions(options, newOption, targetNum + 1, game); - } else { - options.add(newOption); - } - } - } - - @Override - public boolean isTestsMode() { - return isTestMode; - } - - @Override - public void setTestMode(boolean value) { - this.isTestMode = value; - } - - @Override - public boolean isTopCardRevealed() { - return topCardRevealed; - } - - @Override - public void setTopCardRevealed(boolean topCardRevealed) { - this.topCardRevealed = topCardRevealed; - } - - @Override - public UserData getUserData() { - return this.userData; - } - - public UserData getControllingPlayersUserData(Game game) { - if (!isGameUnderControl()) { - Player player = game.getPlayer(getTurnControlledBy()); - if (player.isHuman()) { - return player.getUserData(); - } - } - return this.userData; - } - - @Override - public void setUserData(UserData userData) { - this.userData = userData; - getManaPool().setAutoPayment(userData.isManaPoolAutomatic()); - getManaPool().setAutoPaymentRestricted(userData.isManaPoolAutomaticRestricted()); - } - - @Override - public void addAction(String action - ) { - // do nothing - } - - @Override - public int getActionCount() { - return 0; - } - - @Override - public void setAllowBadMoves(boolean allowBadMoves) { - // do nothing - } - - @Override - public boolean canPayLifeCost(Ability ability) { - if (!canPayLifeCost - && (AbilityType.ACTIVATED.equals(ability.getAbilityType()) - || AbilityType.SPELL.equals(ability.getAbilityType()))) { - return false; - } - return isLifeTotalCanChange(); - } - - @Override - public boolean getCanPayLifeCost() { - return canPayLifeCost; - } - - @Override - public void setCanPayLifeCost(boolean canPayLifeCost) { - this.canPayLifeCost = canPayLifeCost; - } - - @Override - public boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game) { - return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, source.getSourceId(), controllerId, game); - } - - @Override - public void setCanPaySacrificeCostFilter(FilterPermanent filter - ) { - this.sacrificeCostFilter = filter; - } - - @Override - public FilterPermanent getSacrificeCostFilter() { - return sacrificeCostFilter; - } - - @Override - public boolean canLoseByZeroOrLessLife() { - return loseByZeroOrLessLife; - } - - @Override - public void setLoseByZeroOrLessLife(boolean loseByZeroOrLessLife) { - this.loseByZeroOrLessLife = loseByZeroOrLessLife; - } - - @Override - public boolean canPlayCardsFromGraveyard() { - return canPlayCardsFromGraveyard; - } - - @Override - public void setPlayCardsFromGraveyard(boolean playCardsFromGraveyard) { - this.canPlayCardsFromGraveyard = playCardsFromGraveyard; - } - - @Override - public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) { - this.drawsOnOpponentsTurn = drawsOnOpponentsTurn; - } - - @Override - public boolean isDrawsOnOpponentsTurn() { - return drawsOnOpponentsTurn; - } - - @Override - public boolean autoLoseGame() { - return false; - } - - @Override - public void becomesActivePlayer() { - this.passedAllTurns = false; - this.passedUntilEndStepBeforeMyTurn = false; - this.turns++; - } - - @Override - public int getTurns() { - return turns; - } - - @Override - public int getStoredBookmark() { - return storedBookmark; - } - - @Override - public void setStoredBookmark(int storedBookmark) { - this.storedBookmark = storedBookmark; - } - - @Override - public synchronized void resetStoredBookmark(Game game) { - if (this.storedBookmark != -1) { - game.removeBookmark(this.storedBookmark); - } - setStoredBookmark(-1); - } - - @Override - public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { - if (null != game.getContinuousEffects().asThough(card.getId(), - AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) { - // two modes: look at the card or do not look and activate other abilities - String lookMessage = "Look at " + card.getIdName(); - String lookYes = "Yes, look at the card"; - String lookNo = "No, play/activate the card/ability"; - if (chooseUse(Outcome.Benefit, lookMessage, "", lookYes, lookNo, null, game)) { - Cards cards = new CardsImpl(card); - this.lookAtCards(getName() + " - " + card.getIdName() + " - " - + CardUtil.sdf.format(System.currentTimeMillis()), cards, game); - return true; - } - } - return false; - } - - @Override - public void setPriorityTimeLeft(int timeLeft - ) { - priorityTimeLeft = timeLeft; - } - - @Override - public int getPriorityTimeLeft() { - return priorityTimeLeft; - } - - @Override - public boolean hasQuit() { - return quit; - } - - @Override - public boolean hasTimerTimeout() { - return timerTimeout; - } - - @Override - public boolean hasIdleTimeout() { - return idleTimeout; - } - - @Override - public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving) { - this.reachedNextTurnAfterLeaving = reachedNextTurnAfterLeaving; - } - - @Override - public boolean hasReachedNextTurnAfterLeaving() { - return reachedNextTurnAfterLeaving; - } - - @Override - public boolean canJoinTable(Table table - ) { - return !table.userIsBanned(name); - } - - @Override - public void addCommanderId(UUID commanderId - ) { - this.commandersIds.add(commanderId); - } - - @Override - public Set getCommandersIds() { - return this.commandersIds; - } - - @Override - public boolean moveCards(Card card, Zone toZone, Ability source, Game game) { - return moveCards(card, toZone, source, game, false, false, false, null); - } - - @Override - public boolean moveCards(Card card, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { - Set cardList = new HashSet<>(); - if (card != null) { - cardList.add(card); - } - return moveCards(cardList, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); - } - - @Override - public boolean moveCards(Cards cards, Zone toZone, Ability source, Game game) { - return moveCards(cards.getCards(game), toZone, source, game); - } - - @Override - public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game - ) { - return moveCards(cards, toZone, source, game, false, false, false, null); - } - - @Override - public boolean moveCards(Set cards, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { - if (cards.isEmpty()) { - return true; - } - Set successfulMovedCards = new LinkedHashSet<>(); - Zone fromZone = null; - switch (toZone) { - case GRAVEYARD: - fromZone = game.getState().getZone(cards.iterator().next().getId()); - successfulMovedCards = moveCardsToGraveyardWithInfo(cards, source, game, fromZone); - return !successfulMovedCards.isEmpty(); - case BATTLEFIELD: // new logic that does not yet add the permanents to battlefield while replacement effects are handled - List infoList = new ArrayList<>(); - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - ZoneChangeEvent event = new ZoneChangeEvent(card.getId(), source, - byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects); - infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped, source)); - } - infoList = ZonesHandler.moveCards(infoList, game, source); - for (ZoneChangeInfo info : infoList) { - Permanent permanent = game.getPermanent(info.event.getTargetId()); - if (permanent != null) { - successfulMovedCards.add(permanent); - if (!game.isSimulation()) { - Player eventPlayer = game.getPlayer(info.event.getPlayerId()); - if (eventPlayer != null && fromZone != null) { - game.informPlayers(eventPlayer.getLogName() + " puts " - + (info.faceDown ? "a card face down " : permanent.getLogName()) + " from " - + fromZone.toString().toLowerCase(Locale.ENGLISH) + " onto the Battlefield" - + CardUtil.getSourceLogName(game, source, permanent.getId())); - } - } - } - } - // TODO: must be replaced by game.getState().processAction(game), see isInUseableZoneDiesTrigger comments - // about short living LKI problem - //game.getState().processAction(game); - game.applyEffects(); - break; - case HAND: - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - boolean hideCard = fromZone == Zone.LIBRARY - || (card.isFaceDown(game) - && fromZone != Zone.STACK - && fromZone != Zone.BATTLEFIELD); - if (moveCardToHandWithInfo(card, source, game, !hideCard)) { - successfulMovedCards.add(card); - } - } - break; - case EXILED: - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - boolean withName = (fromZone == Zone.BATTLEFIELD - || fromZone == Zone.STACK) - || !card.isFaceDown(game); - if (moveCardToExileWithInfo(card, null, "", source, game, fromZone, withName)) { - successfulMovedCards.add(card); - } - } - break; - case LIBRARY: - for (Card card : cards) { - if (card instanceof Spell) { - fromZone = game.getState().getZone(((Spell) card).getSourceId()); - } else { - fromZone = game.getState().getZone(card.getId()); - } - boolean hideCard = fromZone == Zone.HAND || fromZone == Zone.LIBRARY; - if (moveCardToLibraryWithInfo(card, source, game, fromZone, true, !hideCard)) { - successfulMovedCards.add(card); - } - } - break; - case COMMAND: - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - if (moveCardToCommandWithInfo(card, source, game, fromZone)) { - successfulMovedCards.add(card); - } - } - break; - case OUTSIDE: - for (Card card : cards) { - if (card instanceof Permanent) { - game.getBattlefield().removePermanent(card.getId()); - ZoneChangeEvent event = new ZoneChangeEvent((Permanent) card, source, - byOwner ? card.getOwnerId() : getId(), Zone.BATTLEFIELD, Zone.OUTSIDE, appliedEffects); - game.fireEvent(event); - } - } - break; - default: - throw new UnsupportedOperationException("to Zone" + toZone + " not supported yet"); - } - return !successfulMovedCards.isEmpty(); - } - - @Override - public boolean moveCardsToExile(Card card, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { - Set cards = new HashSet<>(); - if (card != null) { - cards.add(card); - } - return moveCardsToExile(cards, source, game, withName, exileId, exileZoneName); - } - - @Override - public boolean moveCardsToExile(Set cards, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { - if (cards.isEmpty()) { - return true; - } - boolean result = false; - for (Card card : cards) { - Zone fromZone = game.getState().getZone(card.getId()); - result |= moveCardToExileWithInfo(card, exileId, exileZoneName, source, game, fromZone, withName); - } - return result; - } - - @Override - public boolean moveCardToHandWithInfo(Card card, Ability source, Game game, boolean withName) { - boolean result = false; - Zone fromZone = game.getState().getZone(card.getId()); - if (fromZone == Zone.BATTLEFIELD && !(card instanceof Permanent)) { - card = game.getPermanent(card.getId()); - } - if (card.moveToZone(Zone.HAND, source, game, false)) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - if (!game.isSimulation()) { - game.informPlayers(getLogName() + " puts " - + (withName ? card.getLogName() : (card.isFaceDown(game) ? "a face down card" : "a card")) - + " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' - + (card.isOwnedBy(this.getId()) ? "into their hand" : "into its owner's hand" - + CardUtil.getSourceLogName(game, source, card.getId())) - ); - } - result = true; - } - return result; - } - - @Override - public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, Game game, Zone fromZone) { - Set movedCards = new LinkedHashSet<>(); - while (!allCards.isEmpty()) { - // identify cards from one owner - Cards cards = new CardsImpl(); - UUID ownerId = null; - for (Iterator it = allCards.iterator(); it.hasNext(); ) { - Card card = it.next(); - if (cards.isEmpty()) { - ownerId = card.getOwnerId(); - } - if (card.isOwnedBy(ownerId)) { - it.remove(); - cards.add(card); - } - } - // move cards to graveyard in order the owner decides - if (!cards.isEmpty()) { - Player choosingPlayer = this; - if (!Objects.equals(ownerId, this.getId())) { - choosingPlayer = game.getPlayer(ownerId); - } - if (choosingPlayer == null) { - continue; - } - boolean chooseOrder = false; - if (userData.askMoveToGraveOrder()) { - if (cards.size() > 1) { - chooseOrder = choosingPlayer.chooseUse(Outcome.Neutral, - "Choose the order in which the cards go to the graveyard?", source, game); - } - } - if (chooseOrder) { - TargetCard target = new TargetCard(fromZone, - new FilterCard("card to put on the top of your graveyard (last one chosen will be topmost)")); - target.setRequired(true); - while (choosingPlayer.canRespond() && cards.size() > 1) { - choosingPlayer.chooseTarget(Outcome.Neutral, cards, target, source, game); - UUID targetObjectId = target.getFirstTarget(); - Card card = cards.get(targetObjectId, game); - cards.remove(targetObjectId); - if (card != null) { - fromZone = game.getState().getZone(card.getId()); - if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { - movedCards.add(card); - } - } - target.clearChosen(); - } - if (cards.size() == 1) { - Card card = cards.getCards(game).iterator().next(); - if (card != null && choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { - movedCards.add(card); - } - } - } else { - for (Card card : cards.getCards(game)) { - if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { - movedCards.add(card); - } - } - } - } - } - return movedCards; - } - - @Override - public boolean moveCardToGraveyardWithInfo(Card card, Ability source, Game game, Zone fromZone) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToZone(Zone.GRAVEYARD, source, game, false)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - StringBuilder sb = new StringBuilder(this.getLogName()) - .append(" puts ").append(card.getLogName()).append(' ').append(card.isCopy() ? "(Copy) " : "") - .append(fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' : ""); - if (card.isOwnedBy(getId())) { - sb.append("into their graveyard"); - } else { - sb.append("it into its owner's graveyard"); - } - sb.append(CardUtil.getSourceLogName(game, source, card.getId())); - game.informPlayers(sb.toString()); - } - result = true; - } - return result; - } - - @Override - public boolean moveCardToLibraryWithInfo(Card card, Ability source, Game game, Zone fromZone, boolean toTop, boolean withName) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToZone(Zone.LIBRARY, source, game, toTop)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - StringBuilder sb = new StringBuilder(this.getLogName()) - .append(" puts ").append(withName ? card.getLogName() : "a card").append(' '); - if (fromZone != null) { - sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); - } - sb.append("to the ").append(toTop ? "top" : "bottom"); - if (card.isOwnedBy(getId())) { - sb.append(" of their library"); - } else { - Player player = game.getPlayer(card.getOwnerId()); - if (player != null) { - sb.append(" of ").append(player.getLogName()).append("'s library"); - } - } - sb.append(CardUtil.getSourceLogName(game, source, card.getId())); - game.informPlayers(sb.toString()); - } - result = true; - } - return result; - } - - @Override - public boolean moveCardToCommandWithInfo(Card card, Ability source, Game game, Zone fromZone) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToZone(Zone.COMMAND, source, game, true)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - StringBuilder sb = new StringBuilder(this.getLogName()) - .append(" puts ").append(card.getLogName()).append(' '); - if (fromZone != null) { - sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); - } - if (card.isOwnedBy(getId())) { - sb.append(" to their command zone"); - } else { - Player player = game.getPlayer(card.getOwnerId()); - if (player != null) { - sb.append(" to ").append(player.getLogName()).append("'s command zone"); - } - } - sb.append(CardUtil.getSourceLogName(game, source, card.getId())); - game.informPlayers(sb.toString()); - } - result = true; - } - return result; - } - - @Override - public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, Ability source, Game game, Zone fromZone, boolean withName) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToExile(exileId, exileName, source, game)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard) { - // in case it's face down or name was changed by copying from other permanent - Card basicCard = game.getCard(card.getId()); - if (basicCard != null) { - card = basicCard; - } - } else if (card instanceof Spell) { - final Spell spell = (Spell) card; - if (spell.isCopy()) { - // copied spell, only remove from stack - game.getStack().remove(spell, game); - } - } - if (Zone.EXILED.equals(game.getState().getZone(card.getId()))) { // only if target zone was not replaced - game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() - + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' - + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) - + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); - } - - } - result = true; - } - return result; - } - - @Override - public Cards millCards(int toMill, Ability source, Game game) { - GameEvent event = GameEvent.getEvent(GameEvent.EventType.MILL_CARDS, getId(), source, getId(), toMill); - if (game.replaceEvent(event)) { - return new CardsImpl(); - } - Cards cards = new CardsImpl(this.getLibrary().getTopCards(game, event.getAmount())); - this.moveCards(cards, Zone.GRAVEYARD, source, game); - for (Card card : cards.getCards(game)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.MILLED_CARD, card.getId(), source, getId())); - } - return cards; - } - - @Override - public boolean hasOpponent(UUID playerToCheckId, Game game) { - return !this.getId().equals(playerToCheckId) - && game.isOpponent(this, playerToCheckId) - && getInRange().contains(playerToCheckId); - } - - @Override - public void cleanUpOnMatchEnd() { - - } - - @Override - public boolean getPassedAllTurns() { - return passedAllTurns; - } - - @Override - public boolean getPassedUntilNextMain() { - return passedUntilNextMain; - } - - @Override - public boolean getPassedUntilEndOfTurn() { - return passedUntilEndOfTurn; - } - - @Override - public boolean getPassedTurn() { - return passedTurn; - } - - @Override - public boolean getPassedUntilStackResolved() { - return passedUntilStackResolved; - } - - @Override - public boolean getPassedUntilEndStepBeforeMyTurn() { - return passedUntilEndStepBeforeMyTurn; - } - - @Override - public AbilityType getJustActivatedType() { - return justActivatedType; - } - - @Override - public void setJustActivatedType(AbilityType justActivatedType - ) { - this.justActivatedType = justActivatedType; - } - - @Override - public void revokePermissionToSeeHandCards() { - usersAllowedToSeeHandCards.clear(); - } - - @Override - public void addPermissionToShowHandCards(UUID watcherUserId - ) { - usersAllowedToSeeHandCards.add(watcherUserId); - } - - @Override - public boolean isPlayerAllowedToRequestHand(UUID gameId, UUID requesterPlayerId) { - return userData.isAllowRequestHandToPlayer(gameId, requesterPlayerId); - } - - @Override - public void addPlayerToRequestedHandList(UUID gameId, UUID requesterPlayerId) { - userData.addPlayerToRequestedHandList(gameId, requesterPlayerId); - } - - @Override - public boolean hasUserPermissionToSeeHand(UUID userId - ) { - return usersAllowedToSeeHandCards.contains(userId); - } - - @Override - public Set getUsersAllowedToSeeHandCards() { - return usersAllowedToSeeHandCards; - } - - @Override - public void setMatchPlayer(MatchPlayer matchPlayer - ) { - this.matchPlayer = matchPlayer; - } - - @Override - public MatchPlayer getMatchPlayer() { - return matchPlayer; - } - - @Override - public void abortReset() { - abort = false; - } - - @Override - public void signalPlayerConcede() { - - } - - @Override - public boolean scry(int value, Ability source, Game game) { - GameEvent event = new GameEvent(GameEvent.EventType.SCRY, getId(), source, getId(), value, true); - if (game.replaceEvent(event)) { - return false; - } - game.informPlayers(getLogName() + " scries " + event.getAmount() + CardUtil.getSourceLogName(game, source)); - Cards cards = new CardsImpl(); - cards.addAll(getLibrary().getTopCards(game, event.getAmount())); - if (!cards.isEmpty()) { - TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, - new FilterCard("card" + (cards.size() == 1 ? "" : "s") - + " to PUT on the BOTTOM of your library (Scry)")); - chooseTarget(Outcome.Benefit, cards, target, source, game); - putCardsOnBottomOfLibrary(new CardsImpl(target.getTargets()), game, source, true); - cards.removeAll(target.getTargets()); - putCardsOnTopOfLibrary(cards, game, source, true); - } - game.fireEvent(new GameEvent(GameEvent.EventType.SCRIED, getId(), source, getId(), event.getAmount(), true)); - return true; - } - - @Override - public boolean surveil(int value, Ability source, Game game) { - GameEvent event = new GameEvent(GameEvent.EventType.SURVEIL, getId(), source, getId(), value, true); - if (game.replaceEvent(event)) { - return false; - } - game.informPlayers(getLogName() + " surveils " + event.getAmount() + CardUtil.getSourceLogName(game, source)); - Cards cards = new CardsImpl(); - cards.addAll(getLibrary().getTopCards(game, event.getAmount())); - if (!cards.isEmpty()) { - TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, - new FilterCard("cards to PUT into your GRAVEYARD (Surveil)")); - chooseTarget(Outcome.Benefit, cards, target, source, game); - moveCards(new CardsImpl(target.getTargets()), Zone.GRAVEYARD, source, game); - cards.removeAll(target.getTargets()); - putCardsOnTopOfLibrary(cards, game, source, true); - } - game.fireEvent(new GameEvent(GameEvent.EventType.SURVEILED, getId(), source, getId(), event.getAmount(), true)); - return true; - } - - @Override - public boolean addTargets(Ability ability, Game game - ) { - // only used for TestPlayer to preSet Targets - return true; - } - - @Override - public String getHistory() { - return "no available"; - } - - @Override - public boolean hasDesignation(DesignationType designationName) { - for (Designation designation : designations) { - if (designation.getDesignationType().equals(designationName)) { - return true; - } - } - return false; - } - - @Override - public void addDesignation(Designation designation) { - if (!designation.isUnique() || !this.hasDesignation(designation.getDesignationType())) { - designations.add(designation); - } - } - - @Override - public List getDesignations() { - return designations; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - Player obj = (Player) o; - if (this.getId() == null || obj.getId() == null) { - return false; - } - - return this.getId().equals(obj.getId()); - } - - @Override - public int hashCode() { - int hash = 7; - hash = 89 * hash + Objects.hashCode(this.playerId); - return hash; - } - - @Override - public void addPhyrexianToColors(FilterMana colors) { - if (phyrexianColors == null) { - phyrexianColors = colors.copy(); - } else { - if (colors.isWhite()) { - this.phyrexianColors.setWhite(true); - } - if (colors.isBlue()) { - this.phyrexianColors.setBlue(true); - } - if (colors.isBlack()) { - this.phyrexianColors.setBlack(true); - } - if (colors.isRed()) { - this.phyrexianColors.setRed(true); - } - if (colors.isGreen()) { - this.phyrexianColors.setGreen(true); - } - } - } - - @Override - public FilterMana getPhyrexianColors() { - return this.phyrexianColors; - } - - @Override - public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { - return card.getSpellAbility(); - } - - @Override - public String toString() { - return getName() + " (" + super.getClass().getSimpleName() + ")"; - } -} +package mage.players; + +import com.google.common.collect.ImmutableMap; +import mage.*; +import mage.abilities.*; +import mage.abilities.ActivatedAbility.ActivationStatus; +import mage.abilities.common.PassAbility; +import mage.abilities.common.PlayLandAsCommanderAbility; +import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; +import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; +import mage.abilities.costs.*; +import mage.abilities.costs.mana.AlternateManaPaymentAbility; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; +import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; +import mage.abilities.keyword.*; +import mage.abilities.mana.ActivatedManaAbilityImpl; +import mage.abilities.mana.ManaOptions; +import mage.actions.MageDrawAction; +import mage.cards.*; +import mage.cards.decks.Deck; +import mage.choices.Choice; +import mage.choices.ChoiceImpl; +import mage.constants.*; +import mage.counters.Counter; +import mage.counters.CounterType; +import mage.counters.Counters; +import mage.designations.Designation; +import mage.designations.DesignationType; +import mage.filter.FilterCard; +import mage.filter.FilterMana; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.common.FilterCreatureForCombat; +import mage.filter.common.FilterCreatureForCombatBlock; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.PermanentIdPredicate; +import mage.game.*; +import mage.game.combat.CombatGroup; +import mage.game.command.CommandObject; +import mage.game.events.*; +import mage.game.match.MatchPlayer; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentCard; +import mage.game.permanent.PermanentToken; +import mage.game.permanent.token.SquirrelToken; +import mage.game.stack.Spell; +import mage.game.stack.StackAbility; +import mage.game.stack.StackObject; +import mage.game.turn.Step; +import mage.players.net.UserData; +import mage.target.Target; +import mage.target.TargetAmount; +import mage.target.TargetCard; +import mage.target.TargetPermanent; +import mage.target.common.TargetCardInLibrary; +import mage.target.common.TargetDiscard; +import mage.util.CardUtil; +import mage.util.GameLog; +import mage.util.RandomUtil; +import org.apache.log4j.Logger; + +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public abstract class PlayerImpl implements Player, Serializable { + + private static final Logger logger = Logger.getLogger(PlayerImpl.class); + + /** + * Used to cancel waiting requests send to the player + */ + protected boolean abort; + + protected final UUID playerId; + protected String name; + protected boolean human; + protected int life; + protected boolean wins; + protected boolean draws; + protected boolean loses; + protected Library library; + protected Cards sideboard; + protected Cards hand; + protected Graveyard graveyard; + protected Set commandersIds = new HashSet<>(0); + protected Abilities abilities; + protected Counters counters; + protected int landsPlayed; + protected int landsPerTurn = 1; + protected int loyaltyUsePerTurn = 1; + protected int maxHandSize = 7; + protected int maxAttackedBy = Integer.MAX_VALUE; + protected ManaPool manaPool; + // priority control + protected boolean passed; // player passed priority + protected boolean passedTurn; // F4 + protected boolean passedTurnSkipStack; // F6 // TODO: research + protected boolean passedUntilEndOfTurn; // F5 + protected boolean passedUntilNextMain; // F7 + protected boolean passedUntilStackResolved; // F10 + protected Date dateLastAddedToStack; + protected boolean passedUntilEndStepBeforeMyTurn; // F11 + protected boolean skippedAtLeastOnce; // used to track if passed started in specific phase + /** + * This indicates that player passed all turns until their own turn starts + * (F9). Note! This differs from passedTurn as it doesn't care about spells + * and abilities in the stack and will pass them as well. + */ + protected boolean passedAllTurns; // F9 + protected AbilityType justActivatedType; // used to check if priority can be passed automatically + + protected int turns; + protected int storedBookmark = -1; + protected int priorityTimeLeft = Integer.MAX_VALUE; + + // conceded or connection lost game + protected boolean left; + // set if the player quits the complete match + protected boolean quit; + // set if the player lost match because of priority timeout + protected boolean timerTimeout; + // set if the player lost match because of idle timeout + protected boolean idleTimeout; + + protected RangeOfInfluence range; + protected Set inRange = new HashSet<>(); // players list in current range of influence (updates each turn) + + protected boolean isTestMode = false; + protected boolean canGainLife = true; + protected boolean canLoseLife = true; + protected boolean canPayLifeCost = true; + protected boolean loseByZeroOrLessLife = true; + protected boolean canPlayCardsFromGraveyard = true; + protected boolean drawsOnOpponentsTurn = false; + + protected FilterPermanent sacrificeCostFilter; + + protected final List alternativeSourceCosts = new ArrayList<>(); + + protected boolean isGameUnderControl = true; + protected UUID turnController; + protected List turnControllers = new ArrayList<>(); + protected Set playersUnderYourControl = new HashSet<>(); + + protected Set usersAllowedToSeeHandCards = new HashSet<>(); + + protected List attachments = new ArrayList<>(); + + protected boolean topCardRevealed = false; + + // 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn + // or until a specific point in that turn will last until that turn would have begun. + // They neither expire immediately nor last indefinitely. + protected boolean reachedNextTurnAfterLeaving = false; + + // indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs) + // support multiple cards with alternative mana cost + protected Set castSourceIdWithAlternateMana = new HashSet<>(); + protected Map> castSourceIdManaCosts = new HashMap<>(); + protected Map> castSourceIdCosts = new HashMap<>(); + + // indicates that the player is in mana payment phase + protected boolean payManaMode = false; + + protected UserData userData; + protected MatchPlayer matchPlayer; + + protected List designations = new ArrayList<>(); + + // mana colors the player can handle like Phyrexian mana + protected FilterMana phyrexianColors; + + // Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy) + protected final List> availableTriggeredManaList = new ArrayList<>(); + + /** + * During some steps we can't play anything + */ + protected final Map silentPhaseSteps = ImmutableMap.builder(). + put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build(); + + public PlayerImpl(String name, RangeOfInfluence range) { + this(UUID.randomUUID()); + this.name = name; + this.range = range; + hand = new CardsImpl(); + graveyard = new Graveyard(); + abilities = new AbilitiesImpl<>(); + counters = new Counters(); + manaPool = new ManaPool(playerId); + library = new Library(playerId); + sideboard = new CardsImpl(); + phyrexianColors = null; + } + + protected PlayerImpl(UUID id) { + this.playerId = id; + } + + public PlayerImpl(final PlayerImpl player) { + this.abort = player.abort; + this.playerId = player.playerId; + + this.name = player.name; + this.human = player.human; + this.life = player.life; + this.wins = player.wins; + this.draws = player.draws; + this.loses = player.loses; + + this.library = player.library.copy(); + this.sideboard = player.sideboard.copy(); + this.hand = player.hand.copy(); + this.graveyard = player.graveyard.copy(); + this.commandersIds = player.commandersIds; + this.abilities = player.abilities.copy(); + this.counters = player.counters.copy(); + + this.landsPlayed = player.landsPlayed; + this.landsPerTurn = player.landsPerTurn; + this.loyaltyUsePerTurn = player.loyaltyUsePerTurn; + this.maxHandSize = player.maxHandSize; + this.maxAttackedBy = player.maxAttackedBy; + this.manaPool = player.manaPool.copy(); + this.turns = player.turns; + + this.left = player.left; + this.quit = player.quit; + this.timerTimeout = player.timerTimeout; + this.idleTimeout = player.idleTimeout; + this.range = player.range; + this.canGainLife = player.canGainLife; + this.canLoseLife = player.canLoseLife; + this.loseByZeroOrLessLife = player.loseByZeroOrLessLife; + this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard; + this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn; + + this.attachments.addAll(player.attachments); + + this.inRange.addAll(player.inRange); + this.userData = player.userData; + this.matchPlayer = player.matchPlayer; + + this.canPayLifeCost = player.canPayLifeCost; + this.sacrificeCostFilter = player.sacrificeCostFilter; + this.alternativeSourceCosts.addAll(player.alternativeSourceCosts); + this.storedBookmark = player.storedBookmark; + + this.topCardRevealed = player.topCardRevealed; + this.playersUnderYourControl.addAll(player.playersUnderYourControl); + this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); + + this.isTestMode = player.isTestMode; + this.isGameUnderControl = player.isGameUnderControl; + + this.turnController = player.turnController; + this.turnControllers.addAll(player.turnControllers); + + this.passed = player.passed; + this.passedTurn = player.passedTurn; + this.passedTurnSkipStack = player.passedTurnSkipStack; + this.passedUntilEndOfTurn = player.passedUntilEndOfTurn; + this.passedUntilNextMain = player.passedUntilNextMain; + this.passedUntilStackResolved = player.passedUntilStackResolved; + this.dateLastAddedToStack = player.dateLastAddedToStack; + this.passedUntilEndStepBeforeMyTurn = player.passedUntilEndStepBeforeMyTurn; + this.skippedAtLeastOnce = player.skippedAtLeastOnce; + this.passedAllTurns = player.passedAllTurns; + this.justActivatedType = player.justActivatedType; + + this.priorityTimeLeft = player.getPriorityTimeLeft(); + this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; + + this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); + for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { + this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { + this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + this.payManaMode = player.payManaMode; + this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; + for (Designation object : player.designations) { + this.designations.add(object.copy()); + } + } + + @Override + public void restore(Player player) { + this.name = player.getName(); + this.human = player.isHuman(); + this.life = player.getLife(); + + this.passed = player.isPassed(); + + // Don't restore more global states. If restored they are probably cause for unintended draws (https://github.com/magefree/mage/issues/1205). +// this.wins = player.hasWon(); +// this.loses = player.hasLost(); +// this.left = player.hasLeft(); +// this.quit = player.hasQuit(); + // Makes no sense to restore +// this.priorityTimeLeft = player.getPriorityTimeLeft(); +// this.idleTimeout = player.hasIdleTimeout(); +// this.timerTimeout = player.hasTimerTimeout(); + // can't change so no need to restore +// this.isTestMode = player.isTestMode(); + // This is meta data and should'nt be restored by rollback +// this.userData = player.getUserData(); + this.library = player.getLibrary().copy(); + this.sideboard = player.getSideboard().copy(); + this.hand = player.getHand().copy(); + this.graveyard = player.getGraveyard().copy(); + + //noinspection deprecation - it's ok to use it in inner methods + this.commandersIds = new HashSet<>(player.getCommandersIds()); + + this.abilities = player.getAbilities().copy(); + this.counters = player.getCounters().copy(); + + this.landsPlayed = player.getLandsPlayed(); + this.landsPerTurn = player.getLandsPerTurn(); + this.loyaltyUsePerTurn = player.getLoyaltyUsePerTurn(); + this.maxHandSize = player.getMaxHandSize(); + this.maxAttackedBy = player.getMaxAttackedBy(); + this.manaPool = player.getManaPool().copy(); + // Restore user specific settings in case changed since state save + this.manaPool.setAutoPayment(this.getUserData().isManaPoolAutomatic()); + this.manaPool.setAutoPaymentRestricted(this.getUserData().isManaPoolAutomaticRestricted()); + + this.turns = player.getTurns(); + + this.range = player.getRange(); + this.canGainLife = player.isCanGainLife(); + this.canLoseLife = player.isCanLoseLife(); + this.attachments.clear(); + this.attachments.addAll(player.getAttachments()); + + this.inRange.clear(); + this.inRange.addAll(player.getInRange()); + this.canPayLifeCost = player.getCanPayLifeCost(); + this.sacrificeCostFilter = player.getSacrificeCostFilter() != null + ? player.getSacrificeCostFilter().copy() : null; + this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); + this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard(); + this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn(); + this.alternativeSourceCosts.clear(); + this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); + + this.topCardRevealed = player.isTopCardRevealed(); + this.playersUnderYourControl.clear(); + this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl()); + this.isGameUnderControl = player.isGameUnderControl(); + + this.turnController = player.getTurnControlledBy(); + this.turnControllers.clear(); + this.turnControllers.addAll(player.getTurnControllers()); + this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); + + this.clearCastSourceIdManaCosts(); + this.castSourceIdWithAlternateMana.clear(); + this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); + for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { + this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { + this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + + this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null; + + this.designations.clear(); + for (Designation object : player.getDesignations()) { + this.designations.add(object.copy()); + } + + // Don't restore! + // this.storedBookmark + // this.usersAllowedToSeeHandCards + } + + @Override + public void useDeck(Deck deck, Game game) { + library.clear(); + library.addAll(deck.getCards(), game); + sideboard.clear(); + for (Card card : deck.getSideboard()) { + sideboard.add(card); + } + } + + /** + * Cast e.g. from Karn Liberated to restart the current game + * + * @param game + */ + @Override + public void init(Game game) { + init(game, false); + } + + @Override + public void init(Game game, boolean testMode) { + this.abort = false; + if (!testMode) { + this.hand.clear(); + this.graveyard.clear(); + } + this.library.reset(); + this.abilities.clear(); + this.counters.clear(); + this.wins = false; + this.draws = false; + this.loses = false; + this.left = false; + // reset is necessary because in tournament player will be used for each round + this.quit = false; + this.timerTimeout = false; + this.idleTimeout = false; + + this.turns = 0; + this.isGameUnderControl = true; + this.turnController = this.getId(); + this.turnControllers.clear(); + this.playersUnderYourControl.clear(); + + this.passed = false; + this.passedTurn = false; + this.passedTurnSkipStack = false; + this.passedUntilEndOfTurn = false; + this.passedUntilNextMain = false; + this.passedUntilStackResolved = false; + this.dateLastAddedToStack = null; + this.passedUntilEndStepBeforeMyTurn = false; + this.skippedAtLeastOnce = false; + this.passedAllTurns = false; + this.justActivatedType = null; + + this.canGainLife = true; + this.canLoseLife = true; + this.topCardRevealed = false; + this.payManaMode = false; + this.setLife(game.getStartingLife(), game, null); + this.setReachedNextTurnAfterLeaving(false); + + this.clearCastSourceIdManaCosts(); + + this.getManaPool().init(); // needed to remove mana that not empties on step change from previous game if left + this.phyrexianColors = null; + + this.designations.clear(); + } + + /** + * called before apply effects + */ + @Override + public void reset() { + this.abilities.clear(); + this.landsPerTurn = 1; + this.loyaltyUsePerTurn = 1; + this.maxHandSize = 7; + this.maxAttackedBy = Integer.MAX_VALUE; + this.canGainLife = true; + this.canLoseLife = true; + this.canPayLifeCost = true; + this.sacrificeCostFilter = null; + this.loseByZeroOrLessLife = true; + this.canPlayCardsFromGraveyard = false; + this.drawsOnOpponentsTurn = false; + this.topCardRevealed = false; + this.alternativeSourceCosts.clear(); + this.clearCastSourceIdManaCosts(); + this.getManaPool().clearEmptyManaPoolRules(); + this.phyrexianColors = null; + } + + @Override + public Counters getCounters() { + return counters; + } + + @Override + public void beginTurn(Game game) { + this.landsPlayed = 0; + updateRange(game); + } + + @Override + public RangeOfInfluence getRange() { + return range; + } + + @Override + public void updateRange(Game game) { + // 20100423 - 801.2c + // 801.2c The particular players within each player’s range of influence are determined as each turn begins. + // BUT it also uses before game start to fill game and card data in starting game events + inRange.clear(); + inRange.add(this.playerId); + inRange.addAll(getAllNearPlayers(game, true)); + inRange.addAll(getAllNearPlayers(game, false)); + } + + private Set getAllNearPlayers(Game game, boolean needPrevious) { + // find all near players (search from current player position) + Set foundedList = new HashSet<>(); + PlayerList players = game.getState().getPlayerList(this.playerId); + int needAmount = this.getRange().getRange(); // distance to search (0 - ALL range) + int foundedAmount = 0; + while (needAmount == 0 || foundedAmount < needAmount) { + Player foundedPlayer = needPrevious ? players.getPrevious(game) : players.getNext(game, false); + + // PlayerList is inifine, so stops on repeats + if (foundedPlayer == null || foundedPlayer.getId().equals(this.playerId) || foundedList.contains(foundedPlayer.getId())) { + break; + } + // skip leaved player (no needs cause next/previous code already checks it) + + foundedList.add(foundedPlayer.getId()); + foundedAmount++; + } + return foundedList; + } + + @Override + public Set getInRange() { + if (inRange.isEmpty()) { + // runtime check: inRange filled on beginTurn, but unit tests adds cards by cheat engine before game starting, + // so inRange will be empty and some ETB effects can be broken (example: Spark Double puts direct to battlefield). + // Cheat engine already have a workaround, so that error must not be visible in normal situation. + throw new IllegalStateException("Wrong code usage (game is not started, but you call getInRange in some effects)."); + } + + return inRange; + } + + @Override + public Set getPlayersUnderYourControl() { + return this.playersUnderYourControl; + } + + @Override + public void controlPlayersTurn(Game game, UUID playerId) { + Player player = game.getPlayer(playerId); + player.setTurnControlledBy(this.getId()); + game.informPlayers(getLogName() + " controls the turn of " + player.getLogName()); + if (!playerId.equals(this.getId())) { + this.playersUnderYourControl.add(playerId); + if (!player.hasLeft() && !player.hasLost()) { + player.setGameUnderYourControl(false); + } + DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility( + new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), player.getLogName())); + ability.setSourceId(getId()); + ability.setControllerId(getId()); + game.addDelayedTriggeredAbility(ability, null); + } + } + + @Override + public void setTurnControlledBy(UUID playerId) { + this.turnController = playerId; + this.turnControllers.add(playerId); + } + + @Override + public List getTurnControllers() { + return this.turnControllers; + } + + @Override + public UUID getTurnControlledBy() { + return this.turnController; + } + + @Override + public void resetOtherTurnsControlled() { + playersUnderYourControl.clear(); + } + + /** + * returns true if the player has the control itself - false if the player + * is controlled by another player + * + * @return + */ + @Override + public boolean isGameUnderControl() { + return isGameUnderControl; + } + + @Override + public void setGameUnderYourControl(boolean value) { + setGameUnderYourControl(value, true); + } + + @Override + public void setGameUnderYourControl(boolean value, boolean fullRestore) { + this.isGameUnderControl = value; + if (isGameUnderControl) { + if (fullRestore) { + this.turnControllers.clear(); + this.turnController = getId(); + } else { + if (turnControllers.size() > 0) { + this.turnControllers.remove(turnControllers.size() - 1); + } + if (turnControllers.isEmpty()) { + this.turnController = getId(); + } else { + this.turnController = turnControllers.get(turnControllers.size() - 1); + isGameUnderControl = false; + } + } + } + } + + @Override + public void endOfTurn(Game game) { + this.passedTurn = false; + this.passedTurnSkipStack = false; + } + + @Override + public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) { + if (this.hasLost() || this.hasLeft()) { + return false; + } + if (source != null) { + if (abilities.containsKey(ShroudAbility.getInstance().getId()) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game) == null) { + return false; + } + if (abilities.containsKey(HexproofAbility.getInstance().getId()) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { + return false; + } + return !hasProtectionFrom(source, game); + } + return true; + } + + @Override + public boolean hasProtectionFrom(MageObject source, Game game) { + for (ProtectionAbility ability : abilities.getProtectionAbilities()) { + if (!ability.canTarget(source, game)) { + return true; + } + } + return false; + } + + @Override + public int drawCards(int num, Ability source, Game game) { + if (num > 0) { + return game.doAction(source, new MageDrawAction(this, num, null)); + } + return 0; + } + + @Override + public int drawCards(int num, Ability source, Game game, GameEvent event) { + return game.doAction(source, new MageDrawAction(this, num, event)); + } + + @Override + public void discardToMax(Game game) { + if (hand.size() > this.maxHandSize) { + if (!game.isSimulation()) { + game.informPlayers(getLogName() + " discards down to " + + this.maxHandSize + + (this.maxHandSize == 1 + ? " hand card" : " hand cards")); + } + discard(hand.size() - this.maxHandSize, false, false, null, game); + } + } + + /** + * Don't use this in normal card code, it's for more internal use. Always + * use the [Player].moveCards methods if possible for card movement of card + * code. + * + * @param card + * @param game + * @return + */ + @Override + public boolean putInHand(Card card, Game game) { + if (card.isOwnedBy(playerId)) { + card.setZone(Zone.HAND, game); + this.hand.add(card); + } else { + return game.getPlayer(card.getOwnerId()).putInHand(card, game); + } + return true; + } + + @Override + public boolean removeFromHand(Card card, Game game) { + return hand.remove(card.getId()); + } + + @Override + public boolean removeFromLibrary(Card card, Game game) { + if (card == null) { + return false; + } + library.remove(card.getId(), game); + // must return true all the time (some cards can be removed directly from library, see getLibrary().removeFromTop) + // TODO: replace removeFromTop logic to normal with moveToZone + return true; + } + + @Override + public Card discardOne(boolean random, boolean payForCost, Ability source, Game game) { + return discard(1, random, payForCost, source, game).getRandom(game); + } + + @Override + public Cards discard(int amount, boolean random, boolean payForCost, Ability source, Game game) { + if (random) { + return discard(getRandomToDiscard(amount, source, game), payForCost, source, game); + } + return discard(amount, amount, payForCost, source, game); + } + + @Override + public Cards discard(int minAmount, int maxAmount, boolean payForCost, Ability source, Game game) { + return discard(getToDiscard(minAmount, maxAmount, source, game), payForCost, source, game); + } + + @Override + public Cards discard(Cards cards, boolean payForCost, Ability source, Game game) { + Cards discardedCards = new CardsImpl(); + if (cards == null) { + return discardedCards; + } + for (Card card : cards.getCards(game)) { + if (doDiscard(card, source, game, payForCost, false)) { + discardedCards.add(card); + } + } + if (!discardedCards.isEmpty()) { + game.fireEvent(new DiscardedCardsEvent(source, playerId, discardedCards.size(), discardedCards)); + } + return discardedCards; + } + + @Override + public boolean discard(Card card, boolean payForCost, Ability source, Game game) { + return doDiscard(card, source, game, payForCost, true); + } + + private Cards getToDiscard(int minAmount, int maxAmount, Ability source, Game game) { + Cards toDiscard = new CardsImpl(); + if (minAmount > maxAmount) { + return getToDiscard(maxAmount, minAmount, source, game); + } + if (maxAmount < 1) { + return toDiscard; + } + if (getHand().size() <= minAmount) { + toDiscard.addAll(getHand()); + return toDiscard; + } + TargetDiscard target = new TargetDiscard(minAmount, maxAmount, StaticFilters.FILTER_CARD, getId()); + choose(Outcome.Discard, target, source != null ? source.getSourceId() : null, game); + toDiscard.addAll(target.getTargets()); + return toDiscard; + } + + private Cards getRandomToDiscard(int amount, Ability source, Game game) { + Cards toDiscard = new CardsImpl(); + Cards hand = getHand().copy(); + for (int i = 0; i < amount; i++) { + if (hand.isEmpty()) { + break; + } + Card card = hand.getRandom(game); + hand.remove(card); + toDiscard.add(card); + } + return toDiscard; + } + + private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) { + //20100716 - 701.7 + /* 701.7. Discard # + 701.7a To discard a card, move it from its owner’s hand to that player’s graveyard. + 701.7b By default, effects that cause a player to discard a card allow the affected + player to choose which card to discard. Some effects, however, require a random + discard or allow another player to choose which card is discarded. + 701.7c If a card is discarded, but an effect causes it to be put into a hidden zone + instead of into its owner’s graveyard without being revealed, all values of that + card’s characteristics are considered to be undefined. + TODO: + If a card is discarded this way to pay a cost that specifies a characteristic + about the discarded card, that cost payment is illegal; the game returns to + the moment before the cost was paid (see rule 717, "Handling Illegal Actions"). + */ + if (card == null) { + return false; + } + GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, card.getId(), source, playerId); + gameEvent.setFlag(!payForCost); // event from effect (1) or from cost (0) + if (game.replaceEvent(gameEvent, source)) { + return false; + } + // write info to game log first so game log infos from triggered or replacement effects follow in the game log + if (!game.isSimulation()) { + game.informPlayers(getLogName() + " discards " + card.getLogName() + CardUtil.getSourceLogName(game, source)); + } + /* If a card is discarded while Rest in Peace is on the battlefield, abilities that function + * when a card is discarded (such as madness) still work, even though that card never reaches + * a graveyard. In addition, spells or abilities that check the characteristics of a discarded + * card (such as Chandra Ablaze's first ability) can find that card in exile. */ + card.moveToZone(Zone.GRAVEYARD, source, game, false); + // So discard is also successful if card is moved to another zone by replacement effect! + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DISCARDED_CARD, card.getId(), source, playerId)); + + if (fireFinalEvent) { + game.fireEvent(new DiscardedCardsEvent(source, playerId, 1, new CardsImpl(card))); + } + return true; + } + + @Override + public List getAttachments() { + return attachments; + } + + @Override + public boolean addAttachment(UUID permanentId, Ability source, Game game) { + if (!this.attachments.contains(permanentId)) { + Permanent aura = game.getPermanent(permanentId); + if (aura == null) { + aura = game.getPermanentEntering(permanentId); + } + if (aura != null) { + if (!game.replaceEvent(new EnchantPlayerEvent(playerId, aura, source))) { + this.attachments.add(permanentId); + aura.attachTo(playerId, source, game); + game.fireEvent(new EnchantedPlayerEvent(playerId, aura, source)); + return true; + } + } + } + return false; + } + + @Override + public boolean removeAttachment(Permanent attachment, Ability source, Game game) { + if (this.attachments.contains(attachment.getId())) { + if (!game.replaceEvent(new UnattachEvent(playerId, attachment.getId(), attachment, source))) { + this.attachments.remove(attachment.getId()); + attachment.attachTo(null, source, game); + game.fireEvent(new UnattachedEvent(playerId, attachment.getId(), attachment, source)); + return true; + } + } + return false; + } + + @Override + public boolean removeFromBattlefield(Permanent permanent, Ability source, Game game) { + permanent.removeFromCombat(game, false); + game.getBattlefield().removePermanent(permanent.getId()); + if (permanent.getAttachedTo() != null) { + Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); + if (attachedTo != null) { + attachedTo.removeAttachment(permanent.getId(), source, game); + } else { + Player attachedToPlayer = game.getPlayer(permanent.getAttachedTo()); + if (attachedToPlayer != null) { + attachedToPlayer.removeAttachment(permanent, source, game); + } else { + Card attachedToCard = game.getCard(permanent.getAttachedTo()); + if (attachedToCard != null) { + attachedToCard.removeAttachment(permanent.getId(), source, game); + } + } + } + + } + if (permanent.getPairedCard() != null) { + Permanent pairedCard = permanent.getPairedCard().getPermanent(game); + if (pairedCard != null) { + pairedCard.clearPairedCard(); + } + } + if (permanent.getBandedCards() != null && !permanent.getBandedCards().isEmpty()) { + for (UUID bandedId : permanent.getBandedCards()) { + Permanent banded = game.getPermanent(bandedId); + if (banded != null) { + banded.removeBandedCard(permanent.getId()); + } + } + } + return true; + } + + @Override + public boolean putInGraveyard(Card card, Game game) { + if (card.isOwnedBy(playerId)) { + this.graveyard.add(card); + } else { + return game.getPlayer(card.getOwnerId()).putInGraveyard(card, game); + } + return true; + } + + @Override + public boolean removeFromGraveyard(Card card, Game game) { + return this.graveyard.remove(card); + } + + @Override + public boolean putCardsOnBottomOfLibrary(Card card, Game game, Ability source, boolean anyOrder) { + return putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, anyOrder); + } + + @Override + public boolean putCardsOnBottomOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { + if (!cardsToLibrary.isEmpty()) { + Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException + if (!anyOrder) { + // random order + List ids = new ArrayList<>(cards); + Collections.shuffle(ids); + for (UUID id : ids) { + moveObjectToLibrary(id, source, game, false, false); + } + } else { + // user defined order + TargetCard target = new TargetCard(Zone.ALL, + new FilterCard("card ORDER to put on the BOTTOM of your library (last one chosen will be bottommost)")); + target.setRequired(true); + while (cards.size() > 1 && this.canRespond() + && this.choose(Outcome.Neutral, cards, target, game)) { + UUID targetObjectId = target.getFirstTarget(); + if (targetObjectId == null) { + break; + } + cards.remove(targetObjectId); + moveObjectToLibrary(targetObjectId, source, game, false, false); + target.clearChosen(); + } + for (UUID c : cards) { + moveObjectToLibrary(c, source, game, false, false); + } + } + } + return true; + } + + @Override + public boolean shuffleCardsToLibrary(Cards cards, Game game, Ability source) { + if (cards.isEmpty()) { + return true; + } + game.informPlayers(getLogName() + " shuffles " + CardUtil.numberToText(cards.size(), "a") + + " card" + (cards.size() == 1 ? "" : "s") + + " into their library" + CardUtil.getSourceLogName(game, source)); + boolean status = moveCards(cards, Zone.LIBRARY, source, game); + shuffleLibrary(source, game); + return status; + } + + @Override + public boolean shuffleCardsToLibrary(Card card, Game game, Ability source) { + if (card == null) { + return true; + } + return shuffleCardsToLibrary(new CardsImpl(card), game, source); + } + + @Override + public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int xFromTheTop, boolean withName) { + if (card.isOwnedBy(getId())) { + if (library.size() + 1 < xFromTheTop) { + putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, true); + } else { + if (card.moveToZone(Zone.LIBRARY, source, game, true) + && !(card instanceof PermanentToken) && !card.isCopy()) { + Card cardInLib = getLibrary().getFromTop(game); + if (cardInLib != null && cardInLib.getId().equals(card.getId())) { // check needed because e.g. commander can go to command zone + cardInLib = getLibrary().removeFromTop(game); + getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game); + game.informPlayers(withName ? cardInLib.getLogName() : "A card" + + " is put into " + + getLogName() + + "'s library " + + CardUtil.numberToOrdinalText(xFromTheTop) + + " from the top" + CardUtil.getSourceLogName(game, source, cardInLib.getId())); + } + } else { + return false; + } + } + } else { + return game.getPlayer(card.getOwnerId()).putCardOnTopXOfLibrary(card, game, source, xFromTheTop, withName); + } + return true; + } + + /** + * Can be cards or permanents that go to library + * + * @param cardsToLibrary + * @param game + * @param source + * @param anyOrder + * @return + */ + @Override + public boolean putCardsOnTopOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { + if (cardsToLibrary != null && !cardsToLibrary.isEmpty()) { + Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException + if (!anyOrder) { + // random order + List ids = new ArrayList<>(cards); + Collections.shuffle(ids); + for (UUID id : ids) { + moveObjectToLibrary(id, source, game, true, false); + } + } else { + // user defined order + TargetCard target = new TargetCard(Zone.ALL, + new FilterCard("card ORDER to put on the TOP of your library (last one chosen will be topmost)")); + target.setRequired(true); + while (cards.size() > 1 + && this.canRespond() + && this.choose(Outcome.Neutral, cards, target, game)) { + UUID targetObjectId = target.getFirstTarget(); + if (targetObjectId == null) { + break; + } + cards.remove(targetObjectId); + moveObjectToLibrary(targetObjectId, source, game, true, false); + target.clearChosen(); + } + for (UUID c : cards) { + moveObjectToLibrary(c, source, game, true, false); + } + } + } + return true; + } + + @Override + public boolean putCardsOnTopOfLibrary(Card cardToLibrary, Game game, Ability source, boolean anyOrder) { + if (cardToLibrary != null) { + return putCardsOnTopOfLibrary(new CardsImpl(cardToLibrary), game, source, anyOrder); + } + return true; + } + + private boolean moveObjectToLibrary(UUID objectId, Ability source, Game game, boolean toTop, boolean withName) { + MageObject mageObject = game.getObject(objectId); + if (mageObject instanceof Spell && mageObject.isCopy()) { + // Spell copies are not moved as cards, so here the no copy spell has to be selected to move + // (but because copy and original have the same objectId the wrong sepell can be selected from stack). + // So let's check if the original spell is on the stack and has to be selected. // TODO: Better handling so each spell could be selected by a unique id + Spell spellNoCopy = game.getStack().getSpell(source.getSourceId(), false); + if (spellNoCopy != null) { + mageObject = spellNoCopy; + } + } + if (mageObject != null) { + Zone fromZone = game.getState().getZone(objectId); + if ((mageObject instanceof Permanent)) { + return this.moveCardToLibraryWithInfo((Permanent) mageObject, source, game, fromZone, toTop, withName); + } else if (mageObject instanceof Card) { + return this.moveCardToLibraryWithInfo((Card) mageObject, source, game, fromZone, toTop, withName); + } + } + return false; + } + + @Override + public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { + // cost must be copied for data consistence between game simulations + castSourceIdWithAlternateMana.add(sourceId); + castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null); + castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null); + } + + @Override + public Set getCastSourceIdWithAlternateMana() { + return castSourceIdWithAlternateMana; + } + + @Override + public Map> getCastSourceIdCosts() { + return castSourceIdCosts; + } + + @Override + public Map> getCastSourceIdManaCosts() { + return castSourceIdManaCosts; + } + + @Override + public void clearCastSourceIdManaCosts() { + this.castSourceIdCosts.clear(); + this.castSourceIdManaCosts.clear(); + this.castSourceIdWithAlternateMana.clear(); + } + + @Override + public void setPayManaMode(boolean payManaMode) { + this.payManaMode = payManaMode; + } + + @Override + public boolean isInPayManaMode() { + return payManaMode; + } + + @Override + public boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject) { + if (card == null) { + return false; + } + + // play without timing and from any zone + boolean result; + if (card.isLand(game)) { + result = playLand(card, game, true); + } else { + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + result = cast(this.chooseAbilityForCast(card, game, noMana), game, noMana, approvingObject); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + } + + if (!result) { + game.informPlayer(this, "You can't play " + card.getIdName() + '.'); + } + return result; + } + + /** + * @param originalAbility + * @param game + * @param noMana cast it without paying mana costs + * @param approvingObject which object approved the cast + * @return + */ + @Override + public boolean cast(SpellAbility originalAbility, Game game, boolean noMana, ApprovingObject approvingObject) { + if (game == null || originalAbility == null) { + return false; + } + + // Use ability copy to avoid problems with targets and costs on recast (issue https://github.com/magefree/mage/issues/5189). + SpellAbility ability = originalAbility.copy(); + ability.setControllerId(getId()); + ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); + + //20091005 - 601.2a + if (ability.getSourceId() == null) { + logger.error("Ability without sourceId turn " + game.getTurnNum() + ". Ability: " + ability.getRule()); + return false; + } + Card card = game.getCard(ability.getSourceId()); + if (card != null) { + Zone fromZone = game.getState().getZone(card.getMainCard().getId()); + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, + ability.getId(), ability, playerId, approvingObject); + castEvent.setZone(fromZone); + if (!game.replaceEvent(castEvent, ability)) { + int bookmark = game.bookmarkState(); + setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) + card.cast(game, fromZone, ability, playerId); + Spell spell = game.getStack().getSpell(ability.getId()); + if (spell == null) { + logger.error("Got no spell from stack. ability: " + ability.getRule()); + return false; + } + if (card.isCopy()) { + spell.setCopy(true, null); + } + // Update the zcc to the stack + ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); + + // ALTERNATIVE COST from dynamic effects + // some effects set sourceId to cast without paying mana costs or other costs + if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) { + Ability spellAbility = spell.getSpellAbility(); + ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()); + Costs costs = getCastSourceIdCosts().get(ability.getSourceId()); + if (alternateCosts == null) { + noMana = true; + } else { + spellAbility.getManaCosts().clear(); + spellAbility.getManaCostsToPay().clear(); + spellAbility.getManaCosts().add(alternateCosts.copy()); + spellAbility.getManaCostsToPay().add(alternateCosts.copy()); + } + spellAbility.getCosts().clear(); + if (costs != null) { + spellAbility.getCosts().addAll(costs); + } + } + clearCastSourceIdManaCosts(); // TODO: test multiple alternative cost for different cards as same time + + castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, + spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); + castEvent.setZone(fromZone); + game.fireEvent(castEvent); + if (spell.activate(game, noMana)) { + GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, + spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); + castedEvent.setZone(fromZone); + game.fireEvent(castedEvent); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + spell.getActivatedMessage(game)); + } + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + restoreState(bookmark, ability.getRule(), game); + } + } + return false; + } + + @Override + public boolean playLand(Card card, Game game, boolean ignoreTiming) { + // Check for alternate casting possibilities: e.g. land with Morph + if (card == null) { + return false; + } + ActivatedAbility playLandAbility = null; + boolean foundAlternative = false; + for (Ability ability : card.getAbilities(game)) { + // if cast for noMana no Alternative costs are allowed + if ((ability instanceof AlternativeSourceCosts) + || (ability instanceof OptionalAdditionalSourceCosts)) { + foundAlternative = true; + } + if (ability instanceof PlayLandAbility) { + playLandAbility = (ActivatedAbility) ability; + } + } + + // try alternative cast (face down) + if (foundAlternative) { + SpellAbility spellAbility = new SpellAbility(null, "", + game.getState().getZone(card.getId()), SpellAbilityType.FACE_DOWN_CREATURE); + spellAbility.setControllerId(this.getId()); + spellAbility.setSourceId(card.getId()); + if (cast(spellAbility, game, false, null)) { + return true; + } + } + + if (playLandAbility == null) { + return false; + } + + //20091005 - 114.2a + ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game); + if (ignoreTiming) { + if (!canPlayLand()) { + return false; // ignore timing does not mean that more lands than normal can be played + } + } else { + if (!activationStatus.canActivate()) { + return false; + } + } + + //20091005 - 305.1 + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, + card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()))) { + // int bookmark = game.bookmarkState(); + // land events must return original zone (uses for commander watcher) + Zone cardZoneBefore = game.getState().getZone(card.getId()); + GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, + card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); + landEventBefore.setZone(cardZoneBefore); + game.fireEvent(landEventBefore); + + if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { + landsPlayed++; + GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, + card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); + landEventAfter.setZone(cardZoneBefore); + game.fireEvent(landEventAfter); + + String playText = getLogName() + " plays " + card.getLogName(); + if (card instanceof ModalDoubleFacesCardHalf) { + ModalDoubleFacesCard mdfCard = (ModalDoubleFacesCard) card.getMainCard(); + playText = getLogName() + " plays " + GameLog.replaceNameByColoredName(card, card.getName(), mdfCard) + + " as MDF side of " + GameLog.getColoredObjectIdName(mdfCard); + } + game.fireInformEvent(playText); + // game.removeBookmark(bookmark); + resetStoredBookmark(game); // prevent undo after playing a land + return true; + } + // putOntoBattlefield returned false if putOntoBattlefield was replaced by replacement effect (e.g. Kjeldoran Outpost). + // But that would undo the effect completely, + // what makes no real sense. So it makes no sense to generally do a restoreState here. + // restoreState(bookmark, card.getName(), game); + } + // if the to play the land is replaced (e.g. Kjeldoran Outpost and don't sacrificing a Plains) it's a valid state so returning true here + return true; + } + + protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) { + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, + ability.getId(), ability, playerId))) { + int bookmark = game.bookmarkState(); + if (ability.activate(game, false)) { + if (ability.resolve(game)) { + if (ability.isUndoPossible()) { + if (storedBookmark == -1 || storedBookmark > bookmark) { // e.g. useful for undo Nykthos, Shrine to Nyx + setStoredBookmark(bookmark); + } + } else { + resetStoredBookmark(game); + } + return true; + } + } + restoreState(bookmark, ability.getRule(), game); + } + return false; + } + + protected boolean playAbility(ActivatedAbility ability, Game game) { + //20091005 - 602.2a + if (ability.isUsesStack()) { + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, + ability.getId(), ability, playerId))) { + int bookmark = game.bookmarkState(); + setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) + ability.newId(); + ability.setControllerId(playerId); + game.getStack().push(new StackAbility(ability, playerId)); + if (ability.activate(game, false)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATED_ABILITY, + ability.getId(), ability, playerId)); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + ability.getGameLogMessage(game)); + } + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + restoreState(bookmark, ability.getRule(), game); + } + } else { + int bookmark = game.bookmarkState(); + if (ability.activate(game, false)) { + ability.resolve(game); + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + restoreState(bookmark, ability.getRule(), game); + } + return false; + } + + protected boolean specialAction(SpecialAction action, Game game) { + //20091005 - 114 + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_ACTION, + action.getId(), action, getId()))) { + int bookmark = game.bookmarkState(); + if (action.activate(game, false)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_ACTION, + action.getId(), action, getId())); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + action.getGameLogMessage(game)); + } + if (action.resolve(game)) { + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + } + restoreState(bookmark, action.getRule(), game); + } + return false; + } + + protected boolean specialManaPayment(SpecialAction action, Game game) { + //20091005 - 114 + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_MANA_PAYMENT, + action.getId(), action, getId()))) { + int bookmark = game.bookmarkState(); + if (action.activate(game, false)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_MANA_PAYMENT, + action.getId(), action, getId())); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + action.getGameLogMessage(game)); + } + if (action.resolve(game)) { + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + } + restoreState(bookmark, action.getRule(), game); + } + return false; + } + + @Override + public boolean activateAbility(ActivatedAbility ability, Game game) { + if (ability == null) { + return false; + } + boolean result; + if (ability instanceof PassAbility) { + pass(game); + return true; + } + Card card = game.getCard(ability.getSourceId()); + if (ability instanceof PlayLandAsCommanderAbility) { + + // LAND as commander: play land with cost, but without stack + ActivationStatus activationStatus = ability.canActivate(this.playerId, game); + if (!activationStatus.canActivate() || !this.canPlayLand()) { + return false; + } + if (card == null) { + return false; + } + + // as copy, tries to applie cost effects and pays + Ability activatingAbility = ability.copy(); + if (activatingAbility.activate(game, false)) { + result = playLand(card, game, false); + } else { + result = false; + } + + } else if (ability instanceof PlayLandAbility) { + + // LAND as normal card: without cost and stack + result = playLand(card, game, false); + + } else { + + // ABILITY + ActivationStatus activationStatus = ability.canActivate(this.playerId, game); + if (!activationStatus.canActivate()) { + return false; + } + + switch (ability.getAbilityType()) { + case SPECIAL_ACTION: + result = specialAction((SpecialAction) ability.copy(), game); + break; + case SPECIAL_MANA_PAYMENT: + result = specialManaPayment((SpecialAction) ability.copy(), game); + break; + case MANA: + result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game); + break; + case SPELL: + result = cast((SpellAbility) ability, game, false, activationStatus.getApprovingObject()); + break; + default: + result = playAbility(ability.copy(), game); + break; + } + } + + //if player has taken an action then reset all player passed flags + justActivatedType = null; + if (result) { + if (isHuman() + && (ability.getAbilityType() == AbilityType.SPELL + || ability.getAbilityType() == AbilityType.ACTIVATED)) { + if (ability.isUsesStack()) { // if the ability does not use the stack (e.g. Suspend) auto pass would go to next phase unintended + setJustActivatedType(ability.getAbilityType()); + } + } + game.getPlayers().resetPassed(); + } + return result; + } + + @Override + public boolean triggerAbility(TriggeredAbility triggeredAbility, Game game) { + if (triggeredAbility == null) { + logger.warn("Null source in triggerAbility method"); + throw new IllegalArgumentException("source TriggeredAbility must not be null"); + } + //20091005 - 603.3c, 603.3d + int bookmark = game.bookmarkState(); + TriggeredAbility ability = triggeredAbility.copy(); + MageObject sourceObject = ability.getSourceObject(game); + if (sourceObject != null) { + sourceObject.adjustTargets(ability, game); + } + UUID triggerId = null; + if (ability.canChooseTarget(game, playerId)) { + if (ability.isUsesStack()) { + game.getStack().push(new StackAbility(ability, playerId)); + } + if (ability.activate(game, false)) { + if ((ability.isUsesStack() + || ability.getRuleVisible()) + && !game.isSimulation()) { + game.informPlayers(getLogName() + " - " + ability.getGameLogMessage(game)); + } + if (!ability.isUsesStack()) { + ability.resolve(game); + } else { + game.fireEvent(new GameEvent( + GameEvent.EventType.TRIGGERED_ABILITY, + ability.getId(), ability, ability.getControllerId() + )); + triggerId = ability.getId(); + } + game.removeBookmark(bookmark); + return true; + } + } + restoreState(bookmark, triggeredAbility.getRule(), game); // why restore is needed here? (to remove the triggered ability from the stack because of no possible targets) + GameEvent event = new GameEvent( + GameEvent.EventType.ABILITY_TRIGGERED, + triggerId, ability, ability.getControllerId() + ); + game.getState().setValue(event.getId().toString(), ability.getTriggerEvent()); + game.fireEvent(event); + return false; + } + + /** + * Return spells for possible cast Uses in GUI to show only playable spells + * for choosing from the card (example: effect allow to cast card and player + * must choose the spell ability) + * + * @param game + * @param playerId + * @param object + * @param zone + * @param noMana + * @return + */ + public static LinkedHashMap getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) { + // it uses simple check from spellCanBeActivatedRegularlyNow + // reason: no approved info here (e.g. forced to choose spell ability from cast card) + LinkedHashMap useable = new LinkedHashMap<>(); + Abilities allAbilities; + if (object instanceof Card) { + allAbilities = ((Card) object).getAbilities(game); + } else { + allAbilities = object.getAbilities(); + } + + for (Ability ability : allAbilities) { + if (ability instanceof SpellAbility) { + SpellAbility spellAbility = (SpellAbility) ability; + + switch (spellAbility.getSpellAbilityType()) { + case BASE_ALTERNATE: + // rules: + // If you cast a spell “without paying its mana cost,” you can’t choose to cast it for + // any alternative costs. You can, however, pay additional costs, such as kicker costs. + // If the card has any mandatory additional costs, those must be paid to cast the spell. + // (2021-02-05) + if (!noMana) { + if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability + } + return useable; + } + break; + case SPLIT_FUSED: + // rules: + // If you cast a split card with fuse from your hand without paying its mana cost, + // you can choose to use its fuse ability and cast both halves without paying their mana costs. + if (zone == Zone.HAND) { + if (spellAbility.canChooseTarget(game, playerId)) { + useable.put(spellAbility.getId(), spellAbility); + } + } + case SPLIT: + if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getLeftHalfCard().getSpellAbility()); + } + if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getRightHalfCard().getSpellAbility()); + } + return useable; + case SPLIT_AFTERMATH: + if (zone == Zone.GRAVEYARD) { + if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getRightHalfCard().getSpellAbility()); + } + } else { + if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getLeftHalfCard().getSpellAbility()); + } + } + return useable; + default: + if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + useable.put(spellAbility.getId(), spellAbility); + } + } + } + } + return useable; + } + + @Override + public LinkedHashMap getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) { + LinkedHashMap useable = new LinkedHashMap<>(); + // stack abilities - can't activate anything + // spell ability - can activate additional abilities (example: "Lightning Storm") + if (object instanceof StackAbility || object == null) { + return useable; + } + boolean previousState = game.inCheckPlayableState(); + game.setCheckPlayableState(true); + try { + // collect and filter playable activated abilities + // GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right) + Set needIds = CardUtil.getObjectParts(object); + + // workaround to find all abilities first and filter it for one object + List allPlayable = getPlayable(game, true, zone, false); + for (ActivatedAbility ability : allPlayable) { + if (needIds.contains(ability.getSourceId())) { + useable.putIfAbsent(ability.getId(), ability); + } + } + } finally { + game.setCheckPlayableState(previousState); + } + return useable; + } + + protected LinkedHashMap getUseableManaAbilities(MageObject object, Zone zone, Game game) { + LinkedHashMap useable = new LinkedHashMap<>(); + boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); + for (ActivatedManaAbilityImpl ability : object.getAbilities().getActivatedManaAbilities(zone)) { + if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { + if (ability.canActivate(playerId, game).canActivate()) { + useable.put(ability.getId(), ability); + } + } + } + return useable; + } + + @Override + public int getLandsPlayed() { + return landsPlayed; + } + + @Override + public boolean canPlayLand() { + //20091005 - 114.2a + return landsPlayed < landsPerTurn; + } + + protected boolean isActivePlayer(Game game) { + return game.isActivePlayer(this.playerId); + } + + @Override + public void shuffleLibrary(Ability source, Game game) { + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SHUFFLE_LIBRARY, playerId, source, playerId))) { + this.library.shuffle(); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + "'s library is shuffled" + CardUtil.getSourceLogName(game, source)); + } + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SHUFFLED, playerId, source, playerId)); + } + } + + @Override + public void revealCards(Ability source, Cards cards, Game game) { + revealCards(source, null, cards, game, true); + } + + @Override + public void revealCards(String titleSuffix, Cards cards, Game game) { + revealCards(titleSuffix, cards, game, true); + } + + @Override + public void revealCards(String titleSuffix, Cards cards, Game game, boolean postToLog) { + revealCards(null, titleSuffix, cards, game, postToLog); + } + + @Override + public void revealCards(Ability source, String titleSuffix, Cards cards, Game game) { + revealCards(source, titleSuffix, cards, game, true); + } + + @Override + public void revealCards(Ability source, String titleSuffix, Cards cards, Game game, boolean postToLog) { + if (cards == null || cards.isEmpty()) { + return; + } + if (postToLog) { + game.getState().getRevealed().add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); + } else { + game.getState().getRevealed().update(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); + } + if (postToLog && !game.isSimulation()) { + StringBuilder sb = new StringBuilder(getLogName()).append(" reveals "); + int current = 0, last = cards.size(); + for (Card card : cards.getCards(game)) { + current++; + sb.append(GameLog.getColoredObjectName(card)); + if (current < last) { + sb.append(", "); + } + } + sb.append(CardUtil.getSourceLogName(game, source)); + game.informPlayers(sb.toString()); + } + } + + @Override + public void lookAtCards(String titleSuffix, Card card, Game game) { + game.getState().getLookedAt(this.playerId).add(titleSuffix, card); + game.fireUpdatePlayersEvent(); + } + + @Override + public void lookAtCards(String titleSuffix, Cards cards, Game game) { + this.lookAtCards(null, titleSuffix, cards, game); + } + + @Override + public void lookAtCards(Ability source, String titleSuffix, Cards cards, Game game) { + game.getState().getLookedAt(this.playerId).add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); + game.fireUpdatePlayersEvent(); + } + + @Override + public void phasing(Game game) { + //20091005 - 502.1 + List phasedOut = game.getBattlefield().getPhasedOut(game, playerId); + for (Permanent permanent : game.getBattlefield().getPhasedIn(game, playerId)) { + // 502.15i When a permanent phases out, any local enchantments or Equipment + // attached to that permanent phase out at the same time. This alternate way of + // phasing out is known as phasing out "indirectly." An enchantment or Equipment + // that phased out indirectly won't phase in by itself, but instead phases in + // along with the card it's attached to. + Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); + if (!(attachedTo != null && attachedTo.isControlledBy(this.getId()))) { + permanent.phaseOut(game, false); + } + } + for (Permanent permanent : phasedOut) { + if (!permanent.isPhasedOutIndirectly()) { + permanent.phaseIn(game); + } + } + } + + @Override + public void untap(Game game) { + // create list of all "notMoreThan" effects to track which one are consumed + Map>, Integer> notMoreThanEffectsUsage = new HashMap<>(); + for (Entry> restrictionEffect + : game.getContinuousEffects().getApplicableRestrictionUntapNotMoreThanEffects(this, game).entrySet()) { + notMoreThanEffectsUsage.put(restrictionEffect, restrictionEffect.getKey().getNumber()); + } + + if (!notMoreThanEffectsUsage.isEmpty()) { + // create list of all permanents that can be untapped generally + List canBeUntapped = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { + boolean untap = true; + for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { + untap &= effect.canBeUntapped(permanent, null, game, true); + } + if (untap) { + canBeUntapped.add(permanent); + } + } + // selected permanents to untap + List selectedToUntap = new ArrayList<>(); + + // player can cancel the selection of an effect to use a preferred order of restriction effects + boolean playerCanceledSelection; + do { + playerCanceledSelection = false; + // select permanents to untap to consume the "notMoreThan" effects + for (Map.Entry>, Integer> handledEntry : notMoreThanEffectsUsage.entrySet()) { + // select a permanent to untap for this entry + int numberToUntap = handledEntry.getValue(); + if (numberToUntap > 0) { + + List leftForUntap = getPermanentsThatCanBeUntapped(game, + canBeUntapped, + handledEntry.getKey().getKey(), + notMoreThanEffectsUsage); + + FilterControlledPermanent filter = handledEntry.getKey().getKey().getFilter().copy(); + String message = filter.getMessage(); + // omit already from other untap effects selected permanents + for (Permanent permanent : selectedToUntap) { + filter.add(Predicates.not(new PermanentIdPredicate(permanent.getId()))); + } + // while targets left and there is still allowed to untap + while (canRespond() && !leftForUntap.isEmpty() && numberToUntap > 0) { + // player has to select the permanent they want to untap for this restriction + Ability ability = handledEntry.getKey().getValue().iterator().next(); + if (ability != null) { + StringBuilder sb = new StringBuilder(message).append(" to untap").append(" (").append(Math.min(leftForUntap.size(), + numberToUntap)).append(" in total"); + MageObject effectSource = game.getObject(ability.getSourceId()); + if (effectSource != null) { + sb.append(" from ").append(effectSource.getLogName()); + } + sb.append(')'); + filter.setMessage(sb.toString()); + Target target = new TargetPermanent(1, 1, filter, true); + if (!this.chooseTarget(Outcome.Untap, target, ability, game)) { + // player canceled, go on with the next effect (if no other effect available, this effect will be active again) + playerCanceledSelection = true; + break; + } + Permanent selectedPermanent = game.getPermanent(target.getFirstTarget()); + if (leftForUntap.contains(selectedPermanent)) { + selectedToUntap.add(selectedPermanent); + numberToUntap--; + // don't allow to select same permanent twice + filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); + // reduce available untap numbers from other "UntapNotMoreThan" effects if selected permanent applies to their filter too + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getValue() > 0 + && notMoreThanEffect.getKey().getKey().getFilter().match(selectedPermanent, game)) { + notMoreThanEffect.setValue(notMoreThanEffect.getValue() - 1); + } + } + // update the left for untap list + leftForUntap = getPermanentsThatCanBeUntapped(game, + canBeUntapped, + handledEntry.getKey().getKey(), + notMoreThanEffectsUsage); + // remove already selected permanents + for (Permanent permanent : selectedToUntap) { + leftForUntap.remove(permanent); + } + + } else { + // player selected an permanent that is restricted by another effect, disallow it (so AI can select another one) + filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); + if (this.isHuman() && !game.isSimulation()) { + game.informPlayer(this, "This permanent can't be untapped because of other restricting effect."); + } + } + } + } + } + } + + } while (canRespond() && playerCanceledSelection); + + if (!game.isSimulation()) { + // show in log which permanents were selected to untap + for (Permanent permanent : selectedToUntap) { + game.informPlayers(this.getLogName() + " untapped " + permanent.getLogName()); + } + } + // untap if permanent is not concerned by notMoreThan effects or is included in the selectedToUntapList + for (Permanent permanent : canBeUntapped) { + boolean doUntap = true; + if (!selectedToUntap.contains(permanent)) { + // if the permanent is covered by one of the restriction effects, don't untap it + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game)) { + doUntap = false; + break; + } + } + } + if (permanent != null && doUntap) { + permanent.untap(game); + } + + } + + } else { + //20091005 - 502.2 + + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { + boolean untap = true; + for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { + untap &= effect.canBeUntapped(permanent, null, game, true); + } + if (untap) { + permanent.untap(game); + } + } + } + } + + private List getPermanentsThatCanBeUntapped(Game game, List canBeUntapped, RestrictionUntapNotMoreThanEffect handledEffect, Map>, Integer> notMoreThanEffectsUsage) { + List leftForUntap = new ArrayList<>(); + // select permanents that can still be untapped + for (Permanent permanent : canBeUntapped) { + if (handledEffect.getFilter().match(permanent, game)) { // matches the restricted permanents of handled entry + boolean canBeSelected = true; + // check if the permanent is restricted by another restriction that has left no permanent + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game) + && notMoreThanEffect.getValue() == 0) { + canBeSelected = false; + break; + } + } + if (canBeSelected) { + leftForUntap.add(permanent); + } + } + } + return leftForUntap; + } + + @Override + public UUID getId() { + return playerId; + } + + @Override + public Cards getHand() { + return hand; + } + + @Override + public Graveyard getGraveyard() { + return graveyard; + } + + @Override + public ManaPool getManaPool() { + return this.manaPool; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getLogName() { + return GameLog.getColoredPlayerName(name); + } + + @Override + public boolean isHuman() { + return human; + } + + @Override + public Library getLibrary() { + return library; + } + + @Override + public Cards getSideboard() { + return sideboard; + } + + @Override + public int getLife() { + return life; + } + + @Override + public void initLife(int life) { + this.life = life; + } + + @Override + public void setLife(int life, Game game, Ability source) { + // rule 118.5 + if (life > this.life) { + gainLife(life - this.life, game, source); + } else if (life < this.life) { + loseLife(this.life - life, game, source, false); + } + } + + @Override + public void setLifeTotalCanChange(boolean lifeTotalCanChange) { + this.canGainLife = lifeTotalCanChange; + this.canLoseLife = lifeTotalCanChange; + } + + @Override + public boolean isLifeTotalCanChange() { + return canGainLife || canLoseLife; + } + + @Override + public List getAlternativeSourceCosts() { + return alternativeSourceCosts; + } + + @Override + public boolean isCanLoseLife() { + return canLoseLife; + } + + @Override + public void setCanLoseLife(boolean canLoseLife) { + this.canLoseLife = canLoseLife; + } + + @Override + public int loseLife(int amount, Game game, Ability source, boolean atCombat, UUID attackerId) { + if (!canLoseLife || !this.isInGame()) { + return 0; + } + GameEvent event = new GameEvent(GameEvent.EventType.LOSE_LIFE, + playerId, source, playerId, amount, atCombat); + if (!game.replaceEvent(event)) { + this.life = CardUtil.overflowDec(this.life, event.getAmount()); + if (!game.isSimulation()) { + UUID needId = attackerId; + if (needId == null) { + needId = source == null ? null : source.getSourceId(); + } + game.informPlayers(this.getLogName() + " loses " + event.getAmount() + " life" + + (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", "")); + } + if (amount > 0) { + game.fireEvent(new GameEvent(GameEvent.EventType.LOST_LIFE, + playerId, source, playerId, amount, atCombat)); + } + return amount; + } + return 0; + } + + @Override + public int loseLife(int amount, Game game, Ability source, boolean atCombat) { + return loseLife(amount, game, source, atCombat, null); + } + + @Override + public boolean isCanGainLife() { + return canGainLife; + } + + @Override + public void setCanGainLife(boolean canGainLife) { + this.canGainLife = canGainLife; + } + + @Override + public int gainLife(int amount, Game game, Ability source) { + if (!canGainLife || amount <= 0) { + return 0; + } + GameEvent event = new GameEvent(GameEvent.EventType.GAIN_LIFE, + playerId, source, playerId, amount, false); + if (!game.replaceEvent(event)) { + // TODO: lock life at Integer.MAX_VALUE if reached, until it's set to a different amount + // (https://magic.wizards.com/en/articles/archive/news/unstable-faqawaslfaqpaftidawabiajtbt-2017-12-06 - "infinite" life total stays infinite no matter how much is gained or lost) + // this.life += event.getAmount(); + this.life = CardUtil.overflowInc(this.life, event.getAmount()); + if (!game.isSimulation()) { + game.informPlayers(this.getLogName() + " gains " + event.getAmount() + " life" + CardUtil.getSourceLogName(game, source)); + } + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.GAINED_LIFE, + playerId, source, playerId, event.getAmount())); + return event.getAmount(); + } + return 0; + } + + @Override + public void exchangeLife(Player player, Ability source, Game game) { + int lifePlayer1 = getLife(); + int lifePlayer2 = player.getLife(); + if ((lifePlayer1 != lifePlayer2 && this.isLifeTotalCanChange() && player.isLifeTotalCanChange()) + && (lifePlayer1 >= lifePlayer2 || (this.isCanGainLife() && player.isCanLoseLife())) + && (lifePlayer1 <= lifePlayer2 || (this.isCanLoseLife() && player.isCanGainLife()))) { + this.setLife(lifePlayer2, game, source); + player.setLife(lifePlayer1, game, source); + } + } + + @Override + public int damage(int damage, UUID attackerId, Ability source, Game game) { + return doDamage(damage, attackerId, source, game, false, true, null); + } + + @Override + public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable) { + return doDamage(damage, attackerId, source, game, combatDamage, preventable, null); + } + + @Override + public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { + return doDamage(damage, attackerId, source, game, combatDamage, preventable, appliedEffects); + } + + private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { + if (!this.isInGame()) { + return 0; + } + + if (damage < 1) { + return 0; + } + if (!canDamage(game.getObject(attackerId), game)) { + MageObject sourceObject = game.getObject(attackerId); + game.informPlayers(damage + " damage " + + (sourceObject == null ? "" : "from " + sourceObject.getLogName()) + + " to " + getLogName() + + (damage > 1 ? " were" : "was") + " prevented because of protection"); + return 0; + } + DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combatDamage); + event.setAppliedEffects(appliedEffects); + if (game.replaceEvent(event)) { + return 0; + } + int actualDamage = event.getAmount(); + if (actualDamage < 1) { + return 0; + } + UUID sourceControllerId = null; + Abilities sourceAbilities = null; + MageObject attacker = game.getPermanentOrLKIBattlefield(attackerId); + if (attacker == null) { + StackObject stackObject = game.getStack().getStackObject(attackerId); + if (stackObject != null) { + attacker = stackObject.getStackAbility().getSourceObject(game); + } else { + attacker = game.getObject(attackerId); + } + if (attacker instanceof Spell) { + sourceAbilities = ((Spell) attacker).getAbilities(game); + sourceControllerId = ((Spell) attacker).getControllerId(); + } else if (attacker instanceof Card) { + sourceAbilities = ((Card) attacker).getAbilities(game); + sourceControllerId = ((Card) attacker).getOwnerId(); + } else if (attacker instanceof CommandObject) { + sourceControllerId = ((CommandObject) attacker).getControllerId(); + sourceAbilities = attacker.getAbilities(); + } + } else { + sourceAbilities = ((Permanent) attacker).getAbilities(game); + sourceControllerId = ((Permanent) attacker).getControllerId(); + } + if (event.isAsThoughInfect() || (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId()))) { + addCounters(CounterType.POISON.createInstance(actualDamage), sourceControllerId, source, game); + } else { + GameEvent damageToLifeLossEvent = new GameEvent(GameEvent.EventType.DAMAGE_CAUSES_LIFE_LOSS, + playerId, source, playerId, actualDamage, combatDamage); + if (!game.replaceEvent(damageToLifeLossEvent)) { + this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combatDamage, attackerId); + } + } + if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) { + if (combatDamage) { + game.getPermanent(attackerId).markLifelink(actualDamage); + } else { + Player player = game.getPlayer(sourceControllerId); + player.gainLife(actualDamage, game, source); + } + } + // Unstable ability - Earl of Squirrel + if (sourceAbilities != null && sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) { + Player player = game.getPlayer(sourceControllerId); + new SquirrelToken().putOntoBattlefield(actualDamage, game, source, player.getId()); + } + DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combatDamage); + game.fireEvent(damagedEvent); + game.getState().addSimultaneousDamage(damagedEvent, game); + return actualDamage; + } + + @Override + public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) { + boolean returnCode = true; + GameEvent addingAllEvent = GameEvent.getEvent( + GameEvent.EventType.ADD_COUNTERS, playerId, source, + playerAddingCounters, counter.getName(), counter.getCount() + ); + if (!game.replaceEvent(addingAllEvent)) { + int amount = addingAllEvent.getAmount(); + int finalAmount = amount; + boolean isEffectFlag = addingAllEvent.getFlag(); + for (int i = 0; i < amount; i++) { + Counter eventCounter = counter.copy(); + eventCounter.remove(eventCounter.getCount() - 1); + GameEvent addingOneEvent = GameEvent.getEvent( + GameEvent.EventType.ADD_COUNTER, playerId, source, + playerAddingCounters, counter.getName(), 1 + ); + addingOneEvent.setFlag(isEffectFlag); + if (!game.replaceEvent(addingOneEvent)) { + getCounters().addCounter(eventCounter); + GameEvent addedOneEvent = GameEvent.getEvent( + GameEvent.EventType.COUNTER_ADDED, playerId, source, + playerAddingCounters, counter.getName(), 1 + ); + addedOneEvent.setFlag(addingOneEvent.getFlag()); + game.fireEvent(addedOneEvent); + } else { + finalAmount--; + returnCode = false; + } + } + if (finalAmount > 0) { + GameEvent addedAllEvent = GameEvent.getEvent( + GameEvent.EventType.COUNTERS_ADDED, playerId, source, + playerAddingCounters, counter.getName(), amount + ); + addedAllEvent.setFlag(addingAllEvent.getFlag()); + game.fireEvent(addedAllEvent); + } + } else { + returnCode = false; + } + return returnCode; + } + + @Override + public void removeCounters(String name, int amount, Ability source, Game game) { + int finalAmount = 0; + for (int i = 0; i < amount; i++) { + if (!counters.removeCounter(name, 1)) { + break; + } + GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, + getId(), source, (source == null ? null : source.getControllerId())); + event.setData(name); + event.setAmount(1); + game.fireEvent(event); + finalAmount++; + } + GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, + getId(), source, (source == null ? null : source.getControllerId())); + event.setData(name); + event.setAmount(finalAmount); + game.fireEvent(event); + } + + protected boolean canDamage(MageObject source, Game game) { + for (ProtectionAbility ability : abilities.getProtectionAbilities()) { + if (!ability.canTarget(source, game)) { + return false; + } + } + return true; + } + + @Override + public Abilities getAbilities() { + return this.abilities; + } + + @Override + public void addAbility(Ability ability) { + ability.setSourceId(playerId); + this.abilities.add(ability); + } + + @Override + public int getLandsPerTurn() { + return this.landsPerTurn; + } + + @Override + public void setLandsPerTurn(int landsPerTurn) { + this.landsPerTurn = landsPerTurn; + } + + @Override + public int getLoyaltyUsePerTurn() { + return this.loyaltyUsePerTurn; + } + + @Override + public void setLoyaltyUsePerTurn(int loyaltyUsePerTurn) { + this.loyaltyUsePerTurn = loyaltyUsePerTurn; + } + + @Override + public int getMaxHandSize() { + return maxHandSize; + } + + @Override + public void setMaxHandSize(int maxHandSize) { + this.maxHandSize = maxHandSize; + } + + @Override + public void setMaxAttackedBy(int maxAttackedBy) { + this.maxAttackedBy = maxAttackedBy; + } + + @Override + public int getMaxAttackedBy() { + return maxAttackedBy; + } + + @Override + public void setResponseString(String responseString) { + } + + @Override + public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) { + } + + @Override + public void setResponseUUID(UUID responseUUID) { + } + + @Override + public void setResponseBoolean(Boolean responseBoolean) { + } + + @Override + public void setResponseInteger(Integer responseInteger) { + } + + @Override + public boolean isPassed() { + return passed; + } + + @Override + public void pass(Game game) { + this.passed = true; + resetStoredBookmark(game); + } + + @Override + public void resetPassed() { + this.passed = this.loses || this.hasLeft(); + } + + @Override + public void resetPlayerPassedActions() { + this.passed = false; + this.passedTurn = false; + this.passedTurnSkipStack = false; + this.passedUntilEndOfTurn = false; + this.passedUntilNextMain = false; + this.passedUntilStackResolved = false; + this.dateLastAddedToStack = null; + this.passedUntilEndStepBeforeMyTurn = false; + this.skippedAtLeastOnce = false; + this.passedAllTurns = false; + this.justActivatedType = null; + } + + @Override + public void quit(Game game) { + quit = true; + this.concede(game); + logger.debug(getName() + " quits the match."); + game.informPlayers(getLogName() + " quits the match."); + } + + @Override + public void timerTimeout(Game game) { + quit = true; + timerTimeout = true; + this.concede(game); + game.informPlayers(getLogName() + " has run out of time, losing the match."); + } + + @Override + public void idleTimeout(Game game) { + quit = true; + idleTimeout = true; + this.concede(game); + game.informPlayers(getLogName() + " was idle for too long, losing the Match."); + } + + @Override + public void concede(Game game) { + game.setConcedingPlayer(playerId); + lost(game); +// this.left = true; + } + + @Override + public void sendPlayerAction(PlayerAction playerAction, Game game, Object data) { + switch (playerAction) { + case PASS_PRIORITY_UNTIL_MY_NEXT_TURN: // F9 + resetPlayerPassedActions(); + passedAllTurns = true; + this.skip(); + break; + case PASS_PRIORITY_UNTIL_TURN_END_STEP: // F5 + resetPlayerPassedActions(); + passedUntilEndOfTurn = true; + skippedAtLeastOnce = PhaseStep.END_TURN != game.getTurn().getStepType(); + this.skip(); + break; + case PASS_PRIORITY_UNTIL_NEXT_TURN: // F4 + resetPlayerPassedActions(); + passedTurn = true; + this.skip(); + break; + case PASS_PRIORITY_UNTIL_NEXT_TURN_SKIP_STACK: // F6 + resetPlayerPassedActions(); + passedTurnSkipStack = true; + this.skip(); + break; + case PASS_PRIORITY_UNTIL_NEXT_MAIN_PHASE: //F7 + resetPlayerPassedActions(); + passedUntilNextMain = true; + skippedAtLeastOnce = !(game.getTurn().getStepType() == PhaseStep.POSTCOMBAT_MAIN + || game.getTurn().getStepType() == PhaseStep.PRECOMBAT_MAIN); + this.skip(); + break; + case PASS_PRIORITY_UNTIL_STACK_RESOLVED: // Default F10 - Skips until the current stack is resolved + if (!game.getStack().isEmpty()) { // If stack is empty do nothing + resetPlayerPassedActions(); + passedUntilStackResolved = true; + dateLastAddedToStack = game.getStack().getDateLastAdded(); + this.skip(); + } + break; + case PASS_PRIORITY_UNTIL_END_STEP_BEFORE_MY_NEXT_TURN: //F11 + resetPlayerPassedActions(); + passedUntilEndStepBeforeMyTurn = true; + this.skip(); + break; + case PASS_PRIORITY_CANCEL_ALL_ACTIONS: + resetPlayerPassedActions(); + break; + case PERMISSION_REQUESTS_ALLOWED_OFF: + userData.setAllowRequestShowHandCards(false); + break; + case PERMISSION_REQUESTS_ALLOWED_ON: + userData.setAllowRequestShowHandCards(true); + userData.resetRequestedHandPlayersList(game.getId()); // users can send request again + break; + } + logger.trace("PASS Priority: " + playerAction); + } + + @Override + public void leave() { + this.passed = true; + this.loses = true; + this.left = true; + this.abort(); + //20100423 - 800.4a + this.hand.clear(); + this.graveyard.clear(); + this.library.clear(); + } + + @Override + public boolean hasLeft() { + return this.left; + } + + @Override + public void lost(Game game) { + if (canLose(game)) { + lostForced(game); + } + } + + @Override + public void lostForced(Game game) { + logger.debug(this.getName() + " has lost gameId: " + game.getId()); + //20100423 - 603.9 + if (!this.wins) { + this.loses = true; + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LOST, null, null, playerId)); + game.informPlayers(this.getLogName() + " has lost the game."); + } else { + logger.debug(this.getName() + " has already won - stop lost"); + } + // for draw - first all players that have lost have to be set to lost + if (!hasLeft()) { + logger.debug("Game over playerId: " + playerId); + game.setConcedingPlayer(playerId); + } + } + + @Override + public boolean canLose(Game game) { + return hasLeft() // If a player concedes or has left the match they lose also if effect would say otherwise + || !game.replaceEvent(new GameEvent(GameEvent.EventType.LOSES, null, null, playerId)); + } + + @Override + public void won(Game game) { + boolean opponentInGame = false; + for (UUID opponentId : game.getOpponents(playerId)) { + Player opponent = game.getPlayer(opponentId); + + if (opponent != null && opponent.isInGame()) { + opponentInGame = true; + break; + } + } + if (!opponentInGame + || // if no more opponent is in game the wins event may no longer be replaced + !game.replaceEvent(new GameEvent(GameEvent.EventType.WINS, null, null, playerId))) { + logger.debug("player won -> start: " + this.getName()); + if (!this.loses) { + //20130501 - 800.7, 801.16 + // all opponents in range loose the game + for (UUID opponentId : game.getOpponents(playerId)) { + Player opponent = game.getPlayer(opponentId); + if (opponent != null && !opponent.hasLost()) { + logger.debug("player won -> calling opponent lost: " + + this.getName() + " opponent: " + opponent.getName()); + opponent.lostForced(game); + } + } + // if no more opponents alive (independant from range), you win and the game ends + int opponentsAlive = 0; + for (UUID playerIdToCheck : game.getPlayerList()) { + if (game.isOpponent(this, playerIdToCheck)) { // Check without range + Player opponent = game.getPlayer(playerIdToCheck); + if (opponent != null && !opponent.hasLost()) { + opponentsAlive++; + } + } + } + if (opponentsAlive == 0 && !hasWon()) { + logger.debug("player won -> No more opponents alive game won: " + this.getName()); + game.informPlayers(this.getLogName() + " has won the game"); + this.wins = true; + game.end(); + } + } else { + logger.debug("player won -> but already lost before or other players still alive: " + this.getName()); + } + } + } + + @Override + public void drew(Game game) { + if (!hasLost()) { + this.draws = true; + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DRAW_PLAYER, null, null, playerId)); + game.informPlayers("For " + this.getLogName() + " the game is a draw."); + game.setConcedingPlayer(playerId); + } + } + + @Override + public boolean hasLost() { + return this.loses; + } + + @Override + public boolean isInGame() { + return !hasQuit() && !hasLost() && !hasWon() && !hasDrew() && !hasLeft(); + } + + @Override + public boolean canRespond() { // abort is checked here to get out of player requests (as example: after disconnect) + return isInGame() && !abort; + } + + @Override + public boolean hasWon() { + return !this.loses && this.wins; + } + + @Override + public boolean hasDrew() { + return this.draws; + } + + @Override + public void declareAttacker(UUID attackerId, UUID defenderId, Game game, boolean allowUndo) { + if (allowUndo) { + setStoredBookmark(game.bookmarkState()); // makes it possible to UNDO a declared attacker with costs from e.g. Propaganda + } + Permanent attacker = game.getPermanent(attackerId); + if (attacker != null + && attacker.canAttack(defenderId, game) + && attacker.isControlledBy(playerId)) { + if (!game.getCombat().declareAttacker(attackerId, defenderId, playerId, game)) { + game.undo(playerId); + } + } + } + + @Override + public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game) { + declareBlocker(defenderId, blockerId, attackerId, game, true); + } + + @Override + public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game, boolean allowUndo) { + if (isHuman() && allowUndo) { + setStoredBookmark(game.bookmarkState()); + } + Permanent blocker = game.getPermanent(blockerId); + CombatGroup group = game.getCombat().findGroup(attackerId); + if (blocker != null && group != null && group.canBlock(blocker, game)) { + group.addBlocker(blockerId, playerId, game); + game.getCombat().addBlockingGroup(blockerId, attackerId, playerId, game); + } else if (this.isHuman() && !game.isSimulation()) { + game.informPlayer(this, "You can't block this creature."); + } + } + + @Override + public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game) { + return searchLibrary(target, source, game, playerId); + } + + @Override + public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) { + //20091005 - 701.14c + + // searching control can be intercepted by another player, see Opposition Agent + SearchLibraryEvent searchEvent = new SearchLibraryEvent(targetPlayerId, source, playerId, Integer.MAX_VALUE); + if (game.replaceEvent(searchEvent)) { + return false; + } + + Player targetPlayer = game.getPlayer(targetPlayerId); + Player searchingPlayer = this; + Player searchingController = game.getPlayer(searchEvent.getSearchingControllerId()); + if (targetPlayer == null || searchingController == null) { + return false; + } + + String searchInfo = searchingPlayer.getLogName(); + if (!searchingPlayer.getId().equals(searchingController.getId())) { + searchInfo = searchInfo + " under control of " + searchingPlayer.getLogName(); + } + if (targetPlayer.getId().equals(searchingPlayer.getId())) { + searchInfo = searchInfo + " searches their library"; + } else { + searchInfo = searchInfo + " searches the library of " + targetPlayer.getLogName(); + } + + if (!game.isSimulation()) { + game.informPlayers(searchInfo + CardUtil.getSourceLogName(game, source)); + } + + // https://www.reddit.com/r/magicTCG/comments/jj8gh9/opposition_agent_and_panglacial_wurm_interaction/ + // You must take full player control while searching, e.g. you can cast opponent's cards by Panglacial Wurm effect: + // * While you’re searching your library, you may cast Panglacial Wurm from your library. + // So use here same code as Word of Command + // P.S. no needs in searchingController, but it helps with unit tests, see TakeControlWhileSearchingLibraryTest + boolean takeControl = false; + if (!searchingPlayer.getId().equals(searchingController.getId())) { + CardUtil.takeControlUnderPlayerStart(game, searchingController, searchingPlayer, true); + takeControl = true; + } + + Library searchingLibrary = targetPlayer.getLibrary(); + TargetCardInLibrary newTarget = target.copy(); + int count; + int librarySearchLimit = searchEvent.getAmount(); + List cardsFromTop = null; + do { + // TODO: prevent shuffling from moving the visualized cards + if (librarySearchLimit == Integer.MAX_VALUE) { + count = searchingLibrary.count(target.getFilter(), game); + } else { + if (cardsFromTop == null) { + cardsFromTop = new ArrayList<>(searchingLibrary.getTopCards(game, librarySearchLimit)); + } else { + cardsFromTop.retainAll(searchingLibrary.getCards(game)); + } + newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size())); + count = Math.min(searchingLibrary.count(target.getFilter(), game), librarySearchLimit); + } + + if (count < target.getNumberOfTargets()) { + newTarget.setMinNumberOfTargets(count); + } + + // handling Panglacial Wurm - cast cards while searching from own library + if (targetPlayer.getId().equals(searchingPlayer.getId())) { + if (handleCastableCardsWhileLibrarySearching(library, game, targetPlayer)) { + // clear all choices to start from scratch (casted cards must be removed from library) + newTarget.clearChosen(); + continue; + } + } + + if (newTarget.choose(Outcome.Neutral, searchingController.getId(), targetPlayer.getId(), game)) { + target.getTargets().clear(); + for (UUID targetId : newTarget.getTargets()) { + target.add(targetId, game); + } + } + + // END SEARCH + if (takeControl) { + CardUtil.takeControlUnderPlayerEnd(game, searchingController, searchingPlayer); + game.informPlayers("Control of " + searchingPlayer.getLogName() + " is back" + CardUtil.getSourceLogName(game, source)); + } + + LibrarySearchedEvent searchedEvent = new LibrarySearchedEvent(targetPlayer.getId(), source, searchingPlayer.getId(), target); + if (!game.replaceEvent(searchedEvent)) { + game.fireEvent(searchedEvent); + } + break; + } while (true); + + return true; + } + + @Override + public boolean seekCard(FilterCard filter, Ability source, Game game) { + Set cards = this.getLibrary() + .getCards(game) + .stream() + .filter(card -> filter.match(card, source.getSourceId(), getId(), game)) + .collect(Collectors.toSet()); + Card card = RandomUtil.randomFromCollection(cards); + if (card == null) { + return false; + } + game.informPlayers(this.getLogName() + " seeks a card from their library"); + this.moveCards(card, Zone.HAND, source, game); + return true; + } + + @Override + public void lookAtAllLibraries(Ability source, Game game) { + for (UUID playerId : game.getState().getPlayersInRange(this.getId(), game)) { + Player player = game.getPlayer(playerId); + String playerName = this.getName().equals(player.getName()) ? "Your " : player.getName() + "'s "; + playerName += "library"; + Cards cardsInLibrary = new CardsImpl(player.getLibrary().getTopCards(game, player.getLibrary().size())); + lookAtCards(playerName, cardsInLibrary, game); + } + } + + private boolean handleCastableCardsWhileLibrarySearching(Library library, Game game, Player targetPlayer) { + // must return true after cast try (to restart searching process without casted cards) + // uses for handling Panglacial Wurm: + // * While you're searching your library, you may cast Panglacial Wurm from your library. + + List castableCards = library.getCards(game).stream() + .filter(card -> card.getAbilities(game).containsClass(WhileSearchingPlayFromLibraryAbility.class)) + .map(MageItem::getId) + .collect(Collectors.toList()); + if (castableCards.size() == 0) { + return false; + } + + // only humans can use it + if (targetPlayer.isComputer()) { + return false; + } + + if (!targetPlayer.chooseUse(Outcome.AIDontUseIt, "There are " + castableCards.size() + " cards you can cast while searching your library. Cast any of them?", null, game)) { + return false; + } + + boolean casted = false; + TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, StaticFilters.FILTER_CARD); + targetCard.setTargetName("card to cast from library"); + targetCard.setNotTarget(true); + while (castableCards.size() > 0) { + targetCard.clearChosen(); + if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, game)) { + break; + } + + Card card = game.getCard(targetCard.getFirstTarget()); + if (card == null) { + break; + } + + // AI NOTICE: if you want AI implement here then remove selected card from castable after each + // choice (otherwise you catch infinite freeze on uncastable use case) + // casting selected card + // TODO: fix costs (why is Panglacial Wurm automatically accepting payment?) + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + targetPlayer.cast(targetPlayer.chooseAbilityForCast(card, game, false), game, false, null); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + castableCards.remove(card.getId()); + casted = true; + } + return casted; + } + + /** + * @param source + * @param game + * @param winnable + * @return if winnable, true if player won the toss, if not winnable, true + * for heads and false for tails + */ + @Override + public boolean flipCoin(Ability source, Game game, boolean winnable) { + boolean chosen = false; + if (winnable) { + chosen = this.chooseUse(Outcome.Benefit, "Heads or tails?", "", "Heads", "Tails", source, game); + game.informPlayers(getLogName() + " chose " + CardUtil.booleanToFlipName(chosen)); + } + boolean result = this.flipCoinResult(game); + FlipCoinEvent event = new FlipCoinEvent(playerId, source, result, chosen, winnable); + game.replaceEvent(event); + game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult()) + + CardUtil.getSourceLogName(game, source)); + if (event.getFlipCount() > 1) { + boolean canChooseHeads = event.getResult(); + boolean canChooseTails = !event.getResult(); + for (int i = 1; i < event.getFlipCount(); i++) { + boolean tempFlip = this.flipCoinResult(game); + canChooseHeads = canChooseHeads || tempFlip; + canChooseTails = canChooseTails || !tempFlip; + game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(tempFlip)); + } + if (canChooseHeads && canChooseTails) { + event.setResult(chooseUse(Outcome.Benefit, "Choose which flip to keep", + (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), + "Heads", "Tails", source, game + )); + } else { + event.setResult(canChooseHeads); + } + game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); + } + if (event.isWinnable()) { + game.informPlayers(getLogName() + " " + (event.getResult() == event.getChosen() ? "won" : "lost") + " the flip" + + CardUtil.getSourceLogName(game, source)); + } + game.fireEvent(event.createFlippedEvent()); + if (event.isWinnable()) { + return event.getResult() == event.getChosen(); + } + return event.getResult(); + } + + /** + * Return result for next flip coint try (can be contolled in tests) + * + * @return + */ + @Override + public boolean flipCoinResult(Game game) { + return RandomUtil.nextBoolean(); + } + + private static final class RollDieResult { + + // 706.2. + // After the roll, the number indicated on the top face of the die before any modifiers is + // the natural result. The instruction may include modifiers to the roll which add to or + // subtract from the natural result. Modifiers may also come from other sources. After + // considering all applicable modifiers, the final number is the result of the die roll. + private final int naturalResult; + private final int modifier; + private final PlanarDieRollResult planarResult; + + RollDieResult(int naturalResult, int modifier, PlanarDieRollResult planarResult) { + this.naturalResult = naturalResult; + this.modifier = modifier; + this.planarResult = planarResult; + } + + public int getResult() { + return this.naturalResult + this.modifier; + } + + public PlanarDieRollResult getPlanarResult() { + return this.planarResult; + } + } + + @Override + public int rollDieResult(int sides, Game game) { + return RandomUtil.nextInt(sides) + 1; + } + + /** + * Roll single die. Support both die types: planar and numerical. + * + * @param outcome + * @param game + * @param source + * @param rollDieType + * @param sidesAmount + * @param chaosSidesAmount + * @param planarSidesAmount + * @param rollsAmount + * @return + */ + private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType, + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { + if (rollsAmount == 1) { + return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount); + } + Set choices = new HashSet<>(); + for (int j = 0; j < rollsAmount; j++) { + choices.add(rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount)); + } + if (choices.size() == 1) { + return choices.stream().findFirst().orElse(0); + } + + // AI hint - use max/min values + if (this.isComputer()) { + if (rollDieType == RollDieType.NUMERICAL) { + // numerical + if (outcome.isGood()) { + return choices.stream() + .map(Integer.class::cast) + .max(Comparator.naturalOrder()) + .orElse(null); + } else { + return choices.stream() + .map(Integer.class::cast) + .min(Comparator.naturalOrder()) + .orElse(null); + } + } else { + // planar + // priority: chaos -> planar -> blank + return choices.stream() + .map(PlanarDieRollResult.class::cast) + .max(Comparator.comparingInt(PlanarDieRollResult::getAIPriority)) + .orElse(null); + } + } + + Choice choice = new ChoiceImpl(true); + choice.setMessage("Choose which die roll result to keep (the rest will be ignored)"); + choice.setChoices(choices.stream().sorted().map(Object::toString).collect(Collectors.toSet())); + + this.choose(Outcome.Neutral, choice, game); + Object defaultChoice = choices.iterator().next(); + return choices.stream() + .filter(o -> o.toString().equals(choice.getChoice())) + .findFirst() + .orElse(defaultChoice); + } + + private Object rollDieInnerWithReplacement(Game game, Ability source, RollDieType rollDieType, int numSides, int numChaosSides, int numPlanarSides) { + switch (rollDieType) { + + case NUMERICAL: { + int result = rollDieResult(numSides, game); + // Clam-I-Am workaround: + // If you roll a 3 on a six-sided die, you may reroll that die. + if (numSides == 6 + && result == 3 + && game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.REPLACE_ROLLED_DIE, source.getControllerId(), source, source.getControllerId())) + && chooseUse(Outcome.Neutral, "Re-roll the 3?", source, game)) { + result = rollDieResult(numSides, game); + } + return result; + } + + case PLANAR: { + if (numChaosSides + numPlanarSides > numSides) { + numChaosSides = GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES; + numPlanarSides = GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES; + } + // for 9 sides: + // 1..2 - chaos + // 3..7 - blank + // 8..9 - planar + int result = this.rollDieResult(numSides, game); + PlanarDieRollResult roll; + if (result <= numChaosSides) { + roll = PlanarDieRollResult.CHAOS_ROLL; + } else if (result > numSides - numPlanarSides) { + roll = PlanarDieRollResult.PLANAR_ROLL; + } else { + roll = PlanarDieRollResult.BLANK_ROLL; + } + return roll; + } + + default: { + throw new IllegalArgumentException("Unknown roll die type " + rollDieType); + } + } + } + + /** + * @param outcome + * @param source + * @param game + * @param sidesAmount number of sides the dice has + * @param rollsAmount number of tries to roll the dice + * @param ignoreLowestAmount remove the lowest rolls from the results + * @return the number that the player rolled + */ + @Override + public List rollDice(Outcome outcome, Ability source, Game game, int sidesAmount, int rollsAmount, int ignoreLowestAmount) { + return rollDiceInner(outcome, source, game, RollDieType.NUMERICAL, sidesAmount, 0, 0, rollsAmount, ignoreLowestAmount) + .stream() + .map(Integer.class::cast) + .collect(Collectors.toList()); + } + + /** + * Inner code to roll a dice. Support normal and planar types. + * + * @param outcome + * @param source + * @param game + * @param rollDieType die type to roll, e.g. planar or numerical + * @param sidesAmount sides per die + * @param chaosSidesAmount for planar die: chaos sides + * @param planarSidesAmount for planar die: planar sides + * @param rollsAmount rolls + * @param ignoreLowestAmount for numerical die: ignore multiple rolls with + * the lowest values + * @return + */ + private List rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType, + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, + int rollsAmount, int ignoreLowestAmount) { + RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount); + if (ignoreLowestAmount > 0) { + rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount); + } + game.replaceEvent(rollDiceEvent); + + // 706.6. + // In a Planechase game, rolling the planar die will cause any ability that triggers whenever a + // player rolls one or more dice to trigger. However, any effect that refers to a numerical + // result of a die roll, including ones that compare the results of that roll to other rolls + // or to a given number, ignores the rolling of the planar die. See rule 901, “Planechase.” + // ROLL MULTIPLE dies + // results amount can be less than a rolls amount (example: The Big Idea allows rolling 2x instead 1x) + List dieResults = new ArrayList<>(); + List dieRolls = new ArrayList<>(); + for (int i = 0; i < rollDiceEvent.getAmount(); i++) { + // ROLL SINGLE die + RollDieEvent rollDieEvent = new RollDieEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides()); + game.replaceEvent(rollDieEvent); + + Object rollResult; + // big idea logic for numerical rolls only + if (rollDieEvent.getRollDieType() == RollDieType.NUMERICAL && rollDieEvent.getBigIdeaRollsAmount() > 0) { + // rolls 2x + sum results + // The Big Idea: roll two six-sided dice and use the total of those results + int totalSum = 0; + for (int j = 0; j < rollDieEvent.getBigIdeaRollsAmount() + 1; j++) { + int singleResult = (Integer) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount()); + totalSum += singleResult; + dieRolls.add(new RollDieResult(singleResult, rollDieEvent.getResultModifier(), null)); + } + rollResult = totalSum; + } else { + // rolls 1x + switch (rollDieEvent.getRollDieType()) { + default: + case NUMERICAL: { + int naturalResult = (Integer) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount() + ); + dieRolls.add(new RollDieResult(naturalResult, rollDieEvent.getResultModifier(), null)); + rollResult = naturalResult; + break; + } + + case PLANAR: { + PlanarDieRollResult planarResult = (PlanarDieRollResult) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount() + ); + dieRolls.add(new RollDieResult(0, 0, planarResult)); + rollResult = planarResult; + break; + } + } + } + dieResults.add(rollResult); + } + + // ignore the lowest results + // planar dies: due to 706.6. planar die results must be fully ignored + // + // 706.5. + // If a player is instructed to roll two or more dice and ignore the lowest roll, the roll + // that yielded the lowest result is considered to have never happened. No abilities trigger + // because of the ignored roll, and no effects apply to that roll. If multiple results are tied + // for the lowest, the player chooses one of those rolls to be ignored. + int diceRolledTotal = dieRolls.size(); + String ignoreMessage; + if (rollDiceEvent.getRollDieType() == RollDieType.NUMERICAL && rollDiceEvent.getIgnoreLowestAmount() > 0) { + // find ignored values + List ignoredResults = new ArrayList<>(); + for (int i = 0; i < rollDiceEvent.getIgnoreLowestAmount(); i++) { + int min = dieResults.stream().map(Integer.class::cast).mapToInt(Integer::intValue).min().orElse(0); + dieResults.remove(Integer.valueOf(min)); + ignoredResults.add(min); + } + ignoreMessage = String.format( + ignoredResults.size() > 1 ? ", ignoring [%s]" : ", ignoring %s", + ignoredResults + .stream() + .map(x -> "" + x) + .collect(Collectors.joining(", ")) + ); + // remove ignored rolls (they not exist anymore) + List newRolls = new ArrayList<>(); + for (RollDieResult rollDieResult : dieRolls) { + if (ignoredResults.contains(rollDieResult.getResult())) { + ignoredResults.remove((Integer) rollDieResult.getResult()); + } else { + newRolls.add(rollDieResult); + } + } + dieRolls.clear(); + dieRolls.addAll(newRolls); + } else { + ignoreMessage = ""; + } + + // raise affected roll events + for (RollDieResult result : dieRolls) { + game.fireEvent(new DieRolledEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides(), result.naturalResult, result.modifier, result.planarResult)); + } + game.fireEvent(new DiceRolledEvent(rollDiceEvent.getSides(), dieResults, source)); + + String resultString = dieResults + .stream() + .map(Object::toString) + .collect(Collectors.joining(", ")); + String message; + switch (rollDiceEvent.getRollDieType()) { + default: + case NUMERICAL: + // [Roll a die] user rolled 4d6, results: [4, 6], ignoring [1, 3] (source: xxx) + message = String.format("[Roll a die] %s rolled %sd%s, result%s: %s%s%s", + getLogName(), + diceRolledTotal > 1 ? diceRolledTotal : "a ", + rollDiceEvent.getSides(), + dieResults.size() > 1 ? 's' : "", + dieResults.size() > 1 ? '[' + resultString + ']' : resultString, + ignoreMessage, + CardUtil.getSourceLogName(game, source)); + break; + case PLANAR: + // [Roll a planar die] user rolled CHAOS (source: xxx) + message = String.format("[Roll a planar die] %s rolled %s%s", + getLogName(), + dieResults.size() > 1 ? '[' + resultString + ']' : resultString, + CardUtil.getSourceLogName(game, source)); + break; + } + game.informPlayers(message); + return dieResults; + } + + /** + * @param source + * @param game + * @param chaosSidesAmount The number of chaos sides the planar die + * currently has (normally 1 but can be 5) + * @param planarSidesAmount The number of chaos sides the planar die + * currently has (normally 1) + * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll + * or BlankRoll + */ + @Override + public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int chaosSidesAmount, int planarSidesAmount) { + return rollDiceInner(outcome, source, game, RollDieType.PLANAR, GameOptions.PLANECHASE_PLANAR_DIE_TOTAL_SIDES, chaosSidesAmount, planarSidesAmount, 1, 0) + .stream() + .map(o -> (PlanarDieRollResult) o) + .findFirst() + .orElse(PlanarDieRollResult.BLANK_ROLL); + } + + @Override + public List getAvailableAttackers(Game game) { + // TODO: get available opponents and their planeswalkers, check for each if permanent can attack one + return getAvailableAttackers(null, game); + } + + @Override + public List getAvailableAttackers(UUID defenderId, Game game) { + FilterCreatureForCombat filter = new FilterCreatureForCombat(); + List attackers = game.getBattlefield().getAllActivePermanents(filter, playerId, game); + attackers.removeIf(entry -> !entry.canAttack(defenderId, game)); + return attackers; + } + + @Override + public List getAvailableBlockers(Game game) { + FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock(); + return game.getBattlefield().getAllActivePermanents(blockFilter, playerId, game); + } + + /** + * Returns the mana options the player currently has. That means which + * combinations of mana are available to cast spells or activate abilities + * etc. + * + * @param game + * @return + */ + @Override + public ManaOptions getManaAvailable(Game game) { + boolean oldState = game.inCheckPlayableState(); + game.setCheckPlayableState(true); + + ManaOptions availableMana = new ManaOptions(); + availableMana.addMana(manaPool.getMana()); + // conditional mana + for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { + availableMana.addMana(conditionalMana); + } + + List> sourceWithoutManaCosts = new ArrayList<>(); + List> sourceWithCosts = new ArrayList<>(); + for (Card card : getHand().getCards(game)) { + Abilities manaAbilities + = card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game); + for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + ActivatedManaAbilityImpl ability = it.next(); + Abilities noTapAbilities = new AbilitiesImpl<>(ability); + if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { + sourceWithoutManaCosts.add(noTapAbilities); + } else { + sourceWithCosts.add(noTapAbilities); + } + } + } + + for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range + Boolean canUse = null; + boolean canAdd = false; + boolean useLater = false; // sources with mana costs or mana pool dependency + Abilities manaAbilities + = permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true + for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + ActivatedManaAbilityImpl ability = it.next(); + if (canUse == null) { + canUse = permanent.canUseActivatedAbilities(game); + } + if (canUse) { + // abilities without Tap costs have to be handled as separate sources, because they can be used also + if (!ability.hasTapCost()) { + it.remove(); + Abilities noTapAbilities = new AbilitiesImpl<>(ability); + if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { + sourceWithoutManaCosts.add(noTapAbilities); + } else { + sourceWithCosts.add(noTapAbilities); + } + continue; + } + + canAdd = true; + if (!ability.getManaCosts().isEmpty() || ability.isPoolDependant()) { + useLater = true; + break; + } + } + } + if (canAdd) { + if (useLater) { + sourceWithCosts.add(manaAbilities); + } else { + sourceWithoutManaCosts.add(manaAbilities); + } + } + } + + for (Abilities manaAbilities : sourceWithoutManaCosts) { + availableMana.addMana(manaAbilities, game); + } + + boolean anAbilityWasUsed = true; + boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production + while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) { + anAbilityWasUsed = false; + for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext();) { + Abilities manaAbilities = iterator.next(); + if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) { + boolean used; + if (manaAbilities.hasPoolDependantAbilities()) { + used = availableMana.addManaPoolDependant(manaAbilities, game); + } else { + used = availableMana.addManaWithCost(manaAbilities, game); + } + if (used) { + iterator.remove(); + availableMana.removeDuplicated(); + anAbilityWasUsed = true; + } + } + } + if (!anAbilityWasUsed && !usePoolDependantAbilities) { + usePoolDependantAbilities = true; + anAbilityWasUsed = true; + } + } + + // remove duplicated variants (see ManaOptionsTest for info - when that rises) + availableMana.removeDuplicated(); + + game.setCheckPlayableState(oldState); + return availableMana; + } + + /** + * Used during calculation of available mana to gather the amount of + * producable triggered mana caused by using mana sources. So the set value + * is only used during the calculation of the mana produced by one source + * and cleared thereafter + * + * @param netManaAvailable the net mana produced by the triggered mana + * abaility + */ + @Override + public void addAvailableTriggeredMana(List netManaAvailable + ) { + this.availableTriggeredManaList.add(netManaAvailable); + } + + /** + * Used during calculation of available mana to get the amount of producable + * triggered mana caused by using mana sources. The list is cleared as soon + * the value is retrieved during available mana calculation. + * + * @return + */ + @Override + public List> getAvailableTriggeredMana() { + return availableTriggeredManaList; + } + // returns only mana producers that don't require mana payment + + protected List getAvailableManaProducers(Game game) { + List result = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range + Boolean canUse = null; + boolean canAdd = false; + for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { + if (!ability.getManaCosts().isEmpty()) { + canAdd = false; + break; + } + if (canUse == null) { + canUse = permanent.canUseActivatedAbilities(game); + } + if (canUse && ability.canActivate(playerId, game).canActivate()) { + canAdd = true; + } + } + if (canAdd) { + result.add(permanent); + } + } + for (Card card : getHand().getCards(game)) { + boolean canAdd = false; + for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.HAND)) { + if (!ability.getManaCosts().isEmpty()) { + canAdd = false; + break; + } + if (ability.canActivate(playerId, game).canActivate()) { + canAdd = true; + } + } + if (canAdd) { + result.add(card); + } + } + return result; + } + + // returns only mana producers that require mana payment + public List getAvailableManaProducersWithCost(Game game) { + List result = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { + Boolean canUse = null; + for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { + if (canUse == null) { + canUse = permanent.canUseActivatedAbilities(game); + } + if (canUse && ability.canActivate(playerId, game).canActivate() + && !ability.getManaCosts().isEmpty()) { + result.add(permanent); + break; + } + } + } + return result; + } + + /** + * @param ability + * @param availableMana if null, it won't be checked if enough mana is + * available + * @param sourceObject + * @param game + * @return + */ + protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { + if (!(ability instanceof ActivatedManaAbilityImpl)) { + ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability + if (!copy.canActivate(playerId, game).canActivate()) { + return false; + } + if (availableMana != null) { + sourceObject.adjustCosts(copy, game); + game.getContinuousEffects().costModification(copy, game); + } + boolean canBeCastRegularly = true; + if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { + // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... + // 117.6a (...) If an alternative cost is applied to an unpayable cost, + // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. + canBeCastRegularly = false; + } + if (canBeCastRegularly) { + if (canPayMinimumManaCost(copy, availableMana, game)) { + return true; + } + } + + // ALTERNATIVE COST FROM dynamic effects + if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) { + ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()); + Costs costs = getCastSourceIdCosts().get(copy.getSourceId()); + + boolean canPutToPlay = true; + if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { + canPutToPlay = false; + } + if (costs != null && !costs.canPay(copy, copy, playerId, game)) { + canPutToPlay = false; + } + + if (canPutToPlay) { + return true; + } + } + + // ALTERNATIVE COST from source card (any AlternativeSourceCosts) + if (AbilityType.SPELL.equals(ability.getAbilityType())) { + return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); + } + } + return false; + } + + protected boolean canPayMinimumManaCost(ActivatedAbility ability, ManaOptions availableMana, Game game) { + ManaOptions abilityOptions = ability.getMinimumCostToActivate(playerId, game); + if (abilityOptions.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + // Check for pay option with like phyrexian mana + if (getPhyrexianColors() != null) { + addPhyrexianLikePayOptions(abilityOptions, availableMana, game); + } + + ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(), + AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); + for (Mana mana : abilityOptions) { + if (mana.count() == 0) { + return true; + } + for (Mana avail : availableMana) { + // TODO: SPEND_OTHER_MANA effects with getAsThoughManaType can change mana type to pay, + // but that code processing it as any color, need to test and fix another use cases + // (example: Sunglasses of Urza - may spend white mana as though it were red mana) + + // + // add tests for non any color like Sunglasses of Urza + if (approvingObject != null && mana.count() <= avail.count()) { + return true; + } + if (avail instanceof ConditionalMana && !((ConditionalMana) avail).apply(ability, game, getId(), ability.getManaCosts())) { + continue; + } + if (mana.enough(avail)) { // here we need to check if spend mana as though allow to pay the mana cost + return true; + } + } + } + } + return false; + } + + private void addPhyrexianLikePayOptions(ManaOptions abilityOptions, ManaOptions availableMana, Game game) { + int maxLifeMana = getLife() / 2; + if (maxLifeMana > 0) { + Set phyrexianOptions = new HashSet<>(); + for (Mana mana : abilityOptions) { + int availableLifeMana = maxLifeMana; + if (getPhyrexianColors().isBlack()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLACK); + } + if (getPhyrexianColors().isBlue()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLUE); + } + if (getPhyrexianColors().isRed()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.RED); + } + if (getPhyrexianColors().isGreen()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.GREEN); + } + if (getPhyrexianColors().isWhite()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.WHITE); + } + } + abilityOptions.addAll(phyrexianOptions); + } + } + + private int createReducedManaPayOption(int availableLifeMana, Mana oldPayOption, Set phyrexianOptions, ManaType manaType) { + if (oldPayOption.get(manaType) > 0) { + Mana manaCopy = oldPayOption.copy(); + int restVal; + if (availableLifeMana > oldPayOption.get(manaType)) { + restVal = 0; + availableLifeMana -= oldPayOption.get(manaType); + } else { + restVal = CardUtil.overflowDec(oldPayOption.get(manaType), availableLifeMana); + availableLifeMana = 0; + } + manaCopy.set(manaType, restVal); + phyrexianOptions.add(manaCopy); + } + return availableLifeMana; + } + + protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) { + if (sourceObject != null && !(sourceObject instanceof Permanent)) { + Ability copyAbility; // for alternative cost and reduce tries + for (Ability alternateSourceCostsAbility : sourceObject.getAbilities()) { + // if cast for noMana no Alternative costs are allowed + if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) { + if (((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) { + if (alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) { + ManaCostsImpl manaCosts = new ManaCostsImpl(); + for (Cost cost : alternateSourceCostsAbility.getCosts()) { + // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here + if (cost instanceof AlternativeCost2) { + if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); + } + } else { + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); + } + } + } + + if (manaCosts.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + + // alternative cost reduce + copyAbility = ability.copy(); + copyAbility.getManaCostsToPay().clear(); + copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); + sourceObject.adjustCosts(copyAbility, game); + game.getContinuousEffects().costModification(copyAbility, game); + + // reduced all cost + if (copyAbility.getManaCostsToPay().isEmpty()) { + return true; + } + + for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { + if (availableMana.enough(mana)) { + return true; + } + } + } + } + } + } + } + + // controller specific alternate spell costs + for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) { + if (alternateSourceCosts instanceof Ability) { + if (alternateSourceCosts.isAvailable(ability, game)) { + if (((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) { + ManaCostsImpl manaCosts = new ManaCostsImpl(); + for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { + // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here + if (cost instanceof AlternativeCost2) { + if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); + } + } else { + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); + } + } + } + + if (manaCosts.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + + // alternative cost reduce + copyAbility = ability.copy(); + copyAbility.getManaCostsToPay().clear(); + copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); + sourceObject.adjustCosts(copyAbility, game); + game.getContinuousEffects().costModification(copyAbility, game); + + // reduced all cost + if (copyAbility.getManaCostsToPay().isEmpty()) { + return true; + } + + for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { + if (availableMana.enough(mana)) { + return true; + } + } + } + } + } + } + } + } + return false; + } + + protected ActivatedAbility findActivatedAbilityFromPlayable(MageObject object, ManaOptions availableMana, Ability ability, Game game) { + + // special mana to pay spell cost + ManaOptions manaFull = availableMana.copy(); + if (ability instanceof SpellAbility) { + for (AlternateManaPaymentAbility altAbility : CardUtil.getAbilities(object, game).stream() + .filter(a -> a instanceof AlternateManaPaymentAbility) + .map(a -> (AlternateManaPaymentAbility) a) + .collect(Collectors.toList())) { + ManaOptions manaSpecial = altAbility.getManaOptions(ability, game, ability.getManaCostsToPay()); + manaFull.addMana(manaSpecial); + } + } + + // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) + if (ability instanceof ActivatedManaAbilityImpl) { + // mana ability + if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) { + return (ActivatedManaAbilityImpl) ability; + } + } else if (ability instanceof AlternativeSourceCosts) { + // alternative cost must be replaced by real play ability + return findActivatedAbilityFromAlternativeSourceCost(object, manaFull, ability, game); + } else if (ability instanceof ActivatedAbility) { + // all other activated ability + if (canPlay((ActivatedAbility) ability, manaFull, object, game)) { + return (ActivatedAbility) ability; + } + } + + // non playable abilities like static + return null; + } + + protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions availableMana, Ability ability, Game game) { + // return play ability that can activate AlternativeSourceCosts + if (ability instanceof AlternativeSourceCosts && object != null && !(object instanceof Permanent)) { + ActivatedAbility playAbility = null; + if (object.isLand(game)) { + playAbility = (PlayLandAbility) CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); + } else if (object instanceof Card) { + playAbility = ((Card) object).getSpellAbility(); + } + if (playAbility == null) { + return null; + } + + // 707.4.Objects that are cast face down are turned face down before they are put onto the stack + // E.g. no lands per turn limit, no cast restrictions, cost reduce, etc + // Even mana cost can't be checked here without lookahead + // So make it available all the time + boolean canUse; + if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId()) + || (null != game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game)))) { + canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game); + } else { + canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions + } + + if (canUse) { + return playAbility; + } + } + return null; + } + + private void getPlayableFromObjectAll(Game game, Zone fromZone, MageObject object, ManaOptions availableMana, List output) { + if (fromZone == null || object == null) { + return; + } + + // BASIC abilities + if (object instanceof SplitCard) { + SplitCard mainCard = (SplitCard) object; + getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); + } else if (object instanceof ModalDoubleFacesCard) { + ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) object; + getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); + } else if (object instanceof AdventureCard) { + // adventure must use different card characteristics for different spells (main or adventure) + AdventureCard adventureCard = (AdventureCard) object; + getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); + } else if (object instanceof Card) { + getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output); + } else if (object instanceof StackObject) { + // spells on stack are processing by Card above, other stack objects must be ignored + } else { + // other things like CommandObject + getPlayableFromObjectSingle(game, fromZone, object, object.getAbilities(), availableMana, output); + } + + // DYNAMIC ADDED abilities are adds in getAbilities(game) + } + + private void getPlayableFromObjectSingle(Game game, Zone fromZone, MageObject object, Abilities candidateAbilities, ManaOptions availableMana, List output) { + // check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller) + // must check all abilities, not activated only + for (Ability ability : candidateAbilities) { + if (!(ability instanceof ActivatedAbility)) { + continue; + } + boolean isPlaySpell = (ability instanceof SpellAbility); + boolean isPlayLand = (ability instanceof PlayLandAbility); + + // as original controller + // play land restrictions + if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( + GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), + ability, this.getId()), ability, game, true)) { + continue; + } + // cast spell restrictions 1 + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability, this.getId()); + castEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castEvent, ability, game, true)) { + continue; + } + // cast spell restrictions 2 + GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, + ability.getId(), ability, this.getId()); + castLateEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castLateEvent, ability, game, true)) { + continue; + } + + ApprovingObject approvingObject; + if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) { + // play hand from non hand zone (except battlefield - you can't play already played permanents) + approvingObject = game.getContinuousEffects().asThough(object.getId(), + AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); + } else { + // other abilities from direct zones + approvingObject = null; + } + + boolean canActivateAsHandZone = approvingObject != null + || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); + boolean possibleToPlay = canActivateAsHandZone + && ability.getZone().match(Zone.HAND) + && (isPlaySpell || isPlayLand); + + // spell/hand abilities (play from all zones) + // need permitingObject or canPlayCardsFromGraveyard + // zone's abilities (play from specific zone) + // no need in permitingObject + if (fromZone != Zone.ALL && ability.getZone().match(fromZone)) { + possibleToPlay = true; + } + + if (!possibleToPlay) { + continue; + } + + // direct mode (with original controller) + ActivatedAbility playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); + if (playAbility != null && !output.contains(playAbility)) { + output.add(playAbility); + continue; + } + + // from non hand mode (with affected controller) + if (canActivateAsHandZone && ability.getControllerId() != this.getId()) { + UUID savedControllerId = ability.getControllerId(); + ability.setControllerId(this.getId()); + try { + playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); + if (playAbility != null && !output.contains(playAbility)) { + output.add(playAbility); + } + } finally { + ability.setControllerId(savedControllerId); + } + } + } + } + + @Override + public List getPlayable(Game game, boolean hidden) { + return getPlayable(game, hidden, Zone.ALL, true); + } + + /** + * Returns a list of all available spells and abilities the player can + * currently cast/activate with his available resources + * + * @param game + * @param hidden also from hidden objects (e.g. turned face down cards ?) + * @param fromZone of objects from which zone (ALL = from all zones) + * @param hideDuplicatedAbilities if equal abilities exist return only the + * first instance + * @return + */ + public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { + List playable = new ArrayList<>(); + if (shouldSkipGettingPlayable(game)) { + return playable; + } + + boolean previousState = game.inCheckPlayableState(); + game.setCheckPlayableState(true); + try { + ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition)) + boolean fromAll = fromZone.equals(Zone.ALL); + if (hidden && (fromAll || fromZone == Zone.HAND)) { + for (Card card : hand.getCards(game)) { + for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) + if (ability.getZone().match(Zone.HAND)) { + boolean isPlaySpell = (ability instanceof SpellAbility); + boolean isPlayLand = (ability instanceof PlayLandAbility); + + // play land restrictions + if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( + GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), + ability, this.getId()), ability, game, true)) { + continue; + } + // cast spell restrictions 1 + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, + ability.getId(), ability, this.getId()); + castEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castEvent, ability, game, true)) { + continue; + } + // cast spell restrictions 2 + GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, + ability.getId(), ability, this.getId()); + castLateEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castLateEvent, ability, game, true)) { + continue; + } + + ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); + if (playAbility != null && !playable.contains(playAbility)) { + playable.add(playAbility); + } + } + } + } + } + + if (fromAll || fromZone == Zone.GRAVEYARD) { + for (UUID playerId : game.getState().getPlayersInRange(getId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + for (Card card : player.getGraveyard().getCards(game)) { + getPlayableFromObjectAll(game, Zone.GRAVEYARD, card, availableMana, playable); + } + } + } + + if (fromAll || fromZone == Zone.EXILED) { + for (ExileZone exile : game.getExile().getExileZones()) { + for (Card card : exile.getCards(game)) { + getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable); + } + } + } + + // check to play revealed cards + if (fromAll) { + for (Cards revealedCards : game.getState().getRevealed().values()) { + for (Card card : revealedCards.getCards(game)) { + // revealed cards can be from any zones + getPlayableFromObjectAll(game, game.getState().getZone(card.getId()), card, availableMana, playable); + } + } + } + + // outside cards + if (fromAll || fromZone == Zone.OUTSIDE) { + // companion cards + for (Cards companionCards : game.getState().getCompanion().values()) { + for (Card card : companionCards.getCards(game)) { + getPlayableFromObjectAll(game, Zone.OUTSIDE, card, availableMana, playable); + } + } + + // sideboard cards (example: Wish) + for (UUID sideboardCardId : this.getSideboard()) { + Card sideboardCard = game.getCard(sideboardCardId); + if (sideboardCard != null) { + getPlayableFromObjectAll(game, Zone.OUTSIDE, sideboardCard, availableMana, playable); + } + } + } + + // check if it's possible to play the top card of a library + if (fromAll || fromZone == Zone.LIBRARY) { + for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { + Player player = game.getPlayer(playerInRangeId); + if (player != null && player.getLibrary().hasCards()) { + Card card = player.getLibrary().getFromTop(game); + if (card != null) { + getPlayableFromObjectAll(game, Zone.LIBRARY, card, availableMana, playable); + } + } + } + } + + // check the hand zone (Sen Triplets) + // TODO: remove direct hand check (reveal fix in Sen Triplets)? + // human games: cards from opponent's hand must be revealed before play + // AI games: computer can see and play cards from opponent's hand without reveal + if (fromAll || fromZone == Zone.HAND) { + for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { + Player player = game.getPlayer(playerInRangeId); + if (player != null && !player.getHand().isEmpty()) { + for (Card card : player.getHand().getCards(game)) { + if (card != null) { + getPlayableFromObjectAll(game, Zone.HAND, card, availableMana, playable); + } + } + } + } + } + + // eliminate duplicate activated abilities (uses for AI plays) + Map activatedUnique = new HashMap<>(); + List activatedAll = new ArrayList<>(); + + // activated abilities from battlefield objects + if (fromAll || fromZone == Zone.BATTLEFIELD) { + for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { + boolean canUseActivated = permanent.canUseActivatedAbilities(game); + List currentPlayable = new ArrayList<>(); + getPlayableFromObjectAll(game, Zone.BATTLEFIELD, permanent, availableMana, currentPlayable); + for (ActivatedAbility ability : currentPlayable) { + if (ability instanceof SpecialAction || canUseActivated) { + activatedUnique.putIfAbsent(ability.toString(), ability); + activatedAll.add(ability); + } + } + } + } + + // activated abilities from stack objects + if (fromAll || fromZone == Zone.STACK) { + for (StackObject stackObject : game.getState().getStack()) { + List currentPlayable = new ArrayList<>(); + getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable); + for (ActivatedAbility ability : currentPlayable) { + activatedUnique.put(ability.toString(), ability); + activatedAll.add(ability); + } + } + } + + // activated abilities from objects in the command zone (emblems or commanders) + if (fromAll || fromZone == Zone.COMMAND) { + for (CommandObject commandObject : game.getState().getCommand()) { + List currentPlayable = new ArrayList<>(); + getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable); + for (ActivatedAbility ability : currentPlayable) { + activatedUnique.put(ability.toString(), ability); + activatedAll.add(ability); + } + } + } + + if (hideDuplicatedAbilities) { + playable.addAll(activatedUnique.values()); + } else { + playable.addAll(activatedAll); + } + } finally { + game.setCheckPlayableState(previousState); + } + + return playable; + } + + /** + * Creates a list of card ids that are currently playable.
+ * Used to mark the playable cards in GameView Also contains number of + * playable abilities for that object (it's just info, server decides to + * show choose dialog or not) + * + * @param game + * @return A Set of cardIds that are playable and amount of playable + * abilities + */ + @Override + public PlayableObjectsList getPlayableObjects(Game game, Zone zone) { + // collect abilities per object + List playableAbilities = getPlayable(game, true, zone, false); // do not hide duplicated abilities/cards + Map> playableObjects = new HashMap<>(); + for (ActivatedAbility ability : playableAbilities) { + if (ability.getSourceId() != null) { + + // normal card + putToPlayableObjects(playableObjects, ability.getSourceId(), ability); + + // main card - must be marked playable in GUI + Card card = game.getCard(ability.getSourceId()); + if (card != null && card.getMainCard().getId() != card.getId()) { + putToPlayableObjects(playableObjects, card.getMainCard().getId(), ability); + } + + // spell on stack - can have activated abilities, + // so mark it as playable too (users must able to clicks on stack objects) + // example: Lightning Storm + Spell spell = game.getSpell(ability.getSourceId()); + if (spell != null) { + putToPlayableObjects(playableObjects, spell.getId(), ability); + } + } + } + + // collect stats + PlayableObjectsList playableObjectsList = new PlayableObjectsList(playableObjects); + return playableObjectsList; + } + + private void putToPlayableObjects(Map> playableObjects, UUID objectId, ActivatedAbility ability) { + if (!playableObjects.containsKey(objectId)) { + playableObjects.put(objectId, new ArrayList<>()); + } + playableObjects.get(objectId).add(ability); + } + + /** + * Skip "silent" phase step when players are not allowed to cast anything. + * E.g. players can't play or cast anything during declaring attackers. + * + * @param game + * @return + */ + private boolean shouldSkipGettingPlayable(Game game) { + if (game.getStep() == null) { // happens at the start of the game + return true; + } + for (Entry phaseStep : silentPhaseSteps.entrySet()) { + if (game.getPhase() != null + && game.getPhase().getStep() != null + && phaseStep.getKey() == game.getPhase().getStep().getType()) { + if (phaseStep.getValue() == null + || phaseStep.getValue() == game.getPhase().getStep().getStepPart()) { + return true; + } + } + } + return false; + } + + /** + * Only used for AIs + * + * @param ability + * @param game + * @return + */ + @Override + public List getPlayableOptions(Ability ability, Game game) { + List options = new ArrayList<>(); + if (ability.isModal()) { + addModeOptions(options, ability, game); + } else if (!ability.getTargets().getUnchosen().isEmpty()) { + // TODO: Handle other variable costs than mana costs + if (!ability.getManaCosts().getVariableCosts().isEmpty()) { + addVariableXOptions(options, ability, 0, game); + } else { + addTargetOptions(options, ability, 0, game); + } + } else if (!ability.getCosts().getTargets().getUnchosen().isEmpty()) { + addCostTargetOptions(options, ability, 0, game); + } + + return options; + } + + private void addModeOptions(List options, Ability option, Game game) { + // TODO: Support modal spells with more than one selectable mode + for (Mode mode : option.getModes().values()) { + Ability newOption = option.copy(); + newOption.getModes().clearSelectedModes(); + newOption.getModes().addSelectedMode(mode.getId()); + newOption.getModes().setActiveMode(mode); + if (!newOption.getTargets().getUnchosen().isEmpty()) { + if (!newOption.getManaCosts().getVariableCosts().isEmpty()) { + addVariableXOptions(options, newOption, 0, game); + } else { + addTargetOptions(options, newOption, 0, game); + } + } else if (!newOption.getCosts().getTargets().getUnchosen().isEmpty()) { + addCostTargetOptions(options, newOption, 0, game); + } else { + options.add(newOption); + } + } + } + + protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) { + addTargetOptions(options, option, targetNum, game); + } + + protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { + for (Target target : option.getTargets().getUnchosen().get(targetNum).getTargetOptions(option, game)) { + Ability newOption = option.copy(); + if (target instanceof TargetAmount) { + for (UUID targetId : target.getTargets()) { + int amount = target.getTargetAmount(targetId); + newOption.getTargets().get(targetNum).addTarget(targetId, amount, newOption, game, true); + } + } else { + for (UUID targetId : target.getTargets()) { + newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); + } + } + if (targetNum < option.getTargets().size() - 2) { + addTargetOptions(options, newOption, targetNum + 1, game); + } else if (!option.getCosts().getTargets().isEmpty()) { + addCostTargetOptions(options, newOption, 0, game); + } else { + options.add(newOption); + } + } + } + + private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) { + for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(option.getSourceId(), playerId, game)) { + Ability newOption = option.copy(); + newOption.getCosts().getTargets().get(targetNum).addTarget(targetId, option, game, true); + if (targetNum < option.getCosts().getTargets().size() - 1) { + addCostTargetOptions(options, newOption, targetNum + 1, game); + } else { + options.add(newOption); + } + } + } + + @Override + public boolean isTestsMode() { + return isTestMode; + } + + @Override + public void setTestMode(boolean value) { + this.isTestMode = value; + } + + @Override + public boolean isTopCardRevealed() { + return topCardRevealed; + } + + @Override + public void setTopCardRevealed(boolean topCardRevealed) { + this.topCardRevealed = topCardRevealed; + } + + @Override + public UserData getUserData() { + return this.userData; + } + + public UserData getControllingPlayersUserData(Game game) { + if (!isGameUnderControl()) { + Player player = game.getPlayer(getTurnControlledBy()); + if (player.isHuman()) { + return player.getUserData(); + } + } + return this.userData; + } + + @Override + public void setUserData(UserData userData) { + this.userData = userData; + getManaPool().setAutoPayment(userData.isManaPoolAutomatic()); + getManaPool().setAutoPaymentRestricted(userData.isManaPoolAutomaticRestricted()); + } + + @Override + public void addAction(String action + ) { + // do nothing + } + + @Override + public int getActionCount() { + return 0; + } + + @Override + public void setAllowBadMoves(boolean allowBadMoves) { + // do nothing + } + + @Override + public boolean canPayLifeCost(Ability ability) { + if (!canPayLifeCost + && (AbilityType.ACTIVATED.equals(ability.getAbilityType()) + || AbilityType.SPELL.equals(ability.getAbilityType()))) { + return false; + } + return isLifeTotalCanChange(); + } + + @Override + public boolean getCanPayLifeCost() { + return canPayLifeCost; + } + + @Override + public void setCanPayLifeCost(boolean canPayLifeCost) { + this.canPayLifeCost = canPayLifeCost; + } + + @Override + public boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game) { + return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, source.getSourceId(), controllerId, game); + } + + @Override + public void setCanPaySacrificeCostFilter(FilterPermanent filter + ) { + this.sacrificeCostFilter = filter; + } + + @Override + public FilterPermanent getSacrificeCostFilter() { + return sacrificeCostFilter; + } + + @Override + public boolean canLoseByZeroOrLessLife() { + return loseByZeroOrLessLife; + } + + @Override + public void setLoseByZeroOrLessLife(boolean loseByZeroOrLessLife) { + this.loseByZeroOrLessLife = loseByZeroOrLessLife; + } + + @Override + public boolean canPlayCardsFromGraveyard() { + return canPlayCardsFromGraveyard; + } + + @Override + public void setPlayCardsFromGraveyard(boolean playCardsFromGraveyard) { + this.canPlayCardsFromGraveyard = playCardsFromGraveyard; + } + + @Override + public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) { + this.drawsOnOpponentsTurn = drawsOnOpponentsTurn; + } + + @Override + public boolean isDrawsOnOpponentsTurn() { + return drawsOnOpponentsTurn; + } + + @Override + public boolean autoLoseGame() { + return false; + } + + @Override + public void becomesActivePlayer() { + this.passedAllTurns = false; + this.passedUntilEndStepBeforeMyTurn = false; + this.turns++; + } + + @Override + public int getTurns() { + return turns; + } + + @Override + public int getStoredBookmark() { + return storedBookmark; + } + + @Override + public void setStoredBookmark(int storedBookmark) { + this.storedBookmark = storedBookmark; + } + + @Override + public synchronized void resetStoredBookmark(Game game) { + if (this.storedBookmark != -1) { + game.removeBookmark(this.storedBookmark); + } + setStoredBookmark(-1); + } + + @Override + public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { + if (null != game.getContinuousEffects().asThough(card.getId(), + AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) { + // two modes: look at the card or do not look and activate other abilities + String lookMessage = "Look at " + card.getIdName(); + String lookYes = "Yes, look at the card"; + String lookNo = "No, play/activate the card/ability"; + if (chooseUse(Outcome.Benefit, lookMessage, "", lookYes, lookNo, null, game)) { + Cards cards = new CardsImpl(card); + this.lookAtCards(getName() + " - " + card.getIdName() + " - " + + CardUtil.sdf.format(System.currentTimeMillis()), cards, game); + return true; + } + } + return false; + } + + @Override + public void setPriorityTimeLeft(int timeLeft + ) { + priorityTimeLeft = timeLeft; + } + + @Override + public int getPriorityTimeLeft() { + return priorityTimeLeft; + } + + @Override + public boolean hasQuit() { + return quit; + } + + @Override + public boolean hasTimerTimeout() { + return timerTimeout; + } + + @Override + public boolean hasIdleTimeout() { + return idleTimeout; + } + + @Override + public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving) { + this.reachedNextTurnAfterLeaving = reachedNextTurnAfterLeaving; + } + + @Override + public boolean hasReachedNextTurnAfterLeaving() { + return reachedNextTurnAfterLeaving; + } + + @Override + public boolean canJoinTable(Table table + ) { + return !table.userIsBanned(name); + } + + @Override + public void addCommanderId(UUID commanderId + ) { + this.commandersIds.add(commanderId); + } + + @Override + public Set getCommandersIds() { + return this.commandersIds; + } + + @Override + public boolean moveCards(Card card, Zone toZone, Ability source, Game game) { + return moveCards(card, toZone, source, game, false, false, false, null); + } + + @Override + public boolean moveCards(Card card, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { + Set cardList = new HashSet<>(); + if (card != null) { + cardList.add(card); + } + return moveCards(cardList, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); + } + + @Override + public boolean moveCards(Cards cards, Zone toZone, Ability source, Game game) { + return moveCards(cards.getCards(game), toZone, source, game); + } + + @Override + public boolean moveCards(Set cards, Zone toZone, + Ability source, Game game + ) { + return moveCards(cards, toZone, source, game, false, false, false, null); + } + + @Override + public boolean moveCards(Set cards, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { + if (cards.isEmpty()) { + return true; + } + Set successfulMovedCards = new LinkedHashSet<>(); + Zone fromZone = null; + switch (toZone) { + case GRAVEYARD: + fromZone = game.getState().getZone(cards.iterator().next().getId()); + successfulMovedCards = moveCardsToGraveyardWithInfo(cards, source, game, fromZone); + return !successfulMovedCards.isEmpty(); + case BATTLEFIELD: // new logic that does not yet add the permanents to battlefield while replacement effects are handled + List infoList = new ArrayList<>(); + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + ZoneChangeEvent event = new ZoneChangeEvent(card.getId(), source, + byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects); + infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped, source)); + } + infoList = ZonesHandler.moveCards(infoList, game, source); + for (ZoneChangeInfo info : infoList) { + Permanent permanent = game.getPermanent(info.event.getTargetId()); + if (permanent != null) { + successfulMovedCards.add(permanent); + if (!game.isSimulation()) { + Player eventPlayer = game.getPlayer(info.event.getPlayerId()); + if (eventPlayer != null && fromZone != null) { + game.informPlayers(eventPlayer.getLogName() + " puts " + + (info.faceDown ? "a card face down " : permanent.getLogName()) + " from " + + fromZone.toString().toLowerCase(Locale.ENGLISH) + " onto the Battlefield" + + CardUtil.getSourceLogName(game, source, permanent.getId())); + } + } + } + } + // TODO: must be replaced by game.getState().processAction(game), see isInUseableZoneDiesTrigger comments + // about short living LKI problem + //game.getState().processAction(game); + game.applyEffects(); + break; + case HAND: + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + boolean hideCard = fromZone == Zone.LIBRARY + || (card.isFaceDown(game) + && fromZone != Zone.STACK + && fromZone != Zone.BATTLEFIELD); + if (moveCardToHandWithInfo(card, source, game, !hideCard)) { + successfulMovedCards.add(card); + } + } + break; + case EXILED: + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + boolean withName = (fromZone == Zone.BATTLEFIELD + || fromZone == Zone.STACK) + || !card.isFaceDown(game); + if (moveCardToExileWithInfo(card, null, "", source, game, fromZone, withName)) { + successfulMovedCards.add(card); + } + } + break; + case LIBRARY: + for (Card card : cards) { + if (card instanceof Spell) { + fromZone = game.getState().getZone(((Spell) card).getSourceId()); + } else { + fromZone = game.getState().getZone(card.getId()); + } + boolean hideCard = fromZone == Zone.HAND || fromZone == Zone.LIBRARY; + if (moveCardToLibraryWithInfo(card, source, game, fromZone, true, !hideCard)) { + successfulMovedCards.add(card); + } + } + break; + case COMMAND: + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + if (moveCardToCommandWithInfo(card, source, game, fromZone)) { + successfulMovedCards.add(card); + } + } + break; + case OUTSIDE: + for (Card card : cards) { + if (card instanceof Permanent) { + game.getBattlefield().removePermanent(card.getId()); + ZoneChangeEvent event = new ZoneChangeEvent((Permanent) card, source, + byOwner ? card.getOwnerId() : getId(), Zone.BATTLEFIELD, Zone.OUTSIDE, appliedEffects); + game.fireEvent(event); + } + } + break; + default: + throw new UnsupportedOperationException("to Zone" + toZone + " not supported yet"); + } + return !successfulMovedCards.isEmpty(); + } + + @Override + public boolean moveCardsToExile(Card card, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { + Set cards = new HashSet<>(); + if (card != null) { + cards.add(card); + } + return moveCardsToExile(cards, source, game, withName, exileId, exileZoneName); + } + + @Override + public boolean moveCardsToExile(Set cards, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { + if (cards.isEmpty()) { + return true; + } + boolean result = false; + for (Card card : cards) { + Zone fromZone = game.getState().getZone(card.getId()); + result |= moveCardToExileWithInfo(card, exileId, exileZoneName, source, game, fromZone, withName); + } + return result; + } + + @Override + public boolean moveCardToHandWithInfo(Card card, Ability source, Game game, boolean withName) { + boolean result = false; + Zone fromZone = game.getState().getZone(card.getId()); + if (fromZone == Zone.BATTLEFIELD && !(card instanceof Permanent)) { + card = game.getPermanent(card.getId()); + } + if (card.moveToZone(Zone.HAND, source, game, false)) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + if (!game.isSimulation()) { + game.informPlayers(getLogName() + " puts " + + (withName ? card.getLogName() : (card.isFaceDown(game) ? "a face down card" : "a card")) + + " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' + + (card.isOwnedBy(this.getId()) ? "into their hand" : "into its owner's hand" + + CardUtil.getSourceLogName(game, source, card.getId())) + ); + } + result = true; + } + return result; + } + + @Override + public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, Game game, Zone fromZone) { + Set movedCards = new LinkedHashSet<>(); + while (!allCards.isEmpty()) { + // identify cards from one owner + Cards cards = new CardsImpl(); + UUID ownerId = null; + for (Iterator it = allCards.iterator(); it.hasNext();) { + Card card = it.next(); + if (cards.isEmpty()) { + ownerId = card.getOwnerId(); + } + if (card.isOwnedBy(ownerId)) { + it.remove(); + cards.add(card); + } + } + // move cards to graveyard in order the owner decides + if (!cards.isEmpty()) { + Player choosingPlayer = this; + if (!Objects.equals(ownerId, this.getId())) { + choosingPlayer = game.getPlayer(ownerId); + } + if (choosingPlayer == null) { + continue; + } + boolean chooseOrder = false; + if (userData.askMoveToGraveOrder()) { + if (cards.size() > 1) { + chooseOrder = choosingPlayer.chooseUse(Outcome.Neutral, + "Choose the order in which the cards go to the graveyard?", source, game); + } + } + if (chooseOrder) { + TargetCard target = new TargetCard(fromZone, + new FilterCard("card to put on the top of your graveyard (last one chosen will be topmost)")); + target.setRequired(true); + while (choosingPlayer.canRespond() && cards.size() > 1) { + choosingPlayer.chooseTarget(Outcome.Neutral, cards, target, source, game); + UUID targetObjectId = target.getFirstTarget(); + Card card = cards.get(targetObjectId, game); + cards.remove(targetObjectId); + if (card != null) { + fromZone = game.getState().getZone(card.getId()); + if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { + movedCards.add(card); + } + } + target.clearChosen(); + } + if (cards.size() == 1) { + Card card = cards.getCards(game).iterator().next(); + if (card != null && choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { + movedCards.add(card); + } + } + } else { + for (Card card : cards.getCards(game)) { + if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { + movedCards.add(card); + } + } + } + } + } + return movedCards; + } + + @Override + public boolean moveCardToGraveyardWithInfo(Card card, Ability source, Game game, Zone fromZone) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToZone(Zone.GRAVEYARD, source, game, false)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + StringBuilder sb = new StringBuilder(this.getLogName()) + .append(" puts ").append(card.getLogName()).append(' ').append(card.isCopy() ? "(Copy) " : "") + .append(fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' : ""); + if (card.isOwnedBy(getId())) { + sb.append("into their graveyard"); + } else { + sb.append("it into its owner's graveyard"); + } + sb.append(CardUtil.getSourceLogName(game, source, card.getId())); + game.informPlayers(sb.toString()); + } + result = true; + } + return result; + } + + @Override + public boolean moveCardToLibraryWithInfo(Card card, Ability source, Game game, Zone fromZone, boolean toTop, boolean withName) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToZone(Zone.LIBRARY, source, game, toTop)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + StringBuilder sb = new StringBuilder(this.getLogName()) + .append(" puts ").append(withName ? card.getLogName() : "a card").append(' '); + if (fromZone != null) { + sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); + } + sb.append("to the ").append(toTop ? "top" : "bottom"); + if (card.isOwnedBy(getId())) { + sb.append(" of their library"); + } else { + Player player = game.getPlayer(card.getOwnerId()); + if (player != null) { + sb.append(" of ").append(player.getLogName()).append("'s library"); + } + } + sb.append(CardUtil.getSourceLogName(game, source, card.getId())); + game.informPlayers(sb.toString()); + } + result = true; + } + return result; + } + + @Override + public boolean moveCardToCommandWithInfo(Card card, Ability source, Game game, Zone fromZone) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToZone(Zone.COMMAND, source, game, true)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + StringBuilder sb = new StringBuilder(this.getLogName()) + .append(" puts ").append(card.getLogName()).append(' '); + if (fromZone != null) { + sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); + } + if (card.isOwnedBy(getId())) { + sb.append(" to their command zone"); + } else { + Player player = game.getPlayer(card.getOwnerId()); + if (player != null) { + sb.append(" to ").append(player.getLogName()).append("'s command zone"); + } + } + sb.append(CardUtil.getSourceLogName(game, source, card.getId())); + game.informPlayers(sb.toString()); + } + result = true; + } + return result; + } + + @Override + public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, Ability source, Game game, Zone fromZone, boolean withName) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToExile(exileId, exileName, source, game)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard) { + // in case it's face down or name was changed by copying from other permanent + Card basicCard = game.getCard(card.getId()); + if (basicCard != null) { + card = basicCard; + } + } else if (card instanceof Spell) { + final Spell spell = (Spell) card; + if (spell.isCopy()) { + // copied spell, only remove from stack + game.getStack().remove(spell, game); + } + } + if (Zone.EXILED.equals(game.getState().getZone(card.getId()))) { // only if target zone was not replaced + game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() + + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); + } + + } + result = true; + } + return result; + } + + @Override + public Cards millCards(int toMill, Ability source, Game game) { + GameEvent event = GameEvent.getEvent(GameEvent.EventType.MILL_CARDS, getId(), source, getId(), toMill); + if (game.replaceEvent(event)) { + return new CardsImpl(); + } + Cards cards = new CardsImpl(this.getLibrary().getTopCards(game, event.getAmount())); + this.moveCards(cards, Zone.GRAVEYARD, source, game); + for (Card card : cards.getCards(game)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.MILLED_CARD, card.getId(), source, getId())); + } + return cards; + } + + @Override + public boolean hasOpponent(UUID playerToCheckId, Game game) { + return !this.getId().equals(playerToCheckId) + && game.isOpponent(this, playerToCheckId) + && getInRange().contains(playerToCheckId); + } + + @Override + public void cleanUpOnMatchEnd() { + + } + + @Override + public boolean getPassedAllTurns() { + return passedAllTurns; + } + + @Override + public boolean getPassedUntilNextMain() { + return passedUntilNextMain; + } + + @Override + public boolean getPassedUntilEndOfTurn() { + return passedUntilEndOfTurn; + } + + @Override + public boolean getPassedTurn() { + return passedTurn; + } + + @Override + public boolean getPassedUntilStackResolved() { + return passedUntilStackResolved; + } + + @Override + public boolean getPassedUntilEndStepBeforeMyTurn() { + return passedUntilEndStepBeforeMyTurn; + } + + @Override + public AbilityType getJustActivatedType() { + return justActivatedType; + } + + @Override + public void setJustActivatedType(AbilityType justActivatedType + ) { + this.justActivatedType = justActivatedType; + } + + @Override + public void revokePermissionToSeeHandCards() { + usersAllowedToSeeHandCards.clear(); + } + + @Override + public void addPermissionToShowHandCards(UUID watcherUserId + ) { + usersAllowedToSeeHandCards.add(watcherUserId); + } + + @Override + public boolean isPlayerAllowedToRequestHand(UUID gameId, UUID requesterPlayerId) { + return userData.isAllowRequestHandToPlayer(gameId, requesterPlayerId); + } + + @Override + public void addPlayerToRequestedHandList(UUID gameId, UUID requesterPlayerId) { + userData.addPlayerToRequestedHandList(gameId, requesterPlayerId); + } + + @Override + public boolean hasUserPermissionToSeeHand(UUID userId + ) { + return usersAllowedToSeeHandCards.contains(userId); + } + + @Override + public Set getUsersAllowedToSeeHandCards() { + return usersAllowedToSeeHandCards; + } + + @Override + public void setMatchPlayer(MatchPlayer matchPlayer + ) { + this.matchPlayer = matchPlayer; + } + + @Override + public MatchPlayer getMatchPlayer() { + return matchPlayer; + } + + @Override + public void abortReset() { + abort = false; + } + + @Override + public void signalPlayerConcede() { + + } + + @Override + public boolean scry(int value, Ability source, Game game) { + GameEvent event = new GameEvent(GameEvent.EventType.SCRY, getId(), source, getId(), value, true); + if (game.replaceEvent(event)) { + return false; + } + game.informPlayers(getLogName() + " scries " + event.getAmount() + CardUtil.getSourceLogName(game, source)); + Cards cards = new CardsImpl(); + cards.addAll(getLibrary().getTopCards(game, event.getAmount())); + if (!cards.isEmpty()) { + TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, + new FilterCard("card" + (cards.size() == 1 ? "" : "s") + + " to PUT on the BOTTOM of your library (Scry)")); + chooseTarget(Outcome.Benefit, cards, target, source, game); + putCardsOnBottomOfLibrary(new CardsImpl(target.getTargets()), game, source, true); + cards.removeAll(target.getTargets()); + putCardsOnTopOfLibrary(cards, game, source, true); + } + game.fireEvent(new GameEvent(GameEvent.EventType.SCRIED, getId(), source, getId(), event.getAmount(), true)); + return true; + } + + @Override + public boolean surveil(int value, Ability source, Game game) { + GameEvent event = new GameEvent(GameEvent.EventType.SURVEIL, getId(), source, getId(), value, true); + if (game.replaceEvent(event)) { + return false; + } + game.informPlayers(getLogName() + " surveils " + event.getAmount() + CardUtil.getSourceLogName(game, source)); + Cards cards = new CardsImpl(); + cards.addAll(getLibrary().getTopCards(game, event.getAmount())); + if (!cards.isEmpty()) { + TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, + new FilterCard("cards to PUT into your GRAVEYARD (Surveil)")); + chooseTarget(Outcome.Benefit, cards, target, source, game); + moveCards(new CardsImpl(target.getTargets()), Zone.GRAVEYARD, source, game); + cards.removeAll(target.getTargets()); + putCardsOnTopOfLibrary(cards, game, source, true); + } + game.fireEvent(new GameEvent(GameEvent.EventType.SURVEILED, getId(), source, getId(), event.getAmount(), true)); + return true; + } + + @Override + public boolean addTargets(Ability ability, Game game + ) { + // only used for TestPlayer to preSet Targets + return true; + } + + @Override + public String getHistory() { + return "no available"; + } + + @Override + public boolean hasDesignation(DesignationType designationName) { + for (Designation designation : designations) { + if (designation.getDesignationType().equals(designationName)) { + return true; + } + } + return false; + } + + @Override + public void addDesignation(Designation designation) { + if (!designation.isUnique() || !this.hasDesignation(designation.getDesignationType())) { + designations.add(designation); + } + } + + @Override + public List getDesignations() { + return designations; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Player obj = (Player) o; + if (this.getId() == null || obj.getId() == null) { + return false; + } + + return this.getId().equals(obj.getId()); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 89 * hash + Objects.hashCode(this.playerId); + return hash; + } + + @Override + public void addPhyrexianToColors(FilterMana colors) { + if (phyrexianColors == null) { + phyrexianColors = colors.copy(); + } else { + if (colors.isWhite()) { + this.phyrexianColors.setWhite(true); + } + if (colors.isBlue()) { + this.phyrexianColors.setBlue(true); + } + if (colors.isBlack()) { + this.phyrexianColors.setBlack(true); + } + if (colors.isRed()) { + this.phyrexianColors.setRed(true); + } + if (colors.isGreen()) { + this.phyrexianColors.setGreen(true); + } + } + } + + @Override + public FilterMana getPhyrexianColors() { + return this.phyrexianColors; + } + + @Override + public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { + return card.getSpellAbility(); + } + + @Override + public String toString() { + return getName() + " (" + super.getClass().getSimpleName() + ")"; + } +} From bcb42b8f46f45b896f05f0335ca79ba49776d20a Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 22 Sep 2021 21:20:55 -0400 Subject: [PATCH 173/231] simplified ObjectSourcePlayerPredicate interface --- Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java | 2 +- Mage.Sets/src/mage/cards/a/AuraGraft.java | 4 ++-- Mage.Sets/src/mage/cards/b/BarteredCow.java | 2 +- Mage.Sets/src/mage/cards/b/Bioshift.java | 2 +- Mage.Sets/src/mage/cards/b/BoreasCharger.java | 2 +- Mage.Sets/src/mage/cards/c/CeruleanDrake.java | 2 +- Mage.Sets/src/mage/cards/c/ConduitOfRuin.java | 2 +- Mage.Sets/src/mage/cards/c/CrownOfDoom.java | 2 +- Mage.Sets/src/mage/cards/c/CullingScales.java | 2 +- Mage.Sets/src/mage/cards/d/DawnCharm.java | 2 +- Mage.Sets/src/mage/cards/d/DevoutHarpist.java | 2 +- Mage.Sets/src/mage/cards/d/DiamondKnight.java | 2 +- Mage.Sets/src/mage/cards/d/DiseasedVermin.java | 2 +- Mage.Sets/src/mage/cards/d/DreadhordeArcanist.java | 2 +- Mage.Sets/src/mage/cards/e/EarthshakerKhenra.java | 2 +- Mage.Sets/src/mage/cards/e/EliteHeadhunter.java | 2 +- Mage.Sets/src/mage/cards/e/EnchantmentAlteration.java | 2 +- Mage.Sets/src/mage/cards/e/EvilTwin.java | 2 +- Mage.Sets/src/mage/cards/f/FalseOrders.java | 2 +- Mage.Sets/src/mage/cards/f/FireAndBrimstone.java | 2 +- Mage.Sets/src/mage/cards/f/FiresOfInvention.java | 2 +- Mage.Sets/src/mage/cards/f/FirjasRetribution.java | 2 +- Mage.Sets/src/mage/cards/f/FlameSweep.java | 2 +- Mage.Sets/src/mage/cards/f/FrostpyreArcanist.java | 2 +- Mage.Sets/src/mage/cards/g/GomaFadaVanguard.java | 2 +- Mage.Sets/src/mage/cards/g/GyrusWakerOfCorpses.java | 2 +- Mage.Sets/src/mage/cards/h/HaktosTheUnscarred.java | 2 +- Mage.Sets/src/mage/cards/h/HalvarGodOfBattle.java | 2 +- Mage.Sets/src/mage/cards/h/HinderingLight.java | 2 +- Mage.Sets/src/mage/cards/h/HopeOfGhirapur.java | 2 +- Mage.Sets/src/mage/cards/j/JeweledTorque.java | 2 +- Mage.Sets/src/mage/cards/k/KeeperOfTheDead.java | 2 +- Mage.Sets/src/mage/cards/k/KeeperOfTheFlame.java | 2 +- Mage.Sets/src/mage/cards/k/KeeperOfTheMind.java | 2 +- Mage.Sets/src/mage/cards/k/KitesailSkirmisher.java | 2 +- Mage.Sets/src/mage/cards/k/KumenasSpeaker.java | 2 +- Mage.Sets/src/mage/cards/l/Legerdemain.java | 2 +- Mage.Sets/src/mage/cards/m/MalevolentNoble.java | 2 +- Mage.Sets/src/mage/cards/m/MartialImpetus.java | 2 +- Mage.Sets/src/mage/cards/m/MirrorSheen.java | 2 +- Mage.Sets/src/mage/cards/m/Mistfolk.java | 2 +- Mage.Sets/src/mage/cards/m/MistformWarchief.java | 2 +- Mage.Sets/src/mage/cards/m/MuckDrubb.java | 2 +- Mage.Sets/src/mage/cards/o/OathOfDruids.java | 2 +- Mage.Sets/src/mage/cards/o/OathOfGhouls.java | 2 +- Mage.Sets/src/mage/cards/o/OathOfLieges.java | 2 +- Mage.Sets/src/mage/cards/o/OathOfMages.java | 2 +- Mage.Sets/src/mage/cards/o/OathOfScholars.java | 2 +- Mage.Sets/src/mage/cards/o/OldManOfTheSea.java | 2 +- Mage.Sets/src/mage/cards/p/PersonalEnergyShield.java | 2 +- Mage.Sets/src/mage/cards/p/PsychicRebuttal.java | 2 +- Mage.Sets/src/mage/cards/p/Pyramids.java | 2 +- Mage.Sets/src/mage/cards/r/Radiate.java | 4 ++-- Mage.Sets/src/mage/cards/r/RebbecArchitectOfAscension.java | 2 +- Mage.Sets/src/mage/cards/r/RemoveEnchantments.java | 2 +- Mage.Sets/src/mage/cards/r/Ricochet.java | 2 +- Mage.Sets/src/mage/cards/s/SageOfTheBeyond.java | 2 +- Mage.Sets/src/mage/cards/s/SalvageTrader.java | 2 +- Mage.Sets/src/mage/cards/s/SavaenElves.java | 2 +- Mage.Sets/src/mage/cards/s/ShacklesOfTreachery.java | 2 +- Mage.Sets/src/mage/cards/s/ShellOfTheLastKappa.java | 2 +- Mage.Sets/src/mage/cards/s/SilverWyvern.java | 2 +- Mage.Sets/src/mage/cards/s/SilverquillSilencer.java | 2 +- Mage.Sets/src/mage/cards/s/SimicGuildmage.java | 2 +- Mage.Sets/src/mage/cards/s/SkullportMerchant.java | 2 +- Mage.Sets/src/mage/cards/s/SoulShatter.java | 2 +- Mage.Sets/src/mage/cards/s/SpectralDeluge.java | 2 +- Mage.Sets/src/mage/cards/s/SpellstutterSprite.java | 2 +- Mage.Sets/src/mage/cards/s/StumpsquallHydra.java | 2 +- Mage.Sets/src/mage/cards/t/TelimTorsEdict.java | 2 +- Mage.Sets/src/mage/cards/t/ThunderkinAwakener.java | 2 +- Mage.Sets/src/mage/cards/t/Torchling.java | 2 +- Mage.Sets/src/mage/cards/t/TravelersCloak.java | 2 +- Mage.Sets/src/mage/cards/u/UnlivingPsychopath.java | 2 +- Mage.Sets/src/mage/cards/v/VedalkenShackles.java | 2 +- Mage.Sets/src/mage/cards/v/VineGecko.java | 2 +- Mage.Sets/src/mage/cards/w/WickedAkuba.java | 2 +- Mage.Sets/src/mage/cards/y/YasovaDragonclaw.java | 2 +- .../src/main/java/mage/abilities/keyword/MentorAbility.java | 2 +- Mage/src/main/java/mage/constants/TargetController.java | 6 +++--- Mage/src/main/java/mage/filter/FilterCard.java | 2 +- Mage/src/main/java/mage/filter/FilterPermanent.java | 3 +-- Mage/src/main/java/mage/filter/FilterPlayer.java | 2 +- Mage/src/main/java/mage/filter/FilterStackObject.java | 4 +--- .../java/mage/filter/common/FilterPermanentOrPlayer.java | 1 - .../mage/filter/predicate/ObjectSourcePlayerPredicate.java | 2 +- .../filter/predicate/card/CardOnTopOfLibraryPredicate.java | 2 +- .../predicate/card/DefendingPlayerOwnsCardPredicate.java | 2 +- .../filter/predicate/mageobject/AnotherCardPredicate.java | 2 +- .../mage/filter/predicate/mageobject/AnotherPredicate.java | 2 +- .../filter/predicate/mageobject/ChosenColorPredicate.java | 2 +- .../filter/predicate/mageobject/ChosenSubtypePredicate.java | 2 +- .../mageobject/SharesColorWithSourcePredicate.java | 2 +- .../predicate/mageobject/TargetsOnlyOnePlayerPredicate.java | 2 +- .../predicate/mageobject/TargetsPermanentPredicate.java | 2 +- .../filter/predicate/mageobject/TargetsPlayerPredicate.java | 2 +- .../mage/filter/predicate/other/AnotherTargetPredicate.java | 2 +- .../predicate/other/DamagedPlayerThisTurnPredicate.java | 2 +- .../filter/predicate/other/PlayerCanGainLifePredicate.java | 2 +- .../predicate/permanent/AnotherEnchantedPredicate.java | 2 +- .../permanent/AttachedToControlledPermanentPredicate.java | 2 +- .../permanent/BlockingOrBlockedBySourcePredicate.java | 2 +- .../permanent/DefendingPlayerControlsPredicate.java | 2 +- .../permanent/GreatestPowerControlledPredicate.java | 2 +- 104 files changed, 107 insertions(+), 111 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java b/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java index 26d165568c4..a7583635ab4 100644 --- a/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java +++ b/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java @@ -103,7 +103,7 @@ class AkiriFearlessVoyagerTriggeredAbility extends TriggeredAbilityImpl { class AkiriFearlessVoyagerEffect extends OneShotEffect { - private static enum AkiriFearlessVoyagerPredicate implements ObjectSourcePlayerPredicate> { + private static enum AkiriFearlessVoyagerPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/a/AuraGraft.java b/Mage.Sets/src/mage/cards/a/AuraGraft.java index 519b2d25dc3..27fad23790d 100644 --- a/Mage.Sets/src/mage/cards/a/AuraGraft.java +++ b/Mage.Sets/src/mage/cards/a/AuraGraft.java @@ -54,7 +54,7 @@ public final class AuraGraft extends CardImpl { } } -class AttachedToPermanentPredicate implements ObjectSourcePlayerPredicate> { +class AttachedToPermanentPredicate implements ObjectSourcePlayerPredicate { public AttachedToPermanentPredicate() { super(); @@ -66,7 +66,7 @@ class AttachedToPermanentPredicate implements ObjectSourcePlayerPredicate> { +class PermanentCanBeAttachedToPredicate implements ObjectSourcePlayerPredicate { protected Permanent aura; diff --git a/Mage.Sets/src/mage/cards/b/BarteredCow.java b/Mage.Sets/src/mage/cards/b/BarteredCow.java index 9675188862e..707e63fa07a 100644 --- a/Mage.Sets/src/mage/cards/b/BarteredCow.java +++ b/Mage.Sets/src/mage/cards/b/BarteredCow.java @@ -56,7 +56,7 @@ public final class BarteredCow extends CardImpl { } } -enum BarteredCowPredicate implements ObjectSourcePlayerPredicate> { +enum BarteredCowPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/b/Bioshift.java b/Mage.Sets/src/mage/cards/b/Bioshift.java index 9725347d4c7..d8b750d9ac7 100644 --- a/Mage.Sets/src/mage/cards/b/Bioshift.java +++ b/Mage.Sets/src/mage/cards/b/Bioshift.java @@ -100,7 +100,7 @@ class MoveCounterFromTargetToTargetEffect extends OneShotEffect { } } -class SameControllerPredicate implements ObjectSourcePlayerPredicate> { +class SameControllerPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/b/BoreasCharger.java b/Mage.Sets/src/mage/cards/b/BoreasCharger.java index fcc80133551..7169a5d1240 100644 --- a/Mage.Sets/src/mage/cards/b/BoreasCharger.java +++ b/Mage.Sets/src/mage/cards/b/BoreasCharger.java @@ -138,7 +138,7 @@ class BoreasChargerEffect extends OneShotEffect { } } -class BoreasChargerPredicate implements ObjectSourcePlayerPredicate> { +class BoreasChargerPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/c/CeruleanDrake.java b/Mage.Sets/src/mage/cards/c/CeruleanDrake.java index f4c831dce91..c65cdd08337 100644 --- a/Mage.Sets/src/mage/cards/c/CeruleanDrake.java +++ b/Mage.Sets/src/mage/cards/c/CeruleanDrake.java @@ -61,7 +61,7 @@ public final class CeruleanDrake extends CardImpl { } } -enum CeruleanDrakePredicate implements ObjectSourcePlayerPredicate> { +enum CeruleanDrakePredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/c/ConduitOfRuin.java b/Mage.Sets/src/mage/cards/c/ConduitOfRuin.java index dfd4a39e22a..f9e79a610da 100644 --- a/Mage.Sets/src/mage/cards/c/ConduitOfRuin.java +++ b/Mage.Sets/src/mage/cards/c/ConduitOfRuin.java @@ -100,7 +100,7 @@ class ConduitOfRuinWatcher extends Watcher { } } -class FirstCastCreatureSpellPredicate implements ObjectSourcePlayerPredicate> { +class FirstCastCreatureSpellPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/c/CrownOfDoom.java b/Mage.Sets/src/mage/cards/c/CrownOfDoom.java index 295c2a27e2b..6d6b2669340 100644 --- a/Mage.Sets/src/mage/cards/c/CrownOfDoom.java +++ b/Mage.Sets/src/mage/cards/c/CrownOfDoom.java @@ -72,7 +72,7 @@ public final class CrownOfDoom extends CardImpl { } } -enum CrownOfDoomPredicate implements ObjectSourcePlayerPredicate> { +enum CrownOfDoomPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/c/CullingScales.java b/Mage.Sets/src/mage/cards/c/CullingScales.java index 0d3922d4a01..9bddf1d536c 100644 --- a/Mage.Sets/src/mage/cards/c/CullingScales.java +++ b/Mage.Sets/src/mage/cards/c/CullingScales.java @@ -52,7 +52,7 @@ public final class CullingScales extends CardImpl { } -class HasLowestCMCAmongstNonlandPermanentsPredicate implements ObjectSourcePlayerPredicate> { +class HasLowestCMCAmongstNonlandPermanentsPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/d/DawnCharm.java b/Mage.Sets/src/mage/cards/d/DawnCharm.java index db5b065bd31..76e15531189 100644 --- a/Mage.Sets/src/mage/cards/d/DawnCharm.java +++ b/Mage.Sets/src/mage/cards/d/DawnCharm.java @@ -59,7 +59,7 @@ public final class DawnCharm extends CardImpl { } } -class DawnCharmPredicate implements ObjectSourcePlayerPredicate> { +class DawnCharmPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/d/DevoutHarpist.java b/Mage.Sets/src/mage/cards/d/DevoutHarpist.java index 1dbd8aebcc3..0f6c9a2ca2d 100644 --- a/Mage.Sets/src/mage/cards/d/DevoutHarpist.java +++ b/Mage.Sets/src/mage/cards/d/DevoutHarpist.java @@ -56,7 +56,7 @@ public final class DevoutHarpist extends CardImpl { } -class DevoutHarpistPredicate implements ObjectSourcePlayerPredicate> { +class DevoutHarpistPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { Permanent attachment = input.getObject(); diff --git a/Mage.Sets/src/mage/cards/d/DiamondKnight.java b/Mage.Sets/src/mage/cards/d/DiamondKnight.java index 406854098e5..d1e089376ba 100644 --- a/Mage.Sets/src/mage/cards/d/DiamondKnight.java +++ b/Mage.Sets/src/mage/cards/d/DiamondKnight.java @@ -59,7 +59,7 @@ public final class DiamondKnight extends CardImpl { } } -enum DiamondKnightPredicate implements ObjectSourcePlayerPredicate> { +enum DiamondKnightPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/d/DiseasedVermin.java b/Mage.Sets/src/mage/cards/d/DiseasedVermin.java index efc31859241..ad0957bafaf 100644 --- a/Mage.Sets/src/mage/cards/d/DiseasedVermin.java +++ b/Mage.Sets/src/mage/cards/d/DiseasedVermin.java @@ -114,7 +114,7 @@ class DiseasedVerminEffect extends OneShotEffect { } } -class DiseasedVerminPredicate implements ObjectSourcePlayerPredicate> { +class DiseasedVerminPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/d/DreadhordeArcanist.java b/Mage.Sets/src/mage/cards/d/DreadhordeArcanist.java index 8ba2508b57d..58c6deb298f 100644 --- a/Mage.Sets/src/mage/cards/d/DreadhordeArcanist.java +++ b/Mage.Sets/src/mage/cards/d/DreadhordeArcanist.java @@ -69,7 +69,7 @@ public final class DreadhordeArcanist extends CardImpl { } } -enum DreadhordeArcanistPredicate implements ObjectSourcePlayerPredicate> { +enum DreadhordeArcanistPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/e/EarthshakerKhenra.java b/Mage.Sets/src/mage/cards/e/EarthshakerKhenra.java index 126accce1b5..41343176fdb 100644 --- a/Mage.Sets/src/mage/cards/e/EarthshakerKhenra.java +++ b/Mage.Sets/src/mage/cards/e/EarthshakerKhenra.java @@ -68,7 +68,7 @@ public final class EarthshakerKhenra extends CardImpl { } } -enum EarthshakerKhenraPredicate implements ObjectSourcePlayerPredicate> { +enum EarthshakerKhenraPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/e/EliteHeadhunter.java b/Mage.Sets/src/mage/cards/e/EliteHeadhunter.java index d06d5be2602..c28c80af80d 100644 --- a/Mage.Sets/src/mage/cards/e/EliteHeadhunter.java +++ b/Mage.Sets/src/mage/cards/e/EliteHeadhunter.java @@ -63,7 +63,7 @@ public final class EliteHeadhunter extends CardImpl { } } -enum EliteHeadhunterPredicate implements ObjectSourcePlayerPredicate> { +enum EliteHeadhunterPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/e/EnchantmentAlteration.java b/Mage.Sets/src/mage/cards/e/EnchantmentAlteration.java index 2b65428decd..1af558de01c 100644 --- a/Mage.Sets/src/mage/cards/e/EnchantmentAlteration.java +++ b/Mage.Sets/src/mage/cards/e/EnchantmentAlteration.java @@ -62,7 +62,7 @@ public final class EnchantmentAlteration extends CardImpl { } -class SharesEnchantedCardTypePredicate implements ObjectSourcePlayerPredicate> { +class SharesEnchantedCardTypePredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/e/EvilTwin.java b/Mage.Sets/src/mage/cards/e/EvilTwin.java index 047e18248d8..c2f531b9f34 100644 --- a/Mage.Sets/src/mage/cards/e/EvilTwin.java +++ b/Mage.Sets/src/mage/cards/e/EvilTwin.java @@ -75,7 +75,7 @@ class EvilTwinCopyApplier extends CopyApplier { } -class EvilTwinPredicate implements ObjectSourcePlayerPredicate> { +class EvilTwinPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/f/FalseOrders.java b/Mage.Sets/src/mage/cards/f/FalseOrders.java index f788ead8b07..8b4b1cf5bed 100644 --- a/Mage.Sets/src/mage/cards/f/FalseOrders.java +++ b/Mage.Sets/src/mage/cards/f/FalseOrders.java @@ -64,7 +64,7 @@ public final class FalseOrders extends CardImpl { } -enum FalseOrdersDefendingPlayerControlsPredicate implements ObjectSourcePlayerPredicate> { +enum FalseOrdersDefendingPlayerControlsPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/f/FireAndBrimstone.java b/Mage.Sets/src/mage/cards/f/FireAndBrimstone.java index 6dc7fe7f3d7..f7258a53ac3 100644 --- a/Mage.Sets/src/mage/cards/f/FireAndBrimstone.java +++ b/Mage.Sets/src/mage/cards/f/FireAndBrimstone.java @@ -45,7 +45,7 @@ public final class FireAndBrimstone extends CardImpl { } } -enum FireAndBrimstonePredicate implements ObjectSourcePlayerPredicate> { +enum FireAndBrimstonePredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/f/FiresOfInvention.java b/Mage.Sets/src/mage/cards/f/FiresOfInvention.java index 2383d18bfcb..7cc79d35271 100644 --- a/Mage.Sets/src/mage/cards/f/FiresOfInvention.java +++ b/Mage.Sets/src/mage/cards/f/FiresOfInvention.java @@ -52,7 +52,7 @@ public final class FiresOfInvention extends CardImpl { } } -enum FiresOfInventionPredicate implements ObjectSourcePlayerPredicate> { +enum FiresOfInventionPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/f/FirjasRetribution.java b/Mage.Sets/src/mage/cards/f/FirjasRetribution.java index 56f0adbfbfb..d4ea16d5d2f 100644 --- a/Mage.Sets/src/mage/cards/f/FirjasRetribution.java +++ b/Mage.Sets/src/mage/cards/f/FirjasRetribution.java @@ -75,7 +75,7 @@ public final class FirjasRetribution extends CardImpl { } } -enum FirjasRetributionPredicate implements ObjectSourcePlayerPredicate> { +enum FirjasRetributionPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/f/FlameSweep.java b/Mage.Sets/src/mage/cards/f/FlameSweep.java index e40d1c9762e..801cfed2139 100644 --- a/Mage.Sets/src/mage/cards/f/FlameSweep.java +++ b/Mage.Sets/src/mage/cards/f/FlameSweep.java @@ -43,7 +43,7 @@ public final class FlameSweep extends CardImpl { } } -enum FlameSweepPredicate implements ObjectSourcePlayerPredicate> { +enum FlameSweepPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/f/FrostpyreArcanist.java b/Mage.Sets/src/mage/cards/f/FrostpyreArcanist.java index f17f0b31a17..e6b0102c6aa 100644 --- a/Mage.Sets/src/mage/cards/f/FrostpyreArcanist.java +++ b/Mage.Sets/src/mage/cards/f/FrostpyreArcanist.java @@ -79,7 +79,7 @@ public final class FrostpyreArcanist extends CardImpl { } } -enum FrostpyreArcanistPredicate implements ObjectSourcePlayerPredicate> { +enum FrostpyreArcanistPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/g/GomaFadaVanguard.java b/Mage.Sets/src/mage/cards/g/GomaFadaVanguard.java index 725ed0fd9a8..ae5bb8e7cf6 100644 --- a/Mage.Sets/src/mage/cards/g/GomaFadaVanguard.java +++ b/Mage.Sets/src/mage/cards/g/GomaFadaVanguard.java @@ -66,7 +66,7 @@ public final class GomaFadaVanguard extends CardImpl { } } -enum GomaFadaVanguardPredicate implements ObjectSourcePlayerPredicate> { +enum GomaFadaVanguardPredicate implements ObjectSourcePlayerPredicate { instance; private static final FilterPermanent filter = new FilterPermanent(SubType.WARRIOR, ""); diff --git a/Mage.Sets/src/mage/cards/g/GyrusWakerOfCorpses.java b/Mage.Sets/src/mage/cards/g/GyrusWakerOfCorpses.java index 48dc31882cd..28f936bdeb6 100644 --- a/Mage.Sets/src/mage/cards/g/GyrusWakerOfCorpses.java +++ b/Mage.Sets/src/mage/cards/g/GyrusWakerOfCorpses.java @@ -105,7 +105,7 @@ class GyrusWakerOfCorpsesEffect extends OneShotEffect { } } -class GyrusWakerOfCorpsesPowerLessThanSourcePredicate implements ObjectSourcePlayerPredicate> { +class GyrusWakerOfCorpsesPowerLessThanSourcePredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/h/HaktosTheUnscarred.java b/Mage.Sets/src/mage/cards/h/HaktosTheUnscarred.java index 3a585e8ee91..dd5a6e39c14 100644 --- a/Mage.Sets/src/mage/cards/h/HaktosTheUnscarred.java +++ b/Mage.Sets/src/mage/cards/h/HaktosTheUnscarred.java @@ -101,7 +101,7 @@ class HaktosTheUnscarredChooseEffect extends OneShotEffect { } } -enum HaktosTheUnscarredPredicate implements ObjectSourcePlayerPredicate> { +enum HaktosTheUnscarredPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/h/HalvarGodOfBattle.java b/Mage.Sets/src/mage/cards/h/HalvarGodOfBattle.java index 4d2513cc5c0..d2e1568137b 100644 --- a/Mage.Sets/src/mage/cards/h/HalvarGodOfBattle.java +++ b/Mage.Sets/src/mage/cards/h/HalvarGodOfBattle.java @@ -181,7 +181,7 @@ class SwordOfTheRealmsEffect extends OneShotEffect { } } -class HalvarGodOfBattlePredicate implements ObjectSourcePlayerPredicate> { +class HalvarGodOfBattlePredicate implements ObjectSourcePlayerPredicate { private final FilterPermanent filter; diff --git a/Mage.Sets/src/mage/cards/h/HinderingLight.java b/Mage.Sets/src/mage/cards/h/HinderingLight.java index 21838f6aa40..f0589a07876 100644 --- a/Mage.Sets/src/mage/cards/h/HinderingLight.java +++ b/Mage.Sets/src/mage/cards/h/HinderingLight.java @@ -49,7 +49,7 @@ public final class HinderingLight extends CardImpl { } } -class HinderingLightPredicate implements ObjectSourcePlayerPredicate> { +class HinderingLightPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/h/HopeOfGhirapur.java b/Mage.Sets/src/mage/cards/h/HopeOfGhirapur.java index f9a870d5b43..9a8fa224708 100644 --- a/Mage.Sets/src/mage/cards/h/HopeOfGhirapur.java +++ b/Mage.Sets/src/mage/cards/h/HopeOfGhirapur.java @@ -114,7 +114,7 @@ class HopeOfGhirapurCantCastEffect extends ContinuousRuleModifyingEffectImpl { } } -class HopeOfGhirapurPlayerLostLifePredicate implements ObjectSourcePlayerPredicate> { +class HopeOfGhirapurPlayerLostLifePredicate implements ObjectSourcePlayerPredicate { public HopeOfGhirapurPlayerLostLifePredicate() { } diff --git a/Mage.Sets/src/mage/cards/j/JeweledTorque.java b/Mage.Sets/src/mage/cards/j/JeweledTorque.java index 2d3e25c715d..ad295d7912f 100644 --- a/Mage.Sets/src/mage/cards/j/JeweledTorque.java +++ b/Mage.Sets/src/mage/cards/j/JeweledTorque.java @@ -56,7 +56,7 @@ public final class JeweledTorque extends CardImpl { } } -enum JeweledTorquePredicate implements ObjectSourcePlayerPredicate> { +enum JeweledTorquePredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/k/KeeperOfTheDead.java b/Mage.Sets/src/mage/cards/k/KeeperOfTheDead.java index 50249cf421f..387a3a2442d 100644 --- a/Mage.Sets/src/mage/cards/k/KeeperOfTheDead.java +++ b/Mage.Sets/src/mage/cards/k/KeeperOfTheDead.java @@ -74,7 +74,7 @@ public final class KeeperOfTheDead extends CardImpl { } } -class KeeperOfDeadPredicate implements ObjectSourcePlayerPredicate> { +class KeeperOfDeadPredicate implements ObjectSourcePlayerPredicate { private static final FilterCard filter = new FilterCard("creature cards"); diff --git a/Mage.Sets/src/mage/cards/k/KeeperOfTheFlame.java b/Mage.Sets/src/mage/cards/k/KeeperOfTheFlame.java index 9e5cfb6ec3a..6c1d1015ae9 100644 --- a/Mage.Sets/src/mage/cards/k/KeeperOfTheFlame.java +++ b/Mage.Sets/src/mage/cards/k/KeeperOfTheFlame.java @@ -58,7 +58,7 @@ public final class KeeperOfTheFlame extends CardImpl { } } -class KeeperOfTheFlamePredicate implements ObjectSourcePlayerPredicate> { +class KeeperOfTheFlamePredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/k/KeeperOfTheMind.java b/Mage.Sets/src/mage/cards/k/KeeperOfTheMind.java index fb3779ac291..3e943da7277 100644 --- a/Mage.Sets/src/mage/cards/k/KeeperOfTheMind.java +++ b/Mage.Sets/src/mage/cards/k/KeeperOfTheMind.java @@ -102,7 +102,7 @@ enum KeeperOfTheMindAdjuster implements TargetAdjuster { } } -class KeeperOfTheMindPredicate implements ObjectSourcePlayerPredicate> { +class KeeperOfTheMindPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/k/KitesailSkirmisher.java b/Mage.Sets/src/mage/cards/k/KitesailSkirmisher.java index 38e8c6a1767..87ce26c7c37 100644 --- a/Mage.Sets/src/mage/cards/k/KitesailSkirmisher.java +++ b/Mage.Sets/src/mage/cards/k/KitesailSkirmisher.java @@ -69,7 +69,7 @@ public final class KitesailSkirmisher extends CardImpl { } } -enum KitesailSkirmisherPredicate implements ObjectSourcePlayerPredicate> { +enum KitesailSkirmisherPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/k/KumenasSpeaker.java b/Mage.Sets/src/mage/cards/k/KumenasSpeaker.java index 9626941ed44..0e3521712e5 100644 --- a/Mage.Sets/src/mage/cards/k/KumenasSpeaker.java +++ b/Mage.Sets/src/mage/cards/k/KumenasSpeaker.java @@ -48,7 +48,7 @@ public final class KumenasSpeaker extends CardImpl { } } -enum KumenasSpeakerPredicate implements ObjectSourcePlayerPredicate> { +enum KumenasSpeakerPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/l/Legerdemain.java b/Mage.Sets/src/mage/cards/l/Legerdemain.java index 89cff853c42..bf3f729578f 100644 --- a/Mage.Sets/src/mage/cards/l/Legerdemain.java +++ b/Mage.Sets/src/mage/cards/l/Legerdemain.java @@ -55,7 +55,7 @@ public final class Legerdemain extends CardImpl { } } -class SharesTypePredicate implements ObjectSourcePlayerPredicate> { +class SharesTypePredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/m/MalevolentNoble.java b/Mage.Sets/src/mage/cards/m/MalevolentNoble.java index 611ecf4f3b9..71c8dec34a5 100644 --- a/Mage.Sets/src/mage/cards/m/MalevolentNoble.java +++ b/Mage.Sets/src/mage/cards/m/MalevolentNoble.java @@ -58,7 +58,7 @@ public final class MalevolentNoble extends CardImpl { } } -enum MalevolentNoblePredicate implements ObjectSourcePlayerPredicate> { +enum MalevolentNoblePredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/m/MartialImpetus.java b/Mage.Sets/src/mage/cards/m/MartialImpetus.java index e251becd38c..a57193f549a 100644 --- a/Mage.Sets/src/mage/cards/m/MartialImpetus.java +++ b/Mage.Sets/src/mage/cards/m/MartialImpetus.java @@ -64,7 +64,7 @@ public final class MartialImpetus extends CardImpl { } } -enum MartialImpetusPredicate implements ObjectSourcePlayerPredicate> { +enum MartialImpetusPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/m/MirrorSheen.java b/Mage.Sets/src/mage/cards/m/MirrorSheen.java index e658e9a7fe0..c77f0049e60 100644 --- a/Mage.Sets/src/mage/cards/m/MirrorSheen.java +++ b/Mage.Sets/src/mage/cards/m/MirrorSheen.java @@ -53,7 +53,7 @@ public final class MirrorSheen extends CardImpl { } } -class TargetYouPredicate implements ObjectSourcePlayerPredicate> { +class TargetYouPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/m/Mistfolk.java b/Mage.Sets/src/mage/cards/m/Mistfolk.java index 0a6e5a971a1..f4b6c58b68b 100644 --- a/Mage.Sets/src/mage/cards/m/Mistfolk.java +++ b/Mage.Sets/src/mage/cards/m/Mistfolk.java @@ -60,7 +60,7 @@ public final class Mistfolk extends CardImpl { } } -enum MistfolkPredicate implements ObjectSourcePlayerPredicate> { +enum MistfolkPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/m/MistformWarchief.java b/Mage.Sets/src/mage/cards/m/MistformWarchief.java index 4d71ee79c51..bc6260c717f 100644 --- a/Mage.Sets/src/mage/cards/m/MistformWarchief.java +++ b/Mage.Sets/src/mage/cards/m/MistformWarchief.java @@ -58,7 +58,7 @@ public final class MistformWarchief extends CardImpl { } } -class MistformWarchiefPredicate implements ObjectSourcePlayerPredicate> { +class MistformWarchiefPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/m/MuckDrubb.java b/Mage.Sets/src/mage/cards/m/MuckDrubb.java index c892e9bf5e0..47f4327956f 100644 --- a/Mage.Sets/src/mage/cards/m/MuckDrubb.java +++ b/Mage.Sets/src/mage/cards/m/MuckDrubb.java @@ -69,7 +69,7 @@ public final class MuckDrubb extends CardImpl { } } -class SpellWithOnlySingleTargetPredicate implements ObjectSourcePlayerPredicate> { +class SpellWithOnlySingleTargetPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/o/OathOfDruids.java b/Mage.Sets/src/mage/cards/o/OathOfDruids.java index ae1f201e044..ef54f07cafe 100644 --- a/Mage.Sets/src/mage/cards/o/OathOfDruids.java +++ b/Mage.Sets/src/mage/cards/o/OathOfDruids.java @@ -66,7 +66,7 @@ enum OathOfDruidsAdjuster implements TargetAdjuster { } } -class OathOfDruidsPredicate implements ObjectSourcePlayerPredicate> { +class OathOfDruidsPredicate implements ObjectSourcePlayerPredicate { private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(); diff --git a/Mage.Sets/src/mage/cards/o/OathOfGhouls.java b/Mage.Sets/src/mage/cards/o/OathOfGhouls.java index e692587c55a..dc1552060aa 100644 --- a/Mage.Sets/src/mage/cards/o/OathOfGhouls.java +++ b/Mage.Sets/src/mage/cards/o/OathOfGhouls.java @@ -72,7 +72,7 @@ enum OathOfGhoulsAdjuster implements TargetAdjuster { } } -class OathOfGhoulsPredicate implements ObjectSourcePlayerPredicate> { +class OathOfGhoulsPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/o/OathOfLieges.java b/Mage.Sets/src/mage/cards/o/OathOfLieges.java index 643d754d134..d09d5546987 100644 --- a/Mage.Sets/src/mage/cards/o/OathOfLieges.java +++ b/Mage.Sets/src/mage/cards/o/OathOfLieges.java @@ -100,7 +100,7 @@ class OathOfLiegesEffect extends OneShotEffect { } } -class OathOfLiegesPredicate implements ObjectSourcePlayerPredicate> { +class OathOfLiegesPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/o/OathOfMages.java b/Mage.Sets/src/mage/cards/o/OathOfMages.java index 0dbe5e03500..6d6efdef132 100644 --- a/Mage.Sets/src/mage/cards/o/OathOfMages.java +++ b/Mage.Sets/src/mage/cards/o/OathOfMages.java @@ -66,7 +66,7 @@ enum OathOfMagesAdjuster implements TargetAdjuster { } } -enum OathOfMagesPredicate implements ObjectSourcePlayerPredicate> { +enum OathOfMagesPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/o/OathOfScholars.java b/Mage.Sets/src/mage/cards/o/OathOfScholars.java index 02c02dc71e9..194a5709b92 100644 --- a/Mage.Sets/src/mage/cards/o/OathOfScholars.java +++ b/Mage.Sets/src/mage/cards/o/OathOfScholars.java @@ -64,7 +64,7 @@ enum OathOfScholarsAdjuster implements TargetAdjuster { } } -class OathOfScholarsPredicate implements ObjectSourcePlayerPredicate> { +class OathOfScholarsPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/o/OldManOfTheSea.java b/Mage.Sets/src/mage/cards/o/OldManOfTheSea.java index 6e5ee08db4f..4e376e46b3d 100644 --- a/Mage.Sets/src/mage/cards/o/OldManOfTheSea.java +++ b/Mage.Sets/src/mage/cards/o/OldManOfTheSea.java @@ -148,7 +148,7 @@ class SourcePowerGreaterEqualTargetCondition implements Condition { } } -class PowerLowerEqualSourcePredicate implements ObjectSourcePlayerPredicate> { +class PowerLowerEqualSourcePredicate implements ObjectSourcePlayerPredicate { UUID sourceId; diff --git a/Mage.Sets/src/mage/cards/p/PersonalEnergyShield.java b/Mage.Sets/src/mage/cards/p/PersonalEnergyShield.java index a7294fdf46d..7f63b56e162 100644 --- a/Mage.Sets/src/mage/cards/p/PersonalEnergyShield.java +++ b/Mage.Sets/src/mage/cards/p/PersonalEnergyShield.java @@ -46,7 +46,7 @@ public final class PersonalEnergyShield extends CardImpl { } } -class PersonalEnergyFieldPredicate implements ObjectSourcePlayerPredicate> { +class PersonalEnergyFieldPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/p/PsychicRebuttal.java b/Mage.Sets/src/mage/cards/p/PsychicRebuttal.java index 2fcc52ca173..ac8dbac8b09 100644 --- a/Mage.Sets/src/mage/cards/p/PsychicRebuttal.java +++ b/Mage.Sets/src/mage/cards/p/PsychicRebuttal.java @@ -91,7 +91,7 @@ class PsychicRebuttalEffect extends OneShotEffect { } } -class PsychicRebuttalPredicate implements ObjectSourcePlayerPredicate> { +class PsychicRebuttalPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/p/Pyramids.java b/Mage.Sets/src/mage/cards/p/Pyramids.java index 8f14c637860..297ab08af36 100644 --- a/Mage.Sets/src/mage/cards/p/Pyramids.java +++ b/Mage.Sets/src/mage/cards/p/Pyramids.java @@ -60,7 +60,7 @@ public final class Pyramids extends CardImpl { return new Pyramids(this); } } -class PyramidsPredicate implements ObjectSourcePlayerPredicate> { +class PyramidsPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { Permanent attachment = input.getObject(); diff --git a/Mage.Sets/src/mage/cards/r/Radiate.java b/Mage.Sets/src/mage/cards/r/Radiate.java index ad83f123481..5e8eeabc69e 100644 --- a/Mage.Sets/src/mage/cards/r/Radiate.java +++ b/Mage.Sets/src/mage/cards/r/Radiate.java @@ -55,7 +55,7 @@ public final class Radiate extends CardImpl { } } -enum SpellWithOnlySingleTargetPredicate implements ObjectSourcePlayerPredicate> { +enum SpellWithOnlySingleTargetPredicate implements ObjectSourcePlayerPredicate { instance; @Override @@ -79,7 +79,7 @@ enum SpellWithOnlySingleTargetPredicate implements ObjectSourcePlayerPredicate> { +enum SpellWithOnlyPermanentOrPlayerTargetsPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/r/RebbecArchitectOfAscension.java b/Mage.Sets/src/mage/cards/r/RebbecArchitectOfAscension.java index 1720c48f8c3..e5f023a7ba0 100644 --- a/Mage.Sets/src/mage/cards/r/RebbecArchitectOfAscension.java +++ b/Mage.Sets/src/mage/cards/r/RebbecArchitectOfAscension.java @@ -62,7 +62,7 @@ public final class RebbecArchitectOfAscension extends CardImpl { } } -enum RebbecArchitectOfAscensionPredicate implements ObjectSourcePlayerPredicate> { +enum RebbecArchitectOfAscensionPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/r/RemoveEnchantments.java b/Mage.Sets/src/mage/cards/r/RemoveEnchantments.java index 991a19f7d9c..6a345bfdba5 100644 --- a/Mage.Sets/src/mage/cards/r/RemoveEnchantments.java +++ b/Mage.Sets/src/mage/cards/r/RemoveEnchantments.java @@ -74,7 +74,7 @@ public final class RemoveEnchantments extends CardImpl { } } -class AttachedToOpponentControlledAttackingCreaturePredicate implements ObjectSourcePlayerPredicate> { +class AttachedToOpponentControlledAttackingCreaturePredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/r/Ricochet.java b/Mage.Sets/src/mage/cards/r/Ricochet.java index 99f3b66f719..029f4e5d193 100644 --- a/Mage.Sets/src/mage/cards/r/Ricochet.java +++ b/Mage.Sets/src/mage/cards/r/Ricochet.java @@ -57,7 +57,7 @@ public final class Ricochet extends CardImpl { } } -class SpellWithOnlyPlayerTargetsPredicate implements ObjectSourcePlayerPredicate> { +class SpellWithOnlyPlayerTargetsPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/s/SageOfTheBeyond.java b/Mage.Sets/src/mage/cards/s/SageOfTheBeyond.java index 0f3de5243bc..4317d671c37 100644 --- a/Mage.Sets/src/mage/cards/s/SageOfTheBeyond.java +++ b/Mage.Sets/src/mage/cards/s/SageOfTheBeyond.java @@ -59,7 +59,7 @@ public final class SageOfTheBeyond extends CardImpl { } } -enum SageOfTheBeyondPredicate implements ObjectSourcePlayerPredicate> { +enum SageOfTheBeyondPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/s/SalvageTrader.java b/Mage.Sets/src/mage/cards/s/SalvageTrader.java index 76e21c7352f..eb31a44a3e7 100644 --- a/Mage.Sets/src/mage/cards/s/SalvageTrader.java +++ b/Mage.Sets/src/mage/cards/s/SalvageTrader.java @@ -57,7 +57,7 @@ public final class SalvageTrader extends CardImpl { } } -class SameCastingCostPredicate implements ObjectSourcePlayerPredicate> { +class SameCastingCostPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/s/SavaenElves.java b/Mage.Sets/src/mage/cards/s/SavaenElves.java index bc2837ff4ea..5b0d87ac768 100644 --- a/Mage.Sets/src/mage/cards/s/SavaenElves.java +++ b/Mage.Sets/src/mage/cards/s/SavaenElves.java @@ -59,7 +59,7 @@ public final class SavaenElves extends CardImpl { } } -class SavaenElvesPredicate implements ObjectSourcePlayerPredicate> { +class SavaenElvesPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { Permanent attachment = input.getObject(); diff --git a/Mage.Sets/src/mage/cards/s/ShacklesOfTreachery.java b/Mage.Sets/src/mage/cards/s/ShacklesOfTreachery.java index 12af6a0c8b9..06667a49972 100644 --- a/Mage.Sets/src/mage/cards/s/ShacklesOfTreachery.java +++ b/Mage.Sets/src/mage/cards/s/ShacklesOfTreachery.java @@ -56,7 +56,7 @@ public final class ShacklesOfTreachery extends CardImpl { class ShacklesOfTreacheryTriggeredAbility extends TriggeredAbilityImpl { - private enum ShacklesOfTreacheryPredicate implements ObjectSourcePlayerPredicate> { + private enum ShacklesOfTreacheryPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/s/ShellOfTheLastKappa.java b/Mage.Sets/src/mage/cards/s/ShellOfTheLastKappa.java index 34809f201ae..1eade2f0432 100644 --- a/Mage.Sets/src/mage/cards/s/ShellOfTheLastKappa.java +++ b/Mage.Sets/src/mage/cards/s/ShellOfTheLastKappa.java @@ -156,7 +156,7 @@ class ShellOfTheLastKappaCastEffect extends OneShotEffect { } } -class TargetYouPredicate implements ObjectSourcePlayerPredicate> { +class TargetYouPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/s/SilverWyvern.java b/Mage.Sets/src/mage/cards/s/SilverWyvern.java index 1a01717a61f..b1b0e8d21ec 100644 --- a/Mage.Sets/src/mage/cards/s/SilverWyvern.java +++ b/Mage.Sets/src/mage/cards/s/SilverWyvern.java @@ -66,7 +66,7 @@ public final class SilverWyvern extends CardImpl { } } -enum SilverWyvernPredicate implements ObjectSourcePlayerPredicate> { +enum SilverWyvernPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/s/SilverquillSilencer.java b/Mage.Sets/src/mage/cards/s/SilverquillSilencer.java index 8ea5ef8ed9f..fba59f6c3a2 100644 --- a/Mage.Sets/src/mage/cards/s/SilverquillSilencer.java +++ b/Mage.Sets/src/mage/cards/s/SilverquillSilencer.java @@ -65,7 +65,7 @@ public final class SilverquillSilencer extends CardImpl { } } -enum SilverquillSilencerPredicate implements ObjectSourcePlayerPredicate> { +enum SilverquillSilencerPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/s/SimicGuildmage.java b/Mage.Sets/src/mage/cards/s/SimicGuildmage.java index 06cd89b6126..aaa757e9285 100644 --- a/Mage.Sets/src/mage/cards/s/SimicGuildmage.java +++ b/Mage.Sets/src/mage/cards/s/SimicGuildmage.java @@ -120,7 +120,7 @@ class MoveCounterFromTargetToTargetEffect extends OneShotEffect { } } -class SameControllerPredicate implements ObjectSourcePlayerPredicate> { +class SameControllerPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/s/SkullportMerchant.java b/Mage.Sets/src/mage/cards/s/SkullportMerchant.java index 077336654e9..33aea4c8809 100644 --- a/Mage.Sets/src/mage/cards/s/SkullportMerchant.java +++ b/Mage.Sets/src/mage/cards/s/SkullportMerchant.java @@ -60,7 +60,7 @@ public final class SkullportMerchant extends CardImpl { } } -enum SkullportMerchantPredicate implements ObjectSourcePlayerPredicate> { +enum SkullportMerchantPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/s/SoulShatter.java b/Mage.Sets/src/mage/cards/s/SoulShatter.java index cdb4429db05..b690afb3380 100644 --- a/Mage.Sets/src/mage/cards/s/SoulShatter.java +++ b/Mage.Sets/src/mage/cards/s/SoulShatter.java @@ -47,7 +47,7 @@ public final class SoulShatter extends CardImpl { } } -enum SoulShatterPredicate implements ObjectSourcePlayerPredicate> { +enum SoulShatterPredicate implements ObjectSourcePlayerPredicate { instance; private static final FilterPermanent filter = new FilterCreatureOrPlaneswalkerPermanent(); diff --git a/Mage.Sets/src/mage/cards/s/SpectralDeluge.java b/Mage.Sets/src/mage/cards/s/SpectralDeluge.java index be30c1e1822..5bc96cc27d7 100644 --- a/Mage.Sets/src/mage/cards/s/SpectralDeluge.java +++ b/Mage.Sets/src/mage/cards/s/SpectralDeluge.java @@ -50,7 +50,7 @@ public final class SpectralDeluge extends CardImpl { } } -enum SpectralDelugePredicate implements ObjectSourcePlayerPredicate> { +enum SpectralDelugePredicate implements ObjectSourcePlayerPredicate { instance; private static final FilterPermanent filter = new FilterControlledPermanent(SubType.ISLAND); diff --git a/Mage.Sets/src/mage/cards/s/SpellstutterSprite.java b/Mage.Sets/src/mage/cards/s/SpellstutterSprite.java index ad378de75b5..0f6f0719e1c 100644 --- a/Mage.Sets/src/mage/cards/s/SpellstutterSprite.java +++ b/Mage.Sets/src/mage/cards/s/SpellstutterSprite.java @@ -62,7 +62,7 @@ public final class SpellstutterSprite extends CardImpl { } } -enum SpellstutterSpritePredicate implements ObjectSourcePlayerPredicate> { +enum SpellstutterSpritePredicate implements ObjectSourcePlayerPredicate { instance; private static final FilterPermanent filter = new FilterPermanent(); diff --git a/Mage.Sets/src/mage/cards/s/StumpsquallHydra.java b/Mage.Sets/src/mage/cards/s/StumpsquallHydra.java index 110ae1ffab9..e59c1c4e8b1 100644 --- a/Mage.Sets/src/mage/cards/s/StumpsquallHydra.java +++ b/Mage.Sets/src/mage/cards/s/StumpsquallHydra.java @@ -59,7 +59,7 @@ class StumpsquallHydraEffect extends OneShotEffect { filter.add(StumpsquallHydraPredicate.instance); } - private enum StumpsquallHydraPredicate implements ObjectSourcePlayerPredicate> { + private enum StumpsquallHydraPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/t/TelimTorsEdict.java b/Mage.Sets/src/mage/cards/t/TelimTorsEdict.java index c4145bdbeb3..c4f27760e73 100644 --- a/Mage.Sets/src/mage/cards/t/TelimTorsEdict.java +++ b/Mage.Sets/src/mage/cards/t/TelimTorsEdict.java @@ -49,7 +49,7 @@ public final class TelimTorsEdict extends CardImpl { } } -class TelimTorsEdictPredicate implements ObjectSourcePlayerPredicate> { +class TelimTorsEdictPredicate implements ObjectSourcePlayerPredicate { public TelimTorsEdictPredicate() { } diff --git a/Mage.Sets/src/mage/cards/t/ThunderkinAwakener.java b/Mage.Sets/src/mage/cards/t/ThunderkinAwakener.java index a340a43b282..e8d3a109fee 100644 --- a/Mage.Sets/src/mage/cards/t/ThunderkinAwakener.java +++ b/Mage.Sets/src/mage/cards/t/ThunderkinAwakener.java @@ -70,7 +70,7 @@ public final class ThunderkinAwakener extends CardImpl { } } -enum ThunderkinAwakenerPredicate implements ObjectSourcePlayerPredicate> { +enum ThunderkinAwakenerPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/t/Torchling.java b/Mage.Sets/src/mage/cards/t/Torchling.java index 3c3c1b0cfb9..53a094e7b18 100644 --- a/Mage.Sets/src/mage/cards/t/Torchling.java +++ b/Mage.Sets/src/mage/cards/t/Torchling.java @@ -83,7 +83,7 @@ public final class Torchling extends CardImpl { } } -enum TorchlingPredicate implements ObjectSourcePlayerPredicate> { +enum TorchlingPredicate implements ObjectSourcePlayerPredicate { instance; diff --git a/Mage.Sets/src/mage/cards/t/TravelersCloak.java b/Mage.Sets/src/mage/cards/t/TravelersCloak.java index afeaf1e4d0f..230c7c34567 100644 --- a/Mage.Sets/src/mage/cards/t/TravelersCloak.java +++ b/Mage.Sets/src/mage/cards/t/TravelersCloak.java @@ -86,7 +86,7 @@ class TravelersCloakGainAbilityAttachedEffect extends GainAbilityAttachedEffect } } -enum TravelersCloakChosenSubtypePredicate implements ObjectSourcePlayerPredicate> { +enum TravelersCloakChosenSubtypePredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/u/UnlivingPsychopath.java b/Mage.Sets/src/mage/cards/u/UnlivingPsychopath.java index 1fde042523d..490dc55e22e 100644 --- a/Mage.Sets/src/mage/cards/u/UnlivingPsychopath.java +++ b/Mage.Sets/src/mage/cards/u/UnlivingPsychopath.java @@ -63,7 +63,7 @@ public final class UnlivingPsychopath extends CardImpl { } } -class UnlivingPsychopathPowerLessThanSourcePredicate implements ObjectSourcePlayerPredicate> { +class UnlivingPsychopathPowerLessThanSourcePredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/v/VedalkenShackles.java b/Mage.Sets/src/mage/cards/v/VedalkenShackles.java index 2049a078224..2c6adc7e209 100644 --- a/Mage.Sets/src/mage/cards/v/VedalkenShackles.java +++ b/Mage.Sets/src/mage/cards/v/VedalkenShackles.java @@ -62,7 +62,7 @@ public final class VedalkenShackles extends CardImpl { } } -class PowerIslandPredicate implements ObjectSourcePlayerPredicate> { +class PowerIslandPredicate implements ObjectSourcePlayerPredicate { static final FilterLandPermanent filter = new FilterLandPermanent("Island"); static { diff --git a/Mage.Sets/src/mage/cards/v/VineGecko.java b/Mage.Sets/src/mage/cards/v/VineGecko.java index fbdf5006061..420e3b37194 100644 --- a/Mage.Sets/src/mage/cards/v/VineGecko.java +++ b/Mage.Sets/src/mage/cards/v/VineGecko.java @@ -67,7 +67,7 @@ public final class VineGecko extends CardImpl { } } -enum VineGeckoPredicate implements ObjectSourcePlayerPredicate> { +enum VineGeckoPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage.Sets/src/mage/cards/w/WickedAkuba.java b/Mage.Sets/src/mage/cards/w/WickedAkuba.java index da754eaa406..3f601af77f1 100644 --- a/Mage.Sets/src/mage/cards/w/WickedAkuba.java +++ b/Mage.Sets/src/mage/cards/w/WickedAkuba.java @@ -56,7 +56,7 @@ public final class WickedAkuba extends CardImpl { } } -class WickedAkubaPredicate implements ObjectSourcePlayerPredicate> { +class WickedAkubaPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage.Sets/src/mage/cards/y/YasovaDragonclaw.java b/Mage.Sets/src/mage/cards/y/YasovaDragonclaw.java index 70a555f694d..13fd40aa40d 100644 --- a/Mage.Sets/src/mage/cards/y/YasovaDragonclaw.java +++ b/Mage.Sets/src/mage/cards/y/YasovaDragonclaw.java @@ -71,7 +71,7 @@ public final class YasovaDragonclaw extends CardImpl { } } -class YasovaDragonclawPowerLessThanSourcePredicate implements ObjectSourcePlayerPredicate> { +class YasovaDragonclawPowerLessThanSourcePredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage/src/main/java/mage/abilities/keyword/MentorAbility.java b/Mage/src/main/java/mage/abilities/keyword/MentorAbility.java index e7fbee0fb73..6cfe91e6e9e 100644 --- a/Mage/src/main/java/mage/abilities/keyword/MentorAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/MentorAbility.java @@ -45,7 +45,7 @@ public class MentorAbility extends AttacksTriggeredAbility { } -enum MentorAbilityPredicate implements ObjectSourcePlayerPredicate> { +enum MentorAbilityPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage/src/main/java/mage/constants/TargetController.java b/Mage/src/main/java/mage/constants/TargetController.java index 7bdfac067b0..a81c3610c87 100644 --- a/Mage/src/main/java/mage/constants/TargetController.java +++ b/Mage/src/main/java/mage/constants/TargetController.java @@ -50,7 +50,7 @@ public enum TargetController { return controllerPredicate; } - public static class OwnerPredicate implements ObjectSourcePlayerPredicate> { + public static class OwnerPredicate implements ObjectSourcePlayerPredicate { private final TargetController targetOwner; @@ -99,7 +99,7 @@ public enum TargetController { } } - public static class PlayerPredicate implements ObjectSourcePlayerPredicate> { + public static class PlayerPredicate implements ObjectSourcePlayerPredicate { private final TargetController targetPlayer; @@ -143,7 +143,7 @@ public enum TargetController { } } - public static class ControllerPredicate implements ObjectSourcePlayerPredicate> { + public static class ControllerPredicate implements ObjectSourcePlayerPredicate { private final TargetController controller; diff --git a/Mage/src/main/java/mage/filter/FilterCard.java b/Mage/src/main/java/mage/filter/FilterCard.java index 331558337dd..bae5bcd3de9 100644 --- a/Mage/src/main/java/mage/filter/FilterCard.java +++ b/Mage/src/main/java/mage/filter/FilterCard.java @@ -23,7 +23,7 @@ import java.util.stream.Collectors; public class FilterCard extends FilterObject { private static final long serialVersionUID = 1L; - protected List>> extraPredicates = new ArrayList<>(); + protected List> extraPredicates = new ArrayList<>(); public FilterCard() { super("card"); diff --git a/Mage/src/main/java/mage/filter/FilterPermanent.java b/Mage/src/main/java/mage/filter/FilterPermanent.java index 02248373d57..fa995d8a7dd 100644 --- a/Mage/src/main/java/mage/filter/FilterPermanent.java +++ b/Mage/src/main/java/mage/filter/FilterPermanent.java @@ -3,7 +3,6 @@ package mage.filter; import mage.constants.SubType; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; -import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.game.permanent.Permanent; @@ -18,7 +17,7 @@ import java.util.UUID; */ public class FilterPermanent extends FilterObject implements FilterInPlay { - protected List>> extraPredicates = new ArrayList<>(); + protected List> extraPredicates = new ArrayList<>(); public FilterPermanent() { super("permanent"); diff --git a/Mage/src/main/java/mage/filter/FilterPlayer.java b/Mage/src/main/java/mage/filter/FilterPlayer.java index d01aa89994b..7a4e0c726e1 100644 --- a/Mage/src/main/java/mage/filter/FilterPlayer.java +++ b/Mage/src/main/java/mage/filter/FilterPlayer.java @@ -16,7 +16,7 @@ import java.util.UUID; */ public class FilterPlayer extends FilterImpl { - protected List>> extraPredicates = new ArrayList<>(); + protected List> extraPredicates = new ArrayList<>(); public FilterPlayer() { this("player"); diff --git a/Mage/src/main/java/mage/filter/FilterStackObject.java b/Mage/src/main/java/mage/filter/FilterStackObject.java index d3c38ecd332..f6099773dd1 100644 --- a/Mage/src/main/java/mage/filter/FilterStackObject.java +++ b/Mage/src/main/java/mage/filter/FilterStackObject.java @@ -2,10 +2,8 @@ package mage.filter; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; -import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.Predicates; import mage.game.Game; -import mage.game.permanent.Permanent; import mage.game.stack.StackObject; import java.util.ArrayList; @@ -17,7 +15,7 @@ import java.util.UUID; */ public class FilterStackObject extends FilterObject { - protected List>> extraPredicates = new ArrayList<>(); + protected List> extraPredicates = new ArrayList<>(); public FilterStackObject() { this("spell or ability"); diff --git a/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java b/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java index f6fc17d2481..b6b6d18be11 100644 --- a/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java +++ b/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java @@ -5,7 +5,6 @@ import mage.filter.FilterImpl; import mage.filter.FilterInPlay; import mage.filter.FilterPermanent; import mage.filter.FilterPlayer; -import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.Predicate; import mage.game.Game; diff --git a/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayerPredicate.java b/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayerPredicate.java index 73b3679062e..bd451e1112a 100644 --- a/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayerPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/ObjectSourcePlayerPredicate.java @@ -5,5 +5,5 @@ package mage.filter.predicate; * @author North */ @FunctionalInterface -public interface ObjectSourcePlayerPredicate> extends Predicate { +public interface ObjectSourcePlayerPredicate extends Predicate> { } diff --git a/Mage/src/main/java/mage/filter/predicate/card/CardOnTopOfLibraryPredicate.java b/Mage/src/main/java/mage/filter/predicate/card/CardOnTopOfLibraryPredicate.java index 46661a3d88b..a39ce5110bd 100644 --- a/Mage/src/main/java/mage/filter/predicate/card/CardOnTopOfLibraryPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/card/CardOnTopOfLibraryPredicate.java @@ -10,7 +10,7 @@ import mage.players.Player; * @author JayDi85 */ -public enum CardOnTopOfLibraryPredicate implements ObjectSourcePlayerPredicate> { +public enum CardOnTopOfLibraryPredicate implements ObjectSourcePlayerPredicate { YOUR, ANY; diff --git a/Mage/src/main/java/mage/filter/predicate/card/DefendingPlayerOwnsCardPredicate.java b/Mage/src/main/java/mage/filter/predicate/card/DefendingPlayerOwnsCardPredicate.java index a667d02abdf..a863a65c2fd 100644 --- a/Mage/src/main/java/mage/filter/predicate/card/DefendingPlayerOwnsCardPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/card/DefendingPlayerOwnsCardPredicate.java @@ -8,7 +8,7 @@ import mage.game.Game; /** * @author TheElk801 */ -public enum DefendingPlayerOwnsCardPredicate implements ObjectSourcePlayerPredicate> { +public enum DefendingPlayerOwnsCardPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/AnotherCardPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/AnotherCardPredicate.java index 39f3d86867d..6bc62682bca 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/AnotherCardPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/AnotherCardPredicate.java @@ -10,7 +10,7 @@ import mage.game.Game; * * @author North */ -public class AnotherCardPredicate implements ObjectSourcePlayerPredicate> { +public class AnotherCardPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/AnotherPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/AnotherPredicate.java index 1c104eb326e..a7ce4548085 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/AnotherPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/AnotherPredicate.java @@ -9,7 +9,7 @@ import mage.game.Game; /** * @author North */ -public enum AnotherPredicate implements ObjectSourcePlayerPredicate> { +public enum AnotherPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/ChosenColorPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/ChosenColorPredicate.java index 77a920369f2..e0575ae97ce 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/ChosenColorPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/ChosenColorPredicate.java @@ -9,7 +9,7 @@ import mage.game.Game; /** * @author TheElk801 */ -public enum ChosenColorPredicate implements ObjectSourcePlayerPredicate> { +public enum ChosenColorPredicate implements ObjectSourcePlayerPredicate { TRUE(true), FALSE(false); private final boolean value; diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/ChosenSubtypePredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/ChosenSubtypePredicate.java index ed2cdb4cca0..1ac3570785d 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/ChosenSubtypePredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/ChosenSubtypePredicate.java @@ -13,7 +13,7 @@ import mage.game.Game; * * @author LoneFox */ -public enum ChosenSubtypePredicate implements ObjectSourcePlayerPredicate> { +public enum ChosenSubtypePredicate implements ObjectSourcePlayerPredicate { TRUE(true), FALSE(false); private final boolean value; diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/SharesColorWithSourcePredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/SharesColorWithSourcePredicate.java index 56f38b0d622..3beae46614e 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/SharesColorWithSourcePredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/SharesColorWithSourcePredicate.java @@ -10,7 +10,7 @@ import mage.game.Game; * @author LevelX2 */ -public class SharesColorWithSourcePredicate implements ObjectSourcePlayerPredicate> { +public class SharesColorWithSourcePredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsOnlyOnePlayerPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsOnlyOnePlayerPredicate.java index 5510e927ea2..98eea077fd6 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsOnlyOnePlayerPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsOnlyOnePlayerPredicate.java @@ -14,7 +14,7 @@ import mage.target.Target; * * @author jeffwadsworth */ -public class TargetsOnlyOnePlayerPredicate implements ObjectSourcePlayerPredicate> { +public class TargetsOnlyOnePlayerPredicate implements ObjectSourcePlayerPredicate { public TargetsOnlyOnePlayerPredicate() { } diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentPredicate.java index fd16aea94be..ccb8adea32e 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentPredicate.java @@ -15,7 +15,7 @@ import java.util.UUID; /** * @author LoneFox */ -public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate> { +public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate { private final FilterPermanent targetFilter; diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPlayerPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPlayerPredicate.java index e7bf4fe8a18..d5af2774f44 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPlayerPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPlayerPredicate.java @@ -14,7 +14,7 @@ import mage.target.Target; * * @author jeffwadsworth */ -public class TargetsPlayerPredicate implements ObjectSourcePlayerPredicate> { +public class TargetsPlayerPredicate implements ObjectSourcePlayerPredicate { public TargetsPlayerPredicate() { } diff --git a/Mage/src/main/java/mage/filter/predicate/other/AnotherTargetPredicate.java b/Mage/src/main/java/mage/filter/predicate/other/AnotherTargetPredicate.java index 5383ed5731e..5ab2b2f8c4c 100644 --- a/Mage/src/main/java/mage/filter/predicate/other/AnotherTargetPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/other/AnotherTargetPredicate.java @@ -18,7 +18,7 @@ import mage.target.Target; * * @author LevelX2 */ -public class AnotherTargetPredicate implements ObjectSourcePlayerPredicate> { +public class AnotherTargetPredicate implements ObjectSourcePlayerPredicate { private final int targetTag; private final boolean crossModalCheck; diff --git a/Mage/src/main/java/mage/filter/predicate/other/DamagedPlayerThisTurnPredicate.java b/Mage/src/main/java/mage/filter/predicate/other/DamagedPlayerThisTurnPredicate.java index a7935bd77cd..4f3392da66b 100644 --- a/Mage/src/main/java/mage/filter/predicate/other/DamagedPlayerThisTurnPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/other/DamagedPlayerThisTurnPredicate.java @@ -12,7 +12,7 @@ import java.util.UUID; /** * @author LevelX2 */ -public class DamagedPlayerThisTurnPredicate implements ObjectSourcePlayerPredicate> { +public class DamagedPlayerThisTurnPredicate implements ObjectSourcePlayerPredicate { private final TargetController controller; diff --git a/Mage/src/main/java/mage/filter/predicate/other/PlayerCanGainLifePredicate.java b/Mage/src/main/java/mage/filter/predicate/other/PlayerCanGainLifePredicate.java index dcb75de1053..e8f48308e1a 100644 --- a/Mage/src/main/java/mage/filter/predicate/other/PlayerCanGainLifePredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/other/PlayerCanGainLifePredicate.java @@ -10,7 +10,7 @@ import mage.players.Player; * * @author LevelX2 */ -public class PlayerCanGainLifePredicate implements ObjectSourcePlayerPredicate> { +public class PlayerCanGainLifePredicate implements ObjectSourcePlayerPredicate { // public PlayerCanGainLifePredicate() { // } diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/AnotherEnchantedPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/AnotherEnchantedPredicate.java index 75d946dde9f..12bbdd5edbc 100644 --- a/Mage/src/main/java/mage/filter/predicate/permanent/AnotherEnchantedPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/permanent/AnotherEnchantedPredicate.java @@ -10,7 +10,7 @@ import mage.game.permanent.Permanent; * * @author LevelX2 */ -public class AnotherEnchantedPredicate implements ObjectSourcePlayerPredicate> { +public class AnotherEnchantedPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/AttachedToControlledPermanentPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/AttachedToControlledPermanentPredicate.java index 58b8aa6fe96..29f1b839a2f 100644 --- a/Mage/src/main/java/mage/filter/predicate/permanent/AttachedToControlledPermanentPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/permanent/AttachedToControlledPermanentPredicate.java @@ -10,7 +10,7 @@ import mage.game.permanent.Permanent; * * @author North & L_J */ -public class AttachedToControlledPermanentPredicate implements ObjectSourcePlayerPredicate> { +public class AttachedToControlledPermanentPredicate implements ObjectSourcePlayerPredicate { @Override public boolean apply(ObjectSourcePlayer input, Game game) { diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/BlockingOrBlockedBySourcePredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/BlockingOrBlockedBySourcePredicate.java index 6b3048bbf78..4e5af9d9dd6 100644 --- a/Mage/src/main/java/mage/filter/predicate/permanent/BlockingOrBlockedBySourcePredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/permanent/BlockingOrBlockedBySourcePredicate.java @@ -11,7 +11,7 @@ import java.util.UUID; /** * @author TheElk801 */ -public enum BlockingOrBlockedBySourcePredicate implements ObjectSourcePlayerPredicate> { +public enum BlockingOrBlockedBySourcePredicate implements ObjectSourcePlayerPredicate { BLOCKING, BLOCKED_BY, EITHER; diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/DefendingPlayerControlsPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/DefendingPlayerControlsPredicate.java index 89f43c2677f..d6cdf5e23ae 100644 --- a/Mage/src/main/java/mage/filter/predicate/permanent/DefendingPlayerControlsPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/permanent/DefendingPlayerControlsPredicate.java @@ -8,7 +8,7 @@ import mage.game.permanent.Permanent; /** * @author TheElk801 */ -public enum DefendingPlayerControlsPredicate implements ObjectSourcePlayerPredicate> { +public enum DefendingPlayerControlsPredicate implements ObjectSourcePlayerPredicate { instance; @Override diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/GreatestPowerControlledPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/GreatestPowerControlledPredicate.java index 0b6e3d90e93..e2a83ef107c 100644 --- a/Mage/src/main/java/mage/filter/predicate/permanent/GreatestPowerControlledPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/permanent/GreatestPowerControlledPredicate.java @@ -9,7 +9,7 @@ import mage.game.permanent.Permanent; /** * @author jeffwadsworth */ -public enum GreatestPowerControlledPredicate implements ObjectSourcePlayerPredicate> { +public enum GreatestPowerControlledPredicate implements ObjectSourcePlayerPredicate { instance; @Override From 384051d9eb4e0460cc26b9270b9ad663b87a382a Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Thu, 23 Sep 2021 08:06:08 -0400 Subject: [PATCH 174/231] Make BoosterCollator not share state between tables on a server (#8312) --- .../sets/AdventuresInTheForgottenRealms.java | 131 ++--- Mage.Sets/src/mage/sets/DoubleMasters.java | 516 +++++++----------- Mage.Sets/src/mage/sets/Kaldheim.java | 199 +++---- Mage.Sets/src/mage/sets/ModernHorizons2.java | 281 ++++------ Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java | 273 +++------ .../mage/sets/StrixhavenSchoolOfMages.java | 194 +++---- .../src/mage/sets/TherosBeyondDeath.java | 217 +++----- .../src/mage/sets/TimeSpiralRemastered.java | 227 +++----- .../main/java/mage/cards/ExpansionSet.java | 28 +- .../java/mage/collation/BoosterCollator.java | 3 - .../java/mage/collation/BoosterStructure.java | 10 +- .../src/main/java/mage/collation/CardRun.java | 2 +- .../mage/collation/RarityConfiguration.java | 13 +- .../src/main/java/mage/collation/Rotater.java | 24 +- 14 files changed, 712 insertions(+), 1406 deletions(-) diff --git a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java index a2041b71c6f..7eafe71f6ba 100644 --- a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java +++ b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java @@ -23,7 +23,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { } private AdventuresInTheForgottenRealms() { - super("Adventures in the Forgotten Realms", "AFR", ExpansionSet.buildDate(2021, 7, 23), SetType.EXPANSION, new AdventuresInTheForgottenRealmsCollator()); + super("Adventures in the Forgotten Realms", "AFR", ExpansionSet.buildDate(2021, 7, 23), SetType.EXPANSION); this.blockName = "Adventures in the Forgotten Realms"; this.hasBoosters = true; this.hasBasicLands = true; @@ -436,76 +436,41 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Zariel, Archduke of Avernus", 285, Rarity.MYTHIC, mage.cards.z.ZarielArchdukeOfAvernus.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Zombie Ogre", 129, Rarity.COMMON, mage.cards.z.ZombieOgre.class)); } + + @Override + public BoosterCollator createCollator() { + return new AdventuresInTheForgottenRealmsCollator(); + } } // Booster collation info from https://www.lethe.xyz/mtg/collation/afr.html // Using USA collation for common/uncommon, rare collation inferred from other information class AdventuresInTheForgottenRealmsCollator implements BoosterCollator { - private static class AdventuresInTheForgottenRealmsRun extends CardRun { - private static final AdventuresInTheForgottenRealmsRun commonA = new AdventuresInTheForgottenRealmsRun(true, "144", "30", "122", "85", "153", "250", "164", "199", "133", "38", "141", "251", "101", "74", "245", "153", "122", "85", "30", "318", "250", "133", "199", "38", "153", "245", "74", "101", "251", "141", "30", "85", "164", "122", "144", "101", "251", "133", "199", "164", "250", "85", "141", "38", "74", "144", "30", "245", "122", "164", "38", "133", "250", "153", "248", "144", "101", "245", "74", "199", "141", "251", "30", "250", "122", "153", "85", "38", "164", "199", "133", "245", "101", "251", "141", "74", "153", "85", "133", "250", "318", "122", "164", "30", "199", "101", "251", "38", "85", "122", "245", "164", "74", "30", "141", "199", "101", "153", "38", "144", "250", "133", "245", "74", "164", "85", "122", "251", "141", "250", "144", "30", "153", "38", "133", "199", "74", "251", "141", "245", "101"); - private static final AdventuresInTheForgottenRealmsRun commonB = new AdventuresInTheForgottenRealmsRun(true, "115", "182", "37", "47", "134", "119", "204", "16", "70", "146", "103", "189", "31", "51", "139", "102", "177", "14", "72", "158", "128", "205", "10", "46", "168", "108", "203", "43", "75", "150", "110", "178", "19", "52", "142", "123", "174", "40", "83", "140", "94", "213", "34", "73", "130", "109", "206", "9", "71", "162", "97", "183", "1", "65", "148", "118", "187", "11", "84", "159", "115", "204", "37", "51", "146", "119", "182", "31", "47", "134", "102", "177", "16", "70", "139", "103", "189", "14", "46", "168", "108", "205", "10", "72", "158", "128", "178", "19", "75", "150", "123", "203", "43", "52", "140", "110", "174", "40", "73", "142", "97", "206", "9", "83", "130", "118", "183", "34", "71", "162", "94", "213", "1", "65", "159", "109", "187", "11", "84", "148", "248"); - private static final AdventuresInTheForgottenRealmsRun commonC = new AdventuresInTheForgottenRealmsRun(true, "55", "311", "24", "208", "106", "69", "165", "179", "129", "249", "50", "42", "198", "353", "35", "113", "185", "2", "309", "195", "312", "5", "45", "334", "152", "89", "24", "173", "306", "106", "179", "165", "69", "129", "198", "256", "50", "249", "35", "113", "326", "42", "45", "195", "93", "299", "66", "208", "24", "89", "152", "324", "55", "5", "165", "69", "106", "179", "50", "314", "185", "2", "129", "349", "198", "42", "66", "256", "35", "329", "45", "5", "152", "89", "55", "208", "93", "310", "165", "173", "24", "249", "331", "42", "50", "106", "256", "35", "325", "129", "2", "185", "113", "66", "195", "93", "45", "301", "152", "173"); - private static final AdventuresInTheForgottenRealmsRun uncommonA = new AdventuresInTheForgottenRealmsRun(true, "76", "234", "92", "67", "175", "132", "96", "79", "154", "98", "188", "21", "125", "215", "61", "13", "120", "44", "136", "99", "244", "214", "135", "6", "81", "114", "170", "59", "116", "200", "68", "22", "240", "111", "58", "26", "210", "145", "25", "77", "242", "131", "33", "180", "163", "12", "117", "57", "32", "137", "212", "107", "36", "169", "191", "234", "76", "92", "132", "67", "96", "175", "79", "98", "21", "188", "125", "154", "44", "13", "120", "215", "61", "244", "136", "214", "99", "81", "135", "6", "59", "114", "170", "200", "22", "68", "116", "240", "26", "58", "111", "25", "131", "210", "33", "145", "77", "12", "163", "242", "180", "137", "107", "57", "32", "212", "117", "169", "191", "36"); - private static final AdventuresInTheForgottenRealmsRun uncommonB = new AdventuresInTheForgottenRealmsRun(true, "247", "332", "49", "224", "300", "41", "219", "357", "160", "223", "225", "54", "90", "7", "346", "218", "95", "231", "186", "149", "327", "221", "161", "194", "348", "49", "224", "201", "343", "41", "219", "260", "305", "223", "160", "3", "342", "95", "218", "7", "236", "291", "247", "149", "186", "345", "192", "339", "161", "194", "288", "219", "201", "289", "224", "226", "160", "340", "95", "3", "336", "54", "260", "225", "231", "7", "90", "149", "236", "295", "321", "192", "49", "341", "247", "194", "221", "226", "260", "54", "223", "41", "201", "337", "218", "302", "3", "95", "294", "225", "192", "236", "90", "319", "231", "186", "226", "328", "161", "221"); - private static final AdventuresInTheForgottenRealmsRun rareA = new AdventuresInTheForgottenRealmsRun(false, "87", "53", "100", "181", "143", "17", "20", "151", "62", "112", "227", "64", "197", "4", "91", "241", "207", "235", "239", "172", "216", "88", "217", "253", "176", "8", "138", "254", "56", "220", "255", "243", "222", "15", "104", "184", "105", "60", "18", "257", "246", "258", "147", "190", "259", "193", "23", "155", "63", "156", "228", "27", "196", "157", "229", "28", "29", "202", "230", "232", "233", "121", "78", "39", "48", "252", "261", "237", "80", "209", "238", "124", "211", "126", "127", "166", "82", "167", "86", "171", "216", "88", "217", "253", "176", "8", "138", "254", "56", "220", "255", "243", "222", "15", "104", "184", "105", "60", "18", "257", "246", "258", "147", "190", "259", "193", "23", "155", "63", "156", "228", "27", "196", "157", "229", "28", "29", "202", "230", "232", "233", "121", "78", "39", "48", "252", "261", "237", "80", "209", "238", "124", "211", "126", "127", "166", "82", "167", "86", "171"); - private static final AdventuresInTheForgottenRealmsRun rareB = new AdventuresInTheForgottenRealmsRun(false, "87", "53", "292", "286", "143", "282", "287", "293", "290", "284", "344", "283", "296", "4", "91", "241", "333", "298", "239", "285", "297", "88", "217", "350", "176", "8", "317", "351", "307", "338", "352", "243", "222", "15", "104", "184", "313", "60", "18", "354", "246", "355", "147", "190", "356", "193", "23", "155", "308", "320", "228", "303", "330", "157", "229", "304", "29", "202", "230", "232", "233", "121", "78", "39", "48", "397", "358", "237", "80", "335", "347", "396", "211", "315", "316", "166", "82", "322", "86", "323", "297", "88", "217", "350", "176", "8", "317", "351", "307", "338", "352", "243", "222", "15", "104", "184", "313", "60", "18", "354", "246", "355", "147", "190", "356", "193", "23", "155", "308", "320", "228", "303", "330", "157", "229", "304", "29", "202", "230", "232", "233", "121", "78", "39", "48", "397", "358", "237", "80", "335", "347", "396", "211", "315", "316", "166", "82", "322", "86", "323"); - private static final AdventuresInTheForgottenRealmsRun land = new AdventuresInTheForgottenRealmsRun(false, "262", "263", "264", "265", "266", "267", "268", "269", "270", "271", "272", "273", "274", "275", "276", "277", "278", "279", "280", "281"); + private final CardRun commonA = new CardRun(true, "144", "30", "122", "85", "153", "250", "164", "199", "133", "38", "141", "251", "101", "74", "245", "153", "122", "85", "30", "318", "250", "133", "199", "38", "153", "245", "74", "101", "251", "141", "30", "85", "164", "122", "144", "101", "251", "133", "199", "164", "250", "85", "141", "38", "74", "144", "30", "245", "122", "164", "38", "133", "250", "153", "248", "144", "101", "245", "74", "199", "141", "251", "30", "250", "122", "153", "85", "38", "164", "199", "133", "245", "101", "251", "141", "74", "153", "85", "133", "250", "318", "122", "164", "30", "199", "101", "251", "38", "85", "122", "245", "164", "74", "30", "141", "199", "101", "153", "38", "144", "250", "133", "245", "74", "164", "85", "122", "251", "141", "250", "144", "30", "153", "38", "133", "199", "74", "251", "141", "245", "101"); + private final CardRun commonB = new CardRun(true, "115", "182", "37", "47", "134", "119", "204", "16", "70", "146", "103", "189", "31", "51", "139", "102", "177", "14", "72", "158", "128", "205", "10", "46", "168", "108", "203", "43", "75", "150", "110", "178", "19", "52", "142", "123", "174", "40", "83", "140", "94", "213", "34", "73", "130", "109", "206", "9", "71", "162", "97", "183", "1", "65", "148", "118", "187", "11", "84", "159", "115", "204", "37", "51", "146", "119", "182", "31", "47", "134", "102", "177", "16", "70", "139", "103", "189", "14", "46", "168", "108", "205", "10", "72", "158", "128", "178", "19", "75", "150", "123", "203", "43", "52", "140", "110", "174", "40", "73", "142", "97", "206", "9", "83", "130", "118", "183", "34", "71", "162", "94", "213", "1", "65", "159", "109", "187", "11", "84", "148", "248"); + private final CardRun commonC = new CardRun(true, "55", "311", "24", "208", "106", "69", "165", "179", "129", "249", "50", "42", "198", "353", "35", "113", "185", "2", "309", "195", "312", "5", "45", "334", "152", "89", "24", "173", "306", "106", "179", "165", "69", "129", "198", "256", "50", "249", "35", "113", "326", "42", "45", "195", "93", "299", "66", "208", "24", "89", "152", "324", "55", "5", "165", "69", "106", "179", "50", "314", "185", "2", "129", "349", "198", "42", "66", "256", "35", "329", "45", "5", "152", "89", "55", "208", "93", "310", "165", "173", "24", "249", "331", "42", "50", "106", "256", "35", "325", "129", "2", "185", "113", "66", "195", "93", "45", "301", "152", "173"); + private final CardRun uncommonA = new CardRun(true, "76", "234", "92", "67", "175", "132", "96", "79", "154", "98", "188", "21", "125", "215", "61", "13", "120", "44", "136", "99", "244", "214", "135", "6", "81", "114", "170", "59", "116", "200", "68", "22", "240", "111", "58", "26", "210", "145", "25", "77", "242", "131", "33", "180", "163", "12", "117", "57", "32", "137", "212", "107", "36", "169", "191", "234", "76", "92", "132", "67", "96", "175", "79", "98", "21", "188", "125", "154", "44", "13", "120", "215", "61", "244", "136", "214", "99", "81", "135", "6", "59", "114", "170", "200", "22", "68", "116", "240", "26", "58", "111", "25", "131", "210", "33", "145", "77", "12", "163", "242", "180", "137", "107", "57", "32", "212", "117", "169", "191", "36"); + private final CardRun uncommonB = new CardRun(true, "247", "332", "49", "224", "300", "41", "219", "357", "160", "223", "225", "54", "90", "7", "346", "218", "95", "231", "186", "149", "327", "221", "161", "194", "348", "49", "224", "201", "343", "41", "219", "260", "305", "223", "160", "3", "342", "95", "218", "7", "236", "291", "247", "149", "186", "345", "192", "339", "161", "194", "288", "219", "201", "289", "224", "226", "160", "340", "95", "3", "336", "54", "260", "225", "231", "7", "90", "149", "236", "295", "321", "192", "49", "341", "247", "194", "221", "226", "260", "54", "223", "41", "201", "337", "218", "302", "3", "95", "294", "225", "192", "236", "90", "319", "231", "186", "226", "328", "161", "221"); + private final CardRun rareA = new CardRun(false, "87", "53", "100", "181", "143", "17", "20", "151", "62", "112", "227", "64", "197", "4", "91", "241", "207", "235", "239", "172", "216", "88", "217", "253", "176", "8", "138", "254", "56", "220", "255", "243", "222", "15", "104", "184", "105", "60", "18", "257", "246", "258", "147", "190", "259", "193", "23", "155", "63", "156", "228", "27", "196", "157", "229", "28", "29", "202", "230", "232", "233", "121", "78", "39", "48", "252", "261", "237", "80", "209", "238", "124", "211", "126", "127", "166", "82", "167", "86", "171", "216", "88", "217", "253", "176", "8", "138", "254", "56", "220", "255", "243", "222", "15", "104", "184", "105", "60", "18", "257", "246", "258", "147", "190", "259", "193", "23", "155", "63", "156", "228", "27", "196", "157", "229", "28", "29", "202", "230", "232", "233", "121", "78", "39", "48", "252", "261", "237", "80", "209", "238", "124", "211", "126", "127", "166", "82", "167", "86", "171"); + private final CardRun rareB = new CardRun(false, "87", "53", "292", "286", "143", "282", "287", "293", "290", "284", "344", "283", "296", "4", "91", "241", "333", "298", "239", "285", "297", "88", "217", "350", "176", "8", "317", "351", "307", "338", "352", "243", "222", "15", "104", "184", "313", "60", "18", "354", "246", "355", "147", "190", "356", "193", "23", "155", "308", "320", "228", "303", "330", "157", "229", "304", "29", "202", "230", "232", "233", "121", "78", "39", "48", "397", "358", "237", "80", "335", "347", "396", "211", "315", "316", "166", "82", "322", "86", "323", "297", "88", "217", "350", "176", "8", "317", "351", "307", "338", "352", "243", "222", "15", "104", "184", "313", "60", "18", "354", "246", "355", "147", "190", "356", "193", "23", "155", "308", "320", "228", "303", "330", "157", "229", "304", "29", "202", "230", "232", "233", "121", "78", "39", "48", "397", "358", "237", "80", "335", "347", "396", "211", "315", "316", "166", "82", "322", "86", "323"); + private final CardRun land = new CardRun(false, "262", "263", "264", "265", "266", "267", "268", "269", "270", "271", "272", "273", "274", "275", "276", "277", "278", "279", "280", "281"); - private AdventuresInTheForgottenRealmsRun(boolean keepOrder, String... numbers) { - super(keepOrder, numbers); - } - } - - private static class AdventuresInTheForgottenRealmsStructure extends BoosterStructure { - private static final AdventuresInTheForgottenRealmsStructure ABBBBBBCCC = new AdventuresInTheForgottenRealmsStructure( - AdventuresInTheForgottenRealmsRun.commonA, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonC, - AdventuresInTheForgottenRealmsRun.commonC, - AdventuresInTheForgottenRealmsRun.commonC - ); - private static final AdventuresInTheForgottenRealmsStructure AABBBBBBCC = new AdventuresInTheForgottenRealmsStructure( - AdventuresInTheForgottenRealmsRun.commonA, - AdventuresInTheForgottenRealmsRun.commonA, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonB, - AdventuresInTheForgottenRealmsRun.commonC, - AdventuresInTheForgottenRealmsRun.commonC - ); - private static final AdventuresInTheForgottenRealmsStructure AAA = new AdventuresInTheForgottenRealmsStructure( - AdventuresInTheForgottenRealmsRun.uncommonA, - AdventuresInTheForgottenRealmsRun.uncommonA, - AdventuresInTheForgottenRealmsRun.uncommonA - ); - private static final AdventuresInTheForgottenRealmsStructure BBB = new AdventuresInTheForgottenRealmsStructure( - AdventuresInTheForgottenRealmsRun.uncommonB, - AdventuresInTheForgottenRealmsRun.uncommonB, - AdventuresInTheForgottenRealmsRun.uncommonB - ); - private static final AdventuresInTheForgottenRealmsStructure R1 = new AdventuresInTheForgottenRealmsStructure( - AdventuresInTheForgottenRealmsRun.rareA - ); - private static final AdventuresInTheForgottenRealmsStructure R2 = new AdventuresInTheForgottenRealmsStructure( - AdventuresInTheForgottenRealmsRun.rareB - ); - private static final AdventuresInTheForgottenRealmsStructure L1 = new AdventuresInTheForgottenRealmsStructure( - AdventuresInTheForgottenRealmsRun.land - ); - - private AdventuresInTheForgottenRealmsStructure(CardRun... runs) { - super(runs); - } - } + private final BoosterStructure ABBBBBBCCC = new BoosterStructure( + commonA, + commonB, commonB, commonB, commonB, commonB, commonB, + commonC, commonC, commonC + ); + private final BoosterStructure AABBBBBBCC = new BoosterStructure( + commonA, commonA, + commonB, commonB, commonB, commonB, commonB, commonB, + commonC, commonC + ); + private final BoosterStructure AAA = new BoosterStructure(uncommonA, uncommonA, uncommonA); + private final BoosterStructure BBB = new BoosterStructure(uncommonB, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA); + private final BoosterStructure R2 = new BoosterStructure(rareB); + private final BoosterStructure L1 = new BoosterStructure(land); // In order for equal numbers of each common to exist, the average booster must contain: // 1.503 A commons (242 / 161) @@ -514,41 +479,13 @@ class AdventuresInTheForgottenRealmsCollator implements BoosterCollator { // However, boosters with more than six B commons are not known to exist. // This discrepancy is presumably related to foils--the above values are based on // 10 commons per booster, but real boosters contain only 9.67 non-foil commons - private final RarityConfiguration commonRuns = new RarityConfiguration( - AdventuresInTheForgottenRealmsStructure.ABBBBBBCCC, - AdventuresInTheForgottenRealmsStructure.AABBBBBBCC - ); + private final RarityConfiguration commonRuns = new RarityConfiguration(ABBBBBBCCC, AABBBBBBCC); private final RarityConfiguration uncommonRuns = new RarityConfiguration( - false, - AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, - AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, - AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, - AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, - AdventuresInTheForgottenRealmsStructure.AAA, AdventuresInTheForgottenRealmsStructure.AAA, - AdventuresInTheForgottenRealmsStructure.AAA, - AdventuresInTheForgottenRealmsStructure.BBB, AdventuresInTheForgottenRealmsStructure.BBB, - AdventuresInTheForgottenRealmsStructure.BBB, AdventuresInTheForgottenRealmsStructure.BBB, - AdventuresInTheForgottenRealmsStructure.BBB + AAA, AAA, AAA, AAA, AAA, AAA, AAA, AAA, AAA, AAA, AAA, + BBB, BBB, BBB, BBB, BBB ); - private final RarityConfiguration rareRuns = new RarityConfiguration( - false, - AdventuresInTheForgottenRealmsStructure.R1, - AdventuresInTheForgottenRealmsStructure.R1, - AdventuresInTheForgottenRealmsStructure.R1, - AdventuresInTheForgottenRealmsStructure.R2 - ); - private final RarityConfiguration landRuns = new RarityConfiguration( - AdventuresInTheForgottenRealmsStructure.L1 - ); - - - @Override - public void shuffle() { - commonRuns.shuffle(); - uncommonRuns.shuffle(); - rareRuns.shuffle(); - landRuns.shuffle(); - } + private final RarityConfiguration rareRuns = new RarityConfiguration(R1, R1, R1, R2); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); @Override public List makeBooster() { diff --git a/Mage.Sets/src/mage/sets/DoubleMasters.java b/Mage.Sets/src/mage/sets/DoubleMasters.java index 21ab75bf72d..9b368b93aa6 100644 --- a/Mage.Sets/src/mage/sets/DoubleMasters.java +++ b/Mage.Sets/src/mage/sets/DoubleMasters.java @@ -23,7 +23,7 @@ public final class DoubleMasters extends ExpansionSet { } private DoubleMasters() { - super("Double Masters", "2XM", ExpansionSet.buildDate(2020, 8, 7), SetType.SUPPLEMENTAL, new DoubleMastersCollator()); + super("Double Masters", "2XM", ExpansionSet.buildDate(2020, 8, 7), SetType.SUPPLEMENTAL); this.blockName = "Reprint"; this.hasBasicLands = true; this.hasBoosters = true; @@ -418,6 +418,11 @@ public final class DoubleMasters extends ExpansionSet { cards.add(new SetCardInfo("Wurmcoil Engine", 368, Rarity.MYTHIC, mage.cards.w.WurmcoilEngine.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Yavimaya's Embrace", 229, Rarity.UNCOMMON, mage.cards.y.YavimayasEmbrace.class)); } + + @Override + public BoosterCollator createCollator() { + return new DoubleMastersCollator(); + } } // Booster collation info from https://www.lethe.xyz/mtg/collation/2xm.html @@ -428,352 +433,223 @@ public final class DoubleMasters extends ExpansionSet { // TODO: write a test, not sure how right now class DoubleMastersCollator implements BoosterCollator { - private static class DoubleMastersRun extends CardRun { - private static final DoubleMastersRun commonA = new DoubleMastersRun(true, "160", "108", "146", "79", "247", "165", "114", "111", "163", "29", "143", "105", "162", "135", "154", "78", "144", "151", "140", "84", "187", "304", "87", "133", "173", "95", "126", "28", "176", "90", "137", "165", "83", "159", "116", "168", "92", "121", "154", "79", "150", "181", "247", "146", "111", "160", "143", "96", "114", "108", "151", "78", "135", "95", "162", "144", "87", "140", "105", "163", "304", "84", "126", "173", "111", "133", "29", "187", "83", "137", "176", "90", "159", "150", "96", "247", "146", "165", "92", "116", "28", "160", "108", "121", "79", "168", "144", "162", "87", "163", "114", "84", "154", "304", "105", "135", "173", "95", "126", "29", "151", "83", "140", "181", "133", "78", "143", "187", "28", "150", "96", "159", "176", "90", "137", "168", "116", "92", "181", "121"); - private static final DoubleMastersRun commonB = new DoubleMastersRun(true, "250", "70", "259", "305", "45", "80", "261", "60", "288", "331", "294", "63", "255", "263", "46", "230", "262", "50", "257", "256", "44", "283", "237", "74", "157", "277", "59", "330", "280", "52", "254", "329", "250", "69", "239", "331", "45", "287", "288", "42", "115", "273", "40", "305", "269", "63", "294", "257", "50", "80", "230", "60", "256", "259", "46", "261", "283", "44", "237", "330", "70", "263", "255", "52", "262", "254", "40", "157", "287", "115", "69", "277", "273", "59", "329", "280", "74", "250", "257", "60", "331", "261", "45", "239", "269", "42", "288", "305", "63", "80", "283", "50", "294", "259", "70", "263", "237", "44", "255", "262", "46", "230", "157", "59", "256", "330", "52", "254", "277", "74", "280", "329", "69", "287", "115", "42", "239", "269", "40", "273"); - private static final DoubleMastersRun commonC = new DoubleMastersRun(true, "18", "35", "4", "17", "2", "30", "12", "27", "3", "13", "33", "30", "18", "12", "35", "27", "4", "3", "17", "13", "2", "33", "12", "4", "13", "18", "27", "17", "33", "30", "35", "3", "2", "18", "35", "4", "17", "2", "30", "12", "27", "3", "13", "33", "30", "18", "12", "35", "27", "4", "3", "17", "13", "2", "33", "12", "4", "13", "18", "27", "17", "33", "30", "35", "3", "2", "18", "35", "4", "17", "2", "30", "12", "27", "3", "13", "33", "30", "18", "12", "35", "27", "4", "3", "17", "13", "2", "33", "12", "4", "13", "18", "27", "17", "33", "30", "35", "3", "2", "18", "35", "4", "17", "2", "30", "12", "27", "3", "13", "33", "30", "18", "12", "35", "27", "4", "3", "17", "13", "2", "33"); - private static final DoubleMastersRun uncommonA = new DoubleMastersRun(true, "315", "244", "73", "274", "208", "147", "202", "169", "290", "102", "65", "285", "220", "67", "186", "246", "112", "222", "22", "301", "86", "62", "228", "161", "101", "302", "54", "184", "220", "307", "73", "93", "15", "119", "202", "291", "169", "323", "102", "244", "201", "172", "312", "290", "37", "208", "246", "6", "65", "307", "86", "186", "67", "222", "141", "15", "315", "22", "274", "161", "37", "101", "285", "228", "184", "112", "147", "302", "172", "54", "220", "141", "301", "6", "93", "119", "169", "323", "291", "312", "201", "62", "244", "184", "102", "15", "222", "37", "290", "112", "147", "65", "285", "101", "315", "67", "202", "186", "274", "208", "323", "73", "307", "228", "86", "161", "119", "246", "312", "6", "22", "172", "301", "62", "302", "141", "93", "291", "54", "201"); - private static final DoubleMastersRun uncommonB = new DoubleMastersRun(true, "217", "23", "49", "245", "91", "194", "148", "71", "16", "125", "238", "198", "180", "36", "278", "99", "224", "38", "232", "123", "68", "258", "229", "310", "120", "242", "188", "25", "66", "267", "138", "178", "281", "199", "89", "194", "241", "23", "49", "91", "245", "166", "134", "238", "217", "148", "36", "265", "16", "125", "198", "232", "71", "100", "267", "229", "180", "68", "278", "123", "25", "99", "241", "38", "120", "258", "199", "188", "224", "281", "310", "49", "23", "66", "138", "178", "245", "217", "166", "134", "242", "89", "36", "265", "148", "100", "242", "198", "180", "25", "238", "16", "194", "38", "91", "71", "125", "278", "229", "310", "68", "232", "123", "178", "99", "258", "188", "120", "267", "199", "89", "224", "241", "66", "134", "166", "281", "138", "100", "265"); - private static final DoubleMastersRun rareA = new DoubleMastersRun(false, "76", "231", "153", "77", "117", "118", "10", "43", "313", "158", "48", "85", "124", "252", "14", "167", "316", "318", "196", "127", "320", "130", "321", "97", "175", "271", "210", "272", "282", "26", "139", "103", "64", "325", "104", "179", "289", "32", "293", "326", "299", "327", "72", "109", "223", "225", "226", "75", "332", "113", "76", "231", "153", "77", "117", "118", "10", "43", "313", "158", "48", "85", "124", "252", "14", "167", "316", "318", "196", "127", "320", "130", "321", "97", "175", "271", "210", "272", "282", "26", "139", "103", "64", "325", "104", "179", "289", "32", "293", "326", "299", "327", "72", "109", "223", "225", "226", "75", "332", "113", "190", "8", "192", "240", "81", "314", "248", "164", "253", "51", "131", "204", "205", "20", "206", "136", "275", "214", "218", "303"); - private static final DoubleMastersRun rareB = new DoubleMastersRun(false, "122", "82", "317", "55", "264", "174", "324", "24", "284", "215", "328"); - private static final DoubleMastersRun rareC = new DoubleMastersRun(false, "189", "7", "311", "155", "236", "193", "11", "47", "249", "195", "128", "170", "260", "132", "203", "21", "268", "98", "57", "322", "209", "177", "279", "61", "212", "219", "292", "34", "183", "110", "149", "227", "306"); - private static final DoubleMastersRun rareD = new DoubleMastersRun(false, "309", "191", "233", "9", "156", "243", "88", "251", "319", "53", "129", "200", "171", "19", "266", "207", "58", "211", "276", "213", "142", "286", "106", "31", "221", "182", "39"); - private static final DoubleMastersRun rareE = new DoubleMastersRun(false, "5", "41", "152", "234", "235", "197", "94", "56", "1", "270", "107", "145", "295", "296", "297", "298", "300", "216", "185", "308"); - private static final DoubleMastersRun foilUncommonA = new DoubleMastersRun(false, "6", "119", "312", "244", "161", "246", "315", "86", "93", "15", "169", "201", "54", "172", "202", "208", "22", "274", "323", "101", "102", "62", "141", "65", "285", "67", "220", "290", "291", "147", "222", "301", "302", "73", "184", "37", "112", "186", "228", "307"); - private static final DoubleMastersRun foilUncommonB = new DoubleMastersRun(false, "310", "232", "120", "238", "241", "242", "245", "194", "123", "89", "91", "166", "49", "16", "125", "198", "199", "258", "265", "134", "267", "99", "23", "278", "100", "25", "281", "138", "178", "66", "217", "68", "180", "71", "36", "148", "224", "38", "188", "229"); - private static final DoubleMastersRun foilRareA = new DoubleMastersRun(false, "309", "231", "76", "189", "7", "153", "191", "233", "77", "9", "117", "311", "118", "155", "10", "236", "43", "193", "313", "156", "158", "243", "11", "122", "47", "82", "48", "85", "88", "124", "249", "251", "252", "14", "167", "195", "316", "317", "318", "196", "319", "127", "128", "53", "320", "170", "129", "260", "200", "171", "130", "321", "55", "132", "264", "203", "19", "266", "21", "174", "268", "207", "97", "98", "175", "57", "58", "271", "322", "209", "210", "211", "272", "276", "324", "177", "279", "24", "61", "282", "212", "26", "139", "284", "103", "64", "213", "142", "325", "104", "215", "286", "179", "219", "106", "289", "31", "32", "292", "293", "326", "221", "299", "34", "182", "327", "72", "109", "183", "223", "110", "149", "328", "225", "226", "227", "306", "75", "332", "113", "39"); - private static final DoubleMastersRun foilRareB = new DoubleMastersRun(false, "5", "41", "190", "8", "152", "234", "235", "192", "240", "81", "314", "248", "164", "253", "51", "197", "94", "131", "56", "204", "1", "205", "20", "206", "270", "136", "275", "214", "218", "107", "145", "295", "296", "297", "298", "300", "216", "303", "185", "308"); + private final CardRun commonA = new CardRun(true, "160", "108", "146", "79", "247", "165", "114", "111", "163", "29", "143", "105", "162", "135", "154", "78", "144", "151", "140", "84", "187", "304", "87", "133", "173", "95", "126", "28", "176", "90", "137", "165", "83", "159", "116", "168", "92", "121", "154", "79", "150", "181", "247", "146", "111", "160", "143", "96", "114", "108", "151", "78", "135", "95", "162", "144", "87", "140", "105", "163", "304", "84", "126", "173", "111", "133", "29", "187", "83", "137", "176", "90", "159", "150", "96", "247", "146", "165", "92", "116", "28", "160", "108", "121", "79", "168", "144", "162", "87", "163", "114", "84", "154", "304", "105", "135", "173", "95", "126", "29", "151", "83", "140", "181", "133", "78", "143", "187", "28", "150", "96", "159", "176", "90", "137", "168", "116", "92", "181", "121"); + private final CardRun commonB = new CardRun(true, "250", "70", "259", "305", "45", "80", "261", "60", "288", "331", "294", "63", "255", "263", "46", "230", "262", "50", "257", "256", "44", "283", "237", "74", "157", "277", "59", "330", "280", "52", "254", "329", "250", "69", "239", "331", "45", "287", "288", "42", "115", "273", "40", "305", "269", "63", "294", "257", "50", "80", "230", "60", "256", "259", "46", "261", "283", "44", "237", "330", "70", "263", "255", "52", "262", "254", "40", "157", "287", "115", "69", "277", "273", "59", "329", "280", "74", "250", "257", "60", "331", "261", "45", "239", "269", "42", "288", "305", "63", "80", "283", "50", "294", "259", "70", "263", "237", "44", "255", "262", "46", "230", "157", "59", "256", "330", "52", "254", "277", "74", "280", "329", "69", "287", "115", "42", "239", "269", "40", "273"); + private final CardRun commonC = new CardRun(true, "18", "35", "4", "17", "2", "30", "12", "27", "3", "13", "33", "30", "18", "12", "35", "27", "4", "3", "17", "13", "2", "33", "12", "4", "13", "18", "27", "17", "33", "30", "35", "3", "2", "18", "35", "4", "17", "2", "30", "12", "27", "3", "13", "33", "30", "18", "12", "35", "27", "4", "3", "17", "13", "2", "33", "12", "4", "13", "18", "27", "17", "33", "30", "35", "3", "2", "18", "35", "4", "17", "2", "30", "12", "27", "3", "13", "33", "30", "18", "12", "35", "27", "4", "3", "17", "13", "2", "33", "12", "4", "13", "18", "27", "17", "33", "30", "35", "3", "2", "18", "35", "4", "17", "2", "30", "12", "27", "3", "13", "33", "30", "18", "12", "35", "27", "4", "3", "17", "13", "2", "33"); + private final CardRun uncommonA = new CardRun(true, "315", "244", "73", "274", "208", "147", "202", "169", "290", "102", "65", "285", "220", "67", "186", "246", "112", "222", "22", "301", "86", "62", "228", "161", "101", "302", "54", "184", "220", "307", "73", "93", "15", "119", "202", "291", "169", "323", "102", "244", "201", "172", "312", "290", "37", "208", "246", "6", "65", "307", "86", "186", "67", "222", "141", "15", "315", "22", "274", "161", "37", "101", "285", "228", "184", "112", "147", "302", "172", "54", "220", "141", "301", "6", "93", "119", "169", "323", "291", "312", "201", "62", "244", "184", "102", "15", "222", "37", "290", "112", "147", "65", "285", "101", "315", "67", "202", "186", "274", "208", "323", "73", "307", "228", "86", "161", "119", "246", "312", "6", "22", "172", "301", "62", "302", "141", "93", "291", "54", "201"); + private final CardRun uncommonB = new CardRun(true, "217", "23", "49", "245", "91", "194", "148", "71", "16", "125", "238", "198", "180", "36", "278", "99", "224", "38", "232", "123", "68", "258", "229", "310", "120", "242", "188", "25", "66", "267", "138", "178", "281", "199", "89", "194", "241", "23", "49", "91", "245", "166", "134", "238", "217", "148", "36", "265", "16", "125", "198", "232", "71", "100", "267", "229", "180", "68", "278", "123", "25", "99", "241", "38", "120", "258", "199", "188", "224", "281", "310", "49", "23", "66", "138", "178", "245", "217", "166", "134", "242", "89", "36", "265", "148", "100", "242", "198", "180", "25", "238", "16", "194", "38", "91", "71", "125", "278", "229", "310", "68", "232", "123", "178", "99", "258", "188", "120", "267", "199", "89", "224", "241", "66", "134", "166", "281", "138", "100", "265"); + private final CardRun rareA = new CardRun(false, "76", "231", "153", "77", "117", "118", "10", "43", "313", "158", "48", "85", "124", "252", "14", "167", "316", "318", "196", "127", "320", "130", "321", "97", "175", "271", "210", "272", "282", "26", "139", "103", "64", "325", "104", "179", "289", "32", "293", "326", "299", "327", "72", "109", "223", "225", "226", "75", "332", "113", "76", "231", "153", "77", "117", "118", "10", "43", "313", "158", "48", "85", "124", "252", "14", "167", "316", "318", "196", "127", "320", "130", "321", "97", "175", "271", "210", "272", "282", "26", "139", "103", "64", "325", "104", "179", "289", "32", "293", "326", "299", "327", "72", "109", "223", "225", "226", "75", "332", "113", "190", "8", "192", "240", "81", "314", "248", "164", "253", "51", "131", "204", "205", "20", "206", "136", "275", "214", "218", "303"); + private final CardRun rareB = new CardRun(false, "122", "82", "317", "55", "264", "174", "324", "24", "284", "215", "328"); + private final CardRun rareC = new CardRun(false, "189", "7", "311", "155", "236", "193", "11", "47", "249", "195", "128", "170", "260", "132", "203", "21", "268", "98", "57", "322", "209", "177", "279", "61", "212", "219", "292", "34", "183", "110", "149", "227", "306"); + private final CardRun rareD = new CardRun(false, "309", "191", "233", "9", "156", "243", "88", "251", "319", "53", "129", "200", "171", "19", "266", "207", "58", "211", "276", "213", "142", "286", "106", "31", "221", "182", "39"); + private final CardRun rareE = new CardRun(false, "5", "41", "152", "234", "235", "197", "94", "56", "1", "270", "107", "145", "295", "296", "297", "298", "300", "216", "185", "308"); + private final CardRun foilUncommonA = new CardRun(false, "6", "119", "312", "244", "161", "246", "315", "86", "93", "15", "169", "201", "54", "172", "202", "208", "22", "274", "323", "101", "102", "62", "141", "65", "285", "67", "220", "290", "291", "147", "222", "301", "302", "73", "184", "37", "112", "186", "228", "307"); + private final CardRun foilUncommonB = new CardRun(false, "310", "232", "120", "238", "241", "242", "245", "194", "123", "89", "91", "166", "49", "16", "125", "198", "199", "258", "265", "134", "267", "99", "23", "278", "100", "25", "281", "138", "178", "66", "217", "68", "180", "71", "36", "148", "224", "38", "188", "229"); + private final CardRun foilRareA = new CardRun(false, "309", "231", "76", "189", "7", "153", "191", "233", "77", "9", "117", "311", "118", "155", "10", "236", "43", "193", "313", "156", "158", "243", "11", "122", "47", "82", "48", "85", "88", "124", "249", "251", "252", "14", "167", "195", "316", "317", "318", "196", "319", "127", "128", "53", "320", "170", "129", "260", "200", "171", "130", "321", "55", "132", "264", "203", "19", "266", "21", "174", "268", "207", "97", "98", "175", "57", "58", "271", "322", "209", "210", "211", "272", "276", "324", "177", "279", "24", "61", "282", "212", "26", "139", "284", "103", "64", "213", "142", "325", "104", "215", "286", "179", "219", "106", "289", "31", "32", "292", "293", "326", "221", "299", "34", "182", "327", "72", "109", "183", "223", "110", "149", "328", "225", "226", "227", "306", "75", "332", "113", "39"); + private final CardRun foilRareB = new CardRun(false, "5", "41", "190", "8", "152", "234", "235", "192", "240", "81", "314", "248", "164", "253", "51", "197", "94", "131", "56", "204", "1", "205", "20", "206", "270", "136", "275", "214", "218", "107", "145", "295", "296", "297", "298", "300", "216", "303", "185", "308"); - private DoubleMastersRun(boolean keepOrder, String... numbers) { - super(keepOrder, numbers); - } - } + private final BoosterStructure C1 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonC + ); + private final BoosterStructure C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC + ); + private final BoosterStructure C3 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB + ); + private final BoosterStructure U1 = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure U2 = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA, rareC); + private final BoosterStructure R2 = new BoosterStructure(rareA, rareD); + private final BoosterStructure R3 = new BoosterStructure(rareA, rareE); + private final BoosterStructure R4 = new BoosterStructure(rareB, rareC); + private final BoosterStructure R5 = new BoosterStructure(rareB, rareD); + private final BoosterStructure R6 = new BoosterStructure(rareB, rareE); - private static class DoubleMastersStructure extends BoosterStructure { - private static final DoubleMastersStructure C1 = new DoubleMastersStructure( - DoubleMastersRun.commonA, - DoubleMastersRun.commonA, - DoubleMastersRun.commonA, - DoubleMastersRun.commonA, - DoubleMastersRun.commonB, - DoubleMastersRun.commonB, - DoubleMastersRun.commonB, - DoubleMastersRun.commonC - ); - private static final DoubleMastersStructure C2 = new DoubleMastersStructure( - DoubleMastersRun.commonA, - DoubleMastersRun.commonA, - DoubleMastersRun.commonA, - DoubleMastersRun.commonB, - DoubleMastersRun.commonB, - DoubleMastersRun.commonB, - DoubleMastersRun.commonB, - DoubleMastersRun.commonC - ); - private static final DoubleMastersStructure C3 = new DoubleMastersStructure( - DoubleMastersRun.commonA, - DoubleMastersRun.commonA, - DoubleMastersRun.commonA, - DoubleMastersRun.commonA, - DoubleMastersRun.commonB, - DoubleMastersRun.commonB, - DoubleMastersRun.commonB, - DoubleMastersRun.commonB - ); - private static final DoubleMastersStructure U1 = new DoubleMastersStructure( - DoubleMastersRun.uncommonA, - DoubleMastersRun.uncommonB, - DoubleMastersRun.uncommonB - ); - private static final DoubleMastersStructure U2 = new DoubleMastersStructure( - DoubleMastersRun.uncommonA, - DoubleMastersRun.uncommonA, - DoubleMastersRun.uncommonB - ); - private static final DoubleMastersStructure R1 = new DoubleMastersStructure( - DoubleMastersRun.rareA, - DoubleMastersRun.rareC - ); - private static final DoubleMastersStructure R2 = new DoubleMastersStructure( - DoubleMastersRun.rareA, - DoubleMastersRun.rareD - ); - private static final DoubleMastersStructure R3 = new DoubleMastersStructure( - DoubleMastersRun.rareA, - DoubleMastersRun.rareE - ); - private static final DoubleMastersStructure R4 = new DoubleMastersStructure( - DoubleMastersRun.rareB, - DoubleMastersRun.rareC - ); - private static final DoubleMastersStructure R5 = new DoubleMastersStructure( - DoubleMastersRun.rareB, - DoubleMastersRun.rareD - ); - private static final DoubleMastersStructure R6 = new DoubleMastersStructure( - DoubleMastersRun.rareB, - DoubleMastersRun.rareE - ); - private static final DoubleMastersStructure F01 = new DoubleMastersStructure( - DoubleMastersRun.commonA, - DoubleMastersRun.commonB - ); - private static final DoubleMastersStructure F02 = new DoubleMastersStructure( - DoubleMastersRun.commonA, - DoubleMastersRun.commonC - ); - private static final DoubleMastersStructure F03 = new DoubleMastersStructure( - DoubleMastersRun.commonA, - DoubleMastersRun.foilUncommonA - ); - private static final DoubleMastersStructure F04 = new DoubleMastersStructure( - DoubleMastersRun.commonA, - DoubleMastersRun.foilUncommonB - ); - private static final DoubleMastersStructure F05 = new DoubleMastersStructure( - DoubleMastersRun.commonA, - DoubleMastersRun.foilRareA - ); - private static final DoubleMastersStructure F06 = new DoubleMastersStructure( - DoubleMastersRun.commonA, - DoubleMastersRun.foilRareB - ); - private static final DoubleMastersStructure F07 = new DoubleMastersStructure( - DoubleMastersRun.commonB, - DoubleMastersRun.commonC - ); - private static final DoubleMastersStructure F08 = new DoubleMastersStructure( - DoubleMastersRun.commonB, - DoubleMastersRun.foilUncommonA - ); - private static final DoubleMastersStructure F09 = new DoubleMastersStructure( - DoubleMastersRun.commonB, - DoubleMastersRun.foilUncommonB - ); - private static final DoubleMastersStructure F10 = new DoubleMastersStructure( - DoubleMastersRun.commonB, - DoubleMastersRun.foilRareA - ); - private static final DoubleMastersStructure F11 = new DoubleMastersStructure( - DoubleMastersRun.commonB, - DoubleMastersRun.foilRareB - ); - private static final DoubleMastersStructure F12 = new DoubleMastersStructure( - DoubleMastersRun.commonC, - DoubleMastersRun.foilUncommonA - ); - private static final DoubleMastersStructure F13 = new DoubleMastersStructure( - DoubleMastersRun.commonC, - DoubleMastersRun.foilUncommonB - ); - private static final DoubleMastersStructure F14 = new DoubleMastersStructure( - DoubleMastersRun.commonC, - DoubleMastersRun.foilRareA - ); - private static final DoubleMastersStructure F15 = new DoubleMastersStructure( - DoubleMastersRun.commonC, - DoubleMastersRun.foilRareB - ); - private static final DoubleMastersStructure F16 = new DoubleMastersStructure( - DoubleMastersRun.foilUncommonA, - DoubleMastersRun.foilUncommonB - ); - private static final DoubleMastersStructure F17 = new DoubleMastersStructure( - DoubleMastersRun.foilUncommonA, - DoubleMastersRun.foilRareA - ); - private static final DoubleMastersStructure F18 = new DoubleMastersStructure( - DoubleMastersRun.foilUncommonA, - DoubleMastersRun.foilRareB - ); - private static final DoubleMastersStructure F19 = new DoubleMastersStructure( - DoubleMastersRun.foilUncommonB, - DoubleMastersRun.foilRareA - ); - private static final DoubleMastersStructure F20 = new DoubleMastersStructure( - DoubleMastersRun.foilUncommonB, - DoubleMastersRun.foilRareB - ); - private static final DoubleMastersStructure F21 = new DoubleMastersStructure( - DoubleMastersRun.foilRareA, - DoubleMastersRun.foilRareB - ); - - - private DoubleMastersStructure(CardRun... runs) { - super(runs); - } - } + private final BoosterStructure F01 = new BoosterStructure(commonA, commonB); + private final BoosterStructure F02 = new BoosterStructure(commonA, commonC); + private final BoosterStructure F03 = new BoosterStructure(commonA, foilUncommonA); + private final BoosterStructure F04 = new BoosterStructure(commonA, foilUncommonB); + private final BoosterStructure F05 = new BoosterStructure(commonA, foilRareA); + private final BoosterStructure F06 = new BoosterStructure(commonA, foilRareB); + private final BoosterStructure F07 = new BoosterStructure(commonB, commonC); + private final BoosterStructure F08 = new BoosterStructure(commonB, foilUncommonA); + private final BoosterStructure F09 = new BoosterStructure(commonB, foilUncommonB); + private final BoosterStructure F10 = new BoosterStructure(commonB, foilRareA); + private final BoosterStructure F11 = new BoosterStructure(commonB, foilRareB); + private final BoosterStructure F12 = new BoosterStructure(commonC, foilUncommonA); + private final BoosterStructure F13 = new BoosterStructure(commonC, foilUncommonB); + private final BoosterStructure F14 = new BoosterStructure(commonC, foilRareA); + private final BoosterStructure F15 = new BoosterStructure(commonC, foilRareB); + private final BoosterStructure F16 = new BoosterStructure(foilUncommonA, foilUncommonB); + private final BoosterStructure F17 = new BoosterStructure(foilUncommonA, foilRareA); + private final BoosterStructure F18 = new BoosterStructure(foilUncommonA, foilRareB); + private final BoosterStructure F19 = new BoosterStructure(foilUncommonB, foilRareA); + private final BoosterStructure F20 = new BoosterStructure(foilUncommonB, foilRareB); + private final BoosterStructure F21 = new BoosterStructure(foilRareA, foilRareB); private final RarityConfiguration commonRuns = new RarityConfiguration( - false, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C1, DoubleMastersStructure.C2, DoubleMastersStructure.C1, DoubleMastersStructure.C2, - DoubleMastersStructure.C3, DoubleMastersStructure.C3, DoubleMastersStructure.C3 + C1, C2, C1, C2, + C1, C2, C1, C2, + C1, C2, C1, C2, + C1, C2, C1, C2, + C1, C2, C1, C2, + C1, C2, C1, C2, + C1, C2, C1, C2, + C1, C2, C1, C2, + C1, C2, C1, C2, + C1, C2, C1, C2, + C1, C2, C1, C2, + C3, C3, C3 ); private final RarityConfiguration uncommonRuns = new RarityConfiguration( - DoubleMastersStructure.U1, DoubleMastersStructure.U2 + U1, U2 ); private final RarityConfiguration rareRuns = new RarityConfiguration( - false, - DoubleMastersStructure.R1, DoubleMastersStructure.R2, DoubleMastersStructure.R3, - DoubleMastersStructure.R4, DoubleMastersStructure.R5, DoubleMastersStructure.R6 + R1, R2, R3, + R4, R5, R6 ); private final RarityConfiguration foilRuns = new RarityConfiguration( - false, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, - DoubleMastersStructure.F01, DoubleMastersStructure.F01, DoubleMastersStructure.F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, + F01, F01, F01, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, - DoubleMastersStructure.F02, DoubleMastersStructure.F02, DoubleMastersStructure.F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, + F02, F02, F02, - DoubleMastersStructure.F03, DoubleMastersStructure.F03, DoubleMastersStructure.F03, - DoubleMastersStructure.F03, DoubleMastersStructure.F03, DoubleMastersStructure.F03, - DoubleMastersStructure.F03, DoubleMastersStructure.F03, DoubleMastersStructure.F03, - DoubleMastersStructure.F03, DoubleMastersStructure.F03, DoubleMastersStructure.F03, - DoubleMastersStructure.F03, DoubleMastersStructure.F03, DoubleMastersStructure.F03, - DoubleMastersStructure.F03, + F03, F03, F03, + F03, F03, F03, + F03, F03, F03, + F03, F03, F03, + F03, F03, F03, + F03, - DoubleMastersStructure.F04, DoubleMastersStructure.F04, DoubleMastersStructure.F04, - DoubleMastersStructure.F04, DoubleMastersStructure.F04, DoubleMastersStructure.F04, - DoubleMastersStructure.F04, DoubleMastersStructure.F04, DoubleMastersStructure.F04, - DoubleMastersStructure.F04, DoubleMastersStructure.F04, DoubleMastersStructure.F04, - DoubleMastersStructure.F04, DoubleMastersStructure.F04, DoubleMastersStructure.F04, - DoubleMastersStructure.F04, + F04, F04, F04, + F04, F04, F04, + F04, F04, F04, + F04, F04, F04, + F04, F04, F04, + F04, - DoubleMastersStructure.F05, DoubleMastersStructure.F05, DoubleMastersStructure.F05, - DoubleMastersStructure.F05, DoubleMastersStructure.F05, DoubleMastersStructure.F05, - DoubleMastersStructure.F05, DoubleMastersStructure.F05, + F05, F05, F05, + F05, F05, F05, + F05, F05, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, - DoubleMastersStructure.F07, DoubleMastersStructure.F07, DoubleMastersStructure.F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, + F07, F07, F07, - DoubleMastersStructure.F08, DoubleMastersStructure.F08, DoubleMastersStructure.F08, - DoubleMastersStructure.F08, DoubleMastersStructure.F08, DoubleMastersStructure.F08, - DoubleMastersStructure.F08, DoubleMastersStructure.F08, DoubleMastersStructure.F08, - DoubleMastersStructure.F08, DoubleMastersStructure.F08, DoubleMastersStructure.F08, - DoubleMastersStructure.F08, DoubleMastersStructure.F08, DoubleMastersStructure.F08, - DoubleMastersStructure.F08, + F08, F08, F08, + F08, F08, F08, + F08, F08, F08, + F08, F08, F08, + F08, F08, F08, + F08, - DoubleMastersStructure.F09, DoubleMastersStructure.F09, DoubleMastersStructure.F09, - DoubleMastersStructure.F09, DoubleMastersStructure.F09, DoubleMastersStructure.F09, - DoubleMastersStructure.F09, DoubleMastersStructure.F09, DoubleMastersStructure.F09, - DoubleMastersStructure.F09, DoubleMastersStructure.F09, DoubleMastersStructure.F09, - DoubleMastersStructure.F09, DoubleMastersStructure.F09, DoubleMastersStructure.F09, - DoubleMastersStructure.F09, + F09, F09, F09, + F09, F09, F09, + F09, F09, F09, + F09, F09, F09, + F09, F09, F09, + F09, - DoubleMastersStructure.F10, DoubleMastersStructure.F10, DoubleMastersStructure.F10, - DoubleMastersStructure.F10, DoubleMastersStructure.F10, DoubleMastersStructure.F10, - DoubleMastersStructure.F10, DoubleMastersStructure.F10, + F10, F10, F10, + F10, F10, F10, + F10, F10, - DoubleMastersStructure.F12, DoubleMastersStructure.F12, DoubleMastersStructure.F12, - DoubleMastersStructure.F12, DoubleMastersStructure.F12, DoubleMastersStructure.F12, - DoubleMastersStructure.F12, DoubleMastersStructure.F12, DoubleMastersStructure.F12, - DoubleMastersStructure.F12, DoubleMastersStructure.F12, DoubleMastersStructure.F12, - DoubleMastersStructure.F12, DoubleMastersStructure.F12, DoubleMastersStructure.F12, - DoubleMastersStructure.F12, + F12, F12, F12, + F12, F12, F12, + F12, F12, F12, + F12, F12, F12, + F12, F12, F12, + F12, - DoubleMastersStructure.F13, DoubleMastersStructure.F13, DoubleMastersStructure.F13, - DoubleMastersStructure.F13, DoubleMastersStructure.F13, DoubleMastersStructure.F13, - DoubleMastersStructure.F13, DoubleMastersStructure.F13, DoubleMastersStructure.F13, - DoubleMastersStructure.F13, DoubleMastersStructure.F13, DoubleMastersStructure.F13, - DoubleMastersStructure.F13, DoubleMastersStructure.F13, DoubleMastersStructure.F13, - DoubleMastersStructure.F13, + F13, F13, F13, + F13, F13, F13, + F13, F13, F13, + F13, F13, F13, + F13, F13, F13, + F13, - DoubleMastersStructure.F14, DoubleMastersStructure.F14, DoubleMastersStructure.F14, - DoubleMastersStructure.F14, DoubleMastersStructure.F14, DoubleMastersStructure.F14, - DoubleMastersStructure.F14, DoubleMastersStructure.F14, + F14, F14, F14, + F14, F14, F14, + F14, F14, - DoubleMastersStructure.F16, DoubleMastersStructure.F16, DoubleMastersStructure.F16, - DoubleMastersStructure.F16, DoubleMastersStructure.F16, DoubleMastersStructure.F16, - DoubleMastersStructure.F16, DoubleMastersStructure.F16, DoubleMastersStructure.F16, - DoubleMastersStructure.F16, DoubleMastersStructure.F16, DoubleMastersStructure.F16, - DoubleMastersStructure.F16, DoubleMastersStructure.F16, DoubleMastersStructure.F16, - DoubleMastersStructure.F16, + F16, F16, F16, + F16, F16, F16, + F16, F16, F16, + F16, F16, F16, + F16, F16, F16, + F16, - DoubleMastersStructure.F17, DoubleMastersStructure.F17, DoubleMastersStructure.F17, - DoubleMastersStructure.F17, DoubleMastersStructure.F17, DoubleMastersStructure.F17, - DoubleMastersStructure.F17, DoubleMastersStructure.F17, + F17, F17, F17, + F17, F17, F17, + F17, F17, - DoubleMastersStructure.F19, DoubleMastersStructure.F19, DoubleMastersStructure.F19, - DoubleMastersStructure.F19, DoubleMastersStructure.F19, DoubleMastersStructure.F19, - DoubleMastersStructure.F19, DoubleMastersStructure.F19, + F19, F19, F19, + F19, F19, F19, + F19, F19, - DoubleMastersStructure.F06, DoubleMastersStructure.F11, DoubleMastersStructure.F15, - DoubleMastersStructure.F18, DoubleMastersStructure.F20, DoubleMastersStructure.F21 + F06, F11, F15, + F18, F20, F21 ); - @Override - public void shuffle() { - commonRuns.shuffle(); - uncommonRuns.shuffle(); - rareRuns.shuffle(); - foilRuns.shuffle(); - } - @Override public List makeBooster() { List booster = new ArrayList<>(); diff --git a/Mage.Sets/src/mage/sets/Kaldheim.java b/Mage.Sets/src/mage/sets/Kaldheim.java index 4291b9b4b07..6e9f0891920 100644 --- a/Mage.Sets/src/mage/sets/Kaldheim.java +++ b/Mage.Sets/src/mage/sets/Kaldheim.java @@ -30,7 +30,7 @@ public final class Kaldheim extends ExpansionSet { private final List savedSpecialLand = new ArrayList<>(); private Kaldheim() { - super("Kaldheim", "KHM", ExpansionSet.buildDate(2021, 2, 5), SetType.EXPANSION, new KaldheimCollator()); + super("Kaldheim", "KHM", ExpansionSet.buildDate(2021, 2, 5), SetType.EXPANSION); this.blockName = "Kaldheim"; this.hasBasicLands = true; this.hasBoosters = true; @@ -492,101 +492,52 @@ public final class Kaldheim extends ExpansionSet { } return new ArrayList<>(savedSpecialLand); } + + @Override + public BoosterCollator createCollator() { + return new KaldheimCollator(); + } } // Booster collation info from https://www.lethe.xyz/mtg/collation/khm.html // Using USA collation for common/uncommon and JP for rare/mythic class KaldheimCollator implements BoosterCollator { - private static class KaldheimRun extends CardRun { - private static final KaldheimRun commonA = new KaldheimRun(true, "34","77","136","13","78","149","3","47","127","14","67","140","19","54","124","38","49","147","39","55","157","1","53","141","37","66","126","10","71","155","4","65","121","13","77","136","34","78","127","3","47","149","14","54","124","38","67","140","19","55","147","39","49","157","37","53","141","10","65","155","1","71","121","4","66","126"); - private static final KaldheimRun commonB = new KaldheimRun(true, "102","176","87","183","93","184","104","178","117","174","111","171","96","194","84","176","119","180","83","164","89","172","87","175","102","183","104","178","93","174","117","184","111","171","84","194","119","164","96","180","89","176","83","172","102","175","87","178","104","174","93","183","117","171","119","184","84","164","111","194","89","180","96","172","83","175"); - private static final KaldheimRun commonC1 = new KaldheimRun(true, "187","152","242","46","173","23","101","246","48","190","32","151","99","68","267","31","91","192","143","57","100","243","105","16","134","42","196","238","187","46","23","242","152","173","48","32","246","190","151","101","31","68","99","267","91","134","105","57","16","192","100","143","243","196","42"); - private static final KaldheimRun commonC2 = new KaldheimRun(true, "11","193","95","158","17","239","44","159","129","7","118","85","138","74","165","11","129","193","150","72","5","95","159","74","158","17","85","239","118","138","44","7","238","193","150","165","5","72","158","95","11","44","159","239","129","17","85","74","7","118","5","150","165","138","72"); - private static final KaldheimRun uncommonA = new KaldheimRun(true, "215","236","212","208","195","224","332","6","232","18","106","268","209","162","8","76","122","88","182","206","202","62","110","132","200","325","271","211","144","103","215","236","258","56","163","113","28","226","2","58","263","148","232","162","224","208","195","323","268","18","106","6","233","8","76","122","209","88","182","206","202","62","110","132","321","220","271","211","144","258","2","28","263","113","226","103","236","163","56","215","148","58","329","195","6","232","233","18","212","162","268","106","208","103","322","76","122","88","182","206","202","62","110","132","200","220","271","211","144","8","58","28","258","113","56","148","2","263","226","163"); - private static final KaldheimRun uncommonB = new KaldheimRun(true, "30","166","75","201","265","222","45","135","256","191","231","235","36","250","316","128","25","247","264","35","97","186","223","59","60","130","216","80","244","259","217","133","64","245","108","189","331","137","116","253","30","166","75","201","265","327","45","128","256","247","235","36","191","25","170","250","135","231","186","35","60","324","97","130","59","264","244","80","328","259","133","217","64","245","108","189","230","137","116","253","30","166","75","201","265","222","45","256","191","235","170","135","36","128","25","247","250","231","35","223","60","130","97","264","216","186","59","244","80","259","217","133","304","245","108","189","230","137","116","253"); - private static final KaldheimRun rareA = new KaldheimRun(false, "9","12","20","21","24","26","27","29","43","50","51","52","61","63","69","73","79","82","86","90","92","107","109","112","115","120","123","125","131","142","146","153","156","161","167","169","177","179","181","185","188","197","203","204","205","207","210","213","214","219","227","228","229","234","237","240","241","251","252","254","255","260","272","275","9","12","20","21","24","26","27","29","43","50","51","52","61","63","69","73","79","82","86","90","92","107","109","112","115","120","123","125","131","142","146","153","156","161","167","169","177","179","181","185","188","197","203","204","205","207","210","213","214","219","227","228","229","234","237","240","241","251","252","254","255","260","272","275","15","22","33","40","41","70","81","94","98","114","139","145","154","160","168","198","199","218","221","225"); - private static final KaldheimRun rareB = new KaldheimRun(false, "9","12","20","300","24","26","27","301","43","303","51","52","61","63","69","73","79","82","86","90","306","107","109","307","309","310","311","125","131","312","146","153","156","161","167","315","177","317","318","185","188","319","203","204","205","207","210","213","214","219","227","330","229","234","237","240","241","290","291","292","255","293","272","275","9","12","20","300","24","26","27","301","43","303","51","52","61","63","69","73","79","82","86","90","306","107","109","307","309","310","311","125","131","312","146","153","156","161","167","315","177","317","318","185","188","319","203","204","205","207","210","213","214","219","227","330","229","234","237","240","241","290","291","292","255","293","272","275","299","22","294","302","295","305","81","94","296","308","139","297","313","298","314","287","320","288","326","289"); - private static final KaldheimRun land = new KaldheimRun(true, "270","282","248","277","276","280","278","266","270","283","282","285","274","277","281","279","262","284","282","248","283","276","269","276","280","285","281","249","257","284","277","249","281","284","283","266","257","281","269","280","261","276","277","283","249","278","285","248","276","285","279","261","269","257","249","248","283","270","285","277","282","284","270","278","248","279","269","281","274","280","279","257","281","284","277","257","274","273","279","276","262","266","284","281","273","282","278","262","280","279","274","262","282","283","278","262","279","261","285","273","266","283","261","280","284","266","278","270","285","282","280","276","277","273","278","269","273","249","261","274"); + private final CardRun commonA = new CardRun(true, "34","77","136","13","78","149","3","47","127","14","67","140","19","54","124","38","49","147","39","55","157","1","53","141","37","66","126","10","71","155","4","65","121","13","77","136","34","78","127","3","47","149","14","54","124","38","67","140","19","55","147","39","49","157","37","53","141","10","65","155","1","71","121","4","66","126"); + private final CardRun commonB = new CardRun(true, "102","176","87","183","93","184","104","178","117","174","111","171","96","194","84","176","119","180","83","164","89","172","87","175","102","183","104","178","93","174","117","184","111","171","84","194","119","164","96","180","89","176","83","172","102","175","87","178","104","174","93","183","117","171","119","184","84","164","111","194","89","180","96","172","83","175"); + private final CardRun commonC1 = new CardRun(true, "187","152","242","46","173","23","101","246","48","190","32","151","99","68","267","31","91","192","143","57","100","243","105","16","134","42","196","238","187","46","23","242","152","173","48","32","246","190","151","101","31","68","99","267","91","134","105","57","16","192","100","143","243","196","42"); + private final CardRun commonC2 = new CardRun(true, "11","193","95","158","17","239","44","159","129","7","118","85","138","74","165","11","129","193","150","72","5","95","159","74","158","17","85","239","118","138","44","7","238","193","150","165","5","72","158","95","11","44","159","239","129","17","85","74","7","118","5","150","165","138","72"); + private final CardRun uncommonA = new CardRun(true, "215","236","212","208","195","224","332","6","232","18","106","268","209","162","8","76","122","88","182","206","202","62","110","132","200","325","271","211","144","103","215","236","258","56","163","113","28","226","2","58","263","148","232","162","224","208","195","323","268","18","106","6","233","8","76","122","209","88","182","206","202","62","110","132","321","220","271","211","144","258","2","28","263","113","226","103","236","163","56","215","148","58","329","195","6","232","233","18","212","162","268","106","208","103","322","76","122","88","182","206","202","62","110","132","200","220","271","211","144","8","58","28","258","113","56","148","2","263","226","163"); + private final CardRun uncommonB = new CardRun(true, "30","166","75","201","265","222","45","135","256","191","231","235","36","250","316","128","25","247","264","35","97","186","223","59","60","130","216","80","244","259","217","133","64","245","108","189","331","137","116","253","30","166","75","201","265","327","45","128","256","247","235","36","191","25","170","250","135","231","186","35","60","324","97","130","59","264","244","80","328","259","133","217","64","245","108","189","230","137","116","253","30","166","75","201","265","222","45","256","191","235","170","135","36","128","25","247","250","231","35","223","60","130","97","264","216","186","59","244","80","259","217","133","304","245","108","189","230","137","116","253"); + private final CardRun rareA = new CardRun(false, "9","12","20","21","24","26","27","29","43","50","51","52","61","63","69","73","79","82","86","90","92","107","109","112","115","120","123","125","131","142","146","153","156","161","167","169","177","179","181","185","188","197","203","204","205","207","210","213","214","219","227","228","229","234","237","240","241","251","252","254","255","260","272","275","9","12","20","21","24","26","27","29","43","50","51","52","61","63","69","73","79","82","86","90","92","107","109","112","115","120","123","125","131","142","146","153","156","161","167","169","177","179","181","185","188","197","203","204","205","207","210","213","214","219","227","228","229","234","237","240","241","251","252","254","255","260","272","275","15","22","33","40","41","70","81","94","98","114","139","145","154","160","168","198","199","218","221","225"); + private final CardRun rareB = new CardRun(false, "9","12","20","300","24","26","27","301","43","303","51","52","61","63","69","73","79","82","86","90","306","107","109","307","309","310","311","125","131","312","146","153","156","161","167","315","177","317","318","185","188","319","203","204","205","207","210","213","214","219","227","330","229","234","237","240","241","290","291","292","255","293","272","275","9","12","20","300","24","26","27","301","43","303","51","52","61","63","69","73","79","82","86","90","306","107","109","307","309","310","311","125","131","312","146","153","156","161","167","315","177","317","318","185","188","319","203","204","205","207","210","213","214","219","227","330","229","234","237","240","241","290","291","292","255","293","272","275","299","22","294","302","295","305","81","94","296","308","139","297","313","298","314","287","320","288","326","289"); + private final CardRun land = new CardRun(true, "270","282","248","277","276","280","278","266","270","283","282","285","274","277","281","279","262","284","282","248","283","276","269","276","280","285","281","249","257","284","277","249","281","284","283","266","257","281","269","280","261","276","277","283","249","278","285","248","276","285","279","261","269","257","249","248","283","270","285","277","282","284","270","278","248","279","269","281","274","280","279","257","281","284","277","257","274","273","279","276","262","266","284","281","273","282","278","262","280","279","274","262","282","283","278","262","279","261","285","273","266","283","261","280","284","266","278","270","285","282","280","276","277","273","278","269","273","249","261","274"); - private KaldheimRun(boolean keepOrder, String... numbers) { - super(keepOrder, numbers); - } - } - - private static class KaldheimStructure extends BoosterStructure { - private static final KaldheimStructure AABBC1C1C1C1C1C1 = new KaldheimStructure( - KaldheimRun.commonA, - KaldheimRun.commonA, - KaldheimRun.commonB, - KaldheimRun.commonB, - KaldheimRun.commonC1, - KaldheimRun.commonC1, - KaldheimRun.commonC1, - KaldheimRun.commonC1, - KaldheimRun.commonC1, - KaldheimRun.commonC1 - ); - private static final KaldheimStructure AAABBC1C1C1C1C1 = new KaldheimStructure( - KaldheimRun.commonA, - KaldheimRun.commonA, - KaldheimRun.commonA, - KaldheimRun.commonB, - KaldheimRun.commonB, - KaldheimRun.commonC1, - KaldheimRun.commonC1, - KaldheimRun.commonC1, - KaldheimRun.commonC1, - KaldheimRun.commonC1 - ); - private static final KaldheimStructure AAAABBBC2C2C2 = new KaldheimStructure( - KaldheimRun.commonA, - KaldheimRun.commonA, - KaldheimRun.commonA, - KaldheimRun.commonA, - KaldheimRun.commonB, - KaldheimRun.commonB, - KaldheimRun.commonB, - KaldheimRun.commonC2, - KaldheimRun.commonC2, - KaldheimRun.commonC2 - ); - private static final KaldheimStructure AAAABBC2C2C2C2 = new KaldheimStructure( - KaldheimRun.commonA, - KaldheimRun.commonA, - KaldheimRun.commonA, - KaldheimRun.commonA, - KaldheimRun.commonB, - KaldheimRun.commonB, - KaldheimRun.commonC2, - KaldheimRun.commonC2, - KaldheimRun.commonC2, - KaldheimRun.commonC2 - ); - private static final KaldheimStructure AAA = new KaldheimStructure( - KaldheimRun.uncommonA, - KaldheimRun.uncommonA, - KaldheimRun.uncommonA - ); - private static final KaldheimStructure BBB = new KaldheimStructure( - KaldheimRun.uncommonB, - KaldheimRun.uncommonB, - KaldheimRun.uncommonB - ); - private static final KaldheimStructure R1 = new KaldheimStructure( - KaldheimRun.rareA - ); - private static final KaldheimStructure R2 = new KaldheimStructure( - KaldheimRun.rareB - ); - private static final KaldheimStructure L1 = new KaldheimStructure( - KaldheimRun.land - ); - - private KaldheimStructure(CardRun... runs) { - super(runs); - } - } + private final BoosterStructure AABBC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAA = new BoosterStructure(uncommonA, uncommonA, uncommonA); + private final BoosterStructure BBB = new BoosterStructure(uncommonB, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA); + private final BoosterStructure R2 = new BoosterStructure(rareB); + private final BoosterStructure L1 = new BoosterStructure(land); // In order for equal numbers of each common to exist, the average booster must contain: // 3.27 A commons (36 / 11) @@ -596,53 +547,33 @@ class KaldheimCollator implements BoosterCollator { // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs // and with 10 common slots per booster private final RarityConfiguration commonRuns = new RarityConfiguration( - false, - KaldheimStructure.AABBC1C1C1C1C1C1, - KaldheimStructure.AABBC1C1C1C1C1C1, - KaldheimStructure.AABBC1C1C1C1C1C1, - KaldheimStructure.AABBC1C1C1C1C1C1, - KaldheimStructure.AABBC1C1C1C1C1C1, - KaldheimStructure.AAABBC1C1C1C1C1, - KaldheimStructure.AAABBC1C1C1C1C1, - KaldheimStructure.AAABBC1C1C1C1C1, - KaldheimStructure.AAABBC1C1C1C1C1, - KaldheimStructure.AAABBC1C1C1C1C1, - KaldheimStructure.AAABBC1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, - KaldheimStructure.AAAABBC2C2C2C2, - KaldheimStructure.AAAABBC2C2C2C2, - KaldheimStructure.AAAABBC2C2C2C2, - KaldheimStructure.AAAABBC2C2C2C2, - KaldheimStructure.AAAABBC2C2C2C2, - KaldheimStructure.AAAABBC2C2C2C2, - KaldheimStructure.AAAABBC2C2C2C2, - KaldheimStructure.AAAABBBC2C2C2, - KaldheimStructure.AAAABBBC2C2C2, - KaldheimStructure.AAAABBBC2C2C2, - KaldheimStructure.AAAABBBC2C2C2 + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2 ); - private final RarityConfiguration uncommonRuns = new RarityConfiguration( - KaldheimStructure.AAA, - KaldheimStructure.BBB - ); - private final RarityConfiguration rareRuns = new RarityConfiguration( - false, - KaldheimStructure.R1, - KaldheimStructure.R1, - KaldheimStructure.R2 - ); - private final RarityConfiguration landRuns = new RarityConfiguration( - KaldheimStructure.L1 - ); - - - @Override - public void shuffle() { - commonRuns.shuffle(); - uncommonRuns.shuffle(); - rareRuns.shuffle(); - landRuns.shuffle(); - } + private final RarityConfiguration uncommonRuns = new RarityConfiguration(AAA, BBB); + private final RarityConfiguration rareRuns = new RarityConfiguration(R1, R1, R2); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); @Override public List makeBooster() { diff --git a/Mage.Sets/src/mage/sets/ModernHorizons2.java b/Mage.Sets/src/mage/sets/ModernHorizons2.java index 4f6386fe212..f8d7b6843e7 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons2.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons2.java @@ -26,7 +26,7 @@ public final class ModernHorizons2 extends ExpansionSet { } private ModernHorizons2() { - super("Modern Horizons 2", "MH2", ExpansionSet.buildDate(2021, 6, 11), SetType.SUPPLEMENTAL_MODERN_LEGAL, new ModernHorizons2Collator()); + super("Modern Horizons 2", "MH2", ExpansionSet.buildDate(2021, 6, 11), SetType.SUPPLEMENTAL_MODERN_LEGAL); this.blockName = "Modern Horizons 2"; this.hasBasicLands = true; this.hasBoosters = true; @@ -559,6 +559,11 @@ public final class ModernHorizons2 extends ExpansionSet { cards.removeIf(cardInfo -> cardInfo.getCardNumberAsInt() >= 262); return cards; } + + @Override + public BoosterCollator createCollator() { + return new ModernHorizons2Collator(); + } } // Booster collation info from https://www.lethe.xyz/mtg/collation/mh2.html @@ -566,111 +571,48 @@ public final class ModernHorizons2 extends ExpansionSet { // TODO: add reprint variants (waiting on more info for this) class ModernHorizons2Collator implements BoosterCollator { - private static class ModernHorizons2Run extends CardRun { - private static final ModernHorizons2Run commonA = new ModernHorizons2Run(true, "62", "134", "38", "156", "136", "168", "43", "170", "145", "65", "154", "112", "49", "169", "131", "51", "155", "144", "46", "181", "114", "40", "173", "111", "57", "159", "140", "54", "149", "139", "62", "161", "136", "38", "168", "134", "63", "146", "156", "65", "145", "170", "43", "131", "169", "51", "114", "154", "49", "112", "155", "46", "140", "181", "40", "144", "173", "63", "139", "159", "57", "111", "161", "54", "146", "149"); - private static final ModernHorizons2Run commonB = new ModernHorizons2Run(true, "15", "395", "8", "95", "21", "89", "381", "88", "36", "86", "4", "109", "20", "91", "17", "107", "18", "78", "33", "99", "329", "76", "15", "95", "8", "101", "21", "88", "6", "86", "4", "343", "36", "109", "17", "91", "18", "78", "330", "107", "15", "99", "19", "399", "33", "76", "8", "88", "382", "86", "6", "101", "4", "348", "36", "91", "17", "89", "18", "107", "20", "78", "19", "99", "33", "101"); - private static final ModernHorizons2Run commonC1 = new ModernHorizons2Run(true, "3", "246", "24", "253", "190", "103", "226", "187", "239", "252", "230", "255", "11", "213", "249", "83", "256", "104", "194", "13", "82", "24", "257", "193", "245", "3", "196", "235", "246", "188", "222", "190", "253", "187", "252", "230", "226", "255", "11", "239", "103", "249", "194", "104", "13", "82", "213", "256", "196", "245", "83", "193", "257", "188", "235"); - private static final ModernHorizons2Run commonC2 = new ModernHorizons2Run(true, "424", "163", "349", "167", "37", "152", "430", "200", "354", "135", "247", "258", "55", "217", "127", "351", "163", "122", "215", "152", "66", "356", "42", "408", "200", "147", "232", "37", "406", "247", "55", "258", "217", "128", "335", "167", "215", "411", "66", "413", "122", "135", "421", "147", "232", "389", "127", "339", "258", "247", "217", "222", "392", "128", "42"); - private static final ModernHorizons2Run uncommonA = new ModernHorizons2Run(true, "5", "429", "172", "125", "110", "369", "2", "228", "165", "123", "28", "185", "64", "150", "98", "14", "164", "376", "94", "237", "191", "143", "251", "108", "360", "221", "113", "179", "85", "124", "241", "16", "79", "56", "195", "77", "220", "2", "110", "174", "50", "5", "203", "404", "229", "172", "125", "28", "228", "150", "361", "123", "98", "64", "165", "143", "94", "14", "164", "191", "237", "251", "212", "221", "403", "124", "179", "16", "85", "184", "113", "241", "79", "56", "364", "77", "174", "220", "50", "2", "110", "150", "28", "115", "229", "5", "350", "203", "172", "428", "64", "98", "123", "165", "185", "94", "14", "143", "212", "164", "221", "362", "251", "108", "124", "237", "358", "16", "113", "184", "220", "85", "195", "174", "79", "241", "56", "77", "115", "50"); - private static final ModernHorizons2Run uncommonB = new ModernHorizons2Run(true, "74", "373", "141", "60", "183", "31", "70", "233", "10", "130", "211", "72", "180", "133", "41", "160", "346", "25", "121", "48", "210", "90", "177", "201", "34", "73", "9", "434", "119", "105", "1", "426", "53", "84", "175", "45", "141", "327", "60", "142", "209", "74", "61", "158", "233", "72", "133", "180", "10", "130", "375", "160", "31", "90", "48", "183", "210", "70", "384", "100", "41", "121", "201", "9", "73", "240", "177", "34", "45", "84", "415", "61", "119", "1", "53", "347", "223", "141", "60", "209", "142", "7", "158", "74", "133", "180", "31", "394", "211", "10", "233", "130", "183", "70", "100", "48", "160", "90", "25", "374", "121", "41", "177", "9", "240", "73", "201", "34", "175", "45", "223", "84", "338", "1", "119", "105", "61", "158", "142", "7"); - private static final ModernHorizons2Run rareA = new ModernHorizons2Run(false, "12", "12", "22", "22", "23", "23", "26", "26", "27", "27", "29", "29", "30", "32", "35", "35", "39", "39", "44", "44", "47", "47", "52", "58", "58", "59", "59", "67", "68", "68", "69", "71", "71", "75", "80", "80", "81", "81", "87", "92", "92", "93", "93", "96", "96", "97", "97", "102", "106", "106", "116", "116", "117", "117", "118", "118", "120", "120", "126", "129", "129", "132", "132", "137", "137", "138", "148", "148", "151", "153", "153", "157", "162", "162", "166", "166", "171", "171", "176", "176", "178", "182", "182", "186", "186", "189", "189", "192", "197", "198", "198", "199", "202", "204", "204", "205", "205", "206", "206", "207", "207", "208", "208", "214", "214", "216", "216", "218", "218", "219", "219", "224", "224", "225", "225", "227", "231", "231", "234", "236", "236", "238", "242", "242", "243", "243", "244", "244", "248", "248", "250", "250", "254", "254", "259", "259", "260", "260", "261", "261"); - private static final ModernHorizons2Run rareB = new ModernHorizons2Run(false, "328", "328", "331", "331", "332", "332", "333", "334", "334", "336", "336", "337", "340", "340", "341", "341", "342", "344", "344", "345", "345", "352", "352", "353", "353", "355", "355", "357", "357", "359", "359", "363", "365", "366", "366", "367", "368", "370", "370", "371", "371", "372", "372", "377", "377", "378", "378", "379", "380", "380", "383", "383", "385", "385", "386", "386", "388", "388", "390", "390", "391", "391", "393", "396", "396", "397", "397", "398", "398", "400", "400", "401", "401", "402", "405", "405", "407", "407", "409", "409", "410", "412", "412", "414", "414", "417", "417", "418", "418", "420", "422", "422", "425", "425", "427", "427", "431", "432", "432", "433", "435", "435", "436", "436", "437", "437", "438", "438", "439", "439", "440", "440", "441", "441"); - private static final ModernHorizons2Run rareC = new ModernHorizons2Run(false, "304", "305", "306", "307", "309", "310", "311", "312", "313", "315", "316", "317", "318", "323", "324"); - private static final ModernHorizons2Run reprint = new ModernHorizons2Run(false, "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "263", "265", "270", "271", "273", "275", "277", "279", "283", "286", "289", "290", "292", "293", "294", "295", "298", "303", "263", "265", "270", "271", "273", "275", "277", "279", "283", "286", "289", "290", "292", "293", "294", "295", "298", "303", "281", "287", "291", "301"); + private final CardRun commonA = new CardRun(true, "62", "134", "38", "156", "136", "168", "43", "170", "145", "65", "154", "112", "49", "169", "131", "51", "155", "144", "46", "181", "114", "40", "173", "111", "57", "159", "140", "54", "149", "139", "62", "161", "136", "38", "168", "134", "63", "146", "156", "65", "145", "170", "43", "131", "169", "51", "114", "154", "49", "112", "155", "46", "140", "181", "40", "144", "173", "63", "139", "159", "57", "111", "161", "54", "146", "149"); + private final CardRun commonB = new CardRun(true, "15", "395", "8", "95", "21", "89", "381", "88", "36", "86", "4", "109", "20", "91", "17", "107", "18", "78", "33", "99", "329", "76", "15", "95", "8", "101", "21", "88", "6", "86", "4", "343", "36", "109", "17", "91", "18", "78", "330", "107", "15", "99", "19", "399", "33", "76", "8", "88", "382", "86", "6", "101", "4", "348", "36", "91", "17", "89", "18", "107", "20", "78", "19", "99", "33", "101"); + private final CardRun commonC1 = new CardRun(true, "3", "246", "24", "253", "190", "103", "226", "187", "239", "252", "230", "255", "11", "213", "249", "83", "256", "104", "194", "13", "82", "24", "257", "193", "245", "3", "196", "235", "246", "188", "222", "190", "253", "187", "252", "230", "226", "255", "11", "239", "103", "249", "194", "104", "13", "82", "213", "256", "196", "245", "83", "193", "257", "188", "235"); + private final CardRun commonC2 = new CardRun(true, "424", "163", "349", "167", "37", "152", "430", "200", "354", "135", "247", "258", "55", "217", "127", "351", "163", "122", "215", "152", "66", "356", "42", "408", "200", "147", "232", "37", "406", "247", "55", "258", "217", "128", "335", "167", "215", "411", "66", "413", "122", "135", "421", "147", "232", "389", "127", "339", "258", "247", "217", "222", "392", "128", "42"); + private final CardRun uncommonA = new CardRun(true, "5", "429", "172", "125", "110", "369", "2", "228", "165", "123", "28", "185", "64", "150", "98", "14", "164", "376", "94", "237", "191", "143", "251", "108", "360", "221", "113", "179", "85", "124", "241", "16", "79", "56", "195", "77", "220", "2", "110", "174", "50", "5", "203", "404", "229", "172", "125", "28", "228", "150", "361", "123", "98", "64", "165", "143", "94", "14", "164", "191", "237", "251", "212", "221", "403", "124", "179", "16", "85", "184", "113", "241", "79", "56", "364", "77", "174", "220", "50", "2", "110", "150", "28", "115", "229", "5", "350", "203", "172", "428", "64", "98", "123", "165", "185", "94", "14", "143", "212", "164", "221", "362", "251", "108", "124", "237", "358", "16", "113", "184", "220", "85", "195", "174", "79", "241", "56", "77", "115", "50"); + private final CardRun uncommonB = new CardRun(true, "74", "373", "141", "60", "183", "31", "70", "233", "10", "130", "211", "72", "180", "133", "41", "160", "346", "25", "121", "48", "210", "90", "177", "201", "34", "73", "9", "434", "119", "105", "1", "426", "53", "84", "175", "45", "141", "327", "60", "142", "209", "74", "61", "158", "233", "72", "133", "180", "10", "130", "375", "160", "31", "90", "48", "183", "210", "70", "384", "100", "41", "121", "201", "9", "73", "240", "177", "34", "45", "84", "415", "61", "119", "1", "53", "347", "223", "141", "60", "209", "142", "7", "158", "74", "133", "180", "31", "394", "211", "10", "233", "130", "183", "70", "100", "48", "160", "90", "25", "374", "121", "41", "177", "9", "240", "73", "201", "34", "175", "45", "223", "84", "338", "1", "119", "105", "61", "158", "142", "7"); + private final CardRun rareA = new CardRun(false, "12", "12", "22", "22", "23", "23", "26", "26", "27", "27", "29", "29", "30", "32", "35", "35", "39", "39", "44", "44", "47", "47", "52", "58", "58", "59", "59", "67", "68", "68", "69", "71", "71", "75", "80", "80", "81", "81", "87", "92", "92", "93", "93", "96", "96", "97", "97", "102", "106", "106", "116", "116", "117", "117", "118", "118", "120", "120", "126", "129", "129", "132", "132", "137", "137", "138", "148", "148", "151", "153", "153", "157", "162", "162", "166", "166", "171", "171", "176", "176", "178", "182", "182", "186", "186", "189", "189", "192", "197", "198", "198", "199", "202", "204", "204", "205", "205", "206", "206", "207", "207", "208", "208", "214", "214", "216", "216", "218", "218", "219", "219", "224", "224", "225", "225", "227", "231", "231", "234", "236", "236", "238", "242", "242", "243", "243", "244", "244", "248", "248", "250", "250", "254", "254", "259", "259", "260", "260", "261", "261"); + private final CardRun rareB = new CardRun(false, "328", "328", "331", "331", "332", "332", "333", "334", "334", "336", "336", "337", "340", "340", "341", "341", "342", "344", "344", "345", "345", "352", "352", "353", "353", "355", "355", "357", "357", "359", "359", "363", "365", "366", "366", "367", "368", "370", "370", "371", "371", "372", "372", "377", "377", "378", "378", "379", "380", "380", "383", "383", "385", "385", "386", "386", "388", "388", "390", "390", "391", "391", "393", "396", "396", "397", "397", "398", "398", "400", "400", "401", "401", "402", "405", "405", "407", "407", "409", "409", "410", "412", "412", "414", "414", "417", "417", "418", "418", "420", "422", "422", "425", "425", "427", "427", "431", "432", "432", "433", "435", "435", "436", "436", "437", "437", "438", "438", "439", "439", "440", "440", "441", "441"); + private final CardRun rareC = new CardRun(false, "304", "305", "306", "307", "309", "310", "311", "312", "313", "315", "316", "317", "318", "323", "324"); + private final CardRun reprint = new CardRun(false, "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "262", "264", "266", "267", "268", "269", "272", "274", "276", "278", "280", "282", "284", "285", "288", "296", "297", "299", "300", "302", "263", "265", "270", "271", "273", "275", "277", "279", "283", "286", "289", "290", "292", "293", "294", "295", "298", "303", "263", "265", "270", "271", "273", "275", "277", "279", "283", "286", "289", "290", "292", "293", "294", "295", "298", "303", "281", "287", "291", "301"); - private ModernHorizons2Run(boolean keepOrder, String... numbers) { - super(keepOrder, numbers); - } - } - - private static class ModernHorizons2Structure extends BoosterStructure { - private static final ModernHorizons2Structure AAABC1C1C1C1C1C1 = new ModernHorizons2Structure( - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonC1, - ModernHorizons2Run.commonC1, - ModernHorizons2Run.commonC1, - ModernHorizons2Run.commonC1, - ModernHorizons2Run.commonC1, - ModernHorizons2Run.commonC1 - ); - private static final ModernHorizons2Structure AAABBC1C1C1C1C1 = new ModernHorizons2Structure( - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonC1, - ModernHorizons2Run.commonC1, - ModernHorizons2Run.commonC1, - ModernHorizons2Run.commonC1, - ModernHorizons2Run.commonC1 - ); - private static final ModernHorizons2Structure AAABBBC2C2C2C2 = new ModernHorizons2Structure( - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonC2, - ModernHorizons2Run.commonC2, - ModernHorizons2Run.commonC2, - ModernHorizons2Run.commonC2 - ); - private static final ModernHorizons2Structure AAAABBC2C2C2C2 = new ModernHorizons2Structure( - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonC2, - ModernHorizons2Run.commonC2, - ModernHorizons2Run.commonC2, - ModernHorizons2Run.commonC2 - ); - private static final ModernHorizons2Structure AAAABBBC2C2C2 = new ModernHorizons2Structure( - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonA, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonB, - ModernHorizons2Run.commonC2, - ModernHorizons2Run.commonC2, - ModernHorizons2Run.commonC2 - ); - private static final ModernHorizons2Structure AAA = new ModernHorizons2Structure( - ModernHorizons2Run.uncommonA, - ModernHorizons2Run.uncommonA, - ModernHorizons2Run.uncommonA - ); - private static final ModernHorizons2Structure BBB = new ModernHorizons2Structure( - ModernHorizons2Run.uncommonB, - ModernHorizons2Run.uncommonB, - ModernHorizons2Run.uncommonB - ); - private static final ModernHorizons2Structure R1 = new ModernHorizons2Structure( - ModernHorizons2Run.rareA - ); - private static final ModernHorizons2Structure R2 = new ModernHorizons2Structure( - ModernHorizons2Run.rareB - ); - private static final ModernHorizons2Structure R3 = new ModernHorizons2Structure( - ModernHorizons2Run.rareC - ); - private static final ModernHorizons2Structure RP1 = new ModernHorizons2Structure( - ModernHorizons2Run.reprint - ); - - private ModernHorizons2Structure(CardRun... runs) { - super(runs); - } - } + private final BoosterStructure AAABC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, + commonC1, commonC1, commonC1, commonC1, commonC1,commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAA = new BoosterStructure(uncommonA, uncommonA, uncommonA); + private final BoosterStructure BBB = new BoosterStructure(uncommonB, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA); + private final BoosterStructure R2 = new BoosterStructure(rareB); + private final BoosterStructure R3 = new BoosterStructure(rareC); + private final BoosterStructure RP1 = new BoosterStructure(reprint); // In order for equal numbers of each common to exist, the average booster must contain: // 3.27 A commons (36 / 11) @@ -680,82 +622,67 @@ class ModernHorizons2Collator implements BoosterCollator { // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs // and with 10 common slots per booster private final RarityConfiguration commonRuns = new RarityConfiguration( - false, - ModernHorizons2Structure.AAABC1C1C1C1C1C1, - ModernHorizons2Structure.AAABC1C1C1C1C1C1, - ModernHorizons2Structure.AAABC1C1C1C1C1C1, - ModernHorizons2Structure.AAABC1C1C1C1C1C1, - ModernHorizons2Structure.AAABC1C1C1C1C1C1, - ModernHorizons2Structure.AAABBC1C1C1C1C1, - ModernHorizons2Structure.AAABBC1C1C1C1C1, - ModernHorizons2Structure.AAABBC1C1C1C1C1, - ModernHorizons2Structure.AAABBC1C1C1C1C1, - ModernHorizons2Structure.AAABBC1C1C1C1C1, - ModernHorizons2Structure.AAABBC1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, - ModernHorizons2Structure.AAABBBC2C2C2C2, - ModernHorizons2Structure.AAABBBC2C2C2C2, - ModernHorizons2Structure.AAABBBC2C2C2C2, - ModernHorizons2Structure.AAABBBC2C2C2C2, - ModernHorizons2Structure.AAABBBC2C2C2C2, - ModernHorizons2Structure.AAAABBC2C2C2C2, - ModernHorizons2Structure.AAAABBC2C2C2C2, - ModernHorizons2Structure.AAAABBBC2C2C2, - ModernHorizons2Structure.AAAABBBC2C2C2, - ModernHorizons2Structure.AAAABBBC2C2C2, - ModernHorizons2Structure.AAAABBBC2C2C2 - ); - private final RarityConfiguration uncommonRuns = new RarityConfiguration( - ModernHorizons2Structure.AAA, - ModernHorizons2Structure.BBB + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2 ); + private final RarityConfiguration uncommonRuns = new RarityConfiguration(AAA, BBB); private final RarityConfiguration rareRuns = new RarityConfiguration( - false, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, ModernHorizons2Structure.R1, - ModernHorizons2Structure.R1, - ModernHorizons2Structure.R2, ModernHorizons2Structure.R2, - ModernHorizons2Structure.R3 + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, R1, R1, + R1, + R2, R2, + R3 ); - private final RarityConfiguration reprintRuns = new RarityConfiguration( - ModernHorizons2Structure.RP1 - ); - - @Override - public void shuffle() { - commonRuns.shuffle(); - uncommonRuns.shuffle(); - rareRuns.shuffle(); - reprintRuns.shuffle(); - } + private final RarityConfiguration reprintRuns = new RarityConfiguration(RP1); @Override public List makeBooster() { diff --git a/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java b/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java index 7a4ee5bea6d..ff4fa3967ef 100644 --- a/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java +++ b/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java @@ -25,7 +25,7 @@ public final class RiseOfTheEldrazi extends ExpansionSet { } private RiseOfTheEldrazi() { - super("Rise of the Eldrazi", "ROE", ExpansionSet.buildDate(2010, 3, 17), SetType.EXPANSION, new RiseOfTheEldraziCollator()); + super("Rise of the Eldrazi", "ROE", ExpansionSet.buildDate(2010, 3, 17), SetType.EXPANSION); this.blockName = "Zendikar"; this.parentSet = Zendikar.getInstance(); this.hasBoosters = true; @@ -283,135 +283,66 @@ public final class RiseOfTheEldrazi extends ExpansionSet { cards.add(new SetCardInfo("Zof Shade", 132, Rarity.COMMON, mage.cards.z.ZofShade.class)); cards.add(new SetCardInfo("Zulaport Enforcer", 133, Rarity.COMMON, mage.cards.z.ZulaportEnforcer.class)); } + + @Override + public BoosterCollator createCollator() { + return new RiseOfTheEldraziCollator(); + } } // Booster collation info from https://www.lethe.xyz/mtg/collation/roe.html // Using USA collation class RiseOfTheEldraziCollator implements BoosterCollator { - private static class RiseOfTheEldraziRun extends CardRun { - private static final RiseOfTheEldraziRun commonA = new RiseOfTheEldraziRun(true, "153", "16", "186", "60", "228", "148", "111", "145", "29", "213", "72", "110", "138", "36", "174", "133", "88", "222", "56", "138", "42", "203", "142", "121", "223", "88", "213", "43", "97", "56", "145", "222", "111", "187", "29", "149", "60", "16", "148", "74", "186", "130", "41", "133", "166", "223", "65", "36", "203", "149", "110", "142", "41", "65", "228", "187", "97", "72", "153", "42", "130", "174", "43", "166", "74", "121"); - private static final RiseOfTheEldraziRun commonB = new RiseOfTheEldraziRun(true, "144", "30", "196", "83", "98", "22", "195", "135", "59", "106", "209", "30", "147", "83", "118", "195", "106", "201", "22", "144", "76", "123", "154", "44", "199", "95", "59", "76", "209", "144", "44", "19", "123", "54", "98", "201", "135", "76", "199", "106", "147", "22", "59", "196", "19", "135", "118", "30", "201", "54", "147", "95", "44", "196", "118", "154", "83", "199", "98", "19", "54", "209", "95", "154", "195", "123"); - private static final RiseOfTheEldraziRun commonC1 = new RiseOfTheEldraziRun(true, "132", "50", "85", "207", "136", "116", "175", "26", "5", "85", "99", "155", "78", "207", "15", "150", "108", "86", "27", "210", "164", "5", "202", "99", "79", "23", "182", "136", "114", "155", "175", "78", "23", "132", "182", "15", "164", "114", "68", "210", "171", "193", "50", "86", "150", "27", "68", "202", "108", "26", "79", "171", "116", "193", "5"); - private static final RiseOfTheEldraziRun commonC2 = new RiseOfTheEldraziRun(true, "87", "173", "73", "102", "34", "161", "208", "18", "200", "93", "105", "34", "159", "87", "24", "102", "208", "93", "18", "161", "13", "67", "46", "159", "208", "73", "200", "34", "67", "161", "126", "18", "105", "13", "194", "73", "24", "173", "102", "200", "13", "87", "194", "24", "159", "126", "67", "46", "93", "173", "194", "105", "126", "13", "46"); - private static final RiseOfTheEldraziRun uncommonA = new RiseOfTheEldraziRun(true, "168", "8", "189", "63", "28", "127", "190", "168", "10", "40", "162", "226", "119", "204", "77", "8", "53", "103", "216", "163", "63", "48", "9", "204", "122", "28", "221", "146", "61", "189", "143", "122", "53", "226", "2", "71", "178", "143", "103", "77", "48", "216", "61", "178", "2", "127", "146", "37", "221", "71", "179", "162", "94", "9", "66", "40", "119", "179", "220", "163", "10", "190", "37", "94", "66", "220"); - private static final RiseOfTheEldraziRun uncommonB = new RiseOfTheEldraziRun(true, "137", "80", "224", "49", "115", "217", "205", "137", "92", "45", "185", "115", "70", "49", "134", "217", "104", "181", "20", "81", "180", "170", "92", "14", "128", "157", "109", "181", "35", "58", "167", "104", "157", "224", "180", "35", "81", "131", "80", "170", "128", "188", "45", "167", "109", "20", "185", "58", "134", "205", "14", "70", "131", "188"); - private static final RiseOfTheEldraziRun rareA = new RiseOfTheEldraziRun(true, "158", "47", "198", "12", "129", "7", "52", "191", "215", "64", "160", "113", "219", "39", "84", "3", "218", "31", "125", "212", "158", "184", "25", "112", "75", "156", "84", "11", "225", "31", "211", "219", "107", "172", "25", "191", "227", "62", "214", "3", "160", "7", "57", "52", "107", "156", "125", "184", "6", "225", "64", "47", "211", "215", "152", "39", "198", "129", "11", "112", "62", "172", "21", "218", "227", "57"); - private static final RiseOfTheEldraziRun rareB = new RiseOfTheEldraziRun(true, "38", "176", "91", "139", "140", "183", "117", "51", "90", "169", "124", "177", "100", "55", "38", "141", "197", "89", "206", "117", "151", "17", "1", "82", "96", "140", "69", "183", "100", "169", "192", "197", "165", "90", "17", "91", "101", "139", "4", "124", "32", "176", "141", "69", "177", "82", "33", "151", "96", "206", "101", "89", "165", "32", "120"); - private static final RiseOfTheEldraziRun land = new RiseOfTheEldraziRun(false, "229", "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", "240", "241", "242", "243", "244", "245", "246", "247", "248"); - private RiseOfTheEldraziRun(boolean keepOrder, String... numbers) { - super(keepOrder, numbers); - } - } + private final CardRun commonA = new CardRun(true, "153", "16", "186", "60", "228", "148", "111", "145", "29", "213", "72", "110", "138", "36", "174", "133", "88", "222", "56", "138", "42", "203", "142", "121", "223", "88", "213", "43", "97", "56", "145", "222", "111", "187", "29", "149", "60", "16", "148", "74", "186", "130", "41", "133", "166", "223", "65", "36", "203", "149", "110", "142", "41", "65", "228", "187", "97", "72", "153", "42", "130", "174", "43", "166", "74", "121"); + private final CardRun commonB = new CardRun(true, "144", "30", "196", "83", "98", "22", "195", "135", "59", "106", "209", "30", "147", "83", "118", "195", "106", "201", "22", "144", "76", "123", "154", "44", "199", "95", "59", "76", "209", "144", "44", "19", "123", "54", "98", "201", "135", "76", "199", "106", "147", "22", "59", "196", "19", "135", "118", "30", "201", "54", "147", "95", "44", "196", "118", "154", "83", "199", "98", "19", "54", "209", "95", "154", "195", "123"); + private final CardRun commonC1 = new CardRun(true, "132", "50", "85", "207", "136", "116", "175", "26", "5", "85", "99", "155", "78", "207", "15", "150", "108", "86", "27", "210", "164", "5", "202", "99", "79", "23", "182", "136", "114", "155", "175", "78", "23", "132", "182", "15", "164", "114", "68", "210", "171", "193", "50", "86", "150", "27", "68", "202", "108", "26", "79", "171", "116", "193", "5"); + private final CardRun commonC2 = new CardRun(true, "87", "173", "73", "102", "34", "161", "208", "18", "200", "93", "105", "34", "159", "87", "24", "102", "208", "93", "18", "161", "13", "67", "46", "159", "208", "73", "200", "34", "67", "161", "126", "18", "105", "13", "194", "73", "24", "173", "102", "200", "13", "87", "194", "24", "159", "126", "67", "46", "93", "173", "194", "105", "126", "13", "46"); + private final CardRun uncommonA = new CardRun(true, "168", "8", "189", "63", "28", "127", "190", "168", "10", "40", "162", "226", "119", "204", "77", "8", "53", "103", "216", "163", "63", "48", "9", "204", "122", "28", "221", "146", "61", "189", "143", "122", "53", "226", "2", "71", "178", "143", "103", "77", "48", "216", "61", "178", "2", "127", "146", "37", "221", "71", "179", "162", "94", "9", "66", "40", "119", "179", "220", "163", "10", "190", "37", "94", "66", "220"); + private final CardRun uncommonB = new CardRun(true, "137", "80", "224", "49", "115", "217", "205", "137", "92", "45", "185", "115", "70", "49", "134", "217", "104", "181", "20", "81", "180", "170", "92", "14", "128", "157", "109", "181", "35", "58", "167", "104", "157", "224", "180", "35", "81", "131", "80", "170", "128", "188", "45", "167", "109", "20", "185", "58", "134", "205", "14", "70", "131", "188"); + private final CardRun rareA = new CardRun(true, "158", "47", "198", "12", "129", "7", "52", "191", "215", "64", "160", "113", "219", "39", "84", "3", "218", "31", "125", "212", "158", "184", "25", "112", "75", "156", "84", "11", "225", "31", "211", "219", "107", "172", "25", "191", "227", "62", "214", "3", "160", "7", "57", "52", "107", "156", "125", "184", "6", "225", "64", "47", "211", "215", "152", "39", "198", "129", "11", "112", "62", "172", "21", "218", "227", "57"); + private final CardRun rareB = new CardRun(true, "38", "176", "91", "139", "140", "183", "117", "51", "90", "169", "124", "177", "100", "55", "38", "141", "197", "89", "206", "117", "151", "17", "1", "82", "96", "140", "69", "183", "100", "169", "192", "197", "165", "90", "17", "91", "101", "139", "4", "124", "32", "176", "141", "69", "177", "82", "33", "151", "96", "206", "101", "89", "165", "32", "120"); + private final CardRun land = new CardRun(false, "229", "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", "240", "241", "242", "243", "244", "245", "246", "247", "248"); - private static class RiseOfTheEldraziStructure extends BoosterStructure { - private static final RiseOfTheEldraziStructure AAABC1C1C1C1C1C1 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonC1, - RiseOfTheEldraziRun.commonC1, - RiseOfTheEldraziRun.commonC1, - RiseOfTheEldraziRun.commonC1, - RiseOfTheEldraziRun.commonC1, - RiseOfTheEldraziRun.commonC1 - ); - private static final RiseOfTheEldraziStructure AAABBC1C1C1C1C1 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonC1, - RiseOfTheEldraziRun.commonC1, - RiseOfTheEldraziRun.commonC1, - RiseOfTheEldraziRun.commonC1, - RiseOfTheEldraziRun.commonC1 - ); - private static final RiseOfTheEldraziStructure AAABC2C2C2C2C2C2 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2 - ); - private static final RiseOfTheEldraziStructure AAABBC2C2C2C2C2 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2 - ); - private static final RiseOfTheEldraziStructure AAABBBC2C2C2C2 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2 - ); - private static final RiseOfTheEldraziStructure AAAABBBC2C2C2 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2 - ); - private static final RiseOfTheEldraziStructure AAAABBBBC2C2 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonA, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonB, - RiseOfTheEldraziRun.commonC2, - RiseOfTheEldraziRun.commonC2 - ); - private static final RiseOfTheEldraziStructure AAB = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.uncommonA, - RiseOfTheEldraziRun.uncommonA, - RiseOfTheEldraziRun.uncommonB - ); - private static final RiseOfTheEldraziStructure ABB = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.uncommonA, - RiseOfTheEldraziRun.uncommonB, - RiseOfTheEldraziRun.uncommonB - ); - private static final RiseOfTheEldraziStructure R1 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.rareA - ); - private static final RiseOfTheEldraziStructure R2 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.rareB - ); - private static final RiseOfTheEldraziStructure L1 = new RiseOfTheEldraziStructure( - RiseOfTheEldraziRun.land - ); - - private RiseOfTheEldraziStructure(CardRun... runs) { - super(runs); - } - } + private final BoosterStructure AAABC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABC2C2C2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, + commonC2, commonC2, commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBC2C2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBBC2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2 + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA); + private final BoosterStructure R2 = new BoosterStructure(rareB); + private final BoosterStructure L1 = new BoosterStructure(land); // In order for equal numbers of each common to exist, the average booster must contain: // 3.27 A commons (36 / 11) @@ -421,83 +352,43 @@ class RiseOfTheEldraziCollator implements BoosterCollator { // These numbers are the same as all sets with 101 commons in A/B/C1/C2 print runs // The only difference is ROE has two overprinted commons instead of one short-printed private final RarityConfiguration commonRuns = new RarityConfiguration( - false, - RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, - RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, - RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, - RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, - RiseOfTheEldraziStructure.AAABC1C1C1C1C1C1, - RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, - RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, - RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, - RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, - RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, - RiseOfTheEldraziStructure.AAABBC1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, - RiseOfTheEldraziStructure.AAABC2C2C2C2C2C2, - RiseOfTheEldraziStructure.AAABBC2C2C2C2C2, - RiseOfTheEldraziStructure.AAABBBC2C2C2C2, - RiseOfTheEldraziStructure.AAABBBC2C2C2C2, - RiseOfTheEldraziStructure.AAABBBC2C2C2C2, - RiseOfTheEldraziStructure.AAAABBBC2C2C2, - RiseOfTheEldraziStructure.AAAABBBC2C2C2, - RiseOfTheEldraziStructure.AAAABBBC2C2C2, - RiseOfTheEldraziStructure.AAAABBBC2C2C2, - RiseOfTheEldraziStructure.AAAABBBC2C2C2, - RiseOfTheEldraziStructure.AAAABBBBC2C2 + AAABC2C2C2C2C2C2, + AAABBC2C2C2C2C2, + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBBC2C2 ); // In order for equal numbers of each uncommon to exist, the average booster must contain: // 1.65 A uncommons (33 / 20) // 1.35 B uncommons (27 / 20) // These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs private final RarityConfiguration uncommonRuns = new RarityConfiguration( - false, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.AAB, - RiseOfTheEldraziStructure.ABB, - RiseOfTheEldraziStructure.ABB, - RiseOfTheEldraziStructure.ABB, - RiseOfTheEldraziStructure.ABB, - RiseOfTheEldraziStructure.ABB, - RiseOfTheEldraziStructure.ABB, - RiseOfTheEldraziStructure.ABB + AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, + ABB, ABB, ABB, ABB, ABB, ABB, ABB ); private final RarityConfiguration rareRuns = new RarityConfiguration( - false, - RiseOfTheEldraziStructure.R1, - RiseOfTheEldraziStructure.R1, - RiseOfTheEldraziStructure.R1, - RiseOfTheEldraziStructure.R1, - RiseOfTheEldraziStructure.R1, - RiseOfTheEldraziStructure.R1, - RiseOfTheEldraziStructure.R2, - RiseOfTheEldraziStructure.R2, - RiseOfTheEldraziStructure.R2, - RiseOfTheEldraziStructure.R2, - RiseOfTheEldraziStructure.R2 + R1, R1, R1, R1, R1, R1, + R2, R2, R2, R2, R2 ); - private final RarityConfiguration landRuns = new RarityConfiguration( - RiseOfTheEldraziStructure.L1 - ); - - @Override - public void shuffle() { - commonRuns.shuffle(); - uncommonRuns.shuffle(); - rareRuns.shuffle(); - landRuns.shuffle(); - } + private final RarityConfiguration landRuns = new RarityConfiguration(L1); @Override public List makeBooster() { diff --git a/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java b/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java index 3be9f2babb7..d9c1aaa788b 100644 --- a/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java +++ b/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java @@ -29,7 +29,7 @@ public final class StrixhavenSchoolOfMages extends ExpansionSet { } private StrixhavenSchoolOfMages() { - super("Strixhaven: School of Mages", "STX", ExpansionSet.buildDate(2021, 4, 23), SetType.EXPANSION, new StrixhavenSchoolOfMagesCollator()); + super("Strixhaven: School of Mages", "STX", ExpansionSet.buildDate(2021, 4, 23), SetType.EXPANSION); this.blockName = "Strixhaven: School of Mages"; this.hasBoosters = true; this.hasBasicLands = true; @@ -479,157 +479,85 @@ public final class StrixhavenSchoolOfMages extends ExpansionSet { .stream() .forEach(cardInfo -> inBoosterMap.put("STA_" + cardInfo.getCardNumber(), cardInfo)); } + + @Override + public BoosterCollator createCollator() { + return new StrixhavenSchoolOfMagesCollator(); + } } // Booster collation info from https://www.lethe.xyz/mtg/collation/stx.html // Using Belgian collation plus other info inferred from various sources class StrixhavenSchoolOfMagesCollator implements BoosterCollator { - private static class StrixhavenSchoolOfMagesRun extends CardRun { - private static final StrixhavenSchoolOfMagesRun commonA = new StrixhavenSchoolOfMagesRun(true, "249", "206", "182", "226", "237", "255", "210", "209", "239", "226", "251", "185", "219", "243", "206", "164", "215", "238", "256", "184", "239", "208", "254", "237", "185", "223", "251", "206", "166", "182", "164", "238", "204", "233", "184", "210", "182", "252", "243", "254", "208", "194", "255", "243", "249", "201", "204", "194", "235", "239", "166", "251", "223", "252", "237", "208", "233", "241", "219", "255", "201", "194", "164", "252", "170", "223", "241", "215", "166", "249", "237", "238", "184", "210", "243", "209", "235", "252", "204", "210", "185", "233", "249", "256", "239", "235", "209", "170", "201", "226", "241", "215", "206", "256", "208", "254", "185", "251", "226", "170", "184", "256", "235", "233", "209", "238", "254", "219", "201", "241", "182", "164", "194", "223", "170", "204", "166", "219", "215", "255"); - private static final StrixhavenSchoolOfMagesRun commonB = new StrixhavenSchoolOfMagesRun(true, "79", "9", "111", "136", "52", "85", "12", "118", "131", "40", "90", "34", "117", "141", "60", "69", "30", "93", "140", "36", "74", "16", "97", "143", "40", "87", "11", "99", "121", "39", "75", "30", "106", "122", "38", "79", "22", "97", "131", "51", "90", "23", "112", "145", "50", "87", "18", "103", "124", "43", "68", "34", "102", "142", "49", "77", "16", "93", "140", "52", "73", "32", "116", "144", "60", "63", "22", "109", "124", "55", "69", "9", "106", "143", "39", "73", "23", "116", "142", "61", "84", "8", "99", "122", "43", "74", "32", "117", "145", "36", "75", "19", "112", "141", "61", "77", "8", "109", "144", "50", "84", "18", "118", "137", "55", "68", "12", "111", "121", "38", "63", "19", "103", "136", "51", "85", "11", "102", "137", "49"); - private static final StrixhavenSchoolOfMagesRun commonC = new StrixhavenSchoolOfMagesRun(false, "263", "268", "270", "271", "273", "275"); - private static final StrixhavenSchoolOfMagesRun uncommonA = new StrixhavenSchoolOfMagesRun(true, "257", "65", "56", "132", "92", "125", "72", "62", "104", "89", "123", "54", "10", "135", "114", "53", "105", "188", "45", "115", "47", "70", "100", "129", "88", "260", "46", "76", "257", "110", "65", "13", "139", "78", "92", "26", "72", "15", "56", "89", "134", "28", "70", "91", "132", "105", "31", "123", "88", "104", "135", "54", "115", "25", "76", "13", "62", "125", "35", "188", "65", "260", "10", "114", "129", "47", "100", "15", "257", "53", "26", "110", "139", "28", "134", "56", "35", "45", "91", "72", "10", "31", "132", "54", "89", "15", "78", "46", "123", "25", "92", "135", "104", "70", "125", "105", "260", "62", "188", "129", "114", "88", "13", "53", "26", "115", "47", "76", "28", "139", "100", "35", "91", "45", "78", "134", "31", "46", "25", "110"); - private static final StrixhavenSchoolOfMagesRun uncommonB = new StrixhavenSchoolOfMagesRun(true, "229", "218", "216", "222", "212", "198", "71", "177", "202", "138", "258", "162", "81", "175", "213", "224", "220", "176", "227", "107", "247", "186", "169", "216", "261", "197", "242", "126", "262", "198", "24", "225", "207", "229", "190", "202", "81", "178", "200", "193", "41", "162", "71", "171", "59", "212", "218", "222", "227", "138", "231", "220", "258", "175", "169", "186", "107", "193", "250", "197", "176", "171", "126", "261", "247", "227", "213", "177", "262", "207", "190", "224", "225", "24", "216", "200", "242", "71", "178", "41", "212", "59", "198", "81", "231", "220", "229", "218", "202", "169", "175", "222", "193", "197", "250", "213", "186", "126", "177", "190", "258", "138", "162", "107", "261", "224", "200", "176", "178", "24", "247", "262", "207", "171", "225", "59", "231", "242", "41", "250"); - private static final StrixhavenSchoolOfMagesRun rareA = new StrixhavenSchoolOfMagesRun(false, "6", "14", "17", "20", "27", "29", "33", "37", "42", "44", "48", "58", "64", "66", "80", "82", "86", "94", "96", "98", "101", "113", "119", "127", "130", "133", "146", "147", "150", "152", "154", "155", "157", "158", "159", "160", "161", "165", "172", "173", "174", "179", "180", "181", "199", "205", "214", "217", "221", "228", "232", "234", "244", "246", "248", "253", "259", "264", "265", "266", "267", "269", "272", "274", "6", "14", "17", "20", "27", "29", "33", "37", "42", "44", "48", "58", "64", "66", "80", "82", "86", "94", "96", "98", "101", "113", "119", "127", "130", "133", "146", "147", "150", "152", "154", "155", "157", "158", "159", "160", "161", "165", "172", "173", "174", "179", "180", "181", "199", "205", "214", "217", "221", "228", "232", "234", "244", "246", "248", "253", "259", "264", "265", "266", "267", "269", "272", "274", "21", "83", "95", "128", "148", "149", "151", "153", "156", "163", "167", "168", "189", "191", "192", "196", "203", "230", "240", "245"); - private static final StrixhavenSchoolOfMagesRun commonLesson = new StrixhavenSchoolOfMagesRun(true, "4", "187", "183", "195", "211", "3", "1", "236", "2", "187", "195", "4", "183", "211", "1", "2", "236", "3", "183", "187", "4", "195", "2", "211", "3", "1", "236", "187", "183", "195", "4", "1", "211", "3", "2", "236", "187", "4", "183", "195", "3", "1", "2", "211", "236", "187", "195", "4", "183", "2", "1", "211", "3", "187", "183", "4", "236", "195", "1", "3", "211", "236", "2", "187", "4", "195", "183", "3", "211", "1", "2", "236", "187", "4", "195", "183", "211", "3", "1", "2", "236", "183", "195", "4", "187", "1", "211", "3", "2", "236", "187", "4", "195", "183", "2", "211", "3", "1", "187", "236", "4", "183", "195", "211", "1", "236", "3", "2", "195", "187", "183", "4", "2", "236", "3", "211", "1"); - private static final StrixhavenSchoolOfMagesRun rareLesson = new StrixhavenSchoolOfMagesRun(false, "5", "7", "57", "67", "108", "120", "7", "57", "67", "108", "120"); - private static final StrixhavenSchoolOfMagesRun uncommonArchive = new StrixhavenSchoolOfMagesRun(true, "18", "29", "19", "41", "4", "57", "46", "23", "35", "3", "20", "49", "37", "30", "41", "24", "9", "44", "4", "29", "3", "46", "35", "18", "57", "41", "9", "19", "24", "44", "51", "30", "23", "37", "18", "49", "20", "29", "4", "19", "35", "46", "51", "23", "30", "18", "57", "3", "37", "49", "41", "24", "9", "44", "20", "4", "23", "37", "30", "3", "35", "46", "57", "29", "9", "29", "49", "19", "51", "44", "4", "24", "41", "18", "46", "3", "20", "57", "19", "35", "44", "23", "24", "9", "51", "30", "49", "37", "20", "51"); - private static final StrixhavenSchoolOfMagesRun rareArchive = new StrixhavenSchoolOfMagesRun(false, "5", "6", "7", "8", "10", "13", "14", "15", "16", "21", "26", "28", "31", "32", "34", "38", "39", "42", "45", "47", "48", "52", "53", "56", "58", "59", "60", "61", "62", "63", "5", "6", "7", "8", "10", "13", "14", "15", "16", "21", "26", "28", "31", "32", "34", "38", "39", "42", "45", "47", "48", "52", "53", "56", "58", "59", "60", "61", "62", "63", "1", "2", "11", "12", "17", "22", "25", "27", "33", "36", "40", "43", "50", "54", "55"); + private final CardRun commonA = new CardRun(true, "249", "206", "182", "226", "237", "255", "210", "209", "239", "226", "251", "185", "219", "243", "206", "164", "215", "238", "256", "184", "239", "208", "254", "237", "185", "223", "251", "206", "166", "182", "164", "238", "204", "233", "184", "210", "182", "252", "243", "254", "208", "194", "255", "243", "249", "201", "204", "194", "235", "239", "166", "251", "223", "252", "237", "208", "233", "241", "219", "255", "201", "194", "164", "252", "170", "223", "241", "215", "166", "249", "237", "238", "184", "210", "243", "209", "235", "252", "204", "210", "185", "233", "249", "256", "239", "235", "209", "170", "201", "226", "241", "215", "206", "256", "208", "254", "185", "251", "226", "170", "184", "256", "235", "233", "209", "238", "254", "219", "201", "241", "182", "164", "194", "223", "170", "204", "166", "219", "215", "255"); + private final CardRun commonB = new CardRun(true, "79", "9", "111", "136", "52", "85", "12", "118", "131", "40", "90", "34", "117", "141", "60", "69", "30", "93", "140", "36", "74", "16", "97", "143", "40", "87", "11", "99", "121", "39", "75", "30", "106", "122", "38", "79", "22", "97", "131", "51", "90", "23", "112", "145", "50", "87", "18", "103", "124", "43", "68", "34", "102", "142", "49", "77", "16", "93", "140", "52", "73", "32", "116", "144", "60", "63", "22", "109", "124", "55", "69", "9", "106", "143", "39", "73", "23", "116", "142", "61", "84", "8", "99", "122", "43", "74", "32", "117", "145", "36", "75", "19", "112", "141", "61", "77", "8", "109", "144", "50", "84", "18", "118", "137", "55", "68", "12", "111", "121", "38", "63", "19", "103", "136", "51", "85", "11", "102", "137", "49"); + private final CardRun commonC = new CardRun(false, "263", "268", "270", "271", "273", "275"); + private final CardRun uncommonA = new CardRun(true, "257", "65", "56", "132", "92", "125", "72", "62", "104", "89", "123", "54", "10", "135", "114", "53", "105", "188", "45", "115", "47", "70", "100", "129", "88", "260", "46", "76", "257", "110", "65", "13", "139", "78", "92", "26", "72", "15", "56", "89", "134", "28", "70", "91", "132", "105", "31", "123", "88", "104", "135", "54", "115", "25", "76", "13", "62", "125", "35", "188", "65", "260", "10", "114", "129", "47", "100", "15", "257", "53", "26", "110", "139", "28", "134", "56", "35", "45", "91", "72", "10", "31", "132", "54", "89", "15", "78", "46", "123", "25", "92", "135", "104", "70", "125", "105", "260", "62", "188", "129", "114", "88", "13", "53", "26", "115", "47", "76", "28", "139", "100", "35", "91", "45", "78", "134", "31", "46", "25", "110"); + private final CardRun uncommonB = new CardRun(true, "229", "218", "216", "222", "212", "198", "71", "177", "202", "138", "258", "162", "81", "175", "213", "224", "220", "176", "227", "107", "247", "186", "169", "216", "261", "197", "242", "126", "262", "198", "24", "225", "207", "229", "190", "202", "81", "178", "200", "193", "41", "162", "71", "171", "59", "212", "218", "222", "227", "138", "231", "220", "258", "175", "169", "186", "107", "193", "250", "197", "176", "171", "126", "261", "247", "227", "213", "177", "262", "207", "190", "224", "225", "24", "216", "200", "242", "71", "178", "41", "212", "59", "198", "81", "231", "220", "229", "218", "202", "169", "175", "222", "193", "197", "250", "213", "186", "126", "177", "190", "258", "138", "162", "107", "261", "224", "200", "176", "178", "24", "247", "262", "207", "171", "225", "59", "231", "242", "41", "250"); + private final CardRun rareA = new CardRun(false, "6", "14", "17", "20", "27", "29", "33", "37", "42", "44", "48", "58", "64", "66", "80", "82", "86", "94", "96", "98", "101", "113", "119", "127", "130", "133", "146", "147", "150", "152", "154", "155", "157", "158", "159", "160", "161", "165", "172", "173", "174", "179", "180", "181", "199", "205", "214", "217", "221", "228", "232", "234", "244", "246", "248", "253", "259", "264", "265", "266", "267", "269", "272", "274", "6", "14", "17", "20", "27", "29", "33", "37", "42", "44", "48", "58", "64", "66", "80", "82", "86", "94", "96", "98", "101", "113", "119", "127", "130", "133", "146", "147", "150", "152", "154", "155", "157", "158", "159", "160", "161", "165", "172", "173", "174", "179", "180", "181", "199", "205", "214", "217", "221", "228", "232", "234", "244", "246", "248", "253", "259", "264", "265", "266", "267", "269", "272", "274", "21", "83", "95", "128", "148", "149", "151", "153", "156", "163", "167", "168", "189", "191", "192", "196", "203", "230", "240", "245"); + private final CardRun commonLesson = new CardRun(true, "4", "187", "183", "195", "211", "3", "1", "236", "2", "187", "195", "4", "183", "211", "1", "2", "236", "3", "183", "187", "4", "195", "2", "211", "3", "1", "236", "187", "183", "195", "4", "1", "211", "3", "2", "236", "187", "4", "183", "195", "3", "1", "2", "211", "236", "187", "195", "4", "183", "2", "1", "211", "3", "187", "183", "4", "236", "195", "1", "3", "211", "236", "2", "187", "4", "195", "183", "3", "211", "1", "2", "236", "187", "4", "195", "183", "211", "3", "1", "2", "236", "183", "195", "4", "187", "1", "211", "3", "2", "236", "187", "4", "195", "183", "2", "211", "3", "1", "187", "236", "4", "183", "195", "211", "1", "236", "3", "2", "195", "187", "183", "4", "2", "236", "3", "211", "1"); + private final CardRun rareLesson = new CardRun(false, "5", "7", "57", "67", "108", "120", "7", "57", "67", "108", "120"); + private final CardRun uncommonArchive = new CardRun(true, "18", "29", "19", "41", "4", "57", "46", "23", "35", "3", "20", "49", "37", "30", "41", "24", "9", "44", "4", "29", "3", "46", "35", "18", "57", "41", "9", "19", "24", "44", "51", "30", "23", "37", "18", "49", "20", "29", "4", "19", "35", "46", "51", "23", "30", "18", "57", "3", "37", "49", "41", "24", "9", "44", "20", "4", "23", "37", "30", "3", "35", "46", "57", "29", "9", "29", "49", "19", "51", "44", "4", "24", "41", "18", "46", "3", "20", "57", "19", "35", "44", "23", "24", "9", "51", "30", "49", "37", "20", "51"); + private final CardRun rareArchive = new CardRun(false, "5", "6", "7", "8", "10", "13", "14", "15", "16", "21", "26", "28", "31", "32", "34", "38", "39", "42", "45", "47", "48", "52", "53", "56", "58", "59", "60", "61", "62", "63", "5", "6", "7", "8", "10", "13", "14", "15", "16", "21", "26", "28", "31", "32", "34", "38", "39", "42", "45", "47", "48", "52", "53", "56", "58", "59", "60", "61", "62", "63", "1", "2", "11", "12", "17", "22", "25", "27", "33", "36", "40", "43", "50", "54", "55"); - private StrixhavenSchoolOfMagesRun(boolean keepOrder, String... numbers) { - super(keepOrder, numbers); - } - } - - private static class StrixhavenSchoolOfMagesStructure extends BoosterStructure { - private static final StrixhavenSchoolOfMagesStructure AABBBBBBC = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonC - ); - private static final StrixhavenSchoolOfMagesStructure AAABBBBBC = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonC - ); - private static final StrixhavenSchoolOfMagesStructure AAABBBBBB = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonA, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB, - StrixhavenSchoolOfMagesRun.commonB - ); - private static final StrixhavenSchoolOfMagesStructure ABB = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.uncommonA, - StrixhavenSchoolOfMagesRun.uncommonB, - StrixhavenSchoolOfMagesRun.uncommonB - ); - private static final StrixhavenSchoolOfMagesStructure AAB = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.uncommonA, - StrixhavenSchoolOfMagesRun.uncommonA, - StrixhavenSchoolOfMagesRun.uncommonB - ); - private static final StrixhavenSchoolOfMagesStructure R1 = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.rareA - ); - private static final StrixhavenSchoolOfMagesStructure L1 = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.commonLesson - ); - private static final StrixhavenSchoolOfMagesStructure L2 = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.rareLesson - ); - private static final StrixhavenSchoolOfMagesStructure A1 = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.uncommonArchive - ); - private static final StrixhavenSchoolOfMagesStructure A2 = new StrixhavenSchoolOfMagesStructure( - StrixhavenSchoolOfMagesRun.rareArchive - ); - - private StrixhavenSchoolOfMagesStructure(CardRun... runs) { - super(runs); - } - } + private final BoosterStructure AABBBBBBC = new BoosterStructure( + commonA, commonA, + commonB, commonB, commonB, commonB, commonB, commonB, + commonC + ); + private final BoosterStructure AAABBBBBC = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, commonB, commonB, + commonC + ); + private final BoosterStructure AAABBBBBB = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, commonB, commonB, commonB + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA); + private final BoosterStructure L1 = new BoosterStructure(commonLesson); + private final BoosterStructure L2 = new BoosterStructure(rareLesson); + private final BoosterStructure A1 = new BoosterStructure(uncommonArchive); + private final BoosterStructure A2 = new BoosterStructure(rareArchive); // In order for equal numbers of each common to exist, the average booster must contain: // 2.81 A commons (45 / 16) // 5.63 B commons (90 / 16) // 0.56 C commons ( 9 / 16) private final RarityConfiguration commonRuns = new RarityConfiguration( - false, - StrixhavenSchoolOfMagesStructure.AABBBBBBC, - StrixhavenSchoolOfMagesStructure.AABBBBBBC, - StrixhavenSchoolOfMagesStructure.AABBBBBBC, - StrixhavenSchoolOfMagesStructure.AAABBBBBC, - StrixhavenSchoolOfMagesStructure.AAABBBBBC, - StrixhavenSchoolOfMagesStructure.AAABBBBBC, - StrixhavenSchoolOfMagesStructure.AAABBBBBC, - StrixhavenSchoolOfMagesStructure.AAABBBBBC, - StrixhavenSchoolOfMagesStructure.AAABBBBBC, - StrixhavenSchoolOfMagesStructure.AAABBBBBB, - StrixhavenSchoolOfMagesStructure.AAABBBBBB, - StrixhavenSchoolOfMagesStructure.AAABBBBBB, - StrixhavenSchoolOfMagesStructure.AAABBBBBB, - StrixhavenSchoolOfMagesStructure.AAABBBBBB, - StrixhavenSchoolOfMagesStructure.AAABBBBBB, - StrixhavenSchoolOfMagesStructure.AAABBBBBB - ); - private final RarityConfiguration uncommonRuns = new RarityConfiguration( - StrixhavenSchoolOfMagesStructure.ABB, - StrixhavenSchoolOfMagesStructure.AAB - ); - private final RarityConfiguration rareRuns = new RarityConfiguration( - StrixhavenSchoolOfMagesStructure.R1 + AABBBBBBC, + AABBBBBBC, + AABBBBBBC, + AAABBBBBC, + AAABBBBBC, + AAABBBBBC, + AAABBBBBC, + AAABBBBBC, + AAABBBBBC, + AAABBBBBB, + AAABBBBBB, + AAABBBBBB, + AAABBBBBB, + AAABBBBBB, + AAABBBBBB, + AAABBBBBB ); + private final RarityConfiguration uncommonRuns = new RarityConfiguration(AAB, ABB); + private final RarityConfiguration rareRuns = new RarityConfiguration(R1); // The ratio of common to rare/mythic Lesson cards hasn't been officially disclosed // rare/mythic Lessons were observed in 37 out of 468 packs, which is very close to 2/25 private final RarityConfiguration lessonRuns = new RarityConfiguration( - false, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L1, - StrixhavenSchoolOfMagesStructure.L2, StrixhavenSchoolOfMagesStructure.L2 + L1, L1, L1, L1, L1, + L1, L1, L1, L1, L1, + L1, L1, L1, L1, L1, + L1, L1, L1, L1, L1, + L1, L1, L1, L2, L2 ); - private final RarityConfiguration archiveRuns = new RarityConfiguration( - false, - StrixhavenSchoolOfMagesStructure.A1, StrixhavenSchoolOfMagesStructure.A1, - StrixhavenSchoolOfMagesStructure.A2 - ); - - @Override - public void shuffle() { - commonRuns.shuffle(); - uncommonRuns.shuffle(); - rareRuns.shuffle(); - lessonRuns.shuffle(); - archiveRuns.shuffle(); - } + private final RarityConfiguration archiveRuns = new RarityConfiguration(A1, A1, A2); @Override public List makeBooster() { diff --git a/Mage.Sets/src/mage/sets/TherosBeyondDeath.java b/Mage.Sets/src/mage/sets/TherosBeyondDeath.java index 65ca9b2bf8b..fdf322c4a7d 100644 --- a/Mage.Sets/src/mage/sets/TherosBeyondDeath.java +++ b/Mage.Sets/src/mage/sets/TherosBeyondDeath.java @@ -23,7 +23,7 @@ public final class TherosBeyondDeath extends ExpansionSet { } private TherosBeyondDeath() { - super("Theros Beyond Death", "THB", ExpansionSet.buildDate(2020, 1, 24), SetType.EXPANSION, new TherosBeyondDeathCollator()); + super("Theros Beyond Death", "THB", ExpansionSet.buildDate(2020, 1, 24), SetType.EXPANSION); this.blockName = "Theros Beyond Death"; this.hasBoosters = true; this.numBoosterLands = 1; @@ -392,113 +392,56 @@ public final class TherosBeyondDeath extends ExpansionSet { cards.add(new SetCardInfo("Wolfwillow Haven", 357, Rarity.UNCOMMON, mage.cards.w.WolfwillowHaven.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Wrap in Flames", 164, Rarity.COMMON, mage.cards.w.WrapInFlames.class)); } + + @Override + public BoosterCollator createCollator() { + return new TherosBeyondDeathCollator(); + } } // Booster collation info from https://www.lethe.xyz/mtg/collation/thb.html // Using USA collation for common/uncommon, rare collation inferred from other sets class TherosBeyondDeathCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "155", "29", "79", "127", "38", "57", "159", "41", "66", "140", "30", "78", "163", "28", "56", "137", "25", "68", "144", "20", "67", "146", "26", "49", "134", "40", "61", "159", "29", "51", "164", "17", "57", "149", "38", "66", "127", "30", "47", "144", "36", "79", "155", "41", "67", "137", "28", "78", "140", "25", "56", "163", "20", "49", "146", "40", "68", "134", "17", "51", "149", "26", "47", "164", "36", "61"); + private final CardRun commonB = new CardRun(true, "186", "85", "191", "116", "201", "103", "202", "115", "184", "120", "194", "110", "192", "88", "177", "113", "171", "86", "195", "109", "179", "114", "202", "85", "201", "103", "184", "116", "186", "115", "192", "110", "191", "114", "177", "120", "194", "88", "171", "113", "179", "86", "195", "109", "201", "85", "184", "116", "202", "110", "186", "103", "191", "115", "192", "114", "179", "113", "194", "109", "195", "86", "177", "88", "171", "120"); + private final CardRun commonC1 = new CardRun(true, "203", "154", "106", "77", "10", "174", "58", "16", "141", "238", "122", "46", "173", "152", "22", "240", "100", "74", "200", "142", "97", "11", "48", "203", "241", "154", "106", "35", "82", "174", "77", "10", "240", "141", "100", "58", "238", "122", "232", "152", "22", "46", "111", "173", "241", "16", "74", "97", "48", "11", "200", "142", "111", "82", "35"); + private final CardRun commonC2 = new CardRun(true, "44", "96", "197", "145", "232", "34", "126", "204", "249", "54", "135", "231", "187", "175", "44", "143", "95", "96", "197", "135", "107", "6", "32", "204", "126", "34", "54", "249", "145", "231", "187", "96", "6", "143", "44", "107", "34", "175", "135", "249", "95", "197", "54", "204", "126", "32", "6", "175", "95", "231", "145", "107", "187", "32", "143"); + private final CardRun uncommonA = new CardRun(true, "223", "65", "153", "8", "112", "227", "99", "167", "33", "138", "4", "189", "228", "45", "59", "180", "105", "1", "136", "196", "206", "139", "83", "89", "233", "31", "131", "91", "219", "193", "27", "133", "64", "199", "213", "264", "42", "153", "205", "8", "136", "4", "189", "33", "223", "2", "138", "112", "27", "233", "260", "180", "31", "59", "99", "131", "105", "267", "81", "139", "228", "167", "133", "219", "65", "1", "83", "125", "206", "193", "42", "91", "227", "89", "199", "153", "8", "81", "213", "64", "112", "223", "4", "136", "205", "105", "139", "99", "65", "2", "180", "228", "59", "1", "233", "45", "189", "227", "33", "196", "83", "138", "206", "42", "219", "167", "131", "31", "89", "193", "91", "125", "213", "199", "81", "27", "2", "64", "133", "205"); + private final CardRun uncommonB = new CardRun(true, "226", "101", "128", "183", "21", "234", "87", "50", "242", "176", "239", "132", "9", "216", "62", "119", "172", "160", "104", "69", "168", "225", "130", "237", "63", "15", "102", "166", "5", "129", "121", "53", "239", "70", "182", "128", "21", "234", "92", "69", "101", "160", "23", "230", "75", "130", "104", "172", "50", "7", "162", "87", "183", "226", "62", "216", "258", "132", "176", "237", "263", "15", "242", "63", "5", "225", "168", "129", "121", "53", "230", "21", "70", "102", "166", "128", "92", "234", "23", "183", "160", "104", "75", "226", "162", "7", "239", "182", "9", "132", "101", "69", "172", "216", "242", "50", "176", "87", "225", "62", "15", "168", "119", "237", "130", "5", "70", "102", "166", "63", "23", "129", "121", "53", "182", "7", "162", "230", "92", "75"); + private final CardRun rareA = new CardRun(false, "207", "84", "165", "3", "43", "209", "210", "212", "214", "169", "90", "12", "13", "215", "94", "217", "98", "218", "19", "24", "222", "243", "178", "55", "181", "108", "188", "235", "148", "60", "151", "198", "236", "37", "156", "157", "39", "158", "244", "245", "246", "247", "248", "72", "73", "124", "170", "76", "117", "118", "161", "80", "123", "207", "84", "165", "3", "43", "209", "210", "212", "214", "169", "90", "12", "13", "215", "94", "217", "98", "218", "19", "24", "222", "243", "178", "55", "181", "108", "188", "235", "148", "60", "151", "198", "236", "37", "156", "157", "39", "158", "244", "245", "246", "247", "248", "72", "73", "124", "170", "76", "117", "118", "161", "80", "123", "14", "18", "52", "71", "93", "147", "150", "185", "190", "208", "211", "220", "221", "224", "229"); + private final CardRun rareB = new CardRun(false, "207", "84", "165", "3", "43", "209", "210", "212", "214", "169", "90", "12", "13", "215", "94", "217", "98", "218", "19", "24", "222", "243", "178", "55", "181", "108", "188", "235", "148", "60", "151", "198", "236", "37", "156", "157", "39", "158", "244", "245", "246", "247", "248", "72", "73", "124", "170", "76", "117", "118", "161", "80", "123", "207", "84", "165", "3", "43", "209", "210", "212", "214", "169", "90", "12", "13", "215", "94", "217", "98", "218", "19", "24", "222", "243", "178", "55", "181", "108", "188", "235", "148", "60", "151", "198", "236", "37", "156", "157", "39", "158", "244", "245", "246", "247", "248", "72", "73", "124", "170", "76", "117", "118", "161", "80", "123", "255", "259", "52", "261", "262", "147", "265", "266", "190", "256", "257", "268", "221", "224", "229"); + private final CardRun land = new CardRun(false, "250", "251", "252", "253", "254"); - private static class TherosBeyondDeathRun extends CardRun { - private static final TherosBeyondDeathRun commonA = new TherosBeyondDeathRun(true, "155", "29", "79", "127", "38", "57", "159", "41", "66", "140", "30", "78", "163", "28", "56", "137", "25", "68", "144", "20", "67", "146", "26", "49", "134", "40", "61", "159", "29", "51", "164", "17", "57", "149", "38", "66", "127", "30", "47", "144", "36", "79", "155", "41", "67", "137", "28", "78", "140", "25", "56", "163", "20", "49", "146", "40", "68", "134", "17", "51", "149", "26", "47", "164", "36", "61"); - private static final TherosBeyondDeathRun commonB = new TherosBeyondDeathRun(true, "186", "85", "191", "116", "201", "103", "202", "115", "184", "120", "194", "110", "192", "88", "177", "113", "171", "86", "195", "109", "179", "114", "202", "85", "201", "103", "184", "116", "186", "115", "192", "110", "191", "114", "177", "120", "194", "88", "171", "113", "179", "86", "195", "109", "201", "85", "184", "116", "202", "110", "186", "103", "191", "115", "192", "114", "179", "113", "194", "109", "195", "86", "177", "88", "171", "120"); - private static final TherosBeyondDeathRun commonC1 = new TherosBeyondDeathRun(true, "203", "154", "106", "77", "10", "174", "58", "16", "141", "238", "122", "46", "173", "152", "22", "240", "100", "74", "200", "142", "97", "11", "48", "203", "241", "154", "106", "35", "82", "174", "77", "10", "240", "141", "100", "58", "238", "122", "232", "152", "22", "46", "111", "173", "241", "16", "74", "97", "48", "11", "200", "142", "111", "82", "35"); - private static final TherosBeyondDeathRun commonC2 = new TherosBeyondDeathRun(true, "44", "96", "197", "145", "232", "34", "126", "204", "249", "54", "135", "231", "187", "175", "44", "143", "95", "96", "197", "135", "107", "6", "32", "204", "126", "34", "54", "249", "145", "231", "187", "96", "6", "143", "44", "107", "34", "175", "135", "249", "95", "197", "54", "204", "126", "32", "6", "175", "95", "231", "145", "107", "187", "32", "143"); - private static final TherosBeyondDeathRun uncommonA = new TherosBeyondDeathRun(true, "223", "65", "153", "8", "112", "227", "99", "167", "33", "138", "4", "189", "228", "45", "59", "180", "105", "1", "136", "196", "206", "139", "83", "89", "233", "31", "131", "91", "219", "193", "27", "133", "64", "199", "213", "264", "42", "153", "205", "8", "136", "4", "189", "33", "223", "2", "138", "112", "27", "233", "260", "180", "31", "59", "99", "131", "105", "267", "81", "139", "228", "167", "133", "219", "65", "1", "83", "125", "206", "193", "42", "91", "227", "89", "199", "153", "8", "81", "213", "64", "112", "223", "4", "136", "205", "105", "139", "99", "65", "2", "180", "228", "59", "1", "233", "45", "189", "227", "33", "196", "83", "138", "206", "42", "219", "167", "131", "31", "89", "193", "91", "125", "213", "199", "81", "27", "2", "64", "133", "205"); - private static final TherosBeyondDeathRun uncommonB = new TherosBeyondDeathRun(true, "226", "101", "128", "183", "21", "234", "87", "50", "242", "176", "239", "132", "9", "216", "62", "119", "172", "160", "104", "69", "168", "225", "130", "237", "63", "15", "102", "166", "5", "129", "121", "53", "239", "70", "182", "128", "21", "234", "92", "69", "101", "160", "23", "230", "75", "130", "104", "172", "50", "7", "162", "87", "183", "226", "62", "216", "258", "132", "176", "237", "263", "15", "242", "63", "5", "225", "168", "129", "121", "53", "230", "21", "70", "102", "166", "128", "92", "234", "23", "183", "160", "104", "75", "226", "162", "7", "239", "182", "9", "132", "101", "69", "172", "216", "242", "50", "176", "87", "225", "62", "15", "168", "119", "237", "130", "5", "70", "102", "166", "63", "23", "129", "121", "53", "182", "7", "162", "230", "92", "75"); - private static final TherosBeyondDeathRun rareA = new TherosBeyondDeathRun(false, "207", "84", "165", "3", "43", "209", "210", "212", "214", "169", "90", "12", "13", "215", "94", "217", "98", "218", "19", "24", "222", "243", "178", "55", "181", "108", "188", "235", "148", "60", "151", "198", "236", "37", "156", "157", "39", "158", "244", "245", "246", "247", "248", "72", "73", "124", "170", "76", "117", "118", "161", "80", "123", "207", "84", "165", "3", "43", "209", "210", "212", "214", "169", "90", "12", "13", "215", "94", "217", "98", "218", "19", "24", "222", "243", "178", "55", "181", "108", "188", "235", "148", "60", "151", "198", "236", "37", "156", "157", "39", "158", "244", "245", "246", "247", "248", "72", "73", "124", "170", "76", "117", "118", "161", "80", "123", "14", "18", "52", "71", "93", "147", "150", "185", "190", "208", "211", "220", "221", "224", "229"); - private static final TherosBeyondDeathRun rareB = new TherosBeyondDeathRun(false, "207", "84", "165", "3", "43", "209", "210", "212", "214", "169", "90", "12", "13", "215", "94", "217", "98", "218", "19", "24", "222", "243", "178", "55", "181", "108", "188", "235", "148", "60", "151", "198", "236", "37", "156", "157", "39", "158", "244", "245", "246", "247", "248", "72", "73", "124", "170", "76", "117", "118", "161", "80", "123", "207", "84", "165", "3", "43", "209", "210", "212", "214", "169", "90", "12", "13", "215", "94", "217", "98", "218", "19", "24", "222", "243", "178", "55", "181", "108", "188", "235", "148", "60", "151", "198", "236", "37", "156", "157", "39", "158", "244", "245", "246", "247", "248", "72", "73", "124", "170", "76", "117", "118", "161", "80", "123", "255", "259", "52", "261", "262", "147", "265", "266", "190", "256", "257", "268", "221", "224", "229"); - private static final TherosBeyondDeathRun land = new TherosBeyondDeathRun(false, "250", "251", "252", "253", "254"); - - private TherosBeyondDeathRun(boolean keepOrder, String... numbers) { - super(keepOrder, numbers); - } - } - - private static class TherosBeyondDeathStructure extends BoosterStructure { - private static final TherosBeyondDeathStructure AABBC1C1C1C1C1C1 = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonC1, - TherosBeyondDeathRun.commonC1, - TherosBeyondDeathRun.commonC1, - TherosBeyondDeathRun.commonC1, - TherosBeyondDeathRun.commonC1, - TherosBeyondDeathRun.commonC1 - ); - private static final TherosBeyondDeathStructure AAABBC1C1C1C1C1 = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonC1, - TherosBeyondDeathRun.commonC1, - TherosBeyondDeathRun.commonC1, - TherosBeyondDeathRun.commonC1, - TherosBeyondDeathRun.commonC1 - ); - private static final TherosBeyondDeathStructure AAAABBC2C2C2C2 = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonC2, - TherosBeyondDeathRun.commonC2, - TherosBeyondDeathRun.commonC2, - TherosBeyondDeathRun.commonC2 - ); - private static final TherosBeyondDeathStructure AAAABBBC2C2C2 = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonC2, - TherosBeyondDeathRun.commonC2, - TherosBeyondDeathRun.commonC2 - ); - private static final TherosBeyondDeathStructure AAAABBBBC2C2 = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonA, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonB, - TherosBeyondDeathRun.commonC2, - TherosBeyondDeathRun.commonC2 - ); - private static final TherosBeyondDeathStructure ABB = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.uncommonA, - TherosBeyondDeathRun.uncommonB, - TherosBeyondDeathRun.uncommonB - ); - private static final TherosBeyondDeathStructure AAB = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.uncommonA, - TherosBeyondDeathRun.uncommonA, - TherosBeyondDeathRun.uncommonB - ); - private static final TherosBeyondDeathStructure R1 = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.rareA - ); - private static final TherosBeyondDeathStructure R2 = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.rareB - ); - private static final TherosBeyondDeathStructure L1 = new TherosBeyondDeathStructure( - TherosBeyondDeathRun.land - ); - - private TherosBeyondDeathStructure(CardRun... runs) { - super(runs); - } - } + private final BoosterStructure AABBC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAAABBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBBC2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2 + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA); + private final BoosterStructure R2 = new BoosterStructure(rareB); + private final BoosterStructure L1 = new BoosterStructure(land); // In order for equal numbers of each common to exist, the average booster must contain: // 3.27 A commons (36 / 11) @@ -508,53 +451,33 @@ class TherosBeyondDeathCollator implements BoosterCollator { // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs // and with 10 common slots per booster private final RarityConfiguration commonRuns = new RarityConfiguration( - false, - TherosBeyondDeathStructure.AABBC1C1C1C1C1C1, - TherosBeyondDeathStructure.AABBC1C1C1C1C1C1, - TherosBeyondDeathStructure.AABBC1C1C1C1C1C1, - TherosBeyondDeathStructure.AABBC1C1C1C1C1C1, - TherosBeyondDeathStructure.AABBC1C1C1C1C1C1, - TherosBeyondDeathStructure.AAABBC1C1C1C1C1, - TherosBeyondDeathStructure.AAABBC1C1C1C1C1, - TherosBeyondDeathStructure.AAABBC1C1C1C1C1, - TherosBeyondDeathStructure.AAABBC1C1C1C1C1, - TherosBeyondDeathStructure.AAABBC1C1C1C1C1, - TherosBeyondDeathStructure.AAABBC1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, - TherosBeyondDeathStructure.AAAABBC2C2C2C2, - TherosBeyondDeathStructure.AAAABBC2C2C2C2, - TherosBeyondDeathStructure.AAAABBC2C2C2C2, - TherosBeyondDeathStructure.AAAABBC2C2C2C2, - TherosBeyondDeathStructure.AAAABBC2C2C2C2, - TherosBeyondDeathStructure.AAAABBC2C2C2C2, - TherosBeyondDeathStructure.AAAABBC2C2C2C2, - TherosBeyondDeathStructure.AAAABBC2C2C2C2, - TherosBeyondDeathStructure.AAAABBBC2C2C2, - TherosBeyondDeathStructure.AAAABBBC2C2C2, - TherosBeyondDeathStructure.AAAABBBBC2C2 + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBBC2C2 ); - private final RarityConfiguration uncommonRuns = new RarityConfiguration( - TherosBeyondDeathStructure.ABB, - TherosBeyondDeathStructure.AAB - ); - private final RarityConfiguration rareRuns = new RarityConfiguration( - false, - TherosBeyondDeathStructure.R1, - TherosBeyondDeathStructure.R1, - TherosBeyondDeathStructure.R2 - ); - private final RarityConfiguration landRuns = new RarityConfiguration( - TherosBeyondDeathStructure.L1 - ); - - - @Override - public void shuffle() { - commonRuns.shuffle(); - uncommonRuns.shuffle(); - rareRuns.shuffle(); - landRuns.shuffle(); - } + private final RarityConfiguration uncommonRuns = new RarityConfiguration(AAB, ABB); + private final RarityConfiguration rareRuns = new RarityConfiguration(R1, R1, R2); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); @Override public List makeBooster() { diff --git a/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java b/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java index 7952dc33229..dce4984880a 100644 --- a/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java +++ b/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java @@ -28,7 +28,7 @@ public class TimeSpiralRemastered extends ExpansionSet { private final List savedSpecialBonus = new ArrayList<>(); private TimeSpiralRemastered() { - super("Time Spiral Remastered", "TSR", ExpansionSet.buildDate(2021, 3, 19), SetType.SUPPLEMENTAL, new TimeSpiralRemasteredCollator()); + super("Time Spiral Remastered", "TSR", ExpansionSet.buildDate(2021, 3, 19), SetType.SUPPLEMENTAL); this.hasBoosters = true; this.hasBasicLands = true; this.maxCardNumberInBooster = 410; @@ -461,121 +461,60 @@ public class TimeSpiralRemastered extends ExpansionSet { return new ArrayList<>(savedSpecialBonus); } + + @Override + public BoosterCollator createCollator() { + return new TimeSpiralRemasteredCollator(); + } } // Booster collation info from https://www.lethe.xyz/mtg/collation/tsr.html // Using USA collation for common/uncommon, rare and bonus sheet are standard class TimeSpiralRemasteredCollator implements BoosterCollator { - private static class TimeSpiralRemasteredRun extends CardRun { - private static final TimeSpiralRemasteredRun commonA = new TimeSpiralRemasteredRun(true, "176", "65", "177", "94", "192", "70", "86", "191", "84", "168", "80", "69", "173", "73", "154", "87", "197", "92", "166", "67", "183", "78", "171", "81", "155", "95", "190", "70", "191", "58", "168", "63", "162", "89", "157", "65", "173", "69", "159", "73", "176", "94", "192", "86", "197", "84", "166", "67", "177", "92", "171", "80", "162", "81", "155", "78", "154", "87", "157", "95", "190", "58", "159", "63", "183", "89"); - private static final TimeSpiralRemasteredRun commonB = new TimeSpiralRemasteredRun(true, "110", "245", "135", "241", "104", "226", "233", "108", "236", "107", "207", "229", "101", "201", "116", "199", "123", "246", "130", "240", "109", "245", "103", "202", "118", "221", "117", "230", "105", "212", "148", "243", "134", "237", "135", "226", "110", "236", "108", "233", "107", "241", "101", "229", "116", "201", "118", "199", "104", "246", "109", "240", "130", "207", "103", "202", "117", "221", "105", "230", "134", "243", "148", "212", "123", "237"); - private static final TimeSpiralRemasteredRun commonC1 = new TimeSpiralRemasteredRun(true, "44", "223", "20", "140", "53", "21", "181", "17", "184", "2", "205", "151", "42", "119", "74", "1", "161", "272", "10", "228", "12", "68", "6", "285", "7", "48", "53", "20", "133", "44", "223", "21", "140", "151", "2", "184", "17", "74", "42", "181", "228", "1", "10", "119", "12", "68", "285", "48", "272", "6", "205", "161", "7", "133", "185"); - private static final TimeSpiralRemasteredRun commonC2 = new TimeSpiralRemasteredRun(true, "25", "147", "31", "64", "269", "47", "206", "43", "178", "132", "263", "18", "200", "59", "26", "145", "9", "66", "50", "14", "147", "28", "232", "49", "122", "71", "22", "25", "165", "132", "31", "206", "43", "47", "269", "64", "18", "178", "200", "26", "263", "59", "9", "145", "14", "66", "50", "165", "28", "122", "232", "22", "49", "71", "185"); - private static final TimeSpiralRemasteredRun uncommonA = new TimeSpiralRemasteredRun(true, "217", "283", "196", "242", "126", "194", "13", "222", "248", "40", "128", "195", "204", "5", "189", "113", "56", "249", "218", "180", "149", "276", "16", "258", "224", "79", "163", "142", "19", "61", "273", "153", "85", "30", "111", "170", "57", "231", "24", "99", "143", "279", "100", "174", "213", "55", "131", "46", "244", "126", "196", "82", "219", "102", "188", "208", "33", "120", "60", "186", "217", "283", "169", "124", "242", "194", "61", "218", "16", "153", "142", "258", "13", "224", "249", "113", "189", "79", "248", "204", "19", "149", "163", "5", "276", "222", "195", "40", "56", "180", "128", "273", "85", "30", "111", "170", "231", "57", "24", "143", "99", "279", "174", "213", "100", "131", "46", "244", "124", "55", "208", "188", "120", "60", "186", "33", "219", "169", "102", "82"); - private static final TimeSpiralRemasteredRun uncommonB = new TimeSpiralRemasteredRun(true, "38", "152", "141", "29", "72", "225", "11", "158", "88", "271", "23", "76", "247", "160", "139", "250", "216", "288", "264", "45", "93", "252", "137", "275", "214", "39", "54", "129", "193", "227", "35", "112", "83", "282", "38", "164", "254", "115", "90", "37", "211", "152", "72", "141", "29", "247", "250", "158", "88", "11", "271", "225", "76", "139", "23", "160", "216", "288", "93", "45", "252", "137", "275", "214", "54", "264", "39", "227", "129", "193", "35", "282", "83", "112", "37", "254", "211", "38", "115", "90", "164", "29", "72", "152", "141", "225", "11", "88", "271", "158", "247", "23", "139", "76", "250", "160", "288", "252", "216", "93", "45", "137", "264", "214", "54", "275", "227", "129", "39", "193", "112", "35", "282", "83", "254", "211", "115", "37", "164", "90"); - private static final TimeSpiralRemasteredRun rare = new TimeSpiralRemasteredRun(false, "3", "4", "8", "27", "32", "34", "41", "51", "62", "75", "77", "96", "97", "98", "114", "125", "127", "136", "138", "144", "146", "156", "167", "172", "175", "179", "182", "187", "203", "209", "215", "220", "234", "238", "239", "251", "253", "255", "256", "257", "259", "260", "265", "266", "268", "270", "274", "277", "278", "281", "284", "286", "287", "3", "4", "8", "27", "32", "34", "41", "51", "62", "75", "77", "96", "97", "98", "114", "125", "127", "136", "138", "144", "146", "156", "167", "172", "175", "179", "182", "187", "203", "209", "215", "220", "234", "238", "239", "251", "253", "255", "256", "257", "259", "260", "265", "266", "268", "270", "274", "277", "278", "281", "284", "286", "287", "15", "36", "52", "91", "106", "121", "150", "198", "210", "235", "261", "262", "267", "280", "289"); - private static final TimeSpiralRemasteredRun special = new TimeSpiralRemasteredRun(false, "290", "291", "292", "293", "294", "295", "296", "297", "298", "299", "300", "301", "302", "303", "304", "305", "306", "307", "308", "309", "310", "311", "312", "313", "314", "315", "316", "317", "318", "319", "320", "321", "322", "323", "324", "325", "326", "327", "328", "329", "330", "331", "332", "333", "334", "335", "336", "337", "338", "339", "340", "341", "342", "343", "344", "345", "346", "347", "348", "349", "350", "351", "352", "353", "354", "355", "356", "357", "358", "359", "360", "361", "362", "363", "364", "365", "366", "367", "368", "369", "370", "371", "372", "373", "374", "375", "376", "377", "378", "379", "380", "381", "382", "383", "384", "385", "386", "387", "388", "389", "390", "391", "392", "393", "394", "395", "396", "397", "398", "399", "400", "401", "402", "403", "404", "405", "406", "407", "408", "409", "410"); + private final CardRun commonA = new CardRun(true, "176", "65", "177", "94", "192", "70", "86", "191", "84", "168", "80", "69", "173", "73", "154", "87", "197", "92", "166", "67", "183", "78", "171", "81", "155", "95", "190", "70", "191", "58", "168", "63", "162", "89", "157", "65", "173", "69", "159", "73", "176", "94", "192", "86", "197", "84", "166", "67", "177", "92", "171", "80", "162", "81", "155", "78", "154", "87", "157", "95", "190", "58", "159", "63", "183", "89"); + private final CardRun commonB = new CardRun(true, "110", "245", "135", "241", "104", "226", "233", "108", "236", "107", "207", "229", "101", "201", "116", "199", "123", "246", "130", "240", "109", "245", "103", "202", "118", "221", "117", "230", "105", "212", "148", "243", "134", "237", "135", "226", "110", "236", "108", "233", "107", "241", "101", "229", "116", "201", "118", "199", "104", "246", "109", "240", "130", "207", "103", "202", "117", "221", "105", "230", "134", "243", "148", "212", "123", "237"); + private final CardRun commonC1 = new CardRun(true, "44", "223", "20", "140", "53", "21", "181", "17", "184", "2", "205", "151", "42", "119", "74", "1", "161", "272", "10", "228", "12", "68", "6", "285", "7", "48", "53", "20", "133", "44", "223", "21", "140", "151", "2", "184", "17", "74", "42", "181", "228", "1", "10", "119", "12", "68", "285", "48", "272", "6", "205", "161", "7", "133", "185"); + private final CardRun commonC2 = new CardRun(true, "25", "147", "31", "64", "269", "47", "206", "43", "178", "132", "263", "18", "200", "59", "26", "145", "9", "66", "50", "14", "147", "28", "232", "49", "122", "71", "22", "25", "165", "132", "31", "206", "43", "47", "269", "64", "18", "178", "200", "26", "263", "59", "9", "145", "14", "66", "50", "165", "28", "122", "232", "22", "49", "71", "185"); + private final CardRun uncommonA = new CardRun(true, "217", "283", "196", "242", "126", "194", "13", "222", "248", "40", "128", "195", "204", "5", "189", "113", "56", "249", "218", "180", "149", "276", "16", "258", "224", "79", "163", "142", "19", "61", "273", "153", "85", "30", "111", "170", "57", "231", "24", "99", "143", "279", "100", "174", "213", "55", "131", "46", "244", "126", "196", "82", "219", "102", "188", "208", "33", "120", "60", "186", "217", "283", "169", "124", "242", "194", "61", "218", "16", "153", "142", "258", "13", "224", "249", "113", "189", "79", "248", "204", "19", "149", "163", "5", "276", "222", "195", "40", "56", "180", "128", "273", "85", "30", "111", "170", "231", "57", "24", "143", "99", "279", "174", "213", "100", "131", "46", "244", "124", "55", "208", "188", "120", "60", "186", "33", "219", "169", "102", "82"); + private final CardRun uncommonB = new CardRun(true, "38", "152", "141", "29", "72", "225", "11", "158", "88", "271", "23", "76", "247", "160", "139", "250", "216", "288", "264", "45", "93", "252", "137", "275", "214", "39", "54", "129", "193", "227", "35", "112", "83", "282", "38", "164", "254", "115", "90", "37", "211", "152", "72", "141", "29", "247", "250", "158", "88", "11", "271", "225", "76", "139", "23", "160", "216", "288", "93", "45", "252", "137", "275", "214", "54", "264", "39", "227", "129", "193", "35", "282", "83", "112", "37", "254", "211", "38", "115", "90", "164", "29", "72", "152", "141", "225", "11", "88", "271", "158", "247", "23", "139", "76", "250", "160", "288", "252", "216", "93", "45", "137", "264", "214", "54", "275", "227", "129", "39", "193", "112", "35", "282", "83", "254", "211", "115", "37", "164", "90"); + private final CardRun rare = new CardRun(false, "3", "4", "8", "27", "32", "34", "41", "51", "62", "75", "77", "96", "97", "98", "114", "125", "127", "136", "138", "144", "146", "156", "167", "172", "175", "179", "182", "187", "203", "209", "215", "220", "234", "238", "239", "251", "253", "255", "256", "257", "259", "260", "265", "266", "268", "270", "274", "277", "278", "281", "284", "286", "287", "3", "4", "8", "27", "32", "34", "41", "51", "62", "75", "77", "96", "97", "98", "114", "125", "127", "136", "138", "144", "146", "156", "167", "172", "175", "179", "182", "187", "203", "209", "215", "220", "234", "238", "239", "251", "253", "255", "256", "257", "259", "260", "265", "266", "268", "270", "274", "277", "278", "281", "284", "286", "287", "15", "36", "52", "91", "106", "121", "150", "198", "210", "235", "261", "262", "267", "280", "289"); + private final CardRun special = new CardRun(false, "290", "291", "292", "293", "294", "295", "296", "297", "298", "299", "300", "301", "302", "303", "304", "305", "306", "307", "308", "309", "310", "311", "312", "313", "314", "315", "316", "317", "318", "319", "320", "321", "322", "323", "324", "325", "326", "327", "328", "329", "330", "331", "332", "333", "334", "335", "336", "337", "338", "339", "340", "341", "342", "343", "344", "345", "346", "347", "348", "349", "350", "351", "352", "353", "354", "355", "356", "357", "358", "359", "360", "361", "362", "363", "364", "365", "366", "367", "368", "369", "370", "371", "372", "373", "374", "375", "376", "377", "378", "379", "380", "381", "382", "383", "384", "385", "386", "387", "388", "389", "390", "391", "392", "393", "394", "395", "396", "397", "398", "399", "400", "401", "402", "403", "404", "405", "406", "407", "408", "409", "410"); - private TimeSpiralRemasteredRun(boolean keepOrder, String... numbers) { - super(keepOrder, numbers); - } - } - - private static class TimeSpiralRemasteredStructure extends BoosterStructure { - private static final TimeSpiralRemasteredStructure AAABBBC1C1C1C1 = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1 - ); - private static final TimeSpiralRemasteredStructure AAABBC1C1C1C1C1 = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1 - ); - private static final TimeSpiralRemasteredStructure AABBBC1C1C1C1C1 = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1, - TimeSpiralRemasteredRun.commonC1 - ); - private static final TimeSpiralRemasteredStructure AAABBBC2C2C2C2 = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2 - ); - private static final TimeSpiralRemasteredStructure AAABBC2C2C2C2C2 = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2 - ); - private static final TimeSpiralRemasteredStructure AABBBC2C2C2C2C2 = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonA, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonB, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2, - TimeSpiralRemasteredRun.commonC2 - ); - private static final TimeSpiralRemasteredStructure AAA = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.uncommonA, - TimeSpiralRemasteredRun.uncommonA, - TimeSpiralRemasteredRun.uncommonA - ); - private static final TimeSpiralRemasteredStructure BBB = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.uncommonB, - TimeSpiralRemasteredRun.uncommonB, - TimeSpiralRemasteredRun.uncommonB - ); - private static final TimeSpiralRemasteredStructure R1 = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.rare - ); - private static final TimeSpiralRemasteredStructure S1 = new TimeSpiralRemasteredStructure( - TimeSpiralRemasteredRun.special - ); - - private TimeSpiralRemasteredStructure(CardRun... runs) { - super(runs); - } - } + private final BoosterStructure AAABBBC1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, + commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AABBBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, + commonB, commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBC2C2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AABBBC2C2C2C2C2 = new BoosterStructure( + commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAA = new BoosterStructure(uncommonA, uncommonA, uncommonA); + private final BoosterStructure BBB = new BoosterStructure(uncommonB, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rare); + private final BoosterStructure S1 = new BoosterStructure(special); // In order for equal numbers of each common to exist, the average booster must contain: // 2.73 A commons (30 / 11) @@ -583,53 +522,33 @@ class TimeSpiralRemasteredCollator implements BoosterCollator { // 2.27 C1 commons (25 / 11, or 50 / 11 in each C1 booster) // 2.27 C2 commons (25 / 11, or 50 / 11 in each C2 booster) private final RarityConfiguration commonRuns = new RarityConfiguration( - false, - TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, - TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, - TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, - TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, - TimeSpiralRemasteredStructure.AAABBBC1C1C1C1, - TimeSpiralRemasteredStructure.AAABBC1C1C1C1C1, - TimeSpiralRemasteredStructure.AAABBC1C1C1C1C1, - TimeSpiralRemasteredStructure.AAABBC1C1C1C1C1, - TimeSpiralRemasteredStructure.AABBBC1C1C1C1C1, - TimeSpiralRemasteredStructure.AABBBC1C1C1C1C1, - TimeSpiralRemasteredStructure.AABBBC1C1C1C1C1, + AAABBBC1C1C1C1, + AAABBBC1C1C1C1, + AAABBBC1C1C1C1, + AAABBBC1C1C1C1, + AAABBBC1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AABBBC1C1C1C1C1, + AABBBC1C1C1C1C1, + AABBBC1C1C1C1C1, - TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, - TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, - TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, - TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, - TimeSpiralRemasteredStructure.AAABBBC2C2C2C2, - TimeSpiralRemasteredStructure.AAABBC2C2C2C2C2, - TimeSpiralRemasteredStructure.AAABBC2C2C2C2C2, - TimeSpiralRemasteredStructure.AAABBC2C2C2C2C2, - TimeSpiralRemasteredStructure.AABBBC2C2C2C2C2, - TimeSpiralRemasteredStructure.AABBBC2C2C2C2C2, - TimeSpiralRemasteredStructure.AABBBC2C2C2C2C2 + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAABBBC2C2C2C2, + AAABBC2C2C2C2C2, + AAABBC2C2C2C2C2, + AAABBC2C2C2C2C2, + AABBBC2C2C2C2C2, + AABBBC2C2C2C2C2, + AABBBC2C2C2C2C2 ); - private final RarityConfiguration uncommonRuns = new RarityConfiguration( - false, - TimeSpiralRemasteredStructure.AAA, - TimeSpiralRemasteredStructure.AAA, - TimeSpiralRemasteredStructure.AAA, - TimeSpiralRemasteredStructure.BBB, - TimeSpiralRemasteredStructure.BBB - ); - private final RarityConfiguration rareRuns = new RarityConfiguration( - TimeSpiralRemasteredStructure.R1 - ); - private final RarityConfiguration specialRuns = new RarityConfiguration( - TimeSpiralRemasteredStructure.S1 - ); - - @Override - public void shuffle() { - commonRuns.shuffle(); - uncommonRuns.shuffle(); - rareRuns.shuffle(); - specialRuns.shuffle(); - } + private final RarityConfiguration uncommonRuns = new RarityConfiguration(AAA, AAA, AAA, BBB, BBB); + private final RarityConfiguration rareRuns = new RarityConfiguration(R1); + private final RarityConfiguration specialRuns = new RarityConfiguration(S1); @Override public List makeBooster() { diff --git a/Mage/src/main/java/mage/cards/ExpansionSet.java b/Mage/src/main/java/mage/cards/ExpansionSet.java index 23b35ae9d5b..7e20a391071 100644 --- a/Mage/src/main/java/mage/cards/ExpansionSet.java +++ b/Mage/src/main/java/mage/cards/ExpansionSet.java @@ -94,7 +94,6 @@ public abstract class ExpansionSet implements Serializable { protected Date releaseDate; protected ExpansionSet parentSet; protected SetType setType; - protected BoosterCollator boosterCollator; // TODO: 03.10.2018, hasBasicLands can be removed someday -- it's uses to optimize lands search in deck generation and lands adding (search all available lands from sets) protected boolean hasBasicLands = true; @@ -126,20 +125,12 @@ public abstract class ExpansionSet implements Serializable { protected final Map inBoosterMap = new HashMap<>(); public ExpansionSet(String name, String code, Date releaseDate, SetType setType) { - this(name, code, releaseDate, setType, null); - } - - public ExpansionSet(String name, String code, Date releaseDate, SetType setType, BoosterCollator boosterCollator) { this.name = name; this.code = code; this.releaseDate = releaseDate; this.setType = setType; this.maxCardNumberInBooster = Integer.MAX_VALUE; savedCards = new EnumMap<>(Rarity.class); - this.boosterCollator = boosterCollator; - if (this.boosterCollator != null) { - this.boosterCollator.shuffle(); - } } public String getName() { @@ -251,9 +242,14 @@ public abstract class ExpansionSet implements Serializable { } } + public BoosterCollator createCollator() { + return null; + } + public List createBooster() { - if (boosterCollator != null) { - return createBoosterUsingCollator(); + BoosterCollator collator = createCollator(); + if (collator != null) { + return createBoosterUsingCollator(collator); } for (int i = 0; i < 100; i++) {//don't want to somehow loop forever @@ -277,17 +273,11 @@ public abstract class ExpansionSet implements Serializable { return tryBooster(); } - public void shuffleCollator() { - if (boosterCollator != null) { - boosterCollator.shuffle(); - } - } - - private List createBoosterUsingCollator() { + private List createBoosterUsingCollator(BoosterCollator collator) { if (inBoosterMap.isEmpty()) { generateBoosterMap(); } - return boosterCollator + return collator .makeBooster() .stream() .map(inBoosterMap::get) diff --git a/Mage/src/main/java/mage/collation/BoosterCollator.java b/Mage/src/main/java/mage/collation/BoosterCollator.java index 350b4bcbb4f..01e68295ecf 100644 --- a/Mage/src/main/java/mage/collation/BoosterCollator.java +++ b/Mage/src/main/java/mage/collation/BoosterCollator.java @@ -6,8 +6,5 @@ import java.util.List; * @author TheElk801 */ public interface BoosterCollator { - - public void shuffle(); - public List makeBooster(); } diff --git a/Mage/src/main/java/mage/collation/BoosterStructure.java b/Mage/src/main/java/mage/collation/BoosterStructure.java index 23aa81e4a7f..e6d9bab8182 100644 --- a/Mage/src/main/java/mage/collation/BoosterStructure.java +++ b/Mage/src/main/java/mage/collation/BoosterStructure.java @@ -12,11 +12,11 @@ import java.util.List; * * @author TheElk801 */ -public abstract class BoosterStructure { +public class BoosterStructure { private final List slots; - protected BoosterStructure(CardRun... runs) { + public BoosterStructure(CardRun... runs) { this.slots = Arrays.asList(runs); } @@ -27,10 +27,4 @@ public abstract class BoosterStructure { } return cards; } - - public void shuffle() { - for (CardRun run : this.slots) { - run.shuffle(); - } - } } diff --git a/Mage/src/main/java/mage/collation/CardRun.java b/Mage/src/main/java/mage/collation/CardRun.java index 1889b59f0d3..c34876bf9d6 100644 --- a/Mage/src/main/java/mage/collation/CardRun.java +++ b/Mage/src/main/java/mage/collation/CardRun.java @@ -3,7 +3,7 @@ package mage.collation; /** * @author TheElk801 */ -public abstract class CardRun extends Rotater { +public class CardRun extends Rotater { public CardRun(boolean keepOrder, String... numbers) { super(keepOrder, numbers); diff --git a/Mage/src/main/java/mage/collation/RarityConfiguration.java b/Mage/src/main/java/mage/collation/RarityConfiguration.java index 3ba6807b860..9d71685db93 100644 --- a/Mage/src/main/java/mage/collation/RarityConfiguration.java +++ b/Mage/src/main/java/mage/collation/RarityConfiguration.java @@ -13,15 +13,8 @@ public class RarityConfiguration extends Rotater { super(item1, item2); } - public RarityConfiguration(boolean keepOrder, BoosterStructure... items) { - super(keepOrder, items); - } - - @Override - public void shuffle() { - for (BoosterStructure structure : this.items) { - structure.shuffle(); - } - super.shuffle(); + public RarityConfiguration(BoosterStructure... items) { + // change to false if we ever decide to generate sequential boosters + super(true, items); } } diff --git a/Mage/src/main/java/mage/collation/Rotater.java b/Mage/src/main/java/mage/collation/Rotater.java index 6558106e50e..0af5fa77497 100644 --- a/Mage/src/main/java/mage/collation/Rotater.java +++ b/Mage/src/main/java/mage/collation/Rotater.java @@ -2,6 +2,7 @@ package mage.collation; import mage.util.RandomUtil; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -13,9 +14,8 @@ import java.util.List; */ public class Rotater { - protected final List items; - private final boolean keepOrder; - private int position = 0; + private final List items; + private int position; public Rotater(T item) { this(true, item); @@ -26,8 +26,15 @@ public class Rotater { } public Rotater(boolean keepOrder, T... items) { - this.items = Arrays.asList(items); - this.keepOrder = keepOrder; + if (keepOrder) { + this.items = Arrays.asList(items); + this.position = RandomUtil.nextInt(this.items.size()); + } else { + this.items = new ArrayList(); + Collections.addAll(this.items, items); + Collections.shuffle(this.items, RandomUtil.getRandom()); + this.position = 0; + } } public int iterate() { @@ -40,11 +47,4 @@ public class Rotater { public T getNext() { return items.get(iterate()); } - - public void shuffle() { - position = RandomUtil.nextInt(items.size()); - if (!keepOrder) { - Collections.shuffle(items, RandomUtil.getRandom()); - } - } } From 19f71ba68332fe4dbcbbbcc298118202d8b62f37 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 23 Sep 2021 09:16:34 -0400 Subject: [PATCH 175/231] [MIC] Implemented Ruinous Intrusion --- .../src/mage/cards/r/RuinousIntrusion.java | 79 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 80 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RuinousIntrusion.java diff --git a/Mage.Sets/src/mage/cards/r/RuinousIntrusion.java b/Mage.Sets/src/mage/cards/r/RuinousIntrusion.java new file mode 100644 index 00000000000..b46997f519a --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RuinousIntrusion.java @@ -0,0 +1,79 @@ +package mage.cards.r; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RuinousIntrusion extends CardImpl { + + public RuinousIntrusion(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{G}"); + + // Exile target artifact or enchantment. Put X +1/+1 counters on target creature you control, where X is the mana value of the permanent exiled this way. + this.getSpellAbility().addEffect(new RuinousIntrusionEffect()); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT)); + this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); + } + + private RuinousIntrusion(final RuinousIntrusion card) { + super(card); + } + + @Override + public RuinousIntrusion copy() { + return new RuinousIntrusion(this); + } +} + +class RuinousIntrusionEffect extends OneShotEffect { + + RuinousIntrusionEffect() { + super(Outcome.Benefit); + staticText = "exile target artifact or enchantment. Put X +1/+1 counters on target creature you control, " + + "where X is the mana value of the permanent exiled this way"; + } + + private RuinousIntrusionEffect(final RuinousIntrusionEffect effect) { + super(effect); + } + + @Override + public RuinousIntrusionEffect copy() { + return new RuinousIntrusionEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (player == null || permanent == null) { + return false; + } + int mv = permanent.getManaValue(); + player.moveCards(permanent, Zone.EXILED, source, game); + if (mv < 1) { + return true; + } + Permanent creature = game.getPermanent(source.getTargets().get(0).getFirstTarget()); + if (creature != null) { + creature.addCounters(CounterType.P1P1.createInstance(mv), source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 5460a3e54ff..a1dc1677f89 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -117,6 +117,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Riders of Gavony", 93, Rarity.RARE, mage.cards.r.RidersOfGavony.class)); cards.add(new SetCardInfo("Rogue's Passage", 179, Rarity.UNCOMMON, mage.cards.r.RoguesPassage.class)); cards.add(new SetCardInfo("Rooftop Storm", 103, Rarity.RARE, mage.cards.r.RooftopStorm.class)); + cards.add(new SetCardInfo("Ruinous Intrusion", 28, Rarity.RARE, mage.cards.r.RuinousIntrusion.class)); cards.add(new SetCardInfo("Ruthless Deathfang", 154, Rarity.UNCOMMON, mage.cards.r.RuthlessDeathfang.class)); cards.add(new SetCardInfo("Selesnya Sanctuary", 180, Rarity.UNCOMMON, mage.cards.s.SelesnyaSanctuary.class)); cards.add(new SetCardInfo("Shamanic Revelation", 143, Rarity.RARE, mage.cards.s.ShamanicRevelation.class)); From 85b369945454dc5f99490e373e75a1e84dbe0cd8 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 23 Sep 2021 09:31:10 -0400 Subject: [PATCH 176/231] [MIC] Implemented Hordewing Skaab --- .../src/mage/cards/h/HordewingSkaab.java | 115 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 116 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HordewingSkaab.java diff --git a/Mage.Sets/src/mage/cards/h/HordewingSkaab.java b/Mage.Sets/src/mage/cards/h/HordewingSkaab.java new file mode 100644 index 00000000000..ffca3d92a1c --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HordewingSkaab.java @@ -0,0 +1,115 @@ +package mage.cards.h; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.game.Game; +import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPlayerBatchEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HordewingSkaab extends CardImpl { + + private static final FilterPermanent filter = new FilterCreaturePermanent(SubType.ZOMBIE, "Zombies"); + + public HordewingSkaab(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{U}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.HORROR); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Other Zombies you control have flying. + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( + FlyingAbility.getInstance(), Duration.WhileOnBattlefield, filter, true + ))); + + // Whenever one or more Zombies you control deal combat damage to one or more of your opponents, you may draw cards equal to the number of opponents dealt damage this way. If you do, discard that many cards. + this.addAbility(new HordewingSkaabTriggeredAbility()); + } + + private HordewingSkaab(final HordewingSkaab card) { + super(card); + } + + @Override + public HordewingSkaab copy() { + return new HordewingSkaab(this); + } +} + +class HordewingSkaabTriggeredAbility extends TriggeredAbilityImpl { + + HordewingSkaabTriggeredAbility() { + super(Zone.BATTLEFIELD, null, true); + } + + private HordewingSkaabTriggeredAbility(final HordewingSkaabTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PLAYER_BATCH; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + DamagedPlayerBatchEvent dEvent = (DamagedPlayerBatchEvent) event; + Set opponents = new HashSet<>(); + for (DamagedEvent damagedEvent : dEvent.getEvents()) { + if (!damagedEvent.isCombatDamage()) { + continue; + } + Permanent permanent = game.getPermanent(damagedEvent.getSourceId()); + if (permanent == null + || !permanent.isControlledBy(getControllerId()) + || !permanent.hasSubtype(SubType.ZOMBIE, game) + || !game.getOpponents(getControllerId()).contains(damagedEvent.getTargetId())) { + continue; + } + opponents.add(damagedEvent.getTargetId()); + } + if (opponents.size() < 1) { + return false; + } + this.getEffects().clear(); + this.addEffect(new DrawDiscardControllerEffect(opponents.size(), opponents.size())); + return true; + } + + @Override + public HordewingSkaabTriggeredAbility copy() { + return new HordewingSkaabTriggeredAbility(this); + } + + @Override + public String getRule() { + return "Whenever one or more Zombies you control deal combat damage to one " + + "or more of your opponents, you may draw cards equal to the number " + + "of opponents dealt damage this way. If you do, discard that many cards."; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index a1dc1677f89..f5cbf3f332d 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -89,6 +89,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Herald of War", 86, Rarity.RARE, mage.cards.h.HeraldOfWar.class)); cards.add(new SetCardInfo("Heron's Grace Champion", 152, Rarity.RARE, mage.cards.h.HeronsGraceChampion.class)); cards.add(new SetCardInfo("Heronblade Elite", 26, Rarity.RARE, mage.cards.h.HeronbladeElite.class)); + cards.add(new SetCardInfo("Hordewing Skaab", 15, Rarity.RARE, mage.cards.h.HordewingSkaab.class)); cards.add(new SetCardInfo("Hour of Eternity", 102, Rarity.RARE, mage.cards.h.HourOfEternity.class)); cards.add(new SetCardInfo("Hour of Reckoning", 87, Rarity.RARE, mage.cards.h.HourOfReckoning.class)); cards.add(new SetCardInfo("Inspiring Call", 141, Rarity.UNCOMMON, mage.cards.i.InspiringCall.class)); From d4ab251c37cd2111366508a4f108aefcb14d7232 Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Thu, 23 Sep 2021 09:37:23 -0500 Subject: [PATCH 177/231] - removed unnecessary test code from Out of Time --- Mage.Sets/src/mage/cards/o/OutOfTime.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/o/OutOfTime.java b/Mage.Sets/src/mage/cards/o/OutOfTime.java index bf8907d190c..bc0bd0fca97 100644 --- a/Mage.Sets/src/mage/cards/o/OutOfTime.java +++ b/Mage.Sets/src/mage/cards/o/OutOfTime.java @@ -156,7 +156,6 @@ class OutOfTimeLeavesBattlefieldEffect extends OneShotEffect { creature.phaseIn(game); } } - game.getState().processAction(game); return true; } return false; @@ -191,7 +190,6 @@ class OutOfTimeReplacementEffect extends ContinuousRuleModifyingEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { - game.getState().processAction(game); Set creatureIds = (Set) game.getState().getValue("phasedOutCreatures" + source.getId().toString()); return source.getSourceObjectZoneChangeCounter() == game.getState().getZoneChangeCounter(source.getSourceId()) // blinked From 4c88d2ae5e24507f6061f8bb1e482f20f68df74b Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Thu, 23 Sep 2021 12:02:29 -0500 Subject: [PATCH 178/231] - a real fix for #5630 --- Mage/src/main/java/mage/players/PlayerImpl.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 08e3127b259..d6c8c2a37b5 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -634,13 +634,19 @@ public abstract class PlayerImpl implements Player, Serializable { return false; } if (source != null) { + // there is only variant of shroud, so check the instance and any asthougheffects that would ignore it if (abilities.containsKey(ShroudAbility.getInstance().getId()) && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game) == null) { return false; } - if (abilities.containsKey(HexproofAbility.getInstance().getId()) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { - return false; + // check for all variants of hexproof and any asthougheffects that would ignore it + // TODO there may be "prevented by rule-modification" effects, so add them if known + for (Ability a : abilities) { + if (a instanceof HexproofBaseAbility + && ((HexproofBaseAbility) a).checkObject(source, game) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { + return false; + } } return !hasProtectionFrom(source, game); } From 71bebad14a5ac6e63c883afc4e1f797aa0346239 Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Thu, 23 Sep 2021 15:30:40 -0500 Subject: [PATCH 179/231] - Fixed #8313 --- Mage.Sets/src/mage/cards/s/SmolderingEgg.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/s/SmolderingEgg.java b/Mage.Sets/src/mage/cards/s/SmolderingEgg.java index bb5eaec7220..b64c045305d 100644 --- a/Mage.Sets/src/mage/cards/s/SmolderingEgg.java +++ b/Mage.Sets/src/mage/cards/s/SmolderingEgg.java @@ -42,7 +42,7 @@ public final class SmolderingEgg extends CardImpl { // Whenever you cast an instant or sorcery spell, put a number of ember counters on Smoldering Egg equal to the amount of mana spent to cast that spell. Then if Smoldering Egg has seven or more ember counters on it, remove them and transform Smoldering Egg. this.addAbility(new TransformAbility()); this.addAbility(new SpellCastControllerTriggeredAbility( - new SmolderingEggEffect(), StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false + new SmolderingEggEffect(), StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false, true )); } From ea561892f57d4a5f4bc0f1bbd742a24a339de64f Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Thu, 23 Sep 2021 16:45:35 -0500 Subject: [PATCH 180/231] - Fixed #8293 --- .../common/DealtDamageAndDiedTriggeredAbility.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Mage/src/main/java/mage/abilities/common/DealtDamageAndDiedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealtDamageAndDiedTriggeredAbility.java index b2a50fa6b6e..8ef3f6d6725 100644 --- a/Mage/src/main/java/mage/abilities/common/DealtDamageAndDiedTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealtDamageAndDiedTriggeredAbility.java @@ -58,13 +58,14 @@ public class DealtDamageAndDiedTriggeredAbility extends TriggeredAbilityImpl { if (filter.match(zEvent.getTarget(), game)) { boolean damageDealt = false; for (MageObjectReference mor : zEvent.getTarget().getDealtDamageByThisTurn()) { - if (mor.refersTo(getSourceObject(game), game)) { + if (mor.refersTo(game.getLastKnownInformation(getSourceId(), Zone.BATTLEFIELD), game) + || (mor.refersTo(getSourceObject(game), game))) { damageDealt = true; break; } } if (damageDealt) { - if(this.setTargetPointer == SetTargetPointer.PERMANENT) { + if (this.setTargetPointer == SetTargetPointer.PERMANENT) { for (Effect effect : getEffects()) { effect.setTargetPointer(new FixedTarget(event.getTargetId())); } @@ -78,6 +79,6 @@ public class DealtDamageAndDiedTriggeredAbility extends TriggeredAbilityImpl { @Override public String getTriggerPhrase() { - return "Whenever a " + filter.getMessage() + " dealt damage by {this} this turn dies, " ; + return "Whenever a " + filter.getMessage() + " dealt damage by {this} this turn dies, "; } } From 3a3e3fda48632dd4e2b6933aa637c6ef8634941f Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 23 Sep 2021 19:37:47 -0400 Subject: [PATCH 181/231] various files converted from CRLF to LF --- .../src/mage/cards/g/GlimpseTheCosmos.java | 322 +- Mage.Sets/src/mage/sets/MercadianMasques.java | 750 +- .../keywords/CumulativeUpkeepTest.java | 218 +- .../cards/abilities/keywords/EntwineTest.java | 356 +- .../abilities/keywords/ManifestTest.java | 1080 +- .../modification/FerventChampionTest.java | 76 +- .../cards/facedown/PrimordialMistTest.java | 112 +- .../mage/test/cards/facedown/TriggerTest.java | 122 +- ...TimesUsableActivatedManaAbilitiesTest.java | 90 +- .../conditional/CrypticTrilobiteTest.java | 222 +- .../mana/conditional/TitansNestTest.java | 86 +- .../prevention/PreventAllDamageTest.java | 290 +- .../protection/EightAndAHalfTailsTest.java | 94 +- .../cards/single/bfz/ConduitOfRuinTest.java | 100 +- .../cards/single/c17/TheUrDragonTest.java | 96 +- .../cards/single/c18/AminatousAuguryTest.java | 150 +- .../single/c20/PakoArcaneRetrieverTest.java | 192 +- .../test/cards/single/dst/DemonsHornTest.java | 144 +- .../cards/single/eld/BarteredCowTest.java | 176 +- .../cards/single/eld/OnceUponATimeTest.java | 176 +- .../single/eld/SyrGwynHeroOfAshvaleTest.java | 114 +- .../cards/single/grn/PeltCollectorTest.java | 490 +- .../single/hou/TormentOfHailfireTest.java | 176 +- .../single/iko/OboshThePreypiercerTest.java | 66 +- .../cards/single/iko/SkycatSovereignTest.java | 178 +- .../cards/single/mh1/PlagueEngineerTest.java | 76 +- .../cards/single/som/NimDeathmantleTest.java | 100 +- ...rDoomscourgeAndKithkinMourncallerTest.java | 206 +- .../cards/triggers/dies/AshenRiderTest.java | 136 +- .../duel/CommanderColorChangeTest.java | 244 +- Mage/src/main/java/mage/ApprovingObject.java | 56 +- ...aturePutIntoGraveyardTriggeredAbility.java | 248 +- ...idNotAttackThisTurnEnchantedCondition.java | 62 +- .../common/ControllerLifeDividedValue.java | 96 +- .../common/HighestCMCOfPermanentValue.java | 124 +- .../main/java/mage/players/PlayerImpl.java | 10168 ++++++++-------- 36 files changed, 8696 insertions(+), 8696 deletions(-) diff --git a/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java b/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java index 79f2b118f00..e293a529f20 100644 --- a/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java +++ b/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java @@ -1,161 +1,161 @@ -package mage.cards.g; - -import mage.abilities.Ability; -import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; -import mage.abilities.dynamicvalue.common.StaticValue; -import mage.abilities.effects.ReplacementEffectImpl; -import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; -import mage.cards.Card; -import mage.cards.CardImpl; -import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.filter.StaticFilters; -import mage.filter.common.FilterControlledPermanent; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeEvent; -import mage.players.Player; -import java.util.UUID; -import mage.Mana; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.decorator.ConditionalAsThoughEffect; -import mage.abilities.effects.AsThoughEffectImpl; -import mage.watchers.common.ManaSpentToCastWatcher; - -/** - * - * @author jeffwadsworth - */ - -public class GlimpseTheCosmos extends CardImpl { - - public GlimpseTheCosmos(UUID ownerId, CardSetInfo setInfo) { - super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{U}"); - - // Look at the top three cards of your library. Put one of them into your hand and the rest on the bottom of your library in any order. - this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect( - StaticValue.get(3), false, StaticValue.get(1), - StaticFilters.FILTER_CARD, Zone.LIBRARY, false, - false, false, Zone.HAND, false - ).setText("look at the top three cards of your library. " - + "Put one of them into your hand and the rest on the bottom of your library in any order")); - - //As long as you control a Giant, you may cast Glimpse the Cosmos from your graveyard by paying {U} rather than paying its mana cost. If you cast Glimpse the Cosmos this way and it would be put into your graveyard, exile it instead. - this.addAbility(new SimpleStaticAbility(Zone.GRAVEYARD, - new ConditionalAsThoughEffect( - new GlimpseTheCosmosPlayEffect(), - new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT))))); - - this.addAbility(new SimpleStaticAbility(Zone.ALL, new GlimpseTheCosmosReplacementEffect())); - - } - - private GlimpseTheCosmos(final GlimpseTheCosmos card) { - super(card); - } - - @Override - public GlimpseTheCosmos copy() { - return new GlimpseTheCosmos(this); - } - -} - -class GlimpseTheCosmosPlayEffect extends AsThoughEffectImpl { - - public GlimpseTheCosmosPlayEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit); - staticText = "As long as you control a Giant, you may cast {this} from your graveyard by paying {U} rather than paying its mana cost"; - } - - public GlimpseTheCosmosPlayEffect(final GlimpseTheCosmosPlayEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public GlimpseTheCosmosPlayEffect copy() { - return new GlimpseTheCosmosPlayEffect(this); - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (sourceId.equals(source.getSourceId()) - && source.isControlledBy(affectedControllerId)) { - if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { - Player controller = game.getPlayer(affectedControllerId); - if (controller != null) { - controller.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{U}"), null); - return true; - } - } - } - return false; - } - -} - -class GlimpseTheCosmosReplacementEffect extends ReplacementEffectImpl { - - public GlimpseTheCosmosReplacementEffect() { - super(Duration.OneUse, Outcome.Exile); - staticText = "As long as you control a Giant, you may cast {this} from your graveyard by paying {U} rather than paying its mana cost. If you cast {this} this way and it would be put into your graveyard, exile it instead"; - } - - public GlimpseTheCosmosReplacementEffect(final GlimpseTheCosmosReplacementEffect effect) { - super(effect); - } - - @Override - public GlimpseTheCosmosReplacementEffect copy() { - return new GlimpseTheCosmosReplacementEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Card card = game.getCard(event.getTargetId()); - if (card != null) { - discard(); - return controller.moveCards( - card, Zone.EXILED, source, game, false, false, false, event.getAppliedEffects()); - } - } - return false; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ZONE_CHANGE; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - ManaSpentToCastWatcher watcher = game.getState().getWatcher(ManaSpentToCastWatcher.class); - if (watcher == null) { - return false; - } - Mana payment = watcher.getLastManaPayment(source.getSourceId()); - if (payment != null - && payment.getBlue() == 1 // must be blue mana - && payment.count() == 1) { // must be just one - if (event.getTargetId().equals(source.getSourceId()) - && ((ZoneChangeEvent) event).getFromZone() == Zone.STACK - && ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) { - return true; - } - } - return false; - } -} +package mage.cards.g; + +import mage.abilities.Ability; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import java.util.UUID; +import mage.Mana; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.watchers.common.ManaSpentToCastWatcher; + +/** + * + * @author jeffwadsworth + */ + +public class GlimpseTheCosmos extends CardImpl { + + public GlimpseTheCosmos(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{U}"); + + // Look at the top three cards of your library. Put one of them into your hand and the rest on the bottom of your library in any order. + this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect( + StaticValue.get(3), false, StaticValue.get(1), + StaticFilters.FILTER_CARD, Zone.LIBRARY, false, + false, false, Zone.HAND, false + ).setText("look at the top three cards of your library. " + + "Put one of them into your hand and the rest on the bottom of your library in any order")); + + //As long as you control a Giant, you may cast Glimpse the Cosmos from your graveyard by paying {U} rather than paying its mana cost. If you cast Glimpse the Cosmos this way and it would be put into your graveyard, exile it instead. + this.addAbility(new SimpleStaticAbility(Zone.GRAVEYARD, + new ConditionalAsThoughEffect( + new GlimpseTheCosmosPlayEffect(), + new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT))))); + + this.addAbility(new SimpleStaticAbility(Zone.ALL, new GlimpseTheCosmosReplacementEffect())); + + } + + private GlimpseTheCosmos(final GlimpseTheCosmos card) { + super(card); + } + + @Override + public GlimpseTheCosmos copy() { + return new GlimpseTheCosmos(this); + } + +} + +class GlimpseTheCosmosPlayEffect extends AsThoughEffectImpl { + + public GlimpseTheCosmosPlayEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit); + staticText = "As long as you control a Giant, you may cast {this} from your graveyard by paying {U} rather than paying its mana cost"; + } + + public GlimpseTheCosmosPlayEffect(final GlimpseTheCosmosPlayEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public GlimpseTheCosmosPlayEffect copy() { + return new GlimpseTheCosmosPlayEffect(this); + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + if (sourceId.equals(source.getSourceId()) + && source.isControlledBy(affectedControllerId)) { + if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { + Player controller = game.getPlayer(affectedControllerId); + if (controller != null) { + controller.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{U}"), null); + return true; + } + } + } + return false; + } + +} + +class GlimpseTheCosmosReplacementEffect extends ReplacementEffectImpl { + + public GlimpseTheCosmosReplacementEffect() { + super(Duration.OneUse, Outcome.Exile); + staticText = "As long as you control a Giant, you may cast {this} from your graveyard by paying {U} rather than paying its mana cost. If you cast {this} this way and it would be put into your graveyard, exile it instead"; + } + + public GlimpseTheCosmosReplacementEffect(final GlimpseTheCosmosReplacementEffect effect) { + super(effect); + } + + @Override + public GlimpseTheCosmosReplacementEffect copy() { + return new GlimpseTheCosmosReplacementEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + Card card = game.getCard(event.getTargetId()); + if (card != null) { + discard(); + return controller.moveCards( + card, Zone.EXILED, source, game, false, false, false, event.getAppliedEffects()); + } + } + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + ManaSpentToCastWatcher watcher = game.getState().getWatcher(ManaSpentToCastWatcher.class); + if (watcher == null) { + return false; + } + Mana payment = watcher.getLastManaPayment(source.getSourceId()); + if (payment != null + && payment.getBlue() == 1 // must be blue mana + && payment.count() == 1) { // must be just one + if (event.getTargetId().equals(source.getSourceId()) + && ((ZoneChangeEvent) event).getFromZone() == Zone.STACK + && ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) { + return true; + } + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/MercadianMasques.java b/Mage.Sets/src/mage/sets/MercadianMasques.java index 866c8cb036a..a7398bf630e 100644 --- a/Mage.Sets/src/mage/sets/MercadianMasques.java +++ b/Mage.Sets/src/mage/sets/MercadianMasques.java @@ -1,375 +1,375 @@ -package mage.sets; - -import mage.cards.ExpansionSet; -import mage.constants.Rarity; -import mage.constants.SetType; - -/** - * @author North - */ -public final class MercadianMasques extends ExpansionSet { - - private static final MercadianMasques instance = new MercadianMasques(); - - public static MercadianMasques getInstance() { - return instance; - } - - private MercadianMasques() { - super("Mercadian Masques", "MMQ", ExpansionSet.buildDate(1999, 8, 25), SetType.EXPANSION); - this.blockName = "Masques"; - this.hasBoosters = true; - this.numBoosterLands = 0; - this.numBoosterCommon = 11; - this.numBoosterUncommon = 3; - this.numBoosterRare = 1; - this.ratioBoosterMythic = 0; - cards.add(new SetCardInfo("Aerial Caravan", 58, Rarity.RARE, mage.cards.a.AerialCaravan.class)); - cards.add(new SetCardInfo("Afterlife", 1, Rarity.UNCOMMON, mage.cards.a.Afterlife.class)); - cards.add(new SetCardInfo("Alabaster Wall", 2, Rarity.COMMON, mage.cards.a.AlabasterWall.class)); - cards.add(new SetCardInfo("Alley Grifters", 115, Rarity.COMMON, mage.cards.a.AlleyGrifters.class)); - cards.add(new SetCardInfo("Ancestral Mask", 229, Rarity.COMMON, mage.cards.a.AncestralMask.class)); - cards.add(new SetCardInfo("Armistice", 3, Rarity.RARE, mage.cards.a.Armistice.class)); - cards.add(new SetCardInfo("Arms Dealer", 172, Rarity.UNCOMMON, mage.cards.a.ArmsDealer.class)); - cards.add(new SetCardInfo("Arrest", 4, Rarity.UNCOMMON, mage.cards.a.Arrest.class)); - cards.add(new SetCardInfo("Assembly Hall", 286, Rarity.RARE, mage.cards.a.AssemblyHall.class)); - cards.add(new SetCardInfo("Ballista Squad", 5, Rarity.UNCOMMON, mage.cards.b.BallistaSquad.class)); - cards.add(new SetCardInfo("Balloon Peddler", 59, Rarity.COMMON, mage.cards.b.BalloonPeddler.class)); - cards.add(new SetCardInfo("Barbed Wire", 287, Rarity.UNCOMMON, mage.cards.b.BarbedWire.class)); - cards.add(new SetCardInfo("Battle Rampart", 173, Rarity.COMMON, mage.cards.b.BattleRampart.class)); - cards.add(new SetCardInfo("Battle Squadron", 174, Rarity.RARE, mage.cards.b.BattleSquadron.class)); - cards.add(new SetCardInfo("Bifurcate", 230, Rarity.RARE, mage.cards.b.Bifurcate.class)); - cards.add(new SetCardInfo("Black Market", 116, Rarity.RARE, mage.cards.b.BlackMarket.class)); - cards.add(new SetCardInfo("Blaster Mage", 175, Rarity.COMMON, mage.cards.b.BlasterMage.class)); - cards.add(new SetCardInfo("Blockade Runner", 60, Rarity.COMMON, mage.cards.b.BlockadeRunner.class)); - cards.add(new SetCardInfo("Blood Hound", 176, Rarity.RARE, mage.cards.b.BloodHound.class)); - cards.add(new SetCardInfo("Blood Oath", 177, Rarity.RARE, mage.cards.b.BloodOath.class)); - cards.add(new SetCardInfo("Boa Constrictor", 231, Rarity.UNCOMMON, mage.cards.b.BoaConstrictor.class)); - cards.add(new SetCardInfo("Bog Smugglers", 117, Rarity.COMMON, mage.cards.b.BogSmugglers.class)); - cards.add(new SetCardInfo("Bog Witch", 118, Rarity.COMMON, mage.cards.b.BogWitch.class)); - cards.add(new SetCardInfo("Brainstorm", 61, Rarity.COMMON, mage.cards.b.Brainstorm.class)); - cards.add(new SetCardInfo("Brawl", 178, Rarity.RARE, mage.cards.b.Brawl.class)); - cards.add(new SetCardInfo("Briar Patch", 232, Rarity.UNCOMMON, mage.cards.b.BriarPatch.class)); - cards.add(new SetCardInfo("Bribery", 62, Rarity.RARE, mage.cards.b.Bribery.class)); - cards.add(new SetCardInfo("Buoyancy", 63, Rarity.COMMON, mage.cards.b.Buoyancy.class)); - cards.add(new SetCardInfo("Cackling Witch", 119, Rarity.UNCOMMON, mage.cards.c.CacklingWitch.class)); - cards.add(new SetCardInfo("Caller of the Hunt", 233, Rarity.RARE, mage.cards.c.CallerOfTheHunt.class)); - cards.add(new SetCardInfo("Cateran Brute", 120, Rarity.COMMON, mage.cards.c.CateranBrute.class)); - cards.add(new SetCardInfo("Cateran Enforcer", 121, Rarity.UNCOMMON, mage.cards.c.CateranEnforcer.class)); - cards.add(new SetCardInfo("Cateran Kidnappers", 122, Rarity.UNCOMMON, mage.cards.c.CateranKidnappers.class)); - cards.add(new SetCardInfo("Cateran Overlord", 123, Rarity.RARE, mage.cards.c.CateranOverlord.class)); - cards.add(new SetCardInfo("Cateran Persuader", 124, Rarity.COMMON, mage.cards.c.CateranPersuader.class)); - cards.add(new SetCardInfo("Cateran Slaver", 125, Rarity.RARE, mage.cards.c.CateranSlaver.class)); - cards.add(new SetCardInfo("Cateran Summons", 126, Rarity.UNCOMMON, mage.cards.c.CateranSummons.class)); - cards.add(new SetCardInfo("Caustic Wasps", 234, Rarity.UNCOMMON, mage.cards.c.CausticWasps.class)); - cards.add(new SetCardInfo("Cave-In", 180, Rarity.RARE, mage.cards.c.CaveIn.class)); - cards.add(new SetCardInfo("Cavern Crawler", 181, Rarity.COMMON, mage.cards.c.CavernCrawler.class)); - cards.add(new SetCardInfo("Cave Sense", 179, Rarity.COMMON, mage.cards.c.CaveSense.class)); - cards.add(new SetCardInfo("Ceremonial Guard", 182, Rarity.COMMON, mage.cards.c.CeremonialGuard.class)); - cards.add(new SetCardInfo("Chambered Nautilus", 64, Rarity.UNCOMMON, mage.cards.c.ChamberedNautilus.class)); - cards.add(new SetCardInfo("Chameleon Spirit", 65, Rarity.UNCOMMON, mage.cards.c.ChameleonSpirit.class)); - cards.add(new SetCardInfo("Charisma", 66, Rarity.RARE, mage.cards.c.Charisma.class)); - cards.add(new SetCardInfo("Charm Peddler", 6, Rarity.COMMON, mage.cards.c.CharmPeddler.class)); - cards.add(new SetCardInfo("Charmed Griffin", 7, Rarity.UNCOMMON, mage.cards.c.CharmedGriffin.class)); - cards.add(new SetCardInfo("Cho-Arrim Alchemist", 8, Rarity.RARE, mage.cards.c.ChoArrimAlchemist.class)); - cards.add(new SetCardInfo("Cho-Arrim Bruiser", 9, Rarity.RARE, mage.cards.c.ChoArrimBruiser.class)); - cards.add(new SetCardInfo("Cho-Arrim Legate", 10, Rarity.UNCOMMON, mage.cards.c.ChoArrimLegate.class)); - cards.add(new SetCardInfo("Cho-Manno, Revolutionary", 11, Rarity.RARE, mage.cards.c.ChoMannoRevolutionary.class)); - cards.add(new SetCardInfo("Cho-Manno's Blessing", 12, Rarity.COMMON, mage.cards.c.ChoMannosBlessing.class)); - cards.add(new SetCardInfo("Cinder Elemental", 183, Rarity.UNCOMMON, mage.cards.c.CinderElemental.class)); - cards.add(new SetCardInfo("Clear the Land", 235, Rarity.RARE, mage.cards.c.ClearTheLand.class)); - cards.add(new SetCardInfo("Close Quarters", 184, Rarity.UNCOMMON, mage.cards.c.CloseQuarters.class)); - cards.add(new SetCardInfo("Cloud Sprite", 67, Rarity.COMMON, mage.cards.c.CloudSprite.class)); - cards.add(new SetCardInfo("Coastal Piracy", 68, Rarity.UNCOMMON, mage.cards.c.CoastalPiracy.class)); - cards.add(new SetCardInfo("Collective Unconscious", 236, Rarity.RARE, mage.cards.c.CollectiveUnconscious.class)); - cards.add(new SetCardInfo("Common Cause", 13, Rarity.RARE, mage.cards.c.CommonCause.class)); - cards.add(new SetCardInfo("Conspiracy", 127, Rarity.RARE, mage.cards.c.Conspiracy.class)); - cards.add(new SetCardInfo("Cornered Market", 14, Rarity.RARE, mage.cards.c.CorneredMarket.class)); - cards.add(new SetCardInfo("Corrupt Official", 128, Rarity.RARE, mage.cards.c.CorruptOfficial.class)); - cards.add(new SetCardInfo("Counterspell", 69, Rarity.COMMON, mage.cards.c.Counterspell.class)); - cards.add(new SetCardInfo("Cowardice", 70, Rarity.RARE, mage.cards.c.Cowardice.class)); - cards.add(new SetCardInfo("Crackdown", 15, Rarity.RARE, mage.cards.c.Crackdown.class)); - cards.add(new SetCardInfo("Crag Saurian", 185, Rarity.RARE, mage.cards.c.CragSaurian.class)); - cards.add(new SetCardInfo("Crash", 186, Rarity.COMMON, mage.cards.c.Crash.class)); - cards.add(new SetCardInfo("Credit Voucher", 289, Rarity.UNCOMMON, mage.cards.c.CreditVoucher.class)); - cards.add(new SetCardInfo("Crenellated Wall", 290, Rarity.UNCOMMON, mage.cards.c.CrenellatedWall.class)); - cards.add(new SetCardInfo("Crooked Scales", 291, Rarity.RARE, mage.cards.c.CrookedScales.class)); - cards.add(new SetCardInfo("Crossbow Infantry", 16, Rarity.COMMON, mage.cards.c.CrossbowInfantry.class)); - cards.add(new SetCardInfo("Crumbling Sanctuary", 292, Rarity.RARE, mage.cards.c.CrumblingSanctuary.class)); - cards.add(new SetCardInfo("Customs Depot", 71, Rarity.UNCOMMON, mage.cards.c.CustomsDepot.class)); - cards.add(new SetCardInfo("Dark Ritual", 129, Rarity.COMMON, mage.cards.d.DarkRitual.class)); - cards.add(new SetCardInfo("Darting Merfolk", 72, Rarity.COMMON, mage.cards.d.DartingMerfolk.class)); - cards.add(new SetCardInfo("Dawnstrider", 237, Rarity.RARE, mage.cards.d.Dawnstrider.class)); - cards.add(new SetCardInfo("Deadly Insect", 238, Rarity.COMMON, mage.cards.d.DeadlyInsect.class)); - cards.add(new SetCardInfo("Deathgazer", 130, Rarity.UNCOMMON, mage.cards.d.Deathgazer.class)); - cards.add(new SetCardInfo("Deepwood Drummer", 239, Rarity.COMMON, mage.cards.d.DeepwoodDrummer.class)); - cards.add(new SetCardInfo("Deepwood Elder", 240, Rarity.RARE, mage.cards.d.DeepwoodElder.class)); - cards.add(new SetCardInfo("Deepwood Ghoul", 131, Rarity.COMMON, mage.cards.d.DeepwoodGhoul.class)); - cards.add(new SetCardInfo("Deepwood Legate", 132, Rarity.UNCOMMON, mage.cards.d.DeepwoodLegate.class)); - cards.add(new SetCardInfo("Deepwood Tantiv", 241, Rarity.UNCOMMON, mage.cards.d.DeepwoodTantiv.class)); - cards.add(new SetCardInfo("Deepwood Wolverine", 242, Rarity.COMMON, mage.cards.d.DeepwoodWolverine.class)); - cards.add(new SetCardInfo("Dehydration", 73, Rarity.COMMON, mage.cards.d.Dehydration.class)); - cards.add(new SetCardInfo("Delraich", 133, Rarity.RARE, mage.cards.d.Delraich.class)); - cards.add(new SetCardInfo("Desert Twister", 243, Rarity.UNCOMMON, mage.cards.d.DesertTwister.class)); - cards.add(new SetCardInfo("Devout Witness", 17, Rarity.COMMON, mage.cards.d.DevoutWitness.class)); - cards.add(new SetCardInfo("Diplomatic Escort", 74, Rarity.UNCOMMON, mage.cards.d.DiplomaticEscort.class)); - cards.add(new SetCardInfo("Diplomatic Immunity", 75, Rarity.COMMON, mage.cards.d.DiplomaticImmunity.class)); - cards.add(new SetCardInfo("Disenchant", 18, Rarity.COMMON, mage.cards.d.Disenchant.class)); - cards.add(new SetCardInfo("Distorting Lens", 293, Rarity.RARE, mage.cards.d.DistortingLens.class)); - cards.add(new SetCardInfo("Drake Hatchling", 76, Rarity.COMMON, mage.cards.d.DrakeHatchling.class)); - cards.add(new SetCardInfo("Dust Bowl", 316, Rarity.RARE, mage.cards.d.DustBowl.class)); - cards.add(new SetCardInfo("Embargo", 77, Rarity.RARE, mage.cards.e.Embargo.class)); - cards.add(new SetCardInfo("Energy Flux", 78, Rarity.UNCOMMON, mage.cards.e.EnergyFlux.class)); - cards.add(new SetCardInfo("Enslaved Horror", 134, Rarity.UNCOMMON, mage.cards.e.EnslavedHorror.class)); - cards.add(new SetCardInfo("Extortion", 135, Rarity.RARE, mage.cards.e.Extortion.class)); - cards.add(new SetCardInfo("Extravagant Spirit", 79, Rarity.RARE, mage.cards.e.ExtravagantSpirit.class)); - cards.add(new SetCardInfo("Eye of Ramos", 294, Rarity.RARE, mage.cards.e.EyeOfRamos.class)); - cards.add(new SetCardInfo("False Demise", 80, Rarity.UNCOMMON, mage.cards.f.FalseDemise.class)); - cards.add(new SetCardInfo("Ferocity", 245, Rarity.COMMON, mage.cards.f.Ferocity.class)); - cards.add(new SetCardInfo("Flailing Manticore", 187, Rarity.RARE, mage.cards.f.FlailingManticore.class)); - cards.add(new SetCardInfo("Flailing Ogre", 188, Rarity.UNCOMMON, mage.cards.f.FlailingOgre.class)); - cards.add(new SetCardInfo("Flailing Soldier", 189, Rarity.COMMON, mage.cards.f.FlailingSoldier.class)); - cards.add(new SetCardInfo("Flaming Sword", 190, Rarity.COMMON, mage.cards.f.FlamingSword.class)); - cards.add(new SetCardInfo("Food Chain", 246, Rarity.RARE, mage.cards.f.FoodChain.class)); - cards.add(new SetCardInfo("Forced March", 136, Rarity.RARE, mage.cards.f.ForcedMarch.class)); - cards.add(new SetCardInfo("Forest", 347, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Forest", 348, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Forest", 349, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Forest", 350, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Foster", 247, Rarity.RARE, mage.cards.f.Foster.class)); - cards.add(new SetCardInfo("Fountain of Cho", 317, Rarity.UNCOMMON, mage.cards.f.FountainOfCho.class)); - cards.add(new SetCardInfo("Fountain Watch", 19, Rarity.RARE, mage.cards.f.FountainWatch.class)); - cards.add(new SetCardInfo("Fresh Volunteers", 20, Rarity.COMMON, mage.cards.f.FreshVolunteers.class)); - cards.add(new SetCardInfo("Furious Assault", 191, Rarity.COMMON, mage.cards.f.FuriousAssault.class)); - cards.add(new SetCardInfo("Game Preserve", 248, Rarity.RARE, mage.cards.g.GamePreserve.class)); - cards.add(new SetCardInfo("General's Regalia", 295, Rarity.RARE, mage.cards.g.GeneralsRegalia.class)); - cards.add(new SetCardInfo("Gerrard's Irregulars", 192, Rarity.COMMON, mage.cards.g.GerrardsIrregulars.class)); - cards.add(new SetCardInfo("Ghoul's Feast", 137, Rarity.UNCOMMON, mage.cards.g.GhoulsFeast.class)); - cards.add(new SetCardInfo("Giant Caterpillar", 249, Rarity.COMMON, mage.cards.g.GiantCaterpillar.class)); - cards.add(new SetCardInfo("Glowing Anemone", 81, Rarity.UNCOMMON, mage.cards.g.GlowingAnemone.class)); - cards.add(new SetCardInfo("Groundskeeper", 250, Rarity.UNCOMMON, mage.cards.g.Groundskeeper.class)); - cards.add(new SetCardInfo("Gush", 82, Rarity.COMMON, mage.cards.g.Gush.class)); - cards.add(new SetCardInfo("Hammer Mage", 193, Rarity.UNCOMMON, mage.cards.h.HammerMage.class)); - cards.add(new SetCardInfo("Haunted Crossroads", 138, Rarity.UNCOMMON, mage.cards.h.HauntedCrossroads.class)); - cards.add(new SetCardInfo("Heart of Ramos", 296, Rarity.RARE, mage.cards.h.HeartOfRamos.class)); - cards.add(new SetCardInfo("Henge Guardian", 297, Rarity.UNCOMMON, mage.cards.h.HengeGuardian.class)); - cards.add(new SetCardInfo("Henge of Ramos", 318, Rarity.UNCOMMON, mage.cards.h.HengeOfRamos.class)); - cards.add(new SetCardInfo("Hickory Woodlot", 319, Rarity.COMMON, mage.cards.h.HickoryWoodlot.class)); - cards.add(new SetCardInfo("High Market", 320, Rarity.RARE, mage.cards.h.HighMarket.class)); - cards.add(new SetCardInfo("High Seas", 83, Rarity.UNCOMMON, mage.cards.h.HighSeas.class)); - cards.add(new SetCardInfo("Highway Robber", 139, Rarity.COMMON, mage.cards.h.HighwayRobber.class)); - cards.add(new SetCardInfo("Hired Giant", 194, Rarity.UNCOMMON, mage.cards.h.HiredGiant.class)); - cards.add(new SetCardInfo("Honor the Fallen", 21, Rarity.RARE, mage.cards.h.HonorTheFallen.class)); - cards.add(new SetCardInfo("Hoodwink", 84, Rarity.COMMON, mage.cards.h.Hoodwink.class)); - cards.add(new SetCardInfo("Horned Troll", 251, Rarity.COMMON, mage.cards.h.HornedTroll.class)); - cards.add(new SetCardInfo("Horn of Plenty", 298, Rarity.RARE, mage.cards.h.HornOfPlenty.class)); - cards.add(new SetCardInfo("Horn of Ramos", 299, Rarity.RARE, mage.cards.h.HornOfRamos.class)); - cards.add(new SetCardInfo("Howling Wolf", 252, Rarity.COMMON, mage.cards.h.HowlingWolf.class)); - cards.add(new SetCardInfo("Hunted Wumpus", 253, Rarity.UNCOMMON, mage.cards.h.HuntedWumpus.class)); - cards.add(new SetCardInfo("Ignoble Soldier", 22, Rarity.UNCOMMON, mage.cards.i.IgnobleSoldier.class)); - cards.add(new SetCardInfo("Indentured Djinn", 85, Rarity.UNCOMMON, mage.cards.i.IndenturedDjinn.class)); - cards.add(new SetCardInfo("Instigator", 140, Rarity.RARE, mage.cards.i.Instigator.class)); - cards.add(new SetCardInfo("Insubordination", 141, Rarity.COMMON, mage.cards.i.Insubordination.class)); - cards.add(new SetCardInfo("Intimidation", 142, Rarity.UNCOMMON, mage.cards.i.Intimidation.class)); - cards.add(new SetCardInfo("Invigorate", 254, Rarity.COMMON, mage.cards.i.Invigorate.class)); - cards.add(new SetCardInfo("Inviolability", 23, Rarity.COMMON, mage.cards.i.Inviolability.class)); - cards.add(new SetCardInfo("Iron Lance", 300, Rarity.UNCOMMON, mage.cards.i.IronLance.class)); - cards.add(new SetCardInfo("Island", 335, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Island", 336, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Island", 337, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Island", 338, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Ivory Mask", 24, Rarity.RARE, mage.cards.i.IvoryMask.class)); - cards.add(new SetCardInfo("Jeweled Torque", 301, Rarity.UNCOMMON, mage.cards.j.JeweledTorque.class)); - cards.add(new SetCardInfo("Jhovall Queen", 25, Rarity.RARE, mage.cards.j.JhovallQueen.class)); - cards.add(new SetCardInfo("Jhovall Rider", 26, Rarity.UNCOMMON, mage.cards.j.JhovallRider.class)); - cards.add(new SetCardInfo("Karn's Touch", 86, Rarity.RARE, mage.cards.k.KarnsTouch.class)); - cards.add(new SetCardInfo("Kris Mage", 195, Rarity.COMMON, mage.cards.k.KrisMage.class)); - cards.add(new SetCardInfo("Kyren Archive", 302, Rarity.RARE, mage.cards.k.KyrenArchive.class)); - cards.add(new SetCardInfo("Kyren Glider", 196, Rarity.COMMON, mage.cards.k.KyrenGlider.class)); - cards.add(new SetCardInfo("Kyren Legate", 197, Rarity.UNCOMMON, mage.cards.k.KyrenLegate.class)); - cards.add(new SetCardInfo("Kyren Negotiations", 198, Rarity.UNCOMMON, mage.cards.k.KyrenNegotiations.class)); - cards.add(new SetCardInfo("Kyren Sniper", 199, Rarity.COMMON, mage.cards.k.KyrenSniper.class)); - cards.add(new SetCardInfo("Kyren Toy", 303, Rarity.RARE, mage.cards.k.KyrenToy.class)); - cards.add(new SetCardInfo("Land Grant", 255, Rarity.COMMON, mage.cards.l.LandGrant.class)); - cards.add(new SetCardInfo("Larceny", 143, Rarity.UNCOMMON, mage.cards.l.Larceny.class)); - cards.add(new SetCardInfo("Last Breath", 27, Rarity.UNCOMMON, mage.cards.l.LastBreath.class)); - cards.add(new SetCardInfo("Lava Runner", 200, Rarity.RARE, mage.cards.l.LavaRunner.class)); - cards.add(new SetCardInfo("Liability", 144, Rarity.RARE, mage.cards.l.Liability.class)); - cards.add(new SetCardInfo("Lightning Hounds", 201, Rarity.COMMON, mage.cards.l.LightningHounds.class)); - cards.add(new SetCardInfo("Lithophage", 202, Rarity.RARE, mage.cards.l.Lithophage.class)); - cards.add(new SetCardInfo("Lumbering Satyr", 257, Rarity.UNCOMMON, mage.cards.l.LumberingSatyr.class)); - cards.add(new SetCardInfo("Lunge", 203, Rarity.COMMON, mage.cards.l.Lunge.class)); - cards.add(new SetCardInfo("Lure", 258, Rarity.UNCOMMON, mage.cards.l.Lure.class)); - cards.add(new SetCardInfo("Maggot Therapy", 145, Rarity.COMMON, mage.cards.m.MaggotTherapy.class)); - cards.add(new SetCardInfo("Magistrate's Scepter", 304, Rarity.RARE, mage.cards.m.MagistratesScepter.class)); - cards.add(new SetCardInfo("Magistrate's Veto", 204, Rarity.UNCOMMON, mage.cards.m.MagistratesVeto.class)); - cards.add(new SetCardInfo("Megatherium", 259, Rarity.RARE, mage.cards.m.Megatherium.class)); - cards.add(new SetCardInfo("Mercadia's Downfall", 205, Rarity.UNCOMMON, mage.cards.m.MercadiasDownfall.class)); - cards.add(new SetCardInfo("Mercadian Atlas", 305, Rarity.RARE, mage.cards.m.MercadianAtlas.class)); - cards.add(new SetCardInfo("Mercadian Bazaar", 321, Rarity.UNCOMMON, mage.cards.m.MercadianBazaar.class)); - cards.add(new SetCardInfo("Mercadian Lift", 306, Rarity.RARE, mage.cards.m.MercadianLift.class)); - cards.add(new SetCardInfo("Midnight Ritual", 146, Rarity.RARE, mage.cards.m.MidnightRitual.class)); - cards.add(new SetCardInfo("Misdirection", 87, Rarity.RARE, mage.cards.m.Misdirection.class)); - cards.add(new SetCardInfo("Misshapen Fiend", 147, Rarity.COMMON, mage.cards.m.MisshapenFiend.class)); - cards.add(new SetCardInfo("Misstep", 88, Rarity.COMMON, mage.cards.m.Misstep.class)); - cards.add(new SetCardInfo("Molting Harpy", 148, Rarity.UNCOMMON, mage.cards.m.MoltingHarpy.class)); - cards.add(new SetCardInfo("Moment of Silence", 28, Rarity.COMMON, mage.cards.m.MomentOfSilence.class)); - cards.add(new SetCardInfo("Monkey Cage", 307, Rarity.RARE, mage.cards.m.MonkeyCage.class)); - cards.add(new SetCardInfo("Moonlit Wake", 29, Rarity.UNCOMMON, mage.cards.m.MoonlitWake.class)); - cards.add(new SetCardInfo("Mountain", 343, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Mountain", 344, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Mountain", 345, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Mountain", 346, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Muzzle", 30, Rarity.COMMON, mage.cards.m.Muzzle.class)); - cards.add(new SetCardInfo("Natural Affinity", 260, Rarity.RARE, mage.cards.n.NaturalAffinity.class)); - cards.add(new SetCardInfo("Nether Spirit", 149, Rarity.RARE, mage.cards.n.NetherSpirit.class)); - cards.add(new SetCardInfo("Nightwind Glider", 31, Rarity.COMMON, mage.cards.n.NightwindGlider.class)); - cards.add(new SetCardInfo("Noble Purpose", 32, Rarity.UNCOMMON, mage.cards.n.NoblePurpose.class)); - cards.add(new SetCardInfo("Notorious Assassin", 150, Rarity.RARE, mage.cards.n.NotoriousAssassin.class)); - cards.add(new SetCardInfo("Ogre Taskmaster", 206, Rarity.UNCOMMON, mage.cards.o.OgreTaskmaster.class)); - cards.add(new SetCardInfo("Orim's Cure", 33, Rarity.COMMON, mage.cards.o.OrimsCure.class)); - cards.add(new SetCardInfo("Overtaker", 89, Rarity.RARE, mage.cards.o.Overtaker.class)); - cards.add(new SetCardInfo("Panacea", 308, Rarity.UNCOMMON, mage.cards.p.Panacea.class)); - cards.add(new SetCardInfo("Pangosaur", 261, Rarity.RARE, mage.cards.p.Pangosaur.class)); - cards.add(new SetCardInfo("Peat Bog", 322, Rarity.COMMON, mage.cards.p.PeatBog.class)); - cards.add(new SetCardInfo("Pious Warrior", 34, Rarity.COMMON, mage.cards.p.PiousWarrior.class)); - cards.add(new SetCardInfo("Plains", 331, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Plains", 332, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Plains", 333, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Plains", 334, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Port Inspector", 90, Rarity.COMMON, mage.cards.p.PortInspector.class)); - cards.add(new SetCardInfo("Power Matrix", 309, Rarity.RARE, mage.cards.p.PowerMatrix.class)); - cards.add(new SetCardInfo("Pretender's Claim", 151, Rarity.UNCOMMON, mage.cards.p.PretendersClaim.class)); - cards.add(new SetCardInfo("Primeval Shambler", 152, Rarity.UNCOMMON, mage.cards.p.PrimevalShambler.class)); - cards.add(new SetCardInfo("Puffer Extract", 310, Rarity.UNCOMMON, mage.cards.p.PufferExtract.class)); - cards.add(new SetCardInfo("Pulverize", 207, Rarity.RARE, mage.cards.p.Pulverize.class)); - cards.add(new SetCardInfo("Putrefaction", 153, Rarity.UNCOMMON, mage.cards.p.Putrefaction.class)); - cards.add(new SetCardInfo("Puppet's Verdict", 208, Rarity.RARE, mage.cards.p.PuppetsVerdict.class)); - cards.add(new SetCardInfo("Quagmire Lamprey", 154, Rarity.UNCOMMON, mage.cards.q.QuagmireLamprey.class)); - cards.add(new SetCardInfo("Rain of Tears", 155, Rarity.UNCOMMON, mage.cards.r.RainOfTears.class)); - cards.add(new SetCardInfo("Ramosian Captain", 35, Rarity.UNCOMMON, mage.cards.r.RamosianCaptain.class)); - cards.add(new SetCardInfo("Ramosian Commander", 36, Rarity.UNCOMMON, mage.cards.r.RamosianCommander.class)); - cards.add(new SetCardInfo("Ramosian Lieutenant", 37, Rarity.COMMON, mage.cards.r.RamosianLieutenant.class)); - cards.add(new SetCardInfo("Ramosian Rally", 38, Rarity.COMMON, mage.cards.r.RamosianRally.class)); - cards.add(new SetCardInfo("Ramosian Sergeant", 39, Rarity.COMMON, mage.cards.r.RamosianSergeant.class)); - cards.add(new SetCardInfo("Ramosian Sky Marshal", 40, Rarity.RARE, mage.cards.r.RamosianSkyMarshal.class)); - cards.add(new SetCardInfo("Rampart Crawler", 156, Rarity.COMMON, mage.cards.r.RampartCrawler.class)); - cards.add(new SetCardInfo("Rappelling Scouts", 41, Rarity.RARE, mage.cards.r.RappellingScouts.class)); - cards.add(new SetCardInfo("Remote Farm", 323, Rarity.COMMON, mage.cards.r.RemoteFarm.class)); - cards.add(new SetCardInfo("Renounce", 42, Rarity.UNCOMMON, mage.cards.r.Renounce.class)); - cards.add(new SetCardInfo("Revered Elder", 43, Rarity.COMMON, mage.cards.r.ReveredElder.class)); - cards.add(new SetCardInfo("Reverent Mantra", 44, Rarity.RARE, mage.cards.r.ReverentMantra.class)); - cards.add(new SetCardInfo("Revive", 262, Rarity.UNCOMMON, mage.cards.r.Revive.class)); - cards.add(new SetCardInfo("Righteous Aura", 45, Rarity.UNCOMMON, mage.cards.r.RighteousAura.class)); - cards.add(new SetCardInfo("Righteous Indignation", 46, Rarity.UNCOMMON, mage.cards.r.RighteousIndignation.class)); - cards.add(new SetCardInfo("Rishadan Airship", 91, Rarity.COMMON, mage.cards.r.RishadanAirship.class)); - cards.add(new SetCardInfo("Rishadan Brigand", 92, Rarity.RARE, mage.cards.r.RishadanBrigand.class)); - cards.add(new SetCardInfo("Rishadan Cutpurse", 93, Rarity.COMMON, mage.cards.r.RishadanCutpurse.class)); - cards.add(new SetCardInfo("Rishadan Footpad", 94, Rarity.UNCOMMON, mage.cards.r.RishadanFootpad.class)); - cards.add(new SetCardInfo("Rishadan Pawnshop", 311, Rarity.RARE, mage.cards.r.RishadanPawnshop.class)); - cards.add(new SetCardInfo("Rishadan Port", 324, Rarity.RARE, mage.cards.r.RishadanPort.class)); - cards.add(new SetCardInfo("Robber Fly", 209, Rarity.UNCOMMON, mage.cards.r.RobberFly.class)); - cards.add(new SetCardInfo("Rock Badger", 210, Rarity.UNCOMMON, mage.cards.r.RockBadger.class)); - cards.add(new SetCardInfo("Rouse", 157, Rarity.COMMON, mage.cards.r.Rouse.class)); - cards.add(new SetCardInfo("Rushwood Dryad", 263, Rarity.COMMON, mage.cards.r.RushwoodDryad.class)); - cards.add(new SetCardInfo("Rushwood Elemental", 264, Rarity.RARE, mage.cards.r.RushwoodElemental.class)); - cards.add(new SetCardInfo("Rushwood Grove", 325, Rarity.UNCOMMON, mage.cards.r.RushwoodGrove.class)); - cards.add(new SetCardInfo("Rushwood Herbalist", 265, Rarity.COMMON, mage.cards.r.RushwoodHerbalist.class)); - cards.add(new SetCardInfo("Rushwood Legate", 266, Rarity.UNCOMMON, mage.cards.r.RushwoodLegate.class)); - cards.add(new SetCardInfo("Saber Ants", 267, Rarity.UNCOMMON, mage.cards.s.SaberAnts.class)); - cards.add(new SetCardInfo("Sacred Prey", 268, Rarity.COMMON, mage.cards.s.SacredPrey.class)); - cards.add(new SetCardInfo("Sailmonger", 95, Rarity.UNCOMMON, mage.cards.s.Sailmonger.class)); - cards.add(new SetCardInfo("Sand Squid", 96, Rarity.RARE, mage.cards.s.SandSquid.class)); - cards.add(new SetCardInfo("Sandstone Needle", 326, Rarity.COMMON, mage.cards.s.SandstoneNeedle.class)); - cards.add(new SetCardInfo("Saprazzan Bailiff", 97, Rarity.RARE, mage.cards.s.SaprazzanBailiff.class)); - cards.add(new SetCardInfo("Saprazzan Breaker", 98, Rarity.UNCOMMON, mage.cards.s.SaprazzanBreaker.class)); - cards.add(new SetCardInfo("Saprazzan Cove", 327, Rarity.UNCOMMON, mage.cards.s.SaprazzanCove.class)); - cards.add(new SetCardInfo("Saprazzan Heir", 99, Rarity.RARE, mage.cards.s.SaprazzanHeir.class)); - cards.add(new SetCardInfo("Saprazzan Legate", 100, Rarity.UNCOMMON, mage.cards.s.SaprazzanLegate.class)); - cards.add(new SetCardInfo("Saprazzan Outrigger", 101, Rarity.COMMON, mage.cards.s.SaprazzanOutrigger.class)); - cards.add(new SetCardInfo("Saprazzan Raider", 102, Rarity.COMMON, mage.cards.s.SaprazzanRaider.class)); - cards.add(new SetCardInfo("Saprazzan Skerry", 328, Rarity.COMMON, mage.cards.s.SaprazzanSkerry.class)); - cards.add(new SetCardInfo("Scandalmonger", 158, Rarity.UNCOMMON, mage.cards.s.Scandalmonger.class)); - cards.add(new SetCardInfo("Security Detail", 47, Rarity.RARE, mage.cards.s.SecurityDetail.class)); - cards.add(new SetCardInfo("Seismic Mage", 211, Rarity.RARE, mage.cards.s.SeismicMage.class)); - cards.add(new SetCardInfo("Sever Soul", 159, Rarity.COMMON, mage.cards.s.SeverSoul.class)); - cards.add(new SetCardInfo("Shock Troops", 212, Rarity.COMMON, mage.cards.s.ShockTroops.class)); - cards.add(new SetCardInfo("Shoving Match", 103, Rarity.UNCOMMON, mage.cards.s.ShovingMatch.class)); - cards.add(new SetCardInfo("Silent Assassin", 160, Rarity.RARE, mage.cards.s.SilentAssassin.class)); - cards.add(new SetCardInfo("Silverglade Elemental", 269, Rarity.COMMON, mage.cards.s.SilvergladeElemental.class)); - cards.add(new SetCardInfo("Silverglade Pathfinder", 270, Rarity.UNCOMMON, mage.cards.s.SilvergladePathfinder.class)); - cards.add(new SetCardInfo("Sizzle", 213, Rarity.COMMON, mage.cards.s.Sizzle.class)); - cards.add(new SetCardInfo("Skulking Fugitive", 161, Rarity.COMMON, mage.cards.s.SkulkingFugitive.class)); - cards.add(new SetCardInfo("Skull of Ramos", 312, Rarity.RARE, mage.cards.s.SkullOfRamos.class)); - cards.add(new SetCardInfo("Snake Pit", 271, Rarity.UNCOMMON, mage.cards.s.SnakePit.class)); - cards.add(new SetCardInfo("Snorting Gahr", 272, Rarity.COMMON, mage.cards.s.SnortingGahr.class)); - cards.add(new SetCardInfo("Snuff Out", 162, Rarity.COMMON, mage.cards.s.SnuffOut.class)); - cards.add(new SetCardInfo("Soothing Balm", 48, Rarity.COMMON, mage.cards.s.SoothingBalm.class)); - cards.add(new SetCardInfo("Soothsaying", 104, Rarity.UNCOMMON, mage.cards.s.Soothsaying.class)); - cards.add(new SetCardInfo("Soul Channeling", 163, Rarity.COMMON, mage.cards.s.SoulChanneling.class)); - cards.add(new SetCardInfo("Specter's Wail", 164, Rarity.COMMON, mage.cards.s.SpectersWail.class)); - cards.add(new SetCardInfo("Spidersilk Armor", 273, Rarity.COMMON, mage.cards.s.SpidersilkArmor.class)); - cards.add(new SetCardInfo("Spiritual Focus", 49, Rarity.RARE, mage.cards.s.SpiritualFocus.class)); - cards.add(new SetCardInfo("Spontaneous Generation", 274, Rarity.RARE, mage.cards.s.SpontaneousGeneration.class)); - cards.add(new SetCardInfo("Squall", 275, Rarity.COMMON, mage.cards.s.Squall.class)); - cards.add(new SetCardInfo("Squallmonger", 276, Rarity.UNCOMMON, mage.cards.s.Squallmonger.class)); - cards.add(new SetCardInfo("Squee, Goblin Nabob", 214, Rarity.RARE, mage.cards.s.SqueeGoblinNabob.class)); - cards.add(new SetCardInfo("Squeeze", 105, Rarity.RARE, mage.cards.s.Squeeze.class)); - cards.add(new SetCardInfo("Stamina", 277, Rarity.UNCOMMON, mage.cards.s.Stamina.class)); - cards.add(new SetCardInfo("Statecraft", 106, Rarity.RARE, mage.cards.s.Statecraft.class)); - cards.add(new SetCardInfo("Steadfast Guard", 50, Rarity.COMMON, mage.cards.s.SteadfastGuard.class)); - cards.add(new SetCardInfo("Stinging Barrier", 107, Rarity.COMMON, mage.cards.s.StingingBarrier.class)); - cards.add(new SetCardInfo("Stone Rain", 215, Rarity.COMMON, mage.cards.s.StoneRain.class)); - cards.add(new SetCardInfo("Story Circle", 51, Rarity.UNCOMMON, mage.cards.s.StoryCircle.class)); - cards.add(new SetCardInfo("Strongarm Thug", 165, Rarity.UNCOMMON, mage.cards.s.StrongarmThug.class)); - cards.add(new SetCardInfo("Subterranean Hangar", 329, Rarity.UNCOMMON, mage.cards.s.SubterraneanHangar.class)); - cards.add(new SetCardInfo("Sustenance", 278, Rarity.UNCOMMON, mage.cards.s.Sustenance.class)); - cards.add(new SetCardInfo("Swamp", 339, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Swamp", 340, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Swamp", 341, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Swamp", 342, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Task Force", 52, Rarity.COMMON, mage.cards.t.TaskForce.class)); - cards.add(new SetCardInfo("Tectonic Break", 216, Rarity.RARE, mage.cards.t.TectonicBreak.class)); - cards.add(new SetCardInfo("Territorial Dispute", 217, Rarity.RARE, mage.cards.t.TerritorialDispute.class)); - cards.add(new SetCardInfo("Thermal Glider", 53, Rarity.COMMON, mage.cards.t.ThermalGlider.class)); - cards.add(new SetCardInfo("Thieves' Auction", 218, Rarity.RARE, mage.cards.t.ThievesAuction.class)); - cards.add(new SetCardInfo("Thrashing Wumpus", 166, Rarity.RARE, mage.cards.t.ThrashingWumpus.class)); - cards.add(new SetCardInfo("Thunderclap", 219, Rarity.COMMON, mage.cards.t.Thunderclap.class)); - cards.add(new SetCardInfo("Thwart", 108, Rarity.UNCOMMON, mage.cards.t.Thwart.class)); - cards.add(new SetCardInfo("Tidal Bore", 109, Rarity.COMMON, mage.cards.t.TidalBore.class)); - cards.add(new SetCardInfo("Tidal Kraken", 110, Rarity.RARE, mage.cards.t.TidalKraken.class)); - cards.add(new SetCardInfo("Tiger Claws", 279, Rarity.COMMON, mage.cards.t.TigerClaws.class)); - cards.add(new SetCardInfo("Timid Drake", 111, Rarity.UNCOMMON, mage.cards.t.TimidDrake.class)); - cards.add(new SetCardInfo("Tonic Peddler", 54, Rarity.UNCOMMON, mage.cards.t.TonicPeddler.class)); - cards.add(new SetCardInfo("Tooth of Ramos", 313, Rarity.RARE, mage.cards.t.ToothOfRamos.class)); - cards.add(new SetCardInfo("Tower of the Magistrate", 330, Rarity.RARE, mage.cards.t.TowerOfTheMagistrate.class)); - cards.add(new SetCardInfo("Toymaker", 314, Rarity.UNCOMMON, mage.cards.t.Toymaker.class)); - cards.add(new SetCardInfo("Trade Routes", 112, Rarity.RARE, mage.cards.t.TradeRoutes.class)); - cards.add(new SetCardInfo("Tranquility", 280, Rarity.COMMON, mage.cards.t.Tranquility.class)); - cards.add(new SetCardInfo("Trap Runner", 55, Rarity.UNCOMMON, mage.cards.t.TrapRunner.class)); - cards.add(new SetCardInfo("Tremor", 220, Rarity.COMMON, mage.cards.t.Tremor.class)); - cards.add(new SetCardInfo("Two-Headed Dragon", 221, Rarity.RARE, mage.cards.t.TwoHeadedDragon.class)); - cards.add(new SetCardInfo("Undertaker", 167, Rarity.COMMON, mage.cards.u.Undertaker.class)); - cards.add(new SetCardInfo("Unmask", 168, Rarity.RARE, mage.cards.u.Unmask.class)); - cards.add(new SetCardInfo("Unnatural Hunger", 169, Rarity.RARE, mage.cards.u.UnnaturalHunger.class)); - cards.add(new SetCardInfo("Uphill Battle", 222, Rarity.UNCOMMON, mage.cards.u.UphillBattle.class)); - cards.add(new SetCardInfo("Vendetta", 170, Rarity.COMMON, mage.cards.v.Vendetta.class)); - cards.add(new SetCardInfo("Venomous Breath", 281, Rarity.UNCOMMON, mage.cards.v.VenomousBreath.class)); - cards.add(new SetCardInfo("Venomous Dragonfly", 282, Rarity.COMMON, mage.cards.v.VenomousDragonfly.class)); - cards.add(new SetCardInfo("Vernal Equinox", 283, Rarity.RARE, mage.cards.v.VernalEquinox.class)); - cards.add(new SetCardInfo("Vine Dryad", 284, Rarity.RARE, mage.cards.v.VineDryad.class)); - cards.add(new SetCardInfo("Vine Trellis", 285, Rarity.COMMON, mage.cards.v.VineTrellis.class)); - cards.add(new SetCardInfo("Volcanic Wind", 223, Rarity.UNCOMMON, mage.cards.v.VolcanicWind.class)); - cards.add(new SetCardInfo("Wall of Distortion", 171, Rarity.COMMON, mage.cards.w.WallOfDistortion.class)); - cards.add(new SetCardInfo("War Cadence", 224, Rarity.UNCOMMON, mage.cards.w.WarCadence.class)); - cards.add(new SetCardInfo("War Tax", 113, Rarity.UNCOMMON, mage.cards.w.WarTax.class)); - cards.add(new SetCardInfo("Warmonger", 225, Rarity.UNCOMMON, mage.cards.w.Warmonger.class)); - cards.add(new SetCardInfo("Warpath", 226, Rarity.UNCOMMON, mage.cards.w.Warpath.class)); - cards.add(new SetCardInfo("Waterfront Bouncer", 114, Rarity.COMMON, mage.cards.w.WaterfrontBouncer.class)); - cards.add(new SetCardInfo("Wave of Reckoning", 56, Rarity.RARE, mage.cards.w.WaveOfReckoning.class)); - cards.add(new SetCardInfo("Wild Jhovall", 227, Rarity.COMMON, mage.cards.w.WildJhovall.class)); - cards.add(new SetCardInfo("Wishmonger", 57, Rarity.UNCOMMON, mage.cards.w.Wishmonger.class)); - cards.add(new SetCardInfo("Word of Blasting", 228, Rarity.UNCOMMON, mage.cards.w.WordOfBlasting.class)); - cards.add(new SetCardInfo("Worry Beads", 315, Rarity.RARE, mage.cards.w.WorryBeads.class)); - } -} +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * @author North + */ +public final class MercadianMasques extends ExpansionSet { + + private static final MercadianMasques instance = new MercadianMasques(); + + public static MercadianMasques getInstance() { + return instance; + } + + private MercadianMasques() { + super("Mercadian Masques", "MMQ", ExpansionSet.buildDate(1999, 8, 25), SetType.EXPANSION); + this.blockName = "Masques"; + this.hasBoosters = true; + this.numBoosterLands = 0; + this.numBoosterCommon = 11; + this.numBoosterUncommon = 3; + this.numBoosterRare = 1; + this.ratioBoosterMythic = 0; + cards.add(new SetCardInfo("Aerial Caravan", 58, Rarity.RARE, mage.cards.a.AerialCaravan.class)); + cards.add(new SetCardInfo("Afterlife", 1, Rarity.UNCOMMON, mage.cards.a.Afterlife.class)); + cards.add(new SetCardInfo("Alabaster Wall", 2, Rarity.COMMON, mage.cards.a.AlabasterWall.class)); + cards.add(new SetCardInfo("Alley Grifters", 115, Rarity.COMMON, mage.cards.a.AlleyGrifters.class)); + cards.add(new SetCardInfo("Ancestral Mask", 229, Rarity.COMMON, mage.cards.a.AncestralMask.class)); + cards.add(new SetCardInfo("Armistice", 3, Rarity.RARE, mage.cards.a.Armistice.class)); + cards.add(new SetCardInfo("Arms Dealer", 172, Rarity.UNCOMMON, mage.cards.a.ArmsDealer.class)); + cards.add(new SetCardInfo("Arrest", 4, Rarity.UNCOMMON, mage.cards.a.Arrest.class)); + cards.add(new SetCardInfo("Assembly Hall", 286, Rarity.RARE, mage.cards.a.AssemblyHall.class)); + cards.add(new SetCardInfo("Ballista Squad", 5, Rarity.UNCOMMON, mage.cards.b.BallistaSquad.class)); + cards.add(new SetCardInfo("Balloon Peddler", 59, Rarity.COMMON, mage.cards.b.BalloonPeddler.class)); + cards.add(new SetCardInfo("Barbed Wire", 287, Rarity.UNCOMMON, mage.cards.b.BarbedWire.class)); + cards.add(new SetCardInfo("Battle Rampart", 173, Rarity.COMMON, mage.cards.b.BattleRampart.class)); + cards.add(new SetCardInfo("Battle Squadron", 174, Rarity.RARE, mage.cards.b.BattleSquadron.class)); + cards.add(new SetCardInfo("Bifurcate", 230, Rarity.RARE, mage.cards.b.Bifurcate.class)); + cards.add(new SetCardInfo("Black Market", 116, Rarity.RARE, mage.cards.b.BlackMarket.class)); + cards.add(new SetCardInfo("Blaster Mage", 175, Rarity.COMMON, mage.cards.b.BlasterMage.class)); + cards.add(new SetCardInfo("Blockade Runner", 60, Rarity.COMMON, mage.cards.b.BlockadeRunner.class)); + cards.add(new SetCardInfo("Blood Hound", 176, Rarity.RARE, mage.cards.b.BloodHound.class)); + cards.add(new SetCardInfo("Blood Oath", 177, Rarity.RARE, mage.cards.b.BloodOath.class)); + cards.add(new SetCardInfo("Boa Constrictor", 231, Rarity.UNCOMMON, mage.cards.b.BoaConstrictor.class)); + cards.add(new SetCardInfo("Bog Smugglers", 117, Rarity.COMMON, mage.cards.b.BogSmugglers.class)); + cards.add(new SetCardInfo("Bog Witch", 118, Rarity.COMMON, mage.cards.b.BogWitch.class)); + cards.add(new SetCardInfo("Brainstorm", 61, Rarity.COMMON, mage.cards.b.Brainstorm.class)); + cards.add(new SetCardInfo("Brawl", 178, Rarity.RARE, mage.cards.b.Brawl.class)); + cards.add(new SetCardInfo("Briar Patch", 232, Rarity.UNCOMMON, mage.cards.b.BriarPatch.class)); + cards.add(new SetCardInfo("Bribery", 62, Rarity.RARE, mage.cards.b.Bribery.class)); + cards.add(new SetCardInfo("Buoyancy", 63, Rarity.COMMON, mage.cards.b.Buoyancy.class)); + cards.add(new SetCardInfo("Cackling Witch", 119, Rarity.UNCOMMON, mage.cards.c.CacklingWitch.class)); + cards.add(new SetCardInfo("Caller of the Hunt", 233, Rarity.RARE, mage.cards.c.CallerOfTheHunt.class)); + cards.add(new SetCardInfo("Cateran Brute", 120, Rarity.COMMON, mage.cards.c.CateranBrute.class)); + cards.add(new SetCardInfo("Cateran Enforcer", 121, Rarity.UNCOMMON, mage.cards.c.CateranEnforcer.class)); + cards.add(new SetCardInfo("Cateran Kidnappers", 122, Rarity.UNCOMMON, mage.cards.c.CateranKidnappers.class)); + cards.add(new SetCardInfo("Cateran Overlord", 123, Rarity.RARE, mage.cards.c.CateranOverlord.class)); + cards.add(new SetCardInfo("Cateran Persuader", 124, Rarity.COMMON, mage.cards.c.CateranPersuader.class)); + cards.add(new SetCardInfo("Cateran Slaver", 125, Rarity.RARE, mage.cards.c.CateranSlaver.class)); + cards.add(new SetCardInfo("Cateran Summons", 126, Rarity.UNCOMMON, mage.cards.c.CateranSummons.class)); + cards.add(new SetCardInfo("Caustic Wasps", 234, Rarity.UNCOMMON, mage.cards.c.CausticWasps.class)); + cards.add(new SetCardInfo("Cave-In", 180, Rarity.RARE, mage.cards.c.CaveIn.class)); + cards.add(new SetCardInfo("Cavern Crawler", 181, Rarity.COMMON, mage.cards.c.CavernCrawler.class)); + cards.add(new SetCardInfo("Cave Sense", 179, Rarity.COMMON, mage.cards.c.CaveSense.class)); + cards.add(new SetCardInfo("Ceremonial Guard", 182, Rarity.COMMON, mage.cards.c.CeremonialGuard.class)); + cards.add(new SetCardInfo("Chambered Nautilus", 64, Rarity.UNCOMMON, mage.cards.c.ChamberedNautilus.class)); + cards.add(new SetCardInfo("Chameleon Spirit", 65, Rarity.UNCOMMON, mage.cards.c.ChameleonSpirit.class)); + cards.add(new SetCardInfo("Charisma", 66, Rarity.RARE, mage.cards.c.Charisma.class)); + cards.add(new SetCardInfo("Charm Peddler", 6, Rarity.COMMON, mage.cards.c.CharmPeddler.class)); + cards.add(new SetCardInfo("Charmed Griffin", 7, Rarity.UNCOMMON, mage.cards.c.CharmedGriffin.class)); + cards.add(new SetCardInfo("Cho-Arrim Alchemist", 8, Rarity.RARE, mage.cards.c.ChoArrimAlchemist.class)); + cards.add(new SetCardInfo("Cho-Arrim Bruiser", 9, Rarity.RARE, mage.cards.c.ChoArrimBruiser.class)); + cards.add(new SetCardInfo("Cho-Arrim Legate", 10, Rarity.UNCOMMON, mage.cards.c.ChoArrimLegate.class)); + cards.add(new SetCardInfo("Cho-Manno, Revolutionary", 11, Rarity.RARE, mage.cards.c.ChoMannoRevolutionary.class)); + cards.add(new SetCardInfo("Cho-Manno's Blessing", 12, Rarity.COMMON, mage.cards.c.ChoMannosBlessing.class)); + cards.add(new SetCardInfo("Cinder Elemental", 183, Rarity.UNCOMMON, mage.cards.c.CinderElemental.class)); + cards.add(new SetCardInfo("Clear the Land", 235, Rarity.RARE, mage.cards.c.ClearTheLand.class)); + cards.add(new SetCardInfo("Close Quarters", 184, Rarity.UNCOMMON, mage.cards.c.CloseQuarters.class)); + cards.add(new SetCardInfo("Cloud Sprite", 67, Rarity.COMMON, mage.cards.c.CloudSprite.class)); + cards.add(new SetCardInfo("Coastal Piracy", 68, Rarity.UNCOMMON, mage.cards.c.CoastalPiracy.class)); + cards.add(new SetCardInfo("Collective Unconscious", 236, Rarity.RARE, mage.cards.c.CollectiveUnconscious.class)); + cards.add(new SetCardInfo("Common Cause", 13, Rarity.RARE, mage.cards.c.CommonCause.class)); + cards.add(new SetCardInfo("Conspiracy", 127, Rarity.RARE, mage.cards.c.Conspiracy.class)); + cards.add(new SetCardInfo("Cornered Market", 14, Rarity.RARE, mage.cards.c.CorneredMarket.class)); + cards.add(new SetCardInfo("Corrupt Official", 128, Rarity.RARE, mage.cards.c.CorruptOfficial.class)); + cards.add(new SetCardInfo("Counterspell", 69, Rarity.COMMON, mage.cards.c.Counterspell.class)); + cards.add(new SetCardInfo("Cowardice", 70, Rarity.RARE, mage.cards.c.Cowardice.class)); + cards.add(new SetCardInfo("Crackdown", 15, Rarity.RARE, mage.cards.c.Crackdown.class)); + cards.add(new SetCardInfo("Crag Saurian", 185, Rarity.RARE, mage.cards.c.CragSaurian.class)); + cards.add(new SetCardInfo("Crash", 186, Rarity.COMMON, mage.cards.c.Crash.class)); + cards.add(new SetCardInfo("Credit Voucher", 289, Rarity.UNCOMMON, mage.cards.c.CreditVoucher.class)); + cards.add(new SetCardInfo("Crenellated Wall", 290, Rarity.UNCOMMON, mage.cards.c.CrenellatedWall.class)); + cards.add(new SetCardInfo("Crooked Scales", 291, Rarity.RARE, mage.cards.c.CrookedScales.class)); + cards.add(new SetCardInfo("Crossbow Infantry", 16, Rarity.COMMON, mage.cards.c.CrossbowInfantry.class)); + cards.add(new SetCardInfo("Crumbling Sanctuary", 292, Rarity.RARE, mage.cards.c.CrumblingSanctuary.class)); + cards.add(new SetCardInfo("Customs Depot", 71, Rarity.UNCOMMON, mage.cards.c.CustomsDepot.class)); + cards.add(new SetCardInfo("Dark Ritual", 129, Rarity.COMMON, mage.cards.d.DarkRitual.class)); + cards.add(new SetCardInfo("Darting Merfolk", 72, Rarity.COMMON, mage.cards.d.DartingMerfolk.class)); + cards.add(new SetCardInfo("Dawnstrider", 237, Rarity.RARE, mage.cards.d.Dawnstrider.class)); + cards.add(new SetCardInfo("Deadly Insect", 238, Rarity.COMMON, mage.cards.d.DeadlyInsect.class)); + cards.add(new SetCardInfo("Deathgazer", 130, Rarity.UNCOMMON, mage.cards.d.Deathgazer.class)); + cards.add(new SetCardInfo("Deepwood Drummer", 239, Rarity.COMMON, mage.cards.d.DeepwoodDrummer.class)); + cards.add(new SetCardInfo("Deepwood Elder", 240, Rarity.RARE, mage.cards.d.DeepwoodElder.class)); + cards.add(new SetCardInfo("Deepwood Ghoul", 131, Rarity.COMMON, mage.cards.d.DeepwoodGhoul.class)); + cards.add(new SetCardInfo("Deepwood Legate", 132, Rarity.UNCOMMON, mage.cards.d.DeepwoodLegate.class)); + cards.add(new SetCardInfo("Deepwood Tantiv", 241, Rarity.UNCOMMON, mage.cards.d.DeepwoodTantiv.class)); + cards.add(new SetCardInfo("Deepwood Wolverine", 242, Rarity.COMMON, mage.cards.d.DeepwoodWolverine.class)); + cards.add(new SetCardInfo("Dehydration", 73, Rarity.COMMON, mage.cards.d.Dehydration.class)); + cards.add(new SetCardInfo("Delraich", 133, Rarity.RARE, mage.cards.d.Delraich.class)); + cards.add(new SetCardInfo("Desert Twister", 243, Rarity.UNCOMMON, mage.cards.d.DesertTwister.class)); + cards.add(new SetCardInfo("Devout Witness", 17, Rarity.COMMON, mage.cards.d.DevoutWitness.class)); + cards.add(new SetCardInfo("Diplomatic Escort", 74, Rarity.UNCOMMON, mage.cards.d.DiplomaticEscort.class)); + cards.add(new SetCardInfo("Diplomatic Immunity", 75, Rarity.COMMON, mage.cards.d.DiplomaticImmunity.class)); + cards.add(new SetCardInfo("Disenchant", 18, Rarity.COMMON, mage.cards.d.Disenchant.class)); + cards.add(new SetCardInfo("Distorting Lens", 293, Rarity.RARE, mage.cards.d.DistortingLens.class)); + cards.add(new SetCardInfo("Drake Hatchling", 76, Rarity.COMMON, mage.cards.d.DrakeHatchling.class)); + cards.add(new SetCardInfo("Dust Bowl", 316, Rarity.RARE, mage.cards.d.DustBowl.class)); + cards.add(new SetCardInfo("Embargo", 77, Rarity.RARE, mage.cards.e.Embargo.class)); + cards.add(new SetCardInfo("Energy Flux", 78, Rarity.UNCOMMON, mage.cards.e.EnergyFlux.class)); + cards.add(new SetCardInfo("Enslaved Horror", 134, Rarity.UNCOMMON, mage.cards.e.EnslavedHorror.class)); + cards.add(new SetCardInfo("Extortion", 135, Rarity.RARE, mage.cards.e.Extortion.class)); + cards.add(new SetCardInfo("Extravagant Spirit", 79, Rarity.RARE, mage.cards.e.ExtravagantSpirit.class)); + cards.add(new SetCardInfo("Eye of Ramos", 294, Rarity.RARE, mage.cards.e.EyeOfRamos.class)); + cards.add(new SetCardInfo("False Demise", 80, Rarity.UNCOMMON, mage.cards.f.FalseDemise.class)); + cards.add(new SetCardInfo("Ferocity", 245, Rarity.COMMON, mage.cards.f.Ferocity.class)); + cards.add(new SetCardInfo("Flailing Manticore", 187, Rarity.RARE, mage.cards.f.FlailingManticore.class)); + cards.add(new SetCardInfo("Flailing Ogre", 188, Rarity.UNCOMMON, mage.cards.f.FlailingOgre.class)); + cards.add(new SetCardInfo("Flailing Soldier", 189, Rarity.COMMON, mage.cards.f.FlailingSoldier.class)); + cards.add(new SetCardInfo("Flaming Sword", 190, Rarity.COMMON, mage.cards.f.FlamingSword.class)); + cards.add(new SetCardInfo("Food Chain", 246, Rarity.RARE, mage.cards.f.FoodChain.class)); + cards.add(new SetCardInfo("Forced March", 136, Rarity.RARE, mage.cards.f.ForcedMarch.class)); + cards.add(new SetCardInfo("Forest", 347, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Forest", 348, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Forest", 349, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Forest", 350, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Foster", 247, Rarity.RARE, mage.cards.f.Foster.class)); + cards.add(new SetCardInfo("Fountain of Cho", 317, Rarity.UNCOMMON, mage.cards.f.FountainOfCho.class)); + cards.add(new SetCardInfo("Fountain Watch", 19, Rarity.RARE, mage.cards.f.FountainWatch.class)); + cards.add(new SetCardInfo("Fresh Volunteers", 20, Rarity.COMMON, mage.cards.f.FreshVolunteers.class)); + cards.add(new SetCardInfo("Furious Assault", 191, Rarity.COMMON, mage.cards.f.FuriousAssault.class)); + cards.add(new SetCardInfo("Game Preserve", 248, Rarity.RARE, mage.cards.g.GamePreserve.class)); + cards.add(new SetCardInfo("General's Regalia", 295, Rarity.RARE, mage.cards.g.GeneralsRegalia.class)); + cards.add(new SetCardInfo("Gerrard's Irregulars", 192, Rarity.COMMON, mage.cards.g.GerrardsIrregulars.class)); + cards.add(new SetCardInfo("Ghoul's Feast", 137, Rarity.UNCOMMON, mage.cards.g.GhoulsFeast.class)); + cards.add(new SetCardInfo("Giant Caterpillar", 249, Rarity.COMMON, mage.cards.g.GiantCaterpillar.class)); + cards.add(new SetCardInfo("Glowing Anemone", 81, Rarity.UNCOMMON, mage.cards.g.GlowingAnemone.class)); + cards.add(new SetCardInfo("Groundskeeper", 250, Rarity.UNCOMMON, mage.cards.g.Groundskeeper.class)); + cards.add(new SetCardInfo("Gush", 82, Rarity.COMMON, mage.cards.g.Gush.class)); + cards.add(new SetCardInfo("Hammer Mage", 193, Rarity.UNCOMMON, mage.cards.h.HammerMage.class)); + cards.add(new SetCardInfo("Haunted Crossroads", 138, Rarity.UNCOMMON, mage.cards.h.HauntedCrossroads.class)); + cards.add(new SetCardInfo("Heart of Ramos", 296, Rarity.RARE, mage.cards.h.HeartOfRamos.class)); + cards.add(new SetCardInfo("Henge Guardian", 297, Rarity.UNCOMMON, mage.cards.h.HengeGuardian.class)); + cards.add(new SetCardInfo("Henge of Ramos", 318, Rarity.UNCOMMON, mage.cards.h.HengeOfRamos.class)); + cards.add(new SetCardInfo("Hickory Woodlot", 319, Rarity.COMMON, mage.cards.h.HickoryWoodlot.class)); + cards.add(new SetCardInfo("High Market", 320, Rarity.RARE, mage.cards.h.HighMarket.class)); + cards.add(new SetCardInfo("High Seas", 83, Rarity.UNCOMMON, mage.cards.h.HighSeas.class)); + cards.add(new SetCardInfo("Highway Robber", 139, Rarity.COMMON, mage.cards.h.HighwayRobber.class)); + cards.add(new SetCardInfo("Hired Giant", 194, Rarity.UNCOMMON, mage.cards.h.HiredGiant.class)); + cards.add(new SetCardInfo("Honor the Fallen", 21, Rarity.RARE, mage.cards.h.HonorTheFallen.class)); + cards.add(new SetCardInfo("Hoodwink", 84, Rarity.COMMON, mage.cards.h.Hoodwink.class)); + cards.add(new SetCardInfo("Horned Troll", 251, Rarity.COMMON, mage.cards.h.HornedTroll.class)); + cards.add(new SetCardInfo("Horn of Plenty", 298, Rarity.RARE, mage.cards.h.HornOfPlenty.class)); + cards.add(new SetCardInfo("Horn of Ramos", 299, Rarity.RARE, mage.cards.h.HornOfRamos.class)); + cards.add(new SetCardInfo("Howling Wolf", 252, Rarity.COMMON, mage.cards.h.HowlingWolf.class)); + cards.add(new SetCardInfo("Hunted Wumpus", 253, Rarity.UNCOMMON, mage.cards.h.HuntedWumpus.class)); + cards.add(new SetCardInfo("Ignoble Soldier", 22, Rarity.UNCOMMON, mage.cards.i.IgnobleSoldier.class)); + cards.add(new SetCardInfo("Indentured Djinn", 85, Rarity.UNCOMMON, mage.cards.i.IndenturedDjinn.class)); + cards.add(new SetCardInfo("Instigator", 140, Rarity.RARE, mage.cards.i.Instigator.class)); + cards.add(new SetCardInfo("Insubordination", 141, Rarity.COMMON, mage.cards.i.Insubordination.class)); + cards.add(new SetCardInfo("Intimidation", 142, Rarity.UNCOMMON, mage.cards.i.Intimidation.class)); + cards.add(new SetCardInfo("Invigorate", 254, Rarity.COMMON, mage.cards.i.Invigorate.class)); + cards.add(new SetCardInfo("Inviolability", 23, Rarity.COMMON, mage.cards.i.Inviolability.class)); + cards.add(new SetCardInfo("Iron Lance", 300, Rarity.UNCOMMON, mage.cards.i.IronLance.class)); + cards.add(new SetCardInfo("Island", 335, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Island", 336, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Island", 337, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Island", 338, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ivory Mask", 24, Rarity.RARE, mage.cards.i.IvoryMask.class)); + cards.add(new SetCardInfo("Jeweled Torque", 301, Rarity.UNCOMMON, mage.cards.j.JeweledTorque.class)); + cards.add(new SetCardInfo("Jhovall Queen", 25, Rarity.RARE, mage.cards.j.JhovallQueen.class)); + cards.add(new SetCardInfo("Jhovall Rider", 26, Rarity.UNCOMMON, mage.cards.j.JhovallRider.class)); + cards.add(new SetCardInfo("Karn's Touch", 86, Rarity.RARE, mage.cards.k.KarnsTouch.class)); + cards.add(new SetCardInfo("Kris Mage", 195, Rarity.COMMON, mage.cards.k.KrisMage.class)); + cards.add(new SetCardInfo("Kyren Archive", 302, Rarity.RARE, mage.cards.k.KyrenArchive.class)); + cards.add(new SetCardInfo("Kyren Glider", 196, Rarity.COMMON, mage.cards.k.KyrenGlider.class)); + cards.add(new SetCardInfo("Kyren Legate", 197, Rarity.UNCOMMON, mage.cards.k.KyrenLegate.class)); + cards.add(new SetCardInfo("Kyren Negotiations", 198, Rarity.UNCOMMON, mage.cards.k.KyrenNegotiations.class)); + cards.add(new SetCardInfo("Kyren Sniper", 199, Rarity.COMMON, mage.cards.k.KyrenSniper.class)); + cards.add(new SetCardInfo("Kyren Toy", 303, Rarity.RARE, mage.cards.k.KyrenToy.class)); + cards.add(new SetCardInfo("Land Grant", 255, Rarity.COMMON, mage.cards.l.LandGrant.class)); + cards.add(new SetCardInfo("Larceny", 143, Rarity.UNCOMMON, mage.cards.l.Larceny.class)); + cards.add(new SetCardInfo("Last Breath", 27, Rarity.UNCOMMON, mage.cards.l.LastBreath.class)); + cards.add(new SetCardInfo("Lava Runner", 200, Rarity.RARE, mage.cards.l.LavaRunner.class)); + cards.add(new SetCardInfo("Liability", 144, Rarity.RARE, mage.cards.l.Liability.class)); + cards.add(new SetCardInfo("Lightning Hounds", 201, Rarity.COMMON, mage.cards.l.LightningHounds.class)); + cards.add(new SetCardInfo("Lithophage", 202, Rarity.RARE, mage.cards.l.Lithophage.class)); + cards.add(new SetCardInfo("Lumbering Satyr", 257, Rarity.UNCOMMON, mage.cards.l.LumberingSatyr.class)); + cards.add(new SetCardInfo("Lunge", 203, Rarity.COMMON, mage.cards.l.Lunge.class)); + cards.add(new SetCardInfo("Lure", 258, Rarity.UNCOMMON, mage.cards.l.Lure.class)); + cards.add(new SetCardInfo("Maggot Therapy", 145, Rarity.COMMON, mage.cards.m.MaggotTherapy.class)); + cards.add(new SetCardInfo("Magistrate's Scepter", 304, Rarity.RARE, mage.cards.m.MagistratesScepter.class)); + cards.add(new SetCardInfo("Magistrate's Veto", 204, Rarity.UNCOMMON, mage.cards.m.MagistratesVeto.class)); + cards.add(new SetCardInfo("Megatherium", 259, Rarity.RARE, mage.cards.m.Megatherium.class)); + cards.add(new SetCardInfo("Mercadia's Downfall", 205, Rarity.UNCOMMON, mage.cards.m.MercadiasDownfall.class)); + cards.add(new SetCardInfo("Mercadian Atlas", 305, Rarity.RARE, mage.cards.m.MercadianAtlas.class)); + cards.add(new SetCardInfo("Mercadian Bazaar", 321, Rarity.UNCOMMON, mage.cards.m.MercadianBazaar.class)); + cards.add(new SetCardInfo("Mercadian Lift", 306, Rarity.RARE, mage.cards.m.MercadianLift.class)); + cards.add(new SetCardInfo("Midnight Ritual", 146, Rarity.RARE, mage.cards.m.MidnightRitual.class)); + cards.add(new SetCardInfo("Misdirection", 87, Rarity.RARE, mage.cards.m.Misdirection.class)); + cards.add(new SetCardInfo("Misshapen Fiend", 147, Rarity.COMMON, mage.cards.m.MisshapenFiend.class)); + cards.add(new SetCardInfo("Misstep", 88, Rarity.COMMON, mage.cards.m.Misstep.class)); + cards.add(new SetCardInfo("Molting Harpy", 148, Rarity.UNCOMMON, mage.cards.m.MoltingHarpy.class)); + cards.add(new SetCardInfo("Moment of Silence", 28, Rarity.COMMON, mage.cards.m.MomentOfSilence.class)); + cards.add(new SetCardInfo("Monkey Cage", 307, Rarity.RARE, mage.cards.m.MonkeyCage.class)); + cards.add(new SetCardInfo("Moonlit Wake", 29, Rarity.UNCOMMON, mage.cards.m.MoonlitWake.class)); + cards.add(new SetCardInfo("Mountain", 343, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 344, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 345, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 346, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Muzzle", 30, Rarity.COMMON, mage.cards.m.Muzzle.class)); + cards.add(new SetCardInfo("Natural Affinity", 260, Rarity.RARE, mage.cards.n.NaturalAffinity.class)); + cards.add(new SetCardInfo("Nether Spirit", 149, Rarity.RARE, mage.cards.n.NetherSpirit.class)); + cards.add(new SetCardInfo("Nightwind Glider", 31, Rarity.COMMON, mage.cards.n.NightwindGlider.class)); + cards.add(new SetCardInfo("Noble Purpose", 32, Rarity.UNCOMMON, mage.cards.n.NoblePurpose.class)); + cards.add(new SetCardInfo("Notorious Assassin", 150, Rarity.RARE, mage.cards.n.NotoriousAssassin.class)); + cards.add(new SetCardInfo("Ogre Taskmaster", 206, Rarity.UNCOMMON, mage.cards.o.OgreTaskmaster.class)); + cards.add(new SetCardInfo("Orim's Cure", 33, Rarity.COMMON, mage.cards.o.OrimsCure.class)); + cards.add(new SetCardInfo("Overtaker", 89, Rarity.RARE, mage.cards.o.Overtaker.class)); + cards.add(new SetCardInfo("Panacea", 308, Rarity.UNCOMMON, mage.cards.p.Panacea.class)); + cards.add(new SetCardInfo("Pangosaur", 261, Rarity.RARE, mage.cards.p.Pangosaur.class)); + cards.add(new SetCardInfo("Peat Bog", 322, Rarity.COMMON, mage.cards.p.PeatBog.class)); + cards.add(new SetCardInfo("Pious Warrior", 34, Rarity.COMMON, mage.cards.p.PiousWarrior.class)); + cards.add(new SetCardInfo("Plains", 331, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 332, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 333, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 334, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Port Inspector", 90, Rarity.COMMON, mage.cards.p.PortInspector.class)); + cards.add(new SetCardInfo("Power Matrix", 309, Rarity.RARE, mage.cards.p.PowerMatrix.class)); + cards.add(new SetCardInfo("Pretender's Claim", 151, Rarity.UNCOMMON, mage.cards.p.PretendersClaim.class)); + cards.add(new SetCardInfo("Primeval Shambler", 152, Rarity.UNCOMMON, mage.cards.p.PrimevalShambler.class)); + cards.add(new SetCardInfo("Puffer Extract", 310, Rarity.UNCOMMON, mage.cards.p.PufferExtract.class)); + cards.add(new SetCardInfo("Pulverize", 207, Rarity.RARE, mage.cards.p.Pulverize.class)); + cards.add(new SetCardInfo("Putrefaction", 153, Rarity.UNCOMMON, mage.cards.p.Putrefaction.class)); + cards.add(new SetCardInfo("Puppet's Verdict", 208, Rarity.RARE, mage.cards.p.PuppetsVerdict.class)); + cards.add(new SetCardInfo("Quagmire Lamprey", 154, Rarity.UNCOMMON, mage.cards.q.QuagmireLamprey.class)); + cards.add(new SetCardInfo("Rain of Tears", 155, Rarity.UNCOMMON, mage.cards.r.RainOfTears.class)); + cards.add(new SetCardInfo("Ramosian Captain", 35, Rarity.UNCOMMON, mage.cards.r.RamosianCaptain.class)); + cards.add(new SetCardInfo("Ramosian Commander", 36, Rarity.UNCOMMON, mage.cards.r.RamosianCommander.class)); + cards.add(new SetCardInfo("Ramosian Lieutenant", 37, Rarity.COMMON, mage.cards.r.RamosianLieutenant.class)); + cards.add(new SetCardInfo("Ramosian Rally", 38, Rarity.COMMON, mage.cards.r.RamosianRally.class)); + cards.add(new SetCardInfo("Ramosian Sergeant", 39, Rarity.COMMON, mage.cards.r.RamosianSergeant.class)); + cards.add(new SetCardInfo("Ramosian Sky Marshal", 40, Rarity.RARE, mage.cards.r.RamosianSkyMarshal.class)); + cards.add(new SetCardInfo("Rampart Crawler", 156, Rarity.COMMON, mage.cards.r.RampartCrawler.class)); + cards.add(new SetCardInfo("Rappelling Scouts", 41, Rarity.RARE, mage.cards.r.RappellingScouts.class)); + cards.add(new SetCardInfo("Remote Farm", 323, Rarity.COMMON, mage.cards.r.RemoteFarm.class)); + cards.add(new SetCardInfo("Renounce", 42, Rarity.UNCOMMON, mage.cards.r.Renounce.class)); + cards.add(new SetCardInfo("Revered Elder", 43, Rarity.COMMON, mage.cards.r.ReveredElder.class)); + cards.add(new SetCardInfo("Reverent Mantra", 44, Rarity.RARE, mage.cards.r.ReverentMantra.class)); + cards.add(new SetCardInfo("Revive", 262, Rarity.UNCOMMON, mage.cards.r.Revive.class)); + cards.add(new SetCardInfo("Righteous Aura", 45, Rarity.UNCOMMON, mage.cards.r.RighteousAura.class)); + cards.add(new SetCardInfo("Righteous Indignation", 46, Rarity.UNCOMMON, mage.cards.r.RighteousIndignation.class)); + cards.add(new SetCardInfo("Rishadan Airship", 91, Rarity.COMMON, mage.cards.r.RishadanAirship.class)); + cards.add(new SetCardInfo("Rishadan Brigand", 92, Rarity.RARE, mage.cards.r.RishadanBrigand.class)); + cards.add(new SetCardInfo("Rishadan Cutpurse", 93, Rarity.COMMON, mage.cards.r.RishadanCutpurse.class)); + cards.add(new SetCardInfo("Rishadan Footpad", 94, Rarity.UNCOMMON, mage.cards.r.RishadanFootpad.class)); + cards.add(new SetCardInfo("Rishadan Pawnshop", 311, Rarity.RARE, mage.cards.r.RishadanPawnshop.class)); + cards.add(new SetCardInfo("Rishadan Port", 324, Rarity.RARE, mage.cards.r.RishadanPort.class)); + cards.add(new SetCardInfo("Robber Fly", 209, Rarity.UNCOMMON, mage.cards.r.RobberFly.class)); + cards.add(new SetCardInfo("Rock Badger", 210, Rarity.UNCOMMON, mage.cards.r.RockBadger.class)); + cards.add(new SetCardInfo("Rouse", 157, Rarity.COMMON, mage.cards.r.Rouse.class)); + cards.add(new SetCardInfo("Rushwood Dryad", 263, Rarity.COMMON, mage.cards.r.RushwoodDryad.class)); + cards.add(new SetCardInfo("Rushwood Elemental", 264, Rarity.RARE, mage.cards.r.RushwoodElemental.class)); + cards.add(new SetCardInfo("Rushwood Grove", 325, Rarity.UNCOMMON, mage.cards.r.RushwoodGrove.class)); + cards.add(new SetCardInfo("Rushwood Herbalist", 265, Rarity.COMMON, mage.cards.r.RushwoodHerbalist.class)); + cards.add(new SetCardInfo("Rushwood Legate", 266, Rarity.UNCOMMON, mage.cards.r.RushwoodLegate.class)); + cards.add(new SetCardInfo("Saber Ants", 267, Rarity.UNCOMMON, mage.cards.s.SaberAnts.class)); + cards.add(new SetCardInfo("Sacred Prey", 268, Rarity.COMMON, mage.cards.s.SacredPrey.class)); + cards.add(new SetCardInfo("Sailmonger", 95, Rarity.UNCOMMON, mage.cards.s.Sailmonger.class)); + cards.add(new SetCardInfo("Sand Squid", 96, Rarity.RARE, mage.cards.s.SandSquid.class)); + cards.add(new SetCardInfo("Sandstone Needle", 326, Rarity.COMMON, mage.cards.s.SandstoneNeedle.class)); + cards.add(new SetCardInfo("Saprazzan Bailiff", 97, Rarity.RARE, mage.cards.s.SaprazzanBailiff.class)); + cards.add(new SetCardInfo("Saprazzan Breaker", 98, Rarity.UNCOMMON, mage.cards.s.SaprazzanBreaker.class)); + cards.add(new SetCardInfo("Saprazzan Cove", 327, Rarity.UNCOMMON, mage.cards.s.SaprazzanCove.class)); + cards.add(new SetCardInfo("Saprazzan Heir", 99, Rarity.RARE, mage.cards.s.SaprazzanHeir.class)); + cards.add(new SetCardInfo("Saprazzan Legate", 100, Rarity.UNCOMMON, mage.cards.s.SaprazzanLegate.class)); + cards.add(new SetCardInfo("Saprazzan Outrigger", 101, Rarity.COMMON, mage.cards.s.SaprazzanOutrigger.class)); + cards.add(new SetCardInfo("Saprazzan Raider", 102, Rarity.COMMON, mage.cards.s.SaprazzanRaider.class)); + cards.add(new SetCardInfo("Saprazzan Skerry", 328, Rarity.COMMON, mage.cards.s.SaprazzanSkerry.class)); + cards.add(new SetCardInfo("Scandalmonger", 158, Rarity.UNCOMMON, mage.cards.s.Scandalmonger.class)); + cards.add(new SetCardInfo("Security Detail", 47, Rarity.RARE, mage.cards.s.SecurityDetail.class)); + cards.add(new SetCardInfo("Seismic Mage", 211, Rarity.RARE, mage.cards.s.SeismicMage.class)); + cards.add(new SetCardInfo("Sever Soul", 159, Rarity.COMMON, mage.cards.s.SeverSoul.class)); + cards.add(new SetCardInfo("Shock Troops", 212, Rarity.COMMON, mage.cards.s.ShockTroops.class)); + cards.add(new SetCardInfo("Shoving Match", 103, Rarity.UNCOMMON, mage.cards.s.ShovingMatch.class)); + cards.add(new SetCardInfo("Silent Assassin", 160, Rarity.RARE, mage.cards.s.SilentAssassin.class)); + cards.add(new SetCardInfo("Silverglade Elemental", 269, Rarity.COMMON, mage.cards.s.SilvergladeElemental.class)); + cards.add(new SetCardInfo("Silverglade Pathfinder", 270, Rarity.UNCOMMON, mage.cards.s.SilvergladePathfinder.class)); + cards.add(new SetCardInfo("Sizzle", 213, Rarity.COMMON, mage.cards.s.Sizzle.class)); + cards.add(new SetCardInfo("Skulking Fugitive", 161, Rarity.COMMON, mage.cards.s.SkulkingFugitive.class)); + cards.add(new SetCardInfo("Skull of Ramos", 312, Rarity.RARE, mage.cards.s.SkullOfRamos.class)); + cards.add(new SetCardInfo("Snake Pit", 271, Rarity.UNCOMMON, mage.cards.s.SnakePit.class)); + cards.add(new SetCardInfo("Snorting Gahr", 272, Rarity.COMMON, mage.cards.s.SnortingGahr.class)); + cards.add(new SetCardInfo("Snuff Out", 162, Rarity.COMMON, mage.cards.s.SnuffOut.class)); + cards.add(new SetCardInfo("Soothing Balm", 48, Rarity.COMMON, mage.cards.s.SoothingBalm.class)); + cards.add(new SetCardInfo("Soothsaying", 104, Rarity.UNCOMMON, mage.cards.s.Soothsaying.class)); + cards.add(new SetCardInfo("Soul Channeling", 163, Rarity.COMMON, mage.cards.s.SoulChanneling.class)); + cards.add(new SetCardInfo("Specter's Wail", 164, Rarity.COMMON, mage.cards.s.SpectersWail.class)); + cards.add(new SetCardInfo("Spidersilk Armor", 273, Rarity.COMMON, mage.cards.s.SpidersilkArmor.class)); + cards.add(new SetCardInfo("Spiritual Focus", 49, Rarity.RARE, mage.cards.s.SpiritualFocus.class)); + cards.add(new SetCardInfo("Spontaneous Generation", 274, Rarity.RARE, mage.cards.s.SpontaneousGeneration.class)); + cards.add(new SetCardInfo("Squall", 275, Rarity.COMMON, mage.cards.s.Squall.class)); + cards.add(new SetCardInfo("Squallmonger", 276, Rarity.UNCOMMON, mage.cards.s.Squallmonger.class)); + cards.add(new SetCardInfo("Squee, Goblin Nabob", 214, Rarity.RARE, mage.cards.s.SqueeGoblinNabob.class)); + cards.add(new SetCardInfo("Squeeze", 105, Rarity.RARE, mage.cards.s.Squeeze.class)); + cards.add(new SetCardInfo("Stamina", 277, Rarity.UNCOMMON, mage.cards.s.Stamina.class)); + cards.add(new SetCardInfo("Statecraft", 106, Rarity.RARE, mage.cards.s.Statecraft.class)); + cards.add(new SetCardInfo("Steadfast Guard", 50, Rarity.COMMON, mage.cards.s.SteadfastGuard.class)); + cards.add(new SetCardInfo("Stinging Barrier", 107, Rarity.COMMON, mage.cards.s.StingingBarrier.class)); + cards.add(new SetCardInfo("Stone Rain", 215, Rarity.COMMON, mage.cards.s.StoneRain.class)); + cards.add(new SetCardInfo("Story Circle", 51, Rarity.UNCOMMON, mage.cards.s.StoryCircle.class)); + cards.add(new SetCardInfo("Strongarm Thug", 165, Rarity.UNCOMMON, mage.cards.s.StrongarmThug.class)); + cards.add(new SetCardInfo("Subterranean Hangar", 329, Rarity.UNCOMMON, mage.cards.s.SubterraneanHangar.class)); + cards.add(new SetCardInfo("Sustenance", 278, Rarity.UNCOMMON, mage.cards.s.Sustenance.class)); + cards.add(new SetCardInfo("Swamp", 339, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Swamp", 340, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Swamp", 341, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Swamp", 342, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Task Force", 52, Rarity.COMMON, mage.cards.t.TaskForce.class)); + cards.add(new SetCardInfo("Tectonic Break", 216, Rarity.RARE, mage.cards.t.TectonicBreak.class)); + cards.add(new SetCardInfo("Territorial Dispute", 217, Rarity.RARE, mage.cards.t.TerritorialDispute.class)); + cards.add(new SetCardInfo("Thermal Glider", 53, Rarity.COMMON, mage.cards.t.ThermalGlider.class)); + cards.add(new SetCardInfo("Thieves' Auction", 218, Rarity.RARE, mage.cards.t.ThievesAuction.class)); + cards.add(new SetCardInfo("Thrashing Wumpus", 166, Rarity.RARE, mage.cards.t.ThrashingWumpus.class)); + cards.add(new SetCardInfo("Thunderclap", 219, Rarity.COMMON, mage.cards.t.Thunderclap.class)); + cards.add(new SetCardInfo("Thwart", 108, Rarity.UNCOMMON, mage.cards.t.Thwart.class)); + cards.add(new SetCardInfo("Tidal Bore", 109, Rarity.COMMON, mage.cards.t.TidalBore.class)); + cards.add(new SetCardInfo("Tidal Kraken", 110, Rarity.RARE, mage.cards.t.TidalKraken.class)); + cards.add(new SetCardInfo("Tiger Claws", 279, Rarity.COMMON, mage.cards.t.TigerClaws.class)); + cards.add(new SetCardInfo("Timid Drake", 111, Rarity.UNCOMMON, mage.cards.t.TimidDrake.class)); + cards.add(new SetCardInfo("Tonic Peddler", 54, Rarity.UNCOMMON, mage.cards.t.TonicPeddler.class)); + cards.add(new SetCardInfo("Tooth of Ramos", 313, Rarity.RARE, mage.cards.t.ToothOfRamos.class)); + cards.add(new SetCardInfo("Tower of the Magistrate", 330, Rarity.RARE, mage.cards.t.TowerOfTheMagistrate.class)); + cards.add(new SetCardInfo("Toymaker", 314, Rarity.UNCOMMON, mage.cards.t.Toymaker.class)); + cards.add(new SetCardInfo("Trade Routes", 112, Rarity.RARE, mage.cards.t.TradeRoutes.class)); + cards.add(new SetCardInfo("Tranquility", 280, Rarity.COMMON, mage.cards.t.Tranquility.class)); + cards.add(new SetCardInfo("Trap Runner", 55, Rarity.UNCOMMON, mage.cards.t.TrapRunner.class)); + cards.add(new SetCardInfo("Tremor", 220, Rarity.COMMON, mage.cards.t.Tremor.class)); + cards.add(new SetCardInfo("Two-Headed Dragon", 221, Rarity.RARE, mage.cards.t.TwoHeadedDragon.class)); + cards.add(new SetCardInfo("Undertaker", 167, Rarity.COMMON, mage.cards.u.Undertaker.class)); + cards.add(new SetCardInfo("Unmask", 168, Rarity.RARE, mage.cards.u.Unmask.class)); + cards.add(new SetCardInfo("Unnatural Hunger", 169, Rarity.RARE, mage.cards.u.UnnaturalHunger.class)); + cards.add(new SetCardInfo("Uphill Battle", 222, Rarity.UNCOMMON, mage.cards.u.UphillBattle.class)); + cards.add(new SetCardInfo("Vendetta", 170, Rarity.COMMON, mage.cards.v.Vendetta.class)); + cards.add(new SetCardInfo("Venomous Breath", 281, Rarity.UNCOMMON, mage.cards.v.VenomousBreath.class)); + cards.add(new SetCardInfo("Venomous Dragonfly", 282, Rarity.COMMON, mage.cards.v.VenomousDragonfly.class)); + cards.add(new SetCardInfo("Vernal Equinox", 283, Rarity.RARE, mage.cards.v.VernalEquinox.class)); + cards.add(new SetCardInfo("Vine Dryad", 284, Rarity.RARE, mage.cards.v.VineDryad.class)); + cards.add(new SetCardInfo("Vine Trellis", 285, Rarity.COMMON, mage.cards.v.VineTrellis.class)); + cards.add(new SetCardInfo("Volcanic Wind", 223, Rarity.UNCOMMON, mage.cards.v.VolcanicWind.class)); + cards.add(new SetCardInfo("Wall of Distortion", 171, Rarity.COMMON, mage.cards.w.WallOfDistortion.class)); + cards.add(new SetCardInfo("War Cadence", 224, Rarity.UNCOMMON, mage.cards.w.WarCadence.class)); + cards.add(new SetCardInfo("War Tax", 113, Rarity.UNCOMMON, mage.cards.w.WarTax.class)); + cards.add(new SetCardInfo("Warmonger", 225, Rarity.UNCOMMON, mage.cards.w.Warmonger.class)); + cards.add(new SetCardInfo("Warpath", 226, Rarity.UNCOMMON, mage.cards.w.Warpath.class)); + cards.add(new SetCardInfo("Waterfront Bouncer", 114, Rarity.COMMON, mage.cards.w.WaterfrontBouncer.class)); + cards.add(new SetCardInfo("Wave of Reckoning", 56, Rarity.RARE, mage.cards.w.WaveOfReckoning.class)); + cards.add(new SetCardInfo("Wild Jhovall", 227, Rarity.COMMON, mage.cards.w.WildJhovall.class)); + cards.add(new SetCardInfo("Wishmonger", 57, Rarity.UNCOMMON, mage.cards.w.Wishmonger.class)); + cards.add(new SetCardInfo("Word of Blasting", 228, Rarity.UNCOMMON, mage.cards.w.WordOfBlasting.class)); + cards.add(new SetCardInfo("Worry Beads", 315, Rarity.RARE, mage.cards.w.WorryBeads.class)); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java index 8c00b671fdb..a5fc11b7f37 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java @@ -1,110 +1,110 @@ - -package org.mage.test.cards.abilities.keywords; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class CumulativeUpkeepTest extends CardTestPlayerBase { - - @Test - public void basicTest() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); - // Flying; fear - // Cumulative upkeep {B} - addCard(Zone.HAND, playerA, "Phobian Phantasm"); // Creature {1}{B}{B} 3/3 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm"); - - // Phobian Phantasm - CumulativeUpkeepAbility: Cumulative upkeep {B} - setChoice(playerA, true); // Pay {B}? - attack(3, playerA, "Phobian Phantasm"); - checkPermanentCounters("Age counters", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm", CounterType.AGE, 1); - - setChoice(playerA, true); // Pay {B}{B}? - attack(5, playerA, "Phobian Phantasm"); - checkPermanentCounters("Age counters", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm", CounterType.AGE, 2); - - setChoice(playerA, false); // Pay {B}{B}{B}? - - setStopAt(7, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Phobian Phantasm", 1); - - assertLife(playerA, 20); - assertLife(playerB, 14); - } - - - /** - I changed control of a Illusions of Grandeur to an AI after cumulative upkeep had triggered but before it resolved. - I chose not to pay the upkeep cost and then either the AI sacrificed it or I sacrificed it, neither of which should happen. - I can't sacrifice it because it's not under my control. The AI can't sacrifice it because they are not instructed to do so. - - Here is the reminder text for cumulative upkeep: - At the beginning of your upkeep, put an age counter on this permanent, then sacrifice it unless you pay its upkeep cost for each age counter on it. - */ - @Test - public void controlChangeTest() { - setStrictChooseMode(true); - - // Whenever Kor Celebrant or another creature enters the battlefield under your control, you gain 1 life. - addCard(Zone.HAND, playerB, "Kor Celebrant", 1); // Creature {2}{W} - addCard(Zone.BATTLEFIELD, playerB, "Plains", 3); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 6); - // Cumulative upkeep {2} - // When Illusions of Grandeur enters the battlefield, you gain 20 life. - // When Illusions of Grandeur leaves the battlefield, you lose 20 life. - addCard(Zone.HAND, playerA, "Illusions of Grandeur"); // Enchantment {3}{U} - - // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. - addCard(Zone.HAND, playerA, "Puca's Mischief"); // Enchantment {3}{U} - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Illusions of Grandeur"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Kor Celebrant"); - - // Illusions of Grandeur - CumulativeUpkeepAbility: Cumulative upkeep {2} - setChoice(playerA, true); // Pay {2}? - - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Puca's Mischief"); - - setChoice(playerA, "Cumulative upkeep"); // Triggered list (total 2) which trigger goes first on the stack - addTarget(playerA, "Illusions of Grandeur"); // Own target permanent of Puca's Mischief - addTarget(playerA, "Kor Celebrant"); // Opponent's target permanent of Puca's Mischief - - setChoice(playerA, true); // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. - setChoice(playerA, false); // Pay {2}{2}? - - checkPermanentCounters("Age counters", 5, PhaseStep.PRECOMBAT_MAIN, playerB, "Illusions of Grandeur", CounterType.AGE, 2); - - setStopAt(5, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertLife(playerA, 40); - assertLife(playerB, 21); - - assertPermanentCount(playerA, "Kor Celebrant", 1); - assertPermanentCount(playerB, "Illusions of Grandeur", 1); - - - } - - + +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class CumulativeUpkeepTest extends CardTestPlayerBase { + + @Test + public void basicTest() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + // Flying; fear + // Cumulative upkeep {B} + addCard(Zone.HAND, playerA, "Phobian Phantasm"); // Creature {1}{B}{B} 3/3 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm"); + + // Phobian Phantasm - CumulativeUpkeepAbility: Cumulative upkeep {B} + setChoice(playerA, true); // Pay {B}? + attack(3, playerA, "Phobian Phantasm"); + checkPermanentCounters("Age counters", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm", CounterType.AGE, 1); + + setChoice(playerA, true); // Pay {B}{B}? + attack(5, playerA, "Phobian Phantasm"); + checkPermanentCounters("Age counters", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm", CounterType.AGE, 2); + + setChoice(playerA, false); // Pay {B}{B}{B}? + + setStopAt(7, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Phobian Phantasm", 1); + + assertLife(playerA, 20); + assertLife(playerB, 14); + } + + + /** + I changed control of a Illusions of Grandeur to an AI after cumulative upkeep had triggered but before it resolved. + I chose not to pay the upkeep cost and then either the AI sacrificed it or I sacrificed it, neither of which should happen. + I can't sacrifice it because it's not under my control. The AI can't sacrifice it because they are not instructed to do so. + + Here is the reminder text for cumulative upkeep: + At the beginning of your upkeep, put an age counter on this permanent, then sacrifice it unless you pay its upkeep cost for each age counter on it. + */ + @Test + public void controlChangeTest() { + setStrictChooseMode(true); + + // Whenever Kor Celebrant or another creature enters the battlefield under your control, you gain 1 life. + addCard(Zone.HAND, playerB, "Kor Celebrant", 1); // Creature {2}{W} + addCard(Zone.BATTLEFIELD, playerB, "Plains", 3); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + // Cumulative upkeep {2} + // When Illusions of Grandeur enters the battlefield, you gain 20 life. + // When Illusions of Grandeur leaves the battlefield, you lose 20 life. + addCard(Zone.HAND, playerA, "Illusions of Grandeur"); // Enchantment {3}{U} + + // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. + addCard(Zone.HAND, playerA, "Puca's Mischief"); // Enchantment {3}{U} + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Illusions of Grandeur"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Kor Celebrant"); + + // Illusions of Grandeur - CumulativeUpkeepAbility: Cumulative upkeep {2} + setChoice(playerA, true); // Pay {2}? + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Puca's Mischief"); + + setChoice(playerA, "Cumulative upkeep"); // Triggered list (total 2) which trigger goes first on the stack + addTarget(playerA, "Illusions of Grandeur"); // Own target permanent of Puca's Mischief + addTarget(playerA, "Kor Celebrant"); // Opponent's target permanent of Puca's Mischief + + setChoice(playerA, true); // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. + setChoice(playerA, false); // Pay {2}{2}? + + checkPermanentCounters("Age counters", 5, PhaseStep.PRECOMBAT_MAIN, playerB, "Illusions of Grandeur", CounterType.AGE, 2); + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertLife(playerA, 40); + assertLife(playerB, 21); + + assertPermanentCount(playerA, "Kor Celebrant", 1); + assertPermanentCount(playerB, "Illusions of Grandeur", 1); + + + } + + } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java index 787d9d59f03..1946604d6f0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java @@ -1,179 +1,179 @@ -package org.mage.test.cards.abilities.keywords; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * @author LevelX2, JayDi85 - */ - -public class EntwineTest extends CardTestPlayerBase { - - @Test - public void test_CastWithoutEntwine() { - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, false); // not use Entwine - setModeChoice(playerA, "1"); // target creature - addTarget(playerA, "Balduvian Bears"); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Mountain", true, 3); - } - - @Test - public void test_CastEntwine_Normal() { - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2); - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // use Entwine - addTarget(playerA, "Balduvian Bears"); - addTarget(playerA, playerA); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Mountain", true, 3 + 2); - } - - @Test - public void test_CastEntwine_CostReduction() { - addCustomEffect_SpellCostModification(playerA, -4); - - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // -4 as cost reduction - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // use Entwine - addTarget(playerA, "Balduvian Bears"); - addTarget(playerA, playerA); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Mountain", true, 1); - } - - @Test - public void test_CastEntwine_CostIncreasing() { - addCustomEffect_SpellCostModification(playerA, 5); - - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2 + 5); - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // use Entwine - addTarget(playerA, "Balduvian Bears"); - addTarget(playerA, playerA); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Mountain", true, 3 + 2 + 5); - } - - @Test - public void test_CastEntwine_FreeFromHand() { - // You may cast nonland cards from your hand without paying their mana costs. - addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); - - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); // only Entwine pay need - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // cast for free - setChoice(playerA, true); // use Entwine - addTarget(playerA, "Balduvian Bears"); - addTarget(playerA, playerA); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Plains", true, 2); - } - - @Test - public void test_ToothAndNail() { - setStrictChooseMode(true); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); - addCard(Zone.LIBRARY, playerA, "Pillarfield Ox", 1); - - addCard(Zone.BATTLEFIELD, playerA, "Forest", 9); - // Choose one - - // Search your library for up to two creature cards, reveal them, put them into your hand, then shuffle your library; - // or put up to two creature cards from your hand onto the battlefield. - // Entwine {2} - addCard(Zone.HAND, playerA, "Tooth and Nail"); // Sorcery {5}{G}{G} - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tooth and Nail"); - setChoice(playerA, true); // Message: Pay Entwine {2} ? - addTarget(playerA, "Silvercoat Lion^Pillarfield Ox"); - setChoice(playerA, "Silvercoat Lion^Pillarfield Ox"); - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Silvercoat Lion", 1); - assertPermanentCount(playerA, "Pillarfield Ox", 1); - } -} +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author LevelX2, JayDi85 + */ + +public class EntwineTest extends CardTestPlayerBase { + + @Test + public void test_CastWithoutEntwine() { + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, false); // not use Entwine + setModeChoice(playerA, "1"); // target creature + addTarget(playerA, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 3); + } + + @Test + public void test_CastEntwine_Normal() { + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, true); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 3 + 2); + } + + @Test + public void test_CastEntwine_CostReduction() { + addCustomEffect_SpellCostModification(playerA, -4); + + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // -4 as cost reduction + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, true); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 1); + } + + @Test + public void test_CastEntwine_CostIncreasing() { + addCustomEffect_SpellCostModification(playerA, 5); + + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2 + 5); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, true); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 3 + 2 + 5); + } + + @Test + public void test_CastEntwine_FreeFromHand() { + // You may cast nonland cards from your hand without paying their mana costs. + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); // only Entwine pay need + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, true); // cast for free + setChoice(playerA, true); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Plains", true, 2); + } + + @Test + public void test_ToothAndNail() { + setStrictChooseMode(true); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); + addCard(Zone.LIBRARY, playerA, "Pillarfield Ox", 1); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 9); + // Choose one - + // Search your library for up to two creature cards, reveal them, put them into your hand, then shuffle your library; + // or put up to two creature cards from your hand onto the battlefield. + // Entwine {2} + addCard(Zone.HAND, playerA, "Tooth and Nail"); // Sorcery {5}{G}{G} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tooth and Nail"); + setChoice(playerA, true); // Message: Pay Entwine {2} ? + addTarget(playerA, "Silvercoat Lion^Pillarfield Ox"); + setChoice(playerA, "Silvercoat Lion^Pillarfield Ox"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Silvercoat Lion", 1); + assertPermanentCount(playerA, "Pillarfield Ox", 1); + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java index a3c7ccfce2a..125476f40e4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java @@ -1,540 +1,540 @@ -package org.mage.test.cards.abilities.keywords; - -import mage.cards.Card; -import mage.constants.EmptyNames; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.game.permanent.Permanent; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * @author LevelX2 - */ -public class ManifestTest extends CardTestPlayerBase { - - /** - * Tests that ETB triggered abilities did not trigger for manifested cards - */ - @Test - public void testETBTriggeredAbilities() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // Manifest the top card of your library {1}{W} - addCard(Zone.HAND, playerA, "Soul Summons"); - - // Tranquil Cove enters the battlefield tapped. - // When Tranquil Cove enters the battlefield, you gain 1 life. - // {T}: Add {W} or {U}. - addCard(Zone.LIBRARY, playerA, "Tranquil Cove"); - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - // not tapped - assertTapped(EmptyNames.FACE_DOWN_CREATURE.toString(), false); - } - - /** - * If Doomwake Giant gets manifested, it's Constellation trigger may not - * trigger - */ - @Test - public void testETBTriggeredAbilities2() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // Manifest the top card of your library {1}{W} - addCard(Zone.HAND, playerA, "Soul Summons"); - - // Constellation - When Doomwake Giant or another enchantment enters the battlefield - // under your control, creatures your opponents control get -1/-1 until end of turn. - addCard(Zone.LIBRARY, playerA, "Doomwake Giant"); - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion"); - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - // PlayerB's Silvercoat Lion should not have get -1/-1/ - assertPermanentCount(playerB, "Silvercoat Lion", 1); - assertPowerToughness(playerB, "Silvercoat Lion", 2, 2); - } - - /** - * If Doomwake Giant gets manifested, it's Constellation trigger may not - * trigger - */ - @Test - public void testETBTriggeredAbilities3() { - addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - // Exile target creature. Its controller manifests the top card of their library {1}{U} - addCard(Zone.HAND, playerB, "Reality Shift"); - - // Constellation - When Doomwake Giant or another enchantment enters the battlefield - // under your control, creatures your opponents control get -1/-1 until end of turn. - addCard(Zone.LIBRARY, playerA, "Doomwake Giant"); - - addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox"); - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - assertGraveyardCount(playerB, "Reality Shift", 1); - assertExileCount("Silvercoat Lion", 1); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - // PlayerA's Pillarfield Ox should not have get -1/-1/ - assertPermanentCount(playerB, "Pillarfield Ox", 1); - assertPowerToughness(playerB, "Pillarfield Ox", 2, 4); - } - - /** - * If Doomwake Giant gets manifested, it's Constellation trigger may not - * trigger - */ - @Test - public void testNylea() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - // Exile target creature. Its controller manifests the top card of their library {1}{U} - addCard(Zone.HAND, playerB, "Reality Shift"); - - // As long as your devotion to white is less than five, Nylea isn't a creature. - // (Each {G} in the mana costs of permanents you control counts towards your devotion to green.) - addCard(Zone.LIBRARY, playerA, "Nylea, God of the Hunt"); - - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - assertGraveyardCount(playerB, "Reality Shift", 1); - assertExileCount("Silvercoat Lion", 1); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - - } - - /* - Had a Foundry Street Denizen and another creature out. - Opponent Reality Shift'ed the other creature, manifested card was a red creature. This pumped the foundry street denizen even though it shouldn't. - */ - @Test - public void testColorOfManifestedCardDoesNotCount() { - addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - // Exile target creature. Its controller manifests the top card of their library {1}{U} - addCard(Zone.HAND, playerB, "Reality Shift"); - - // Gore Swine {2}{R} - // 4/1 - addCard(Zone.LIBRARY, playerA, "Gore Swine"); - - // Whenever another red creature enters the battlefield under your control, Foundry Street Denizen gets +1/+0 until end of turn. - addCard(Zone.BATTLEFIELD, playerA, "Foundry Street Denizen"); - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - assertGraveyardCount(playerB, "Reality Shift", 1); - assertExileCount("Silvercoat Lion", 1); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - assertPowerToughness(playerA, "Foundry Street Denizen", 1, 1); - - } - - /* - I casted a Silence the Believers on a manifested card. It moved to the exile zone face-down. - */ - @Test - public void testCardGetsExiledFaceUp() { - addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); - // Exile target creature. Its controller manifests the top card of their library {1}{U} - addCard(Zone.HAND, playerB, "Reality Shift"); - // Silence the Believers - Instant {2}{B}{B} - // Strive — Silence the Believers costs more to cast for each target beyond the first. - // Exile any number of target creatures and all Auras attached to them. - addCard(Zone.HAND, playerB, "Silence the Believers"); - // Gore Swine {2}{R} - // 4/1 - addCard(Zone.LIBRARY, playerA, "Gore Swine"); - - // Whenever another red creature enters the battlefield under your control, Foundry Street Denizen gets +1/+0 until end of turn. - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); - // showBattlefield("A battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerA); - // showBattlefield("B battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerB); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Silence the Believers", EmptyNames.FACE_DOWN_CREATURE.toString()); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - assertGraveyardCount(playerB, "Reality Shift", 1); - assertExileCount("Silvercoat Lion", 1); - assertExileCount("Gore Swine", 1); - // no facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); - - for (Card card : currentGame.getExile().getAllCards(currentGame)) { - if (card.getName().equals("Gore Swine")) { - Assert.assertTrue("Gore Swine may not be face down in exile", !card.isFaceDown(currentGame)); - } - } - - } - - // Qarsi High Priest went to manifest Illusory Gains, - // but it made me choose a target for gains, then enchanted the card to that creature. - @Test - public void testManifestAura() { - - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - addCard(Zone.LIBRARY, playerB, "Illusory Gains", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - setStopAt(2, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerB, "Illusory Gains", 0); - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - - // a facedown creature is on the battlefield - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - - } - - // Check if a Megamorph card is manifested and turned face up by their megamorph ability - // it gets the +1/+1 counter. - // 701.33c - // If a card with morph is manifested, its controller may turn that card face up using - // either the procedure described in rule 702.36e to turn a face-down permanent with morph face up - // or the procedure described above to turn a manifested permanent face up. - @Test - public void testManifestMegamorph_TurnUpByMegamorphCost() { - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - addCard(Zone.BATTLEFIELD, playerB, "Forest", 6); - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - // Reach (This creature can block creatures with flying.) - // Megamorph {5}{G} - addCard(Zone.LIBRARY, playerB, "Aerie Bowmasters", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{5}{G}: Turn"); - - setStrictChooseMode(true); - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); - assertPermanentCount(playerB, "Aerie Bowmasters", 1); - assertPowerToughness(playerB, "Aerie Bowmasters", 4, 5); // 3/4 and the +1/+1 counter from Megamorph - Permanent aerie = getPermanent("Aerie Bowmasters", playerB); - Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); - } - - @Test - public void testManifestMegamorph_TurnUpBySimpleCost() { - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - addCard(Zone.BATTLEFIELD, playerB, "Forest", 4); - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - // {2}{G}{G} - // Reach (This creature can block creatures with flying.) - // Megamorph {5}{G} - addCard(Zone.LIBRARY, playerB, "Aerie Bowmasters", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{2}{G}{G}: Turn"); - - setStrictChooseMode(true); - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); - assertPermanentCount(playerB, "Aerie Bowmasters", 1); - assertPowerToughness(playerB, "Aerie Bowmasters", 3, 4); // 3/4 without counter (megamorph not used) - Permanent aerie = getPermanent("Aerie Bowmasters", playerB); - Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); - } - - /** - * When a Forest came manifested into play my Courser of Kruphix gained me a - * life. - */ - @Test - public void testManifestForest() { - - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - // Play with the top card of your library revealed. - // You may play the top card of your library if it's a land card. - // Whenever a land enters the battlefield under your control, you gain 1 life. - addCard(Zone.BATTLEFIELD, playerB, "Courser of Kruphix", 1); - - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - addCard(Zone.LIBRARY, playerB, "Forest", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - - } - - /** - * Whisperwood Elemental - Its sacrifice ability doesn't work.. - */ - @Test - public void testWhisperwoodElemental() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - // Seismic Rupture deals 2 damage to each creature without flying. - addCard(Zone.HAND, playerA, "Seismic Rupture", 1); - - // At the beginning of your end step, manifest the top card of your library. - // Sacrifice Whisperwood Elemental: Until end of turn, face-up, nontoken creatures you control gain "When this creature dies, manifest the top card of your library." - addCard(Zone.BATTLEFIELD, playerB, "Whisperwood Elemental", 1); - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Sacrifice"); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Seismic Rupture"); - setChoice(playerB, "When {this} dies"); // Order of triggers - - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerA, "Seismic Rupture", 1); - assertGraveyardCount(playerB, "Whisperwood Elemental", 1); - assertGraveyardCount(playerB, "Silvercoat Lion", 2); - - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 2); - - } - - /** - * I sacrificed a manifested face-down Smothering Abomination to Nantuko - * Husk and it made me draw a card. - */ - @Test - public void testDiesTriggeredAbilitiesOfManifestedCreatures() { - - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - - // Sacrifice a creature: Nantuko Husk gets +2/+2 until end of turn. - addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk", 1); - - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - // Devoid - // Flying - // At the beginning of your upkeep, sacrifice a creature - // Whenever you sacrifice a creature, draw a card. - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - addCard(Zone.LIBRARY, playerB, "Smothering Abomination", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Sacrifice a creature"); - setChoice(playerB, EmptyNames.FACE_DOWN_CREATURE.toString()); - - setStrictChooseMode(true); - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertPermanentCount(playerB, "Qarsi High Priest", 1); - assertPermanentCount(playerB, "Nantuko Husk", 1); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - assertGraveyardCount(playerB, "Smothering Abomination", 1); - - assertPowerToughness(playerB, "Nantuko Husk", 4, 4); - - assertHandCount(playerB, "Mountain", 1); - - } - - @Test - public void test_ManifestSorceryAndBlinkIt() { - - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1); - addCard(Zone.BATTLEFIELD, playerB, "Plains", 2); - - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - // Exile target creature you control, then return that card to the battlefield under your control. - addCard(Zone.HAND, playerB, "Cloudshift", 1); //Instant {W} - - - // Devoid - // Flying - // At the beginning of your upkeep, sacrifice a creature - // Whenever you sacrifice a creature, draw a card. - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - addCard(Zone.LIBRARY, playerB, "Lightning Bolt", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - waitStackResolved(2, PhaseStep.PRECOMBAT_MAIN, playerB); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Cloudshift", EmptyNames.FACE_DOWN_CREATURE.toString()); - - setStrictChooseMode(true); - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertPermanentCount(playerB, "Qarsi High Priest", 1); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - assertGraveyardCount(playerB, "Cloudshift", 1); - - assertPermanentCount(playerB, "Lightning Bolt", 0); - assertExileCount(playerB, "Lightning Bolt", 1); - - assertHandCount(playerB, "Mountain", 1); - - } -} +package org.mage.test.cards.abilities.keywords; + +import mage.cards.Card; +import mage.constants.EmptyNames; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author LevelX2 + */ +public class ManifestTest extends CardTestPlayerBase { + + /** + * Tests that ETB triggered abilities did not trigger for manifested cards + */ + @Test + public void testETBTriggeredAbilities() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // Manifest the top card of your library {1}{W} + addCard(Zone.HAND, playerA, "Soul Summons"); + + // Tranquil Cove enters the battlefield tapped. + // When Tranquil Cove enters the battlefield, you gain 1 life. + // {T}: Add {W} or {U}. + addCard(Zone.LIBRARY, playerA, "Tranquil Cove"); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + // not tapped + assertTapped(EmptyNames.FACE_DOWN_CREATURE.toString(), false); + } + + /** + * If Doomwake Giant gets manifested, it's Constellation trigger may not + * trigger + */ + @Test + public void testETBTriggeredAbilities2() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // Manifest the top card of your library {1}{W} + addCard(Zone.HAND, playerA, "Soul Summons"); + + // Constellation - When Doomwake Giant or another enchantment enters the battlefield + // under your control, creatures your opponents control get -1/-1 until end of turn. + addCard(Zone.LIBRARY, playerA, "Doomwake Giant"); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion"); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + // PlayerB's Silvercoat Lion should not have get -1/-1/ + assertPermanentCount(playerB, "Silvercoat Lion", 1); + assertPowerToughness(playerB, "Silvercoat Lion", 2, 2); + } + + /** + * If Doomwake Giant gets manifested, it's Constellation trigger may not + * trigger + */ + @Test + public void testETBTriggeredAbilities3() { + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + // Exile target creature. Its controller manifests the top card of their library {1}{U} + addCard(Zone.HAND, playerB, "Reality Shift"); + + // Constellation - When Doomwake Giant or another enchantment enters the battlefield + // under your control, creatures your opponents control get -1/-1 until end of turn. + addCard(Zone.LIBRARY, playerA, "Doomwake Giant"); + + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox"); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerB, "Reality Shift", 1); + assertExileCount("Silvercoat Lion", 1); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + // PlayerA's Pillarfield Ox should not have get -1/-1/ + assertPermanentCount(playerB, "Pillarfield Ox", 1); + assertPowerToughness(playerB, "Pillarfield Ox", 2, 4); + } + + /** + * If Doomwake Giant gets manifested, it's Constellation trigger may not + * trigger + */ + @Test + public void testNylea() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + // Exile target creature. Its controller manifests the top card of their library {1}{U} + addCard(Zone.HAND, playerB, "Reality Shift"); + + // As long as your devotion to white is less than five, Nylea isn't a creature. + // (Each {G} in the mana costs of permanents you control counts towards your devotion to green.) + addCard(Zone.LIBRARY, playerA, "Nylea, God of the Hunt"); + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerB, "Reality Shift", 1); + assertExileCount("Silvercoat Lion", 1); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + + } + + /* + Had a Foundry Street Denizen and another creature out. + Opponent Reality Shift'ed the other creature, manifested card was a red creature. This pumped the foundry street denizen even though it shouldn't. + */ + @Test + public void testColorOfManifestedCardDoesNotCount() { + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + // Exile target creature. Its controller manifests the top card of their library {1}{U} + addCard(Zone.HAND, playerB, "Reality Shift"); + + // Gore Swine {2}{R} + // 4/1 + addCard(Zone.LIBRARY, playerA, "Gore Swine"); + + // Whenever another red creature enters the battlefield under your control, Foundry Street Denizen gets +1/+0 until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Foundry Street Denizen"); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerB, "Reality Shift", 1); + assertExileCount("Silvercoat Lion", 1); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + assertPowerToughness(playerA, "Foundry Street Denizen", 1, 1); + + } + + /* + I casted a Silence the Believers on a manifested card. It moved to the exile zone face-down. + */ + @Test + public void testCardGetsExiledFaceUp() { + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); + // Exile target creature. Its controller manifests the top card of their library {1}{U} + addCard(Zone.HAND, playerB, "Reality Shift"); + // Silence the Believers - Instant {2}{B}{B} + // Strive — Silence the Believers costs more to cast for each target beyond the first. + // Exile any number of target creatures and all Auras attached to them. + addCard(Zone.HAND, playerB, "Silence the Believers"); + // Gore Swine {2}{R} + // 4/1 + addCard(Zone.LIBRARY, playerA, "Gore Swine"); + + // Whenever another red creature enters the battlefield under your control, Foundry Street Denizen gets +1/+0 until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); + // showBattlefield("A battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerA); + // showBattlefield("B battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerB); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Silence the Believers", EmptyNames.FACE_DOWN_CREATURE.toString()); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerB, "Reality Shift", 1); + assertExileCount("Silvercoat Lion", 1); + assertExileCount("Gore Swine", 1); + // no facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + + for (Card card : currentGame.getExile().getAllCards(currentGame)) { + if (card.getName().equals("Gore Swine")) { + Assert.assertTrue("Gore Swine may not be face down in exile", !card.isFaceDown(currentGame)); + } + } + + } + + // Qarsi High Priest went to manifest Illusory Gains, + // but it made me choose a target for gains, then enchanted the card to that creature. + @Test + public void testManifestAura() { + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + addCard(Zone.LIBRARY, playerB, "Illusory Gains", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, "Illusory Gains", 0); + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + // a facedown creature is on the battlefield + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + + } + + // Check if a Megamorph card is manifested and turned face up by their megamorph ability + // it gets the +1/+1 counter. + // 701.33c + // If a card with morph is manifested, its controller may turn that card face up using + // either the procedure described in rule 702.36e to turn a face-down permanent with morph face up + // or the procedure described above to turn a manifested permanent face up. + @Test + public void testManifestMegamorph_TurnUpByMegamorphCost() { + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerB, "Forest", 6); + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // Reach (This creature can block creatures with flying.) + // Megamorph {5}{G} + addCard(Zone.LIBRARY, playerB, "Aerie Bowmasters", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{5}{G}: Turn"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + assertPermanentCount(playerB, "Aerie Bowmasters", 1); + assertPowerToughness(playerB, "Aerie Bowmasters", 4, 5); // 3/4 and the +1/+1 counter from Megamorph + Permanent aerie = getPermanent("Aerie Bowmasters", playerB); + Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); + } + + @Test + public void testManifestMegamorph_TurnUpBySimpleCost() { + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerB, "Forest", 4); + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // {2}{G}{G} + // Reach (This creature can block creatures with flying.) + // Megamorph {5}{G} + addCard(Zone.LIBRARY, playerB, "Aerie Bowmasters", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{2}{G}{G}: Turn"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + assertPermanentCount(playerB, "Aerie Bowmasters", 1); + assertPowerToughness(playerB, "Aerie Bowmasters", 3, 4); // 3/4 without counter (megamorph not used) + Permanent aerie = getPermanent("Aerie Bowmasters", playerB); + Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); + } + + /** + * When a Forest came manifested into play my Courser of Kruphix gained me a + * life. + */ + @Test + public void testManifestForest() { + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + // Play with the top card of your library revealed. + // You may play the top card of your library if it's a land card. + // Whenever a land enters the battlefield under your control, you gain 1 life. + addCard(Zone.BATTLEFIELD, playerB, "Courser of Kruphix", 1); + + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + addCard(Zone.LIBRARY, playerB, "Forest", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + + } + + /** + * Whisperwood Elemental - Its sacrifice ability doesn't work.. + */ + @Test + public void testWhisperwoodElemental() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // Seismic Rupture deals 2 damage to each creature without flying. + addCard(Zone.HAND, playerA, "Seismic Rupture", 1); + + // At the beginning of your end step, manifest the top card of your library. + // Sacrifice Whisperwood Elemental: Until end of turn, face-up, nontoken creatures you control gain "When this creature dies, manifest the top card of your library." + addCard(Zone.BATTLEFIELD, playerB, "Whisperwood Elemental", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Sacrifice"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Seismic Rupture"); + setChoice(playerB, "When {this} dies"); // Order of triggers + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerA, "Seismic Rupture", 1); + assertGraveyardCount(playerB, "Whisperwood Elemental", 1); + assertGraveyardCount(playerB, "Silvercoat Lion", 2); + + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 2); + + } + + /** + * I sacrificed a manifested face-down Smothering Abomination to Nantuko + * Husk and it made me draw a card. + */ + @Test + public void testDiesTriggeredAbilitiesOfManifestedCreatures() { + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + + // Sacrifice a creature: Nantuko Husk gets +2/+2 until end of turn. + addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk", 1); + + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // Devoid + // Flying + // At the beginning of your upkeep, sacrifice a creature + // Whenever you sacrifice a creature, draw a card. + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + addCard(Zone.LIBRARY, playerB, "Smothering Abomination", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Sacrifice a creature"); + setChoice(playerB, EmptyNames.FACE_DOWN_CREATURE.toString()); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertPermanentCount(playerB, "Qarsi High Priest", 1); + assertPermanentCount(playerB, "Nantuko Husk", 1); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + assertGraveyardCount(playerB, "Smothering Abomination", 1); + + assertPowerToughness(playerB, "Nantuko Husk", 4, 4); + + assertHandCount(playerB, "Mountain", 1); + + } + + @Test + public void test_ManifestSorceryAndBlinkIt() { + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerB, "Plains", 2); + + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // Exile target creature you control, then return that card to the battlefield under your control. + addCard(Zone.HAND, playerB, "Cloudshift", 1); //Instant {W} + + + // Devoid + // Flying + // At the beginning of your upkeep, sacrifice a creature + // Whenever you sacrifice a creature, draw a card. + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + addCard(Zone.LIBRARY, playerB, "Lightning Bolt", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + waitStackResolved(2, PhaseStep.PRECOMBAT_MAIN, playerB); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Cloudshift", EmptyNames.FACE_DOWN_CREATURE.toString()); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertPermanentCount(playerB, "Qarsi High Priest", 1); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + assertGraveyardCount(playerB, "Cloudshift", 1); + + assertPermanentCount(playerB, "Lightning Bolt", 0); + assertExileCount(playerB, "Lightning Bolt", 1); + + assertHandCount(playerB, "Mountain", 1); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/FerventChampionTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/FerventChampionTest.java index 2370ee55c17..b74dfe0b12a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/FerventChampionTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/FerventChampionTest.java @@ -1,39 +1,39 @@ -package org.mage.test.cards.cost.modification; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class FerventChampionTest extends CardTestPlayerBase { - - @Test - public void testFerventChampion() { - setStrictChooseMode(true); - // First strike, Haste - // Whenever Fervent Champion attacks, another target attacking Knight you control gets +1/+0 until end of turn. - // Equip abilities you activate that target Fervent Champion cost {3} less to activate. - addCard(Zone.BATTLEFIELD, playerA, "Fervent Champion"); - - // Equipped creature gets +2/+2 and has protection from red and from blue. - // Whenever equipped creature deals combat damage to a player, Sword of Fire - // and Ice deals 2 damage to any target and you draw a card. - // Equip {2} - addCard(Zone.BATTLEFIELD, playerA, "Sword of Fire and Ice", 1); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip"); - addTarget(playerA, "Fervent Champion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, "Fervent Champion", 3,3); - } +package org.mage.test.cards.cost.modification; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class FerventChampionTest extends CardTestPlayerBase { + + @Test + public void testFerventChampion() { + setStrictChooseMode(true); + // First strike, Haste + // Whenever Fervent Champion attacks, another target attacking Knight you control gets +1/+0 until end of turn. + // Equip abilities you activate that target Fervent Champion cost {3} less to activate. + addCard(Zone.BATTLEFIELD, playerA, "Fervent Champion"); + + // Equipped creature gets +2/+2 and has protection from red and from blue. + // Whenever equipped creature deals combat damage to a player, Sword of Fire + // and Ice deals 2 damage to any target and you draw a card. + // Equip {2} + addCard(Zone.BATTLEFIELD, playerA, "Sword of Fire and Ice", 1); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip"); + addTarget(playerA, "Fervent Champion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Fervent Champion", 3,3); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/facedown/PrimordialMistTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/facedown/PrimordialMistTest.java index bf7a287c1dd..0167be3c333 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/facedown/PrimordialMistTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/facedown/PrimordialMistTest.java @@ -1,56 +1,56 @@ -package org.mage.test.cards.facedown; - -import mage.constants.EmptyNames; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class PrimordialMistTest extends CardTestPlayerBase { - - /** - * I have Brine Elemental played face down as a morph, an artifact which has - * been manifested and Kadena which has been turned face by Ixidron. I can't - * seem to activate Primordial Mist's second ability for any of these kinds - * of face down creatures: - */ - @Test - public void test_ExileAndCastMorphFaceDownCard() { - setStrictChooseMode(true); - - // At the beginning of your end step, you may manifest the top card of your library. - // Exile a face-down permanent you control face-up: You may play that card this turn - addCard(Zone.BATTLEFIELD, playerA, "Primordial Mist"); - // Morph {5}{U}{U} - // When Brine Elemental is turned face up, each opponent skips their next untap step. - addCard(Zone.HAND, playerA, "Brine Elemental"); // Creature {5}{U}{U} (5/4) - addCard(Zone.BATTLEFIELD, playerA, "Island", 9); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brine Elemental"); - setChoice(playerA, true); // cast it face down as 2/2 creature - - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Exile a face-down permanent you control"); - setChoice(playerA, EmptyNames.FACE_DOWN_CREATURE.toString()); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Brine Elemental"); - setChoice(playerA, false); // cast it face down as 2/2 creature - - setChoice(playerA, true); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertExileCount(playerA, 0); - - assertPowerToughness(playerA, "Brine Elemental", 5, 4); - - } -} +package org.mage.test.cards.facedown; + +import mage.constants.EmptyNames; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class PrimordialMistTest extends CardTestPlayerBase { + + /** + * I have Brine Elemental played face down as a morph, an artifact which has + * been manifested and Kadena which has been turned face by Ixidron. I can't + * seem to activate Primordial Mist's second ability for any of these kinds + * of face down creatures: + */ + @Test + public void test_ExileAndCastMorphFaceDownCard() { + setStrictChooseMode(true); + + // At the beginning of your end step, you may manifest the top card of your library. + // Exile a face-down permanent you control face-up: You may play that card this turn + addCard(Zone.BATTLEFIELD, playerA, "Primordial Mist"); + // Morph {5}{U}{U} + // When Brine Elemental is turned face up, each opponent skips their next untap step. + addCard(Zone.HAND, playerA, "Brine Elemental"); // Creature {5}{U}{U} (5/4) + addCard(Zone.BATTLEFIELD, playerA, "Island", 9); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brine Elemental"); + setChoice(playerA, true); // cast it face down as 2/2 creature + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Exile a face-down permanent you control"); + setChoice(playerA, EmptyNames.FACE_DOWN_CREATURE.toString()); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Brine Elemental"); + setChoice(playerA, false); // cast it face down as 2/2 creature + + setChoice(playerA, true); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertExileCount(playerA, 0); + + assertPowerToughness(playerA, "Brine Elemental", 5, 4); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/facedown/TriggerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/facedown/TriggerTest.java index 54035a4cfe1..2a673dd635a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/facedown/TriggerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/facedown/TriggerTest.java @@ -1,62 +1,62 @@ - -package org.mage.test.cards.facedown; - -import mage.cards.Card; -import mage.constants.EmptyNames; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - - - -public class TriggerTest extends CardTestPlayerBase { - - /** - * Midnight Reaper triggers when dies face down #7063 - * Ixidron has turned Midnight Reaper and Balduvian Bears face down: - * - */ - - // test that cards imprinted using Summoner's Egg are face down - @Test - public void testReaperDoesNotTriggerDiesTriggerFaceDown() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 5); - // As Ixidron enters the battlefield, turn all other nontoken creatures face down. - // Ixidron's power and toughness are each equal to the number of face-down creatures on the battlefield. - addCard(Zone.HAND, playerA, "Ixidron"); // Creature {3}{U}{U} (*/*) - // Whenever a nontoken creature you control dies, Midnight Reaper deals 1 damage to you and you draw a card. - addCard(Zone.BATTLEFIELD, playerA, "Midnight Reaper"); // Creature {2}{B} - - addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); - addCard(Zone.HAND, playerB, "Lightning Bolt"); // Instant 3 damage - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ixidron"); - - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", EmptyNames.FACE_DOWN_CREATURE.toString()); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerB, "Lightning Bolt", 1); - - assertGraveyardCount(playerA, "Midnight Reaper", 1); - assertGraveyardCount(playerA, "Ixidron", 1); - - assertHandCount(playerA, 0); - assertLife(playerA, 20); - - } + +package org.mage.test.cards.facedown; + +import mage.cards.Card; +import mage.constants.EmptyNames; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + + + +public class TriggerTest extends CardTestPlayerBase { + + /** + * Midnight Reaper triggers when dies face down #7063 + * Ixidron has turned Midnight Reaper and Balduvian Bears face down: + * + */ + + // test that cards imprinted using Summoner's Egg are face down + @Test + public void testReaperDoesNotTriggerDiesTriggerFaceDown() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + // As Ixidron enters the battlefield, turn all other nontoken creatures face down. + // Ixidron's power and toughness are each equal to the number of face-down creatures on the battlefield. + addCard(Zone.HAND, playerA, "Ixidron"); // Creature {3}{U}{U} (*/*) + // Whenever a nontoken creature you control dies, Midnight Reaper deals 1 damage to you and you draw a card. + addCard(Zone.BATTLEFIELD, playerA, "Midnight Reaper"); // Creature {2}{B} + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.HAND, playerB, "Lightning Bolt"); // Instant 3 damage + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ixidron"); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", EmptyNames.FACE_DOWN_CREATURE.toString()); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Lightning Bolt", 1); + + assertGraveyardCount(playerA, "Midnight Reaper", 1); + assertGraveyardCount(playerA, "Ixidron", 1); + + assertHandCount(playerA, 0); + assertLife(playerA, 20); + + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/MultipleTimesUsableActivatedManaAbilitiesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/MultipleTimesUsableActivatedManaAbilitiesTest.java index f6fe75c5fe3..83a9f0f0df2 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/MultipleTimesUsableActivatedManaAbilitiesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/MultipleTimesUsableActivatedManaAbilitiesTest.java @@ -1,45 +1,45 @@ -package org.mage.test.cards.mana; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class MultipleTimesUsableActivatedManaAbilitiesTest extends CardTestPlayerBase { - - /** - * Seton, Krosan Protector - only seems to get counted as if it were one - * mana for determining if a spell can be cast, regardless of how many - * druids you have in playF - */ - @Test - public void testCanBeCastWithSetonKrosanProtector() { - // Tap an untapped Druid you control: Add {G}. - addCard(Zone.BATTLEFIELD, playerA, "Seton, Krosan Protector", 1); // Creature {G}{G}{G} - addCard(Zone.BATTLEFIELD, playerA, "Citanul Druid", 3); - - addCard(Zone.HAND, playerA, "Leatherback Baloth", 1); // Creature 4/5 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Leatherback Baloth"); - - setChoice(playerA, "Citanul Druid"); - setChoice(playerA, "Citanul Druid"); - setChoice(playerA, "Citanul Druid"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - - - setStrictChooseMode(true); - execute(); - - assertAllCommandsUsed(); - - assertTappedCount("Citanul Druid", true, 3); - assertPermanentCount(playerA, "Leatherback Baloth", 1); - } - -} +package org.mage.test.cards.mana; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class MultipleTimesUsableActivatedManaAbilitiesTest extends CardTestPlayerBase { + + /** + * Seton, Krosan Protector - only seems to get counted as if it were one + * mana for determining if a spell can be cast, regardless of how many + * druids you have in playF + */ + @Test + public void testCanBeCastWithSetonKrosanProtector() { + // Tap an untapped Druid you control: Add {G}. + addCard(Zone.BATTLEFIELD, playerA, "Seton, Krosan Protector", 1); // Creature {G}{G}{G} + addCard(Zone.BATTLEFIELD, playerA, "Citanul Druid", 3); + + addCard(Zone.HAND, playerA, "Leatherback Baloth", 1); // Creature 4/5 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Leatherback Baloth"); + + setChoice(playerA, "Citanul Druid"); + setChoice(playerA, "Citanul Druid"); + setChoice(playerA, "Citanul Druid"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertTappedCount("Citanul Druid", true, 3); + assertPermanentCount(playerA, "Leatherback Baloth", 1); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java index 20c0c9f449c..0cdf671f508 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java @@ -1,112 +1,112 @@ -package org.mage.test.cards.mana.conditional; - -import mage.abilities.mana.ManaOptions; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; -import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions; - -/** - * - * @author LevelX2 - */ - -public class CrypticTrilobiteTest extends CardTestPlayerBase { - - @Test - public void testAvailableManaCalculation(){ - setStrictChooseMode(true); - - // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. - // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. - // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. - addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} - - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); - setChoice(playerA, "X=5"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Cryptic Trilobite", 1); - - ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); - Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); - assertManaOptions("{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); - } - - @Test - public void testUse(){ - setStrictChooseMode(true); - - // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. - // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. - // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. - addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} - // Flying - // {2}: Deathknell Kami gets +1/+1 until end of turn. Sacrifice it at the beginning of the next end step. - // Soulshift 1 (When this creature dies, you may return target Spirit card with converted mana cost 1 or less from your graveyard to your hand.) - addCard(Zone.BATTLEFIELD, playerA, "Deathknell Kami"); // Creature (0/1) - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); - setChoice(playerA, "X=5"); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Cryptic Trilobite", 1); - assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 3); - - assertPowerToughness(playerA, "Deathknell Kami", 2, 3); - - ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); - Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); - assertManaOptions("{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); - } - - @Test - public void testCantUse(){ - setStrictChooseMode(true); - - // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. - // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. - // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. - addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} - - // {4}{W}: Return another target creature you control to its owner's hand. - addCard(Zone.HAND, playerA, "Aegis Automaton"); // Creature {2} (0/2) - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); - setChoice(playerA, "X=5"); - - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); - - checkPlayableAbility("can't play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Aegis Automaton", false); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Cryptic Trilobite", 1); - assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 5); - } - - +package org.mage.test.cards.mana.conditional; + +import mage.abilities.mana.ManaOptions; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; +import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions; + +/** + * + * @author LevelX2 + */ + +public class CrypticTrilobiteTest extends CardTestPlayerBase { + + @Test + public void testAvailableManaCalculation(){ + setStrictChooseMode(true); + + // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. + // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. + // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. + addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); + setChoice(playerA, "X=5"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Cryptic Trilobite", 1); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); + assertManaOptions("{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); + } + + @Test + public void testUse(){ + setStrictChooseMode(true); + + // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. + // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. + // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. + addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} + // Flying + // {2}: Deathknell Kami gets +1/+1 until end of turn. Sacrifice it at the beginning of the next end step. + // Soulshift 1 (When this creature dies, you may return target Spirit card with converted mana cost 1 or less from your graveyard to your hand.) + addCard(Zone.BATTLEFIELD, playerA, "Deathknell Kami"); // Creature (0/1) + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); + setChoice(playerA, "X=5"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Cryptic Trilobite", 1); + assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 3); + + assertPowerToughness(playerA, "Deathknell Kami", 2, 3); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); + assertManaOptions("{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); + } + + @Test + public void testCantUse(){ + setStrictChooseMode(true); + + // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. + // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. + // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. + addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} + + // {4}{W}: Return another target creature you control to its owner's hand. + addCard(Zone.HAND, playerA, "Aegis Automaton"); // Creature {2} (0/2) + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); + setChoice(playerA, "X=5"); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + checkPlayableAbility("can't play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Aegis Automaton", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Cryptic Trilobite", 1); + assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 5); + } + + } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/TitansNestTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/TitansNestTest.java index 89cfbed0095..a61f627ded6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/TitansNestTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/TitansNestTest.java @@ -1,44 +1,44 @@ - -package org.mage.test.cards.mana.conditional; - -import mage.abilities.mana.ManaOptions; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; -import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions; - -/** - * - * @author LevelX2 - */ -public class TitansNestTest extends CardTestPlayerBase { - - @Test - public void testTitansNest(){ - setStrictChooseMode(true); - - // At the beginning of your upkeep, look at the top card of your library. You may put that card into your graveyard. - // Exile a card from your graveyard: Add {C}. Spend this mana only to cast a colored spell without {X} in its mana cost. - addCard(Zone.HAND, playerA, "Titans' Nest"); // Enchantment {1}{B}{G}{U} - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - addCard(Zone.BATTLEFIELD, playerA, "Island", 1); - - addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Titans' Nest"); - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Titans' Nest", 1); - - ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); - Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); - assertManaOptions("{C}{C}[{TitansNestManaCondition}]", manaOptions); - } + +package org.mage.test.cards.mana.conditional; + +import mage.abilities.mana.ManaOptions; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; +import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions; + +/** + * + * @author LevelX2 + */ +public class TitansNestTest extends CardTestPlayerBase { + + @Test + public void testTitansNest(){ + setStrictChooseMode(true); + + // At the beginning of your upkeep, look at the top card of your library. You may put that card into your graveyard. + // Exile a card from your graveyard: Add {C}. Spend this mana only to cast a colored spell without {X} in its mana cost. + addCard(Zone.HAND, playerA, "Titans' Nest"); // Enchantment {1}{B}{G}{U} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + + addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Titans' Nest"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Titans' Nest", 1); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); + assertManaOptions("{C}{C}[{TitansNestManaCondition}]", manaOptions); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventAllDamageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventAllDamageTest.java index 28e27aa624f..c6fe1401b8a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventAllDamageTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventAllDamageTest.java @@ -1,145 +1,145 @@ -package org.mage.test.cards.prevention; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class PreventAllDamageTest extends CardTestPlayerBase { - - @Test - public void test_SafePassage() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); - // Prevent all damage that would be dealt to you and creatures you control this turn. - addCard(Zone.HAND, playerA, "Safe Passage"); // Instant {2}{W} - - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // (2/2) - - addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 2); // (2/4) - addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); - - addCard(Zone.HAND, playerB, "Lightning Bolt", 2); // Instnat {R} - - castSpell(2, PhaseStep.UPKEEP, playerA, "Safe Passage"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); - - attack(2, playerB, "Pillarfield Ox"); - attack(2, playerB, "Pillarfield Ox"); - - block(2, playerA, "Silvercoat Lion", "Pillarfield Ox"); - - setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Safe Passage", 1); - assertPermanentCount(playerA, "Silvercoat Lion", 1); - - assertGraveyardCount(playerB, "Lightning Bolt", 2); - - assertLife(playerA, 20); - assertLife(playerB, 20); - - } - - @Test - public void test_EtherealHaze() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); - // Prevent all damage that would be dealt by creatures this turn. - addCard(Zone.HAND, playerA, "Ethereal Haze"); // Instant {W} - - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // (2/2) - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); // (2/4) - addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); - - addCard(Zone.HAND, playerB, "Lightning Bolt", 1); // Instant {R} - - castSpell(2, PhaseStep.UPKEEP, playerA, "Ethereal Haze"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); - - attack(2, playerB, "Silvercoat Lion"); - attack(2, playerB, "Silvercoat Lion"); - - block(2, playerA, "Silvercoat Lion", "Silvercoat Lion"); - - setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Ethereal Haze", 1); - assertPermanentCount(playerA, "Silvercoat Lion", 1); - - assertPermanentCount(playerB, "Silvercoat Lion", 2); - assertGraveyardCount(playerB, "Lightning Bolt", 1); - - assertLife(playerA, 17); - assertLife(playerB, 20); - - } - - @Test - public void test_EnergyStorm() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // Cumulative upkeep {1} - // Prevent all damage that would be dealt by instant and sorcery spells. - // Creatures with flying don't untap during their controllers' untap steps. - addCard(Zone.HAND, playerA, "Energy Storm"); // ENCHANTMENT {1}{W} - - // Flying, vigilance - addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 1); // (2/2) - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); // (2/2) - - addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4); - addCard(Zone.HAND, playerB, "Lightning Bolt", 2); // Instant {R} - // Fire Ambush deals 3 damage to any target. - addCard(Zone.HAND, playerB, "Fire Ambush", 2); // Sorcery {1}{R} - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Energy Storm"); - - attack(1, playerA, "Abbey Griffin"); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", playerA); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Abbey Griffin"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Fire Ambush", playerA); - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Fire Ambush", "Abbey Griffin"); - - attack(2, playerB, "Silvercoat Lion"); - - setChoice(playerA, false); // Pay {1}? Energy Storm - CumulativeUpkeepAbility: Cumulative upkeep {1} - - setStopAt(3, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Energy Storm", 1); - assertPermanentCount(playerA, "Abbey Griffin", 1); - - assertPermanentCount(playerB, "Silvercoat Lion", 1); - assertGraveyardCount(playerB, "Lightning Bolt", 2); - assertGraveyardCount(playerB, "Fire Ambush", 2); - - assertLife(playerA, 18); - assertLife(playerB, 18); - - } -} +package org.mage.test.cards.prevention; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class PreventAllDamageTest extends CardTestPlayerBase { + + @Test + public void test_SafePassage() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + // Prevent all damage that would be dealt to you and creatures you control this turn. + addCard(Zone.HAND, playerA, "Safe Passage"); // Instant {2}{W} + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // (2/2) + + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 2); // (2/4) + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); + + addCard(Zone.HAND, playerB, "Lightning Bolt", 2); // Instnat {R} + + castSpell(2, PhaseStep.UPKEEP, playerA, "Safe Passage"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); + + attack(2, playerB, "Pillarfield Ox"); + attack(2, playerB, "Pillarfield Ox"); + + block(2, playerA, "Silvercoat Lion", "Pillarfield Ox"); + + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Safe Passage", 1); + assertPermanentCount(playerA, "Silvercoat Lion", 1); + + assertGraveyardCount(playerB, "Lightning Bolt", 2); + + assertLife(playerA, 20); + assertLife(playerB, 20); + + } + + @Test + public void test_EtherealHaze() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + // Prevent all damage that would be dealt by creatures this turn. + addCard(Zone.HAND, playerA, "Ethereal Haze"); // Instant {W} + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // (2/2) + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); // (2/4) + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); + + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); // Instant {R} + + castSpell(2, PhaseStep.UPKEEP, playerA, "Ethereal Haze"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + + attack(2, playerB, "Silvercoat Lion"); + attack(2, playerB, "Silvercoat Lion"); + + block(2, playerA, "Silvercoat Lion", "Silvercoat Lion"); + + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Ethereal Haze", 1); + assertPermanentCount(playerA, "Silvercoat Lion", 1); + + assertPermanentCount(playerB, "Silvercoat Lion", 2); + assertGraveyardCount(playerB, "Lightning Bolt", 1); + + assertLife(playerA, 17); + assertLife(playerB, 20); + + } + + @Test + public void test_EnergyStorm() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // Cumulative upkeep {1} + // Prevent all damage that would be dealt by instant and sorcery spells. + // Creatures with flying don't untap during their controllers' untap steps. + addCard(Zone.HAND, playerA, "Energy Storm"); // ENCHANTMENT {1}{W} + + // Flying, vigilance + addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 1); // (2/2) + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); // (2/2) + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4); + addCard(Zone.HAND, playerB, "Lightning Bolt", 2); // Instant {R} + // Fire Ambush deals 3 damage to any target. + addCard(Zone.HAND, playerB, "Fire Ambush", 2); // Sorcery {1}{R} + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Energy Storm"); + + attack(1, playerA, "Abbey Griffin"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Abbey Griffin"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Fire Ambush", playerA); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Fire Ambush", "Abbey Griffin"); + + attack(2, playerB, "Silvercoat Lion"); + + setChoice(playerA, false); // Pay {1}? Energy Storm - CumulativeUpkeepAbility: Cumulative upkeep {1} + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Energy Storm", 1); + assertPermanentCount(playerA, "Abbey Griffin", 1); + + assertPermanentCount(playerB, "Silvercoat Lion", 1); + assertGraveyardCount(playerB, "Lightning Bolt", 2); + assertGraveyardCount(playerB, "Fire Ambush", 2); + + assertLife(playerA, 18); + assertLife(playerB, 18); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/protection/EightAndAHalfTailsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/protection/EightAndAHalfTailsTest.java index 4fd955ec87f..69f900e8dbc 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/protection/EightAndAHalfTailsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/protection/EightAndAHalfTailsTest.java @@ -1,48 +1,48 @@ -package org.mage.test.cards.protection; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - - -/** - * - * @author LevelX2 - */ -public class EightAndAHalfTailsTest extends CardTestPlayerBase { - - @Test - public void testProtectingPlaneswalker() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - - // Activated abilities of artifacts your opponents control can't be activated. - // +1: Until your next turn, up to one target noncreature artifact becomes an artifact creature with power and toughness equal to its converted mana cost. - // -2: You may choose an artifact card you own from outside the game or in exile, reveal that card, and put it into your hand. - addCard(Zone.BATTLEFIELD, playerA, "Karn, the Great Creator"); // Planeswalker (5) - - // {1}{W}: Target permanent you control gains protection from white until end of turn. - // {1}: Target spell or permanent becomes white until end of turn. - addCard(Zone.BATTLEFIELD, playerA, "Eight-and-a-Half-Tails"); // Creature - - // Flying, double strike - // Whenever a creature you control deals combat damage to a player, you and that player each gain that much life. - // At the beginning of your end step, if you have at least 15 life more than your starting life total, each player Angel of Destiny attacked this turn loses the game. - addCard(Zone.BATTLEFIELD, playerB, "Angel of Destiny"); // Creature - - attack(2, playerB, "Angel of Destiny", "Karn, the Great Creator"); - activateAbility(2, PhaseStep.DECLARE_ATTACKERS, playerA, "{1}{W}: Target permanent you control gains protection from white until end of turn."); - addTarget(playerA, "Karn, the Great Creator"); - - setStopAt(2, PhaseStep.END_COMBAT); - execute(); - - assertPermanentCount(playerA, "Karn, the Great Creator", 1); - assertCounterCount("Karn, the Great Creator", CounterType.LOYALTY, 5); - - } - +package org.mage.test.cards.protection; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + + +/** + * + * @author LevelX2 + */ +public class EightAndAHalfTailsTest extends CardTestPlayerBase { + + @Test + public void testProtectingPlaneswalker() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + // Activated abilities of artifacts your opponents control can't be activated. + // +1: Until your next turn, up to one target noncreature artifact becomes an artifact creature with power and toughness equal to its converted mana cost. + // -2: You may choose an artifact card you own from outside the game or in exile, reveal that card, and put it into your hand. + addCard(Zone.BATTLEFIELD, playerA, "Karn, the Great Creator"); // Planeswalker (5) + + // {1}{W}: Target permanent you control gains protection from white until end of turn. + // {1}: Target spell or permanent becomes white until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Eight-and-a-Half-Tails"); // Creature + + // Flying, double strike + // Whenever a creature you control deals combat damage to a player, you and that player each gain that much life. + // At the beginning of your end step, if you have at least 15 life more than your starting life total, each player Angel of Destiny attacked this turn loses the game. + addCard(Zone.BATTLEFIELD, playerB, "Angel of Destiny"); // Creature + + attack(2, playerB, "Angel of Destiny", "Karn, the Great Creator"); + activateAbility(2, PhaseStep.DECLARE_ATTACKERS, playerA, "{1}{W}: Target permanent you control gains protection from white until end of turn."); + addTarget(playerA, "Karn, the Great Creator"); + + setStopAt(2, PhaseStep.END_COMBAT); + execute(); + + assertPermanentCount(playerA, "Karn, the Great Creator", 1); + assertCounterCount("Karn, the Great Creator", CounterType.LOYALTY, 5); + + } + } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/ConduitOfRuinTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/ConduitOfRuinTest.java index 727dda1f049..83eaf3783df 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/ConduitOfRuinTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/ConduitOfRuinTest.java @@ -1,51 +1,51 @@ -package org.mage.test.cards.single.bfz; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class ConduitOfRuinTest extends CardTestPlayerBase { - - @Test - public void testCast() { - setStrictChooseMode(true); - - // Emrakul, the Aeons Torn can't be countered. - // When you cast Emrakul, take an extra turn after this one. - // Flying, protection from colored spells, annihilator 6 - // When Emrakul is put into a graveyard from anywhere, its owner shuffles their graveyard into their library. - addCard(Zone.LIBRARY, playerA, "Emrakul, the Aeons Torn"); // Creature {15} 15/15 - - // When you cast Conduit of Ruin, you may search your library for a colorless creature card with converted mana cost 7 or greater, then shuffle your library and put that card on top of it. - // The first creature spell you cast each turn costs {2} less to cast. - addCard(Zone.HAND, playerA, "Conduit of Ruin"); // Creature {6} 5/5 - addCard(Zone.BATTLEFIELD, playerA, "Plains", 13); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Conduit of Ruin"); - setChoice(playerA, true); // When you cast this spell, you may search... - addTarget(playerA, "Emrakul, the Aeons Torn"); - - setStopAt(3, PhaseStep.DRAW); - - execute(); - - assertLibraryCount(playerA, "Emrakul, the Aeons Torn", 0); - assertHandCount(playerA, "Emrakul, the Aeons Torn", 1); - - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Emrakul, the Aeons Torn"); - - setStopAt(3, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Conduit of Ruin", 1); - assertPermanentCount(playerA, "Emrakul, the Aeons Torn", 1); - } +package org.mage.test.cards.single.bfz; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class ConduitOfRuinTest extends CardTestPlayerBase { + + @Test + public void testCast() { + setStrictChooseMode(true); + + // Emrakul, the Aeons Torn can't be countered. + // When you cast Emrakul, take an extra turn after this one. + // Flying, protection from colored spells, annihilator 6 + // When Emrakul is put into a graveyard from anywhere, its owner shuffles their graveyard into their library. + addCard(Zone.LIBRARY, playerA, "Emrakul, the Aeons Torn"); // Creature {15} 15/15 + + // When you cast Conduit of Ruin, you may search your library for a colorless creature card with converted mana cost 7 or greater, then shuffle your library and put that card on top of it. + // The first creature spell you cast each turn costs {2} less to cast. + addCard(Zone.HAND, playerA, "Conduit of Ruin"); // Creature {6} 5/5 + addCard(Zone.BATTLEFIELD, playerA, "Plains", 13); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Conduit of Ruin"); + setChoice(playerA, true); // When you cast this spell, you may search... + addTarget(playerA, "Emrakul, the Aeons Torn"); + + setStopAt(3, PhaseStep.DRAW); + + execute(); + + assertLibraryCount(playerA, "Emrakul, the Aeons Torn", 0); + assertHandCount(playerA, "Emrakul, the Aeons Torn", 1); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Emrakul, the Aeons Torn"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Conduit of Ruin", 1); + assertPermanentCount(playerA, "Emrakul, the Aeons Torn", 1); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/TheUrDragonTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/TheUrDragonTest.java index a1e26783b40..8d6b7560017 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/TheUrDragonTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/TheUrDragonTest.java @@ -1,49 +1,49 @@ -package org.mage.test.cards.single.c17; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class TheUrDragonTest extends CardTestPlayerBase { - - - @Test - public void test_basic() { - setStrictChooseMode(true); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); - skipInitShuffling(); - // Eminence — As long as The Ur-Dragon is in the command zone or on the battlefield, other Dragon spells you cast cost 1 less to cast. - // Flying - // Whenever one or more Dragons you control attack, draw that many cards, then you may put a permanent card from your hand onto the battlefield. - addCard(Zone.BATTLEFIELD, playerA, "The Ur-Dragon", 1); // Creature (10/10) - // Flying - // {R}: Dragon Hatchling gets +1/+0 until end of turn. - addCard(Zone.HAND, playerA, "Dragon Hatchling", 2); // Creature Dragon {1}{R} (0/1) - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dragon Hatchling"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dragon Hatchling"); - - attack(3, playerA, "The Ur-Dragon"); - attack(3, playerA, "Dragon Hatchling"); - attack(3, playerA, "Dragon Hatchling"); - setChoice(playerA, true); // Put a permanent card from your hand onto the battlefield? - setChoice(playerA, "Silvercoat Lion"); - - setStopAt(3, PhaseStep.END_COMBAT); - - execute(); - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Dragon Hatchling", 2); - assertPermanentCount(playerA, "Silvercoat Lion", 1 ); - assertHandCount(playerA, 3); - - } +package org.mage.test.cards.single.c17; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class TheUrDragonTest extends CardTestPlayerBase { + + + @Test + public void test_basic() { + setStrictChooseMode(true); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); + skipInitShuffling(); + // Eminence — As long as The Ur-Dragon is in the command zone or on the battlefield, other Dragon spells you cast cost 1 less to cast. + // Flying + // Whenever one or more Dragons you control attack, draw that many cards, then you may put a permanent card from your hand onto the battlefield. + addCard(Zone.BATTLEFIELD, playerA, "The Ur-Dragon", 1); // Creature (10/10) + // Flying + // {R}: Dragon Hatchling gets +1/+0 until end of turn. + addCard(Zone.HAND, playerA, "Dragon Hatchling", 2); // Creature Dragon {1}{R} (0/1) + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dragon Hatchling"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dragon Hatchling"); + + attack(3, playerA, "The Ur-Dragon"); + attack(3, playerA, "Dragon Hatchling"); + attack(3, playerA, "Dragon Hatchling"); + setChoice(playerA, true); // Put a permanent card from your hand onto the battlefield? + setChoice(playerA, "Silvercoat Lion"); + + setStopAt(3, PhaseStep.END_COMBAT); + + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Dragon Hatchling", 2); + assertPermanentCount(playerA, "Silvercoat Lion", 1 ); + assertHandCount(playerA, 3); + + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/AminatousAuguryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/AminatousAuguryTest.java index a8d36115ce2..167c09d5adf 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/AminatousAuguryTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/AminatousAuguryTest.java @@ -1,75 +1,75 @@ - -package org.mage.test.cards.single.c18; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class AminatousAuguryTest extends CardTestPlayerBase { - - @Test - public void testCastMultiple() { - setStrictChooseMode(true); - - - addCard(Zone.LIBRARY, playerA, "Pillarfield Ox"); // Creature (2/4) - // As an additional cost to cast this spell, discard a card. - // Draw two cards. - addCard(Zone.LIBRARY, playerA, "Tormenting Voice"); // Sorcery - // {1}: Adarkar Sentinel gets +0/+1 until end of turn. - addCard(Zone.LIBRARY, playerA, "Adarkar Sentinel"); // Artifact Creature {5} (3/3) - addCard(Zone.LIBRARY, playerA, "Storm Crow"); - // You have hexproof. (You can't be the target of spells or abilities your opponents control.) - addCard(Zone.LIBRARY, playerA, "Aegis of the Gods"); // Enchantment Creature {1}{W} (2/1) - addCard(Zone.LIBRARY, playerA, "Lightning Bolt"); // Instant - addCard(Zone.LIBRARY, playerA, "Badlands"); - skipInitShuffling(); - // Exile the top eight cards of your library. You may put a land card from among them onto the battlefield. - // Until end of turn, for each nonland card type, you may cast a card of that type from among the exiled cards - // without paying its mana cost. - addCard(Zone.HAND, playerA, "Aminatou's Augury"); // SORCERY {6}{U}{U} - addCard(Zone.HAND, playerA, "Mountain"); - addCard(Zone.HAND, playerA, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerA, "Island", 8); - - playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mountain"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aminatou's Augury"); - setChoice(playerA, true); // Put a land from among the exiled cards into play? - setChoice(playerA, "Badlands"); // Select a land card - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Adarkar Sentinel"); - setChoice(playerA, "Artifact"); // Which card type do you want to consume? - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aegis of the Gods"); - setChoice(playerA, "Enchantment"); // Which card type do you want to consume? - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Storm Crow"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tormenting Voice"); - setChoice(playerA, "Silvercoat Lion"); // Select a card (discard cost) - - checkPlayableAbility("Cannot cast second creature from exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Pillarfield Ox", Boolean.FALSE); // Type Creature type is already consumed - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Aminatou's Augury", 1); - assertPermanentCount(playerA, "Mountain", 1); - assertPermanentCount(playerA, "Badlands", 1); - assertPermanentCount(playerA, "Adarkar Sentinel", 1); - assertPermanentCount(playerA, "Aegis of the Gods", 1); - assertPermanentCount(playerA, "Storm Crow", 1); - assertGraveyardCount(playerA, "Lightning Bolt", 1); - - assertLife(playerA, 20); - assertLife(playerB, 17); - - assertHandCount(playerA, 2); - assertGraveyardCount(playerA, "Silvercoat Lion",1); - assertExileCount(playerA, 2); - } - -} + +package org.mage.test.cards.single.c18; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class AminatousAuguryTest extends CardTestPlayerBase { + + @Test + public void testCastMultiple() { + setStrictChooseMode(true); + + + addCard(Zone.LIBRARY, playerA, "Pillarfield Ox"); // Creature (2/4) + // As an additional cost to cast this spell, discard a card. + // Draw two cards. + addCard(Zone.LIBRARY, playerA, "Tormenting Voice"); // Sorcery + // {1}: Adarkar Sentinel gets +0/+1 until end of turn. + addCard(Zone.LIBRARY, playerA, "Adarkar Sentinel"); // Artifact Creature {5} (3/3) + addCard(Zone.LIBRARY, playerA, "Storm Crow"); + // You have hexproof. (You can't be the target of spells or abilities your opponents control.) + addCard(Zone.LIBRARY, playerA, "Aegis of the Gods"); // Enchantment Creature {1}{W} (2/1) + addCard(Zone.LIBRARY, playerA, "Lightning Bolt"); // Instant + addCard(Zone.LIBRARY, playerA, "Badlands"); + skipInitShuffling(); + // Exile the top eight cards of your library. You may put a land card from among them onto the battlefield. + // Until end of turn, for each nonland card type, you may cast a card of that type from among the exiled cards + // without paying its mana cost. + addCard(Zone.HAND, playerA, "Aminatou's Augury"); // SORCERY {6}{U}{U} + addCard(Zone.HAND, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerA, "Island", 8); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mountain"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aminatou's Augury"); + setChoice(playerA, true); // Put a land from among the exiled cards into play? + setChoice(playerA, "Badlands"); // Select a land card + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Adarkar Sentinel"); + setChoice(playerA, "Artifact"); // Which card type do you want to consume? + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aegis of the Gods"); + setChoice(playerA, "Enchantment"); // Which card type do you want to consume? + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Storm Crow"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tormenting Voice"); + setChoice(playerA, "Silvercoat Lion"); // Select a card (discard cost) + + checkPlayableAbility("Cannot cast second creature from exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Pillarfield Ox", Boolean.FALSE); // Type Creature type is already consumed + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Aminatou's Augury", 1); + assertPermanentCount(playerA, "Mountain", 1); + assertPermanentCount(playerA, "Badlands", 1); + assertPermanentCount(playerA, "Adarkar Sentinel", 1); + assertPermanentCount(playerA, "Aegis of the Gods", 1); + assertPermanentCount(playerA, "Storm Crow", 1); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + + assertLife(playerA, 20); + assertLife(playerB, 17); + + assertHandCount(playerA, 2); + assertGraveyardCount(playerA, "Silvercoat Lion",1); + assertExileCount(playerA, 2); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c20/PakoArcaneRetrieverTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c20/PakoArcaneRetrieverTest.java index 5f7ab83c382..849e3ede9b5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c20/PakoArcaneRetrieverTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c20/PakoArcaneRetrieverTest.java @@ -1,96 +1,96 @@ - -package org.mage.test.cards.single.c20; - -import mage.cards.Card; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class PakoArcaneRetrieverTest extends CardTestPlayerBase { - - @Test - public void test_CheckExiled() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); - addCard(Zone.LIBRARY, playerB, "Pillarfield Ox", 1); - - skipInitShuffling(); - // Partner with Pako, Arcane Retriever - // You may play noncreature cards from exile with fetch counters on them if you - // exiled them, and you may spend mana as though it were mana of any color to cast those spells. - addCard(Zone.BATTLEFIELD, playerA, "Haldan, Avid Arcanist", 1); - // Partner with Haldan, Avid Arcanist - // Haste - // Whenever Pako, Arcane Retriever attacks, exile the top card of each player's library and put a fetch counter on each of them. Put a +1/+1 counter on Pako for each noncreature card exiled this way. - addCard(Zone.BATTLEFIELD, playerA, "Pako, Arcane Retriever", 1); // Creature {3}{R}{G} (3/3) - - attack(1, playerA, "Pako, Arcane Retriever"); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertLife(playerA, 20); - assertLife(playerB, 17); - - assertExileCount(playerA, "Silvercoat Lion", 1); - assertExileCount(playerB, "Pillarfield Ox", 1); - - for(Card card :currentGame.getExile().getAllCards(currentGame)) { - Assert.assertTrue(card.getName() + " has a fetch counter",card.getCounters(currentGame).getCount(CounterType.FETCH) == 1); - } - - } - - @Test - public void test_CastExiled() { - // setStrictChooseMode(true); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); - - addCard(Zone.LIBRARY, playerA, "Lightning Bolt", 1); // Instant 3 damge - // Create a 3/3 green Centaur creature token. - addCard(Zone.LIBRARY, playerB, "Call of the Conclave", 1); // Sorcery {W}{G} - - skipInitShuffling(); - // Partner with Pako, Arcane Retriever - // You may play noncreature cards from exile with fetch counters on them if you - // exiled them, and you may spend mana as though it were mana of any color to cast those spells. - addCard(Zone.BATTLEFIELD, playerA, "Haldan, Avid Arcanist", 1); - // Partner with Haldan, Avid Arcanist - // Haste - // Whenever Pako, Arcane Retriever attacks, exile the top card of each player's library and put a fetch counter on each of them. Put a +1/+1 counter on Pako for each noncreature card exiled this way. - addCard(Zone.BATTLEFIELD, playerA, "Pako, Arcane Retriever", 1); // Creature {3}{R}{G} (3/3) - - attack(1, playerA, "Pako, Arcane Retriever"); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Call of the Conclave"); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertLife(playerA, 20); - assertLife(playerB, 12); // 3+2 (attack) + 3 Lighning Bolt - - assertGraveyardCount(playerA, "Lightning Bolt", 1); - assertGraveyardCount(playerB, "Call of the Conclave", 1); - - - } -} + +package org.mage.test.cards.single.c20; + +import mage.cards.Card; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class PakoArcaneRetrieverTest extends CardTestPlayerBase { + + @Test + public void test_CheckExiled() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); + addCard(Zone.LIBRARY, playerB, "Pillarfield Ox", 1); + + skipInitShuffling(); + // Partner with Pako, Arcane Retriever + // You may play noncreature cards from exile with fetch counters on them if you + // exiled them, and you may spend mana as though it were mana of any color to cast those spells. + addCard(Zone.BATTLEFIELD, playerA, "Haldan, Avid Arcanist", 1); + // Partner with Haldan, Avid Arcanist + // Haste + // Whenever Pako, Arcane Retriever attacks, exile the top card of each player's library and put a fetch counter on each of them. Put a +1/+1 counter on Pako for each noncreature card exiled this way. + addCard(Zone.BATTLEFIELD, playerA, "Pako, Arcane Retriever", 1); // Creature {3}{R}{G} (3/3) + + attack(1, playerA, "Pako, Arcane Retriever"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 17); + + assertExileCount(playerA, "Silvercoat Lion", 1); + assertExileCount(playerB, "Pillarfield Ox", 1); + + for(Card card :currentGame.getExile().getAllCards(currentGame)) { + Assert.assertTrue(card.getName() + " has a fetch counter",card.getCounters(currentGame).getCount(CounterType.FETCH) == 1); + } + + } + + @Test + public void test_CastExiled() { + // setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + addCard(Zone.LIBRARY, playerA, "Lightning Bolt", 1); // Instant 3 damge + // Create a 3/3 green Centaur creature token. + addCard(Zone.LIBRARY, playerB, "Call of the Conclave", 1); // Sorcery {W}{G} + + skipInitShuffling(); + // Partner with Pako, Arcane Retriever + // You may play noncreature cards from exile with fetch counters on them if you + // exiled them, and you may spend mana as though it were mana of any color to cast those spells. + addCard(Zone.BATTLEFIELD, playerA, "Haldan, Avid Arcanist", 1); + // Partner with Haldan, Avid Arcanist + // Haste + // Whenever Pako, Arcane Retriever attacks, exile the top card of each player's library and put a fetch counter on each of them. Put a +1/+1 counter on Pako for each noncreature card exiled this way. + addCard(Zone.BATTLEFIELD, playerA, "Pako, Arcane Retriever", 1); // Creature {3}{R}{G} (3/3) + + attack(1, playerA, "Pako, Arcane Retriever"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Call of the Conclave"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 12); // 3+2 (attack) + 3 Lighning Bolt + + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertGraveyardCount(playerB, "Call of the Conclave", 1); + + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dst/DemonsHornTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dst/DemonsHornTest.java index 6f91f4cbded..c8c47a1beed 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dst/DemonsHornTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dst/DemonsHornTest.java @@ -1,72 +1,72 @@ -package org.mage.test.cards.single.dst; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class DemonsHornTest extends CardTestPlayerBase { - - - @Test - public void testWithBlackSpell() { - setStrictChooseMode(true); - - // When Abyssal Gatekeeper dies, each player sacrifices a creature. - addCard(Zone.HAND, playerA, "Abyssal Gatekeeper", 1); // Creature {2}{B} 1/1 - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); - - // Whenever a player casts a black spell, you may gain 1 life. - addCard(Zone.BATTLEFIELD, playerB, "Demon's Horn", 1); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Abyssal Gatekeeper"); - setChoice(playerB, true); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Abyssal Gatekeeper", 1); - - assertLife(playerA, 20); - assertLife(playerB, 21); - } - - /** - * https://github.com/magefree/mage/issues/6890 - * - * Color == Color Identity #6890 - * - * Alesha, Who Smiles at Death triggers Demon's Horn - */ - @Test - public void testSpellWithBlackManaOnlyInTriggeredOptionalCost() { - setStrictChooseMode(true); - - // First strike - // Whenever Alesha, Who Smiles at Death attacks, you may pay {W/B}{W/B}. If you do, return target creature card with power 2 or less from your graveyard to the battlefield tapped and attacking. - addCard(Zone.HAND, playerA, "Alesha, Who Smiles at Death", 1); // Creature {2}{R} 3/2 - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - - // Whenever a player casts a black spell, you may gain 1 life. - addCard(Zone.BATTLEFIELD, playerB, "Demon's Horn", 1); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alesha, Who Smiles at Death"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Alesha, Who Smiles at Death", 1); - - assertLife(playerA, 20); - assertLife(playerB, 20); - } - -} +package org.mage.test.cards.single.dst; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class DemonsHornTest extends CardTestPlayerBase { + + + @Test + public void testWithBlackSpell() { + setStrictChooseMode(true); + + // When Abyssal Gatekeeper dies, each player sacrifices a creature. + addCard(Zone.HAND, playerA, "Abyssal Gatekeeper", 1); // Creature {2}{B} 1/1 + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + + // Whenever a player casts a black spell, you may gain 1 life. + addCard(Zone.BATTLEFIELD, playerB, "Demon's Horn", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Abyssal Gatekeeper"); + setChoice(playerB, true); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Abyssal Gatekeeper", 1); + + assertLife(playerA, 20); + assertLife(playerB, 21); + } + + /** + * https://github.com/magefree/mage/issues/6890 + * + * Color == Color Identity #6890 + * + * Alesha, Who Smiles at Death triggers Demon's Horn + */ + @Test + public void testSpellWithBlackManaOnlyInTriggeredOptionalCost() { + setStrictChooseMode(true); + + // First strike + // Whenever Alesha, Who Smiles at Death attacks, you may pay {W/B}{W/B}. If you do, return target creature card with power 2 or less from your graveyard to the battlefield tapped and attacking. + addCard(Zone.HAND, playerA, "Alesha, Who Smiles at Death", 1); // Creature {2}{R} 3/2 + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + // Whenever a player casts a black spell, you may gain 1 life. + addCard(Zone.BATTLEFIELD, playerB, "Demon's Horn", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alesha, Who Smiles at Death"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Alesha, Who Smiles at Death", 1); + + assertLife(playerA, 20); + assertLife(playerB, 20); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/BarteredCowTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/BarteredCowTest.java index 4e951974c1e..7be461ae765 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/BarteredCowTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/BarteredCowTest.java @@ -1,88 +1,88 @@ -package org.mage.test.cards.single.eld; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class BarteredCowTest extends CardTestPlayerBase { - - @Test - public void testDiesTrigger() { - setStrictChooseMode(true); - - // When Bartered Cow dies or when you discard it, create a Food token. - addCard(Zone.BATTLEFIELD, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 - - addCard(Zone.HAND, playerB, "Lightning Bolt", 1); - addCard(Zone.BATTLEFIELD, playerB, "Mountain"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Bartered Cow"); - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerB, "Lightning Bolt", 1); - assertGraveyardCount(playerA, "Bartered Cow", 1); - assertPermanentCount(playerA, "Food", 1); - } - - @Test - public void testDiscardTrigger() { - setStrictChooseMode(true); - - // When Bartered Cow dies or when you discard it, create a Food token. - addCard(Zone.HAND, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 - // Choose one — - // • Target player discards a card. - // • Target creature gets +2/-1 until end of turn. - // • Target creature gains swampwalk until end of turn. (It can't be blocked as long as defending player controls a Swamp.) - addCard(Zone.HAND, playerB, "Funeral Charm", 1); // Instant {B} - addCard(Zone.BATTLEFIELD, playerB, "Swamp"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Funeral Charm"); - setModeChoice(playerB, "1"); - addTarget(playerB, playerA); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerB, "Funeral Charm", 1); - assertGraveyardCount(playerA, "Bartered Cow", 1); - assertPermanentCount(playerA, "Food", 1); - } - - @Test - public void testDiscardTriggerWithTorturedExistence() { - setStrictChooseMode(true); - - // When Bartered Cow dies or when you discard it, create a Food token. - addCard(Zone.HAND, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 - - // {B}, Discard a creature card: Return target creature card from your graveyard to your hand. - addCard(Zone.BATTLEFIELD, playerA, "Tortured Existence", 1); // Instant {B} - addCard(Zone.BATTLEFIELD, playerA, "Swamp"); - addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion"); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{B}, Discard a creature card"); - setChoice(playerA, "Bartered Cow"); - addTarget(playerA, "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Bartered Cow", 1); - assertHandCount(playerA, "Silvercoat Lion", 1); - assertPermanentCount(playerA, "Food", 1); - } - -} +package org.mage.test.cards.single.eld; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class BarteredCowTest extends CardTestPlayerBase { + + @Test + public void testDiesTrigger() { + setStrictChooseMode(true); + + // When Bartered Cow dies or when you discard it, create a Food token. + addCard(Zone.BATTLEFIELD, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 + + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Bartered Cow"); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Lightning Bolt", 1); + assertGraveyardCount(playerA, "Bartered Cow", 1); + assertPermanentCount(playerA, "Food", 1); + } + + @Test + public void testDiscardTrigger() { + setStrictChooseMode(true); + + // When Bartered Cow dies or when you discard it, create a Food token. + addCard(Zone.HAND, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 + // Choose one — + // • Target player discards a card. + // • Target creature gets +2/-1 until end of turn. + // • Target creature gains swampwalk until end of turn. (It can't be blocked as long as defending player controls a Swamp.) + addCard(Zone.HAND, playerB, "Funeral Charm", 1); // Instant {B} + addCard(Zone.BATTLEFIELD, playerB, "Swamp"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Funeral Charm"); + setModeChoice(playerB, "1"); + addTarget(playerB, playerA); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Funeral Charm", 1); + assertGraveyardCount(playerA, "Bartered Cow", 1); + assertPermanentCount(playerA, "Food", 1); + } + + @Test + public void testDiscardTriggerWithTorturedExistence() { + setStrictChooseMode(true); + + // When Bartered Cow dies or when you discard it, create a Food token. + addCard(Zone.HAND, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 + + // {B}, Discard a creature card: Return target creature card from your graveyard to your hand. + addCard(Zone.BATTLEFIELD, playerA, "Tortured Existence", 1); // Instant {B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{B}, Discard a creature card"); + setChoice(playerA, "Bartered Cow"); + addTarget(playerA, "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Bartered Cow", 1); + assertHandCount(playerA, "Silvercoat Lion", 1); + assertPermanentCount(playerA, "Food", 1); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java index ec736c19a57..2982fe655c5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java @@ -1,89 +1,89 @@ -package org.mage.test.cards.single.eld; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class OnceUponATimeTest extends CardTestPlayerBase { - - @Test - public void test_castRegularly() { - setStrictChooseMode(true); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); - addCard(Zone.LIBRARY, playerA, "Plains", 4); - skipInitShuffling(); - - // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. - // Look at the top five cards of your library. - // You may reveal a creature or land card from among them and put it into your hand. - // Put the rest on the bottom of your library in a random order. - addCard(Zone.HAND, playerA, "Once Upon a Time"); // Instant {1}{G} - addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); - addCard(Zone.HAND, playerA, "Forest", 1); - - playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest"); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); - - setChoice(playerA, false); // Cast without paying its mana cost? - - setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? - setChoice(playerA, "Silvercoat Lion"); - - setStopAt(2, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Once Upon a Time", 1); - assertTappedCount("Forest", true, 2); - assertHandCount(playerA, "Silvercoat Lion", 1); - } - - @Test - public void test_castForFree() { - setStrictChooseMode(true); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); - addCard(Zone.LIBRARY, playerA, "Plains", 4); - - addCard(Zone.LIBRARY, playerB, "Silvercoat Lion", 5); - - skipInitShuffling(); - - // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. - // Look at the top five cards of your library. - // You may reveal a creature or land card from among them and put it into your hand. - // Put the rest on the bottom of your library in a random order. - addCard(Zone.HAND, playerA, "Once Upon a Time"); // Instant {1}{G} - addCard(Zone.HAND, playerB, "Once Upon a Time"); // Instant {1}{G} - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); - setChoice(playerA, true); // Cast without paying its mana cost? - setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? - setChoice(playerA, "Silvercoat Lion"); - - castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Once Upon a Time"); - setChoice(playerB, true); // Cast without paying its mana cost? - setChoice(playerB, true); // Do you wish to reveal a creature or land card and put into your hand? - setChoice(playerB, "Silvercoat Lion"); - - setStopAt(2, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Once Upon a Time", 1); - assertGraveyardCount(playerB, "Once Upon a Time", 1); - - assertHandCount(playerA, "Silvercoat Lion", 1); - assertHandCount(playerB, "Silvercoat Lion", 2); - } +package org.mage.test.cards.single.eld; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class OnceUponATimeTest extends CardTestPlayerBase { + + @Test + public void test_castRegularly() { + setStrictChooseMode(true); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); + addCard(Zone.LIBRARY, playerA, "Plains", 4); + skipInitShuffling(); + + // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. + // Look at the top five cards of your library. + // You may reveal a creature or land card from among them and put it into your hand. + // Put the rest on the bottom of your library in a random order. + addCard(Zone.HAND, playerA, "Once Upon a Time"); // Instant {1}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.HAND, playerA, "Forest", 1); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); + + setChoice(playerA, false); // Cast without paying its mana cost? + + setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? + setChoice(playerA, "Silvercoat Lion"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Once Upon a Time", 1); + assertTappedCount("Forest", true, 2); + assertHandCount(playerA, "Silvercoat Lion", 1); + } + + @Test + public void test_castForFree() { + setStrictChooseMode(true); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); + addCard(Zone.LIBRARY, playerA, "Plains", 4); + + addCard(Zone.LIBRARY, playerB, "Silvercoat Lion", 5); + + skipInitShuffling(); + + // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. + // Look at the top five cards of your library. + // You may reveal a creature or land card from among them and put it into your hand. + // Put the rest on the bottom of your library in a random order. + addCard(Zone.HAND, playerA, "Once Upon a Time"); // Instant {1}{G} + addCard(Zone.HAND, playerB, "Once Upon a Time"); // Instant {1}{G} + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); + setChoice(playerA, true); // Cast without paying its mana cost? + setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? + setChoice(playerA, "Silvercoat Lion"); + + castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Once Upon a Time"); + setChoice(playerB, true); // Cast without paying its mana cost? + setChoice(playerB, true); // Do you wish to reveal a creature or land card and put into your hand? + setChoice(playerB, "Silvercoat Lion"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Once Upon a Time", 1); + assertGraveyardCount(playerB, "Once Upon a Time", 1); + + assertHandCount(playerA, "Silvercoat Lion", 1); + assertHandCount(playerB, "Silvercoat Lion", 2); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/SyrGwynHeroOfAshvaleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/SyrGwynHeroOfAshvaleTest.java index d00136c897a..eab21ba8758 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/SyrGwynHeroOfAshvaleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/SyrGwynHeroOfAshvaleTest.java @@ -1,57 +1,57 @@ -package org.mage.test.cards.single.eld; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class SyrGwynHeroOfAshvaleTest extends CardTestPlayerBase { - - @Test - public void equipKnightTest() { - // Equipped creature gets +2/+2 and has trample and lifelink. - addCard(Zone.BATTLEFIELD, playerA, "Behemoth Sledge"); // Artifact - Equipment {1}{G}{W} - - // Vigilance, menace - // Whenever an equipped creature you control attacks, you draw a card and you lose 1 life. - // Equipment you control have equip Knight {0}. - addCard(Zone.BATTLEFIELD, playerA, "Syr Gwyn, Hero of Ashvale"); // Legendary Creature {3}{R}{W}{B} 5/5 Human Knight - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip Knight", "Syr Gwyn, Hero of Ashvale"); - - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertPowerToughness(playerA, "Syr Gwyn, Hero of Ashvale", 7, 7); - - } - - @Test - public void equipKnightTestInstantSpeed() { - // Equipped creature gets +2/+2 and has trample and lifelink. - addCard(Zone.BATTLEFIELD, playerA, "Behemoth Sledge"); // Artifact - Equipment {1}{G}{W} - - // You may activate equip abilities any time you could cast an instant. - addCard(Zone.BATTLEFIELD, playerA, "Leonin Shikari", 2); // Creature 2/2 - - // Vigilance, menace - // Whenever an equipped creature you control attacks, you draw a card and you lose 1 life. - // Equipment you control have equip Knight {0}. - addCard(Zone.BATTLEFIELD, playerA, "Syr Gwyn, Hero of Ashvale"); // Legendary Creature {3}{R}{W}{B} 5/5 Human Knight - - activateAbility(1, PhaseStep.DECLARE_ATTACKERS, playerA, "Equip Knight", "Syr Gwyn, Hero of Ashvale"); - - - setStopAt(1, PhaseStep.END_COMBAT); - execute(); - - assertPowerToughness(playerA, "Syr Gwyn, Hero of Ashvale", 7, 7); - - } -} +package org.mage.test.cards.single.eld; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class SyrGwynHeroOfAshvaleTest extends CardTestPlayerBase { + + @Test + public void equipKnightTest() { + // Equipped creature gets +2/+2 and has trample and lifelink. + addCard(Zone.BATTLEFIELD, playerA, "Behemoth Sledge"); // Artifact - Equipment {1}{G}{W} + + // Vigilance, menace + // Whenever an equipped creature you control attacks, you draw a card and you lose 1 life. + // Equipment you control have equip Knight {0}. + addCard(Zone.BATTLEFIELD, playerA, "Syr Gwyn, Hero of Ashvale"); // Legendary Creature {3}{R}{W}{B} 5/5 Human Knight + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip Knight", "Syr Gwyn, Hero of Ashvale"); + + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPowerToughness(playerA, "Syr Gwyn, Hero of Ashvale", 7, 7); + + } + + @Test + public void equipKnightTestInstantSpeed() { + // Equipped creature gets +2/+2 and has trample and lifelink. + addCard(Zone.BATTLEFIELD, playerA, "Behemoth Sledge"); // Artifact - Equipment {1}{G}{W} + + // You may activate equip abilities any time you could cast an instant. + addCard(Zone.BATTLEFIELD, playerA, "Leonin Shikari", 2); // Creature 2/2 + + // Vigilance, menace + // Whenever an equipped creature you control attacks, you draw a card and you lose 1 life. + // Equipment you control have equip Knight {0}. + addCard(Zone.BATTLEFIELD, playerA, "Syr Gwyn, Hero of Ashvale"); // Legendary Creature {3}{R}{W}{B} 5/5 Human Knight + + activateAbility(1, PhaseStep.DECLARE_ATTACKERS, playerA, "Equip Knight", "Syr Gwyn, Hero of Ashvale"); + + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertPowerToughness(playerA, "Syr Gwyn, Hero of Ashvale", 7, 7); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/PeltCollectorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/PeltCollectorTest.java index 1833604d257..ea87437b731 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/PeltCollectorTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/PeltCollectorTest.java @@ -1,245 +1,245 @@ -package org.mage.test.cards.single.grn; - -import mage.abilities.keyword.TrampleAbility; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import mage.filter.Filter; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * @author LevelX2, TheElk801 - */ -public class PeltCollectorTest extends CardTestPlayerBase { - - private static final String collector = "Pelt Collector"; - private static final String lion = "Silvercoat Lion"; - private static final String trostani = "Trostani Discordant"; - private static final String bear = "Grizzly Bears"; - private static final String murder = "Murder"; - private static final String courser = "Centaur Courser"; - private static final String growth = "Giant Growth"; - private static final String karstoderm = "Karstoderm"; - - @Test - public void test_Simple() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // Whenever another creature you control enters the battlefield or dies, if that creature's power is greater than Pelt Collector's, put a +1/+1 counter on Pelt Collector. - // As long as Pelt Collector has three or more +1/+1 counters on it, it has trample. - addCard(Zone.HAND, playerA, collector, 1); // Creature {G} - addCard(Zone.HAND, playerA, lion, 1); // Creature {1}{W} - - addCard(Zone.BATTLEFIELD, playerB, collector, 1);// Creature {G} - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, collector); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerB, collector, 1, 1); - - assertPowerToughness(playerA, lion, 2, 2); - assertPowerToughness(playerA, collector, 2, 2); - assertAbility(playerA, collector, TrampleAbility.getInstance(), false); - assertAbility(playerB, collector, TrampleAbility.getInstance(), false); - } - - /** - * To determine if Pelt Collector’s first ability triggers when a creature - * enters the battlefield, use the creature’s power after applying any - * static abilities (such as that of Trostani Discordant) that modify its - * power. - */ - @Test - public void test_TrostaniDiscordant() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); - // Whenever another creature you control enters the battlefield or dies, if that creature's power is greater than Pelt Collector's, put a +1/+1 counter on Pelt Collector. - // As long as Pelt Collector has three or more +1/+1 counters on it, it has trample. - addCard(Zone.HAND, playerA, collector, 1); // Creature {G} - addCard(Zone.HAND, playerA, lion, 1); // Creature {1}{W} - // Other creatures you control get +1/+1. - // When Trostani Discordant enters the battlefield, create two 1/1 white Soldier creature tokens with lifelink. - // At the beginning of your end step, each player gains control of all creatures they own. - addCard(Zone.HAND, playerA, trostani, 1); // Creature {3}{G}{W} /1/4) - - addCard(Zone.BATTLEFIELD, playerB, collector, 1);// Creature {G} - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, trostani); - - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, collector); - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, lion); - - setStopAt(3, PhaseStep.BEGIN_COMBAT); - - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerB, collector, 1, 1); - - assertPowerToughness(playerA, "Soldier", 2, 2, Filter.ComparisonScope.All); - - assertPowerToughness(playerA, lion, 3, 3); - assertPowerToughness(playerA, collector, 3, 3); - assertAbility(playerA, collector, TrampleAbility.getInstance(), false); - assertAbility(playerB, collector, TrampleAbility.getInstance(), false); - - } - - - @Test - public void testEntersTrigger() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, bear); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 2, 2); - assertCounterCount(collector, CounterType.P1P1, 1); - } - - @Test - public void testEntersTrigger2() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, karstoderm); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, karstoderm); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 2, 2); - assertCounterCount(collector, CounterType.P1P1, 1); - } - - @Test - public void testDiesTrigger() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, bear); - addCard(Zone.HAND, playerA, murder); - addCard(Zone.BATTLEFIELD, playerA, "Bayou", 5); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, bear); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 2, 2); - assertCounterCount(collector, CounterType.P1P1, 1); - } - - @Test - public void testDiesTrigger2() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, courser); - addCard(Zone.HAND, playerA, murder); - addCard(Zone.BATTLEFIELD, playerA, "Bayou", 6); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, courser); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, courser); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 3, 3); - assertCounterCount(collector, CounterType.P1P1, 2); - } - - @Test - public void testDiesTrigger3() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, karstoderm); - addCard(Zone.HAND, playerA, murder); - addCard(Zone.BATTLEFIELD, playerA, "Bayou", 7); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, karstoderm); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, karstoderm); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 3, 3); - assertCounterCount(collector, CounterType.P1P1, 2); - } - - @Test - public void testInterveningIf() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, bear); - addCard(Zone.HAND, playerA, growth); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, growth, collector); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 4, 4); - assertCounterCount(collector, CounterType.P1P1, 0); - } - - @Test - public void testInterveningIf2() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, bear); - addCard(Zone.HAND, playerA, "Scar"); - addCard(Zone.BATTLEFIELD, playerA, "Bayou", 3); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scar", bear); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 1, 1); - assertCounterCount(collector, CounterType.P1P1, 0); - } -} +package org.mage.test.cards.single.grn; + +import mage.abilities.keyword.TrampleAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.Filter; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author LevelX2, TheElk801 + */ +public class PeltCollectorTest extends CardTestPlayerBase { + + private static final String collector = "Pelt Collector"; + private static final String lion = "Silvercoat Lion"; + private static final String trostani = "Trostani Discordant"; + private static final String bear = "Grizzly Bears"; + private static final String murder = "Murder"; + private static final String courser = "Centaur Courser"; + private static final String growth = "Giant Growth"; + private static final String karstoderm = "Karstoderm"; + + @Test + public void test_Simple() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // Whenever another creature you control enters the battlefield or dies, if that creature's power is greater than Pelt Collector's, put a +1/+1 counter on Pelt Collector. + // As long as Pelt Collector has three or more +1/+1 counters on it, it has trample. + addCard(Zone.HAND, playerA, collector, 1); // Creature {G} + addCard(Zone.HAND, playerA, lion, 1); // Creature {1}{W} + + addCard(Zone.BATTLEFIELD, playerB, collector, 1);// Creature {G} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, collector); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerB, collector, 1, 1); + + assertPowerToughness(playerA, lion, 2, 2); + assertPowerToughness(playerA, collector, 2, 2); + assertAbility(playerA, collector, TrampleAbility.getInstance(), false); + assertAbility(playerB, collector, TrampleAbility.getInstance(), false); + } + + /** + * To determine if Pelt Collector’s first ability triggers when a creature + * enters the battlefield, use the creature’s power after applying any + * static abilities (such as that of Trostani Discordant) that modify its + * power. + */ + @Test + public void test_TrostaniDiscordant() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + // Whenever another creature you control enters the battlefield or dies, if that creature's power is greater than Pelt Collector's, put a +1/+1 counter on Pelt Collector. + // As long as Pelt Collector has three or more +1/+1 counters on it, it has trample. + addCard(Zone.HAND, playerA, collector, 1); // Creature {G} + addCard(Zone.HAND, playerA, lion, 1); // Creature {1}{W} + // Other creatures you control get +1/+1. + // When Trostani Discordant enters the battlefield, create two 1/1 white Soldier creature tokens with lifelink. + // At the beginning of your end step, each player gains control of all creatures they own. + addCard(Zone.HAND, playerA, trostani, 1); // Creature {3}{G}{W} /1/4) + + addCard(Zone.BATTLEFIELD, playerB, collector, 1);// Creature {G} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, trostani); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, collector); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, lion); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerB, collector, 1, 1); + + assertPowerToughness(playerA, "Soldier", 2, 2, Filter.ComparisonScope.All); + + assertPowerToughness(playerA, lion, 3, 3); + assertPowerToughness(playerA, collector, 3, 3); + assertAbility(playerA, collector, TrampleAbility.getInstance(), false); + assertAbility(playerB, collector, TrampleAbility.getInstance(), false); + + } + + + @Test + public void testEntersTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, bear); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 2, 2); + assertCounterCount(collector, CounterType.P1P1, 1); + } + + @Test + public void testEntersTrigger2() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, karstoderm); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, karstoderm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 2, 2); + assertCounterCount(collector, CounterType.P1P1, 1); + } + + @Test + public void testDiesTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, bear); + addCard(Zone.HAND, playerA, murder); + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 5); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, bear); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 2, 2); + assertCounterCount(collector, CounterType.P1P1, 1); + } + + @Test + public void testDiesTrigger2() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, courser); + addCard(Zone.HAND, playerA, murder); + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 6); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, courser); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, courser); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 3, 3); + assertCounterCount(collector, CounterType.P1P1, 2); + } + + @Test + public void testDiesTrigger3() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, karstoderm); + addCard(Zone.HAND, playerA, murder); + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 7); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, karstoderm); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, karstoderm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 3, 3); + assertCounterCount(collector, CounterType.P1P1, 2); + } + + @Test + public void testInterveningIf() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, bear); + addCard(Zone.HAND, playerA, growth); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, growth, collector); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 4, 4); + assertCounterCount(collector, CounterType.P1P1, 0); + } + + @Test + public void testInterveningIf2() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, bear); + addCard(Zone.HAND, playerA, "Scar"); + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scar", bear); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 1, 1); + assertCounterCount(collector, CounterType.P1P1, 0); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/hou/TormentOfHailfireTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/hou/TormentOfHailfireTest.java index 4d2d4d2aff6..7dc01ca0e75 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/hou/TormentOfHailfireTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/hou/TormentOfHailfireTest.java @@ -1,88 +1,88 @@ -package org.mage.test.cards.single.hou; - -import java.io.FileNotFoundException; - -import mage.constants.MultiplayerAttackOption; -import mage.constants.PhaseStep; -import mage.constants.RangeOfInfluence; -import mage.constants.Zone; -import mage.game.FreeForAll; -import mage.game.Game; -import mage.game.GameException; -import mage.game.mulligan.MulliganType; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestMultiPlayerBase; - -/** - * - * @author LevelX2 - */ -public class TormentOfHailfireTest extends CardTestMultiPlayerBase { - - @Override - protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - // Start Life = 2 - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 20); - // Player order: A -> D -> C -> B - playerA = createPlayer(game, playerA, "PlayerA"); - playerB = createPlayer(game, playerB, "PlayerB"); - playerC = createPlayer(game, playerC, "PlayerC"); - playerD = createPlayer(game, playerD, "PlayerD"); - return game; - } - - @Test - public void test_Normal() { - setStrictChooseMode(true); - - // Repeat the following process X times. Each opponent loses 3 life unless they sacrifice a nonland permanent or discards a card. - addCard(Zone.HAND, playerA, "Torment of Hailfire", 1); // Sorcery {X}{B}{B} - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 12); - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); - addCard(Zone.HAND, playerB, "Plains", 1); - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion", 3); - - addCard(Zone.BATTLEFIELD, playerD, "Silvercoat Lion", 3); - addCard(Zone.HAND, playerD, "Plains", 1); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Torment of Hailfire"); - setChoice(playerA, "X=10"); - - setChoice(playerD, true);// Sacrifices a nonland permanent? - setChoice(playerD, "Silvercoat Lion"); - - setChoice(playerB, true);// Sacrifices a nonland permanent? - setChoice(playerB, "Silvercoat Lion"); - - setChoice(playerD, true);// Sacrifices a nonland permanent? - setChoice(playerD, "Silvercoat Lion"); - - setChoice(playerB, true);// Sacrifices a nonland permanent? - setChoice(playerB, "Silvercoat Lion"); - - setChoice(playerD, false);// Sacrifices a nonland permanent? - setChoice(playerD, true);// Discard a card? - - setChoice(playerB, true);// Discard a card? - - setChoice(playerD, true);// Sacrifices a nonland permanent? - setChoice(playerD, "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Torment of Hailfire", 1); - - assertLife(playerA, 20); - assertLife(playerC, 20); - assertLife(playerD, 2); - assertLife(playerB, -1); - Assert.assertFalse("Player B is dead", playerB.isInGame()); - - } -} +package org.mage.test.cards.single.hou; + +import java.io.FileNotFoundException; + +import mage.constants.MultiplayerAttackOption; +import mage.constants.PhaseStep; +import mage.constants.RangeOfInfluence; +import mage.constants.Zone; +import mage.game.FreeForAll; +import mage.game.Game; +import mage.game.GameException; +import mage.game.mulligan.MulliganType; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBase; + +/** + * + * @author LevelX2 + */ +public class TormentOfHailfireTest extends CardTestMultiPlayerBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + // Start Life = 2 + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 20); + // Player order: A -> D -> C -> B + playerA = createPlayer(game, playerA, "PlayerA"); + playerB = createPlayer(game, playerB, "PlayerB"); + playerC = createPlayer(game, playerC, "PlayerC"); + playerD = createPlayer(game, playerD, "PlayerD"); + return game; + } + + @Test + public void test_Normal() { + setStrictChooseMode(true); + + // Repeat the following process X times. Each opponent loses 3 life unless they sacrifice a nonland permanent or discards a card. + addCard(Zone.HAND, playerA, "Torment of Hailfire", 1); // Sorcery {X}{B}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 12); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); + addCard(Zone.HAND, playerB, "Plains", 1); + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion", 3); + + addCard(Zone.BATTLEFIELD, playerD, "Silvercoat Lion", 3); + addCard(Zone.HAND, playerD, "Plains", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Torment of Hailfire"); + setChoice(playerA, "X=10"); + + setChoice(playerD, true);// Sacrifices a nonland permanent? + setChoice(playerD, "Silvercoat Lion"); + + setChoice(playerB, true);// Sacrifices a nonland permanent? + setChoice(playerB, "Silvercoat Lion"); + + setChoice(playerD, true);// Sacrifices a nonland permanent? + setChoice(playerD, "Silvercoat Lion"); + + setChoice(playerB, true);// Sacrifices a nonland permanent? + setChoice(playerB, "Silvercoat Lion"); + + setChoice(playerD, false);// Sacrifices a nonland permanent? + setChoice(playerD, true);// Discard a card? + + setChoice(playerB, true);// Discard a card? + + setChoice(playerD, true);// Sacrifices a nonland permanent? + setChoice(playerD, "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Torment of Hailfire", 1); + + assertLife(playerA, 20); + assertLife(playerC, 20); + assertLife(playerD, 2); + assertLife(playerB, -1); + Assert.assertFalse("Player B is dead", playerB.isInGame()); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/OboshThePreypiercerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/OboshThePreypiercerTest.java index c80bb289e79..0614756504c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/OboshThePreypiercerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/OboshThePreypiercerTest.java @@ -1,34 +1,34 @@ -package org.mage.test.cards.single.iko; - - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * @author LevelX2 - */ - -public class OboshThePreypiercerTest extends CardTestPlayerBase { - - @Test - public void testZeroCMSIsHandledAsOdd() { - setStrictChooseMode(true); - // At the beginning of your upkeep, flip a coin. If you lose the flip, Mana Crypt deals 3 damage to you. - // {T}: Add {C}{C}. - addCard(Zone.BATTLEFIELD, playerA, "Mana Crypt"); - // Companion — Your starting deck contains only cards with odd converted mana costs and land cards. - // If a source you control with an odd converted mana cost would deal damage to a permanent or player, it deals double that damage to that permanent or player instead. - addCard(Zone.BATTLEFIELD, playerA, "Obosh, the Preypiercer"); - - // lose the flip - setFlipCoinResult(playerA, false); - - setStopAt(1, PhaseStep.PRECOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - } +package org.mage.test.cards.single.iko; + + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author LevelX2 + */ + +public class OboshThePreypiercerTest extends CardTestPlayerBase { + + @Test + public void testZeroCMSIsHandledAsOdd() { + setStrictChooseMode(true); + // At the beginning of your upkeep, flip a coin. If you lose the flip, Mana Crypt deals 3 damage to you. + // {T}: Add {C}{C}. + addCard(Zone.BATTLEFIELD, playerA, "Mana Crypt"); + // Companion — Your starting deck contains only cards with odd converted mana costs and land cards. + // If a source you control with an odd converted mana cost would deal damage to a permanent or player, it deals double that damage to that permanent or player instead. + addCard(Zone.BATTLEFIELD, playerA, "Obosh, the Preypiercer"); + + // lose the flip + setFlipCoinResult(playerA, false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/SkycatSovereignTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/SkycatSovereignTest.java index ddd6ca85e2a..d94ae7d1b5c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/SkycatSovereignTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/SkycatSovereignTest.java @@ -1,90 +1,90 @@ -package org.mage.test.cards.single.iko; - -import mage.ObjectColor; -import mage.abilities.keyword.FlyingAbility; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class SkycatSovereignTest extends CardTestPlayerBase { - - @Test - public void test_BoostFromFlyers() { - setStrictChooseMode(true); - - // Flying - // Skycat Sovereign gets +1/+1 for each other creature you control with flying. - // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. - addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) - - // Flying, vigilance - addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) - - setStopAt(1, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - assertPowerToughness(playerA, "Skycat Sovereign", 3, 3); - } - - /** - * Skycat Sovereign still gets +1/+1 for each creature that is supposed to have flying when there's an opposing Archetype of Imagination. - */ - @Test - public void test_NoBoostIfFlyingLost() { - setStrictChooseMode(true); - - // Flying - // Skycat Sovereign gets +1/+1 for each other creature you control with flying. - // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. - addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) - - // Flying, vigilance - addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) - - // Creatures you control have flying. - // Creatures your opponents control lose flying and can't have or gain flying. - addCard(Zone.BATTLEFIELD, playerB, "Archetype of Imagination"); // - - setStopAt(1, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - assertPowerToughness(playerA, "Skycat Sovereign", 1, 1); - } - - @Test - public void test_BoostFromToken() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains"); - addCard(Zone.BATTLEFIELD, playerA, "Island", 3); - // Flying - // Skycat Sovereign gets +1/+1 for each other creature you control with flying. - // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. - addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) - - // Flying, vigilance - addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{W}{U}: Create"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Cat Bird", 1); - assertColor(playerA, "Cat Bird", ObjectColor.WHITE, true); - assertColor(playerA, "Cat Bird", ObjectColor.BLUE, false); - assertAbility(playerA, "Cat Bird", FlyingAbility.getInstance(), true); - - assertPowerToughness(playerA, "Skycat Sovereign", 4, 4); - } +package org.mage.test.cards.single.iko; + +import mage.ObjectColor; +import mage.abilities.keyword.FlyingAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class SkycatSovereignTest extends CardTestPlayerBase { + + @Test + public void test_BoostFromFlyers() { + setStrictChooseMode(true); + + // Flying + // Skycat Sovereign gets +1/+1 for each other creature you control with flying. + // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. + addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) + + // Flying, vigilance + addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + assertPowerToughness(playerA, "Skycat Sovereign", 3, 3); + } + + /** + * Skycat Sovereign still gets +1/+1 for each creature that is supposed to have flying when there's an opposing Archetype of Imagination. + */ + @Test + public void test_NoBoostIfFlyingLost() { + setStrictChooseMode(true); + + // Flying + // Skycat Sovereign gets +1/+1 for each other creature you control with flying. + // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. + addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) + + // Flying, vigilance + addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) + + // Creatures you control have flying. + // Creatures your opponents control lose flying and can't have or gain flying. + addCard(Zone.BATTLEFIELD, playerB, "Archetype of Imagination"); // + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + assertPowerToughness(playerA, "Skycat Sovereign", 1, 1); + } + + @Test + public void test_BoostFromToken() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + // Flying + // Skycat Sovereign gets +1/+1 for each other creature you control with flying. + // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. + addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) + + // Flying, vigilance + addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{W}{U}: Create"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Cat Bird", 1); + assertColor(playerA, "Cat Bird", ObjectColor.WHITE, true); + assertColor(playerA, "Cat Bird", ObjectColor.BLUE, false); + assertAbility(playerA, "Cat Bird", FlyingAbility.getInstance(), true); + + assertPowerToughness(playerA, "Skycat Sovereign", 4, 4); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh1/PlagueEngineerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh1/PlagueEngineerTest.java index 2ef638aea1d..8626a0bc76a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh1/PlagueEngineerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh1/PlagueEngineerTest.java @@ -1,39 +1,39 @@ -package org.mage.test.cards.single.mh1; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class PlagueEngineerTest extends CardTestPlayerBase { - - @Test - public void test_Standard() { - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); - // Deathtouch - // As Plague Engineer enters the battlefield, choose a creature type. - // Creatures of the chosen type your opponents control get -1/-1. - addCard(Zone.HAND, playerA, "Plague Engineer"); // Creature {2}{B} (2/2) - - addCard(Zone.BATTLEFIELD, playerA, "Defiant Elf", 2); //Creature - Elf (1/1) - addCard(Zone.BATTLEFIELD, playerB, "Defiant Elf", 2); //Creature - Elf (1/1) - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plague Engineer"); - setChoice(playerA, "Elf"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertPermanentCount(playerA, "Plague Engineer", 1); - - assertPermanentCount(playerA, "Defiant Elf", 2); - assertGraveyardCount(playerB, "Defiant Elf", 2); - - assertLife(playerA, 20); - assertLife(playerB, 20); - } +package org.mage.test.cards.single.mh1; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class PlagueEngineerTest extends CardTestPlayerBase { + + @Test + public void test_Standard() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + // Deathtouch + // As Plague Engineer enters the battlefield, choose a creature type. + // Creatures of the chosen type your opponents control get -1/-1. + addCard(Zone.HAND, playerA, "Plague Engineer"); // Creature {2}{B} (2/2) + + addCard(Zone.BATTLEFIELD, playerA, "Defiant Elf", 2); //Creature - Elf (1/1) + addCard(Zone.BATTLEFIELD, playerB, "Defiant Elf", 2); //Creature - Elf (1/1) + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plague Engineer"); + setChoice(playerA, "Elf"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Plague Engineer", 1); + + assertPermanentCount(playerA, "Defiant Elf", 2); + assertGraveyardCount(playerB, "Defiant Elf", 2); + + assertLife(playerA, 20); + assertLife(playerB, 20); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/som/NimDeathmantleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/som/NimDeathmantleTest.java index 24990a66999..44104a5f8c1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/som/NimDeathmantleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/som/NimDeathmantleTest.java @@ -1,51 +1,51 @@ -package org.mage.test.cards.single.som; - -import mage.abilities.keyword.IntimidateAbility; -import mage.constants.CardType; -import mage.constants.PhaseStep; -import mage.constants.SubType; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class NimDeathmantleTest extends CardTestPlayerBase { - - @Test - public void test_Basic() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); - // Equipped creature gets +2/+2, has intimidate, and is a black Zombie. - // Whenever a nontoken creature is put into your graveyard from the battlefield, you may pay {4}. If you do, return that card to the battlefield and attach Nim Deathmantle to it. - addCard(Zone.HAND, playerA, "Nim Deathmantle"); // Artifact Equipment {2} - - addCard(Zone.BATTLEFIELD, playerB, "Mountain"); - addCard(Zone.HAND, playerB, "Lightning Bolt"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nim Deathmantle"); - - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); - setChoice(playerA, true); // Message: Nim Deathmantle - Pay {4}? - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerB, "Lightning Bolt",1); - - assertPermanentCount(playerA, "Nim Deathmantle", 1); - - assertPowerToughness(playerA, "Silvercoat Lion", 4, 4); - assertAbility(playerA, "Silvercoat Lion", IntimidateAbility.getInstance(), true); - assertType("Silvercoat Lion", CardType.CREATURE, SubType.ZOMBIE); - - } +package org.mage.test.cards.single.som; + +import mage.abilities.keyword.IntimidateAbility; +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class NimDeathmantleTest extends CardTestPlayerBase { + + @Test + public void test_Basic() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + // Equipped creature gets +2/+2, has intimidate, and is a black Zombie. + // Whenever a nontoken creature is put into your graveyard from the battlefield, you may pay {4}. If you do, return that card to the battlefield and attach Nim Deathmantle to it. + addCard(Zone.HAND, playerA, "Nim Deathmantle"); // Artifact Equipment {2} + + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nim Deathmantle"); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); + setChoice(playerA, true); // Message: Nim Deathmantle - Pay {4}? + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Lightning Bolt",1); + + assertPermanentCount(playerA, "Nim Deathmantle", 1); + + assertPowerToughness(playerA, "Silvercoat Lion", 4, 4); + assertAbility(playerA, "Silvercoat Lion", IntimidateAbility.getInstance(), true); + assertType("Silvercoat Lion", CardType.CREATURE, SubType.ZOMBIE); + + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/KardurDoomscourgeAndKithkinMourncallerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/KardurDoomscourgeAndKithkinMourncallerTest.java index 4895bca9749..206a3dea495 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/KardurDoomscourgeAndKithkinMourncallerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/KardurDoomscourgeAndKithkinMourncallerTest.java @@ -1,103 +1,103 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package org.mage.test.cards.triggers; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author jeffwadsworth - */ -public class KardurDoomscourgeAndKithkinMourncallerTest extends CardTestPlayerBase { - - @Test - public void testKDRemovedFromCombatViaRegenerateAbility() { - setStrictChooseMode(true); - // Kardur, Doomscourge: if an attacking creature dies, each opponent loses 1 life and you gain 1 life - addCard(Zone.BATTLEFIELD, playerA, "Kardur, Doomscourge"); - addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); - addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); - addCard(Zone.HAND, playerA, "Regenerate"); - addCard(Zone.HAND, playerA, "Terror"); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Regenerate", "Elvish Archers"); - - attack(1, playerA, "Elvish Archers"); - block(1, playerB, "Serra Angel", "Elvish Archers"); // regeneration shield used up and EA is removed from combat - - castSpell(1, PhaseStep.END_COMBAT, playerA, "Terror", "Elvish Archers"); // still within the combat phase, the EA is destroyed/dies - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Elvish Archers", 1); - - // does not fire due to the Elvish Archers not in an attacking state - assertLife(playerA, 20); - assertLife(playerB, 20); - - } - - @Test - public void testSuccessfulKDTrigger() { - setStrictChooseMode(true); - // Kardur, Doomscourge: if an attacking creature dies, each opponent loses 1 life and you gain 1 life - addCard(Zone.BATTLEFIELD, playerA, "Kardur, Doomscourge"); - addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); // 2/2 first strike - addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4 vigilance - - attack(1, playerA, "Elvish Archers"); - block(1, playerB, "Serra Angel", "Elvish Archers"); // Elvish Archer dies causing KD to trigger - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Elvish Archers", 1); - - // successful fire so playerA gains 1 life and playerB loses 1 life - assertLife(playerA, 21); - assertLife(playerB, 19); - - } - - @Test - public void testKMTrigger() { - setStrictChooseMode(true); - // Kithkin Mourncaller: if an elf or kithkin dies, you may draw a card - addCard(Zone.BATTLEFIELD, playerA, "Kithkin Mourncaller"); - addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); // 2/1 first strike - addCard(Zone.BATTLEFIELD, playerA, "Pearled Unicorn"); // 2/2 - addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4 vigilance - addCard(Zone.BATTLEFIELD, playerB, "Runeclaw Bear"); // 2/2 - addCard(Zone.LIBRARY, playerA, "Island", 2); // used for draw trigger - - attack(1, playerA, "Elvish Archers"); - attack(1, playerA, "Pearled Unicorn"); - block(1, playerB, "Serra Angel", "Elvish Archers"); // Elvish Achers will die and trigger KM - block(1, playerB, "Runeclaw Bear", "Pearled Unicorn"); // Pearled Unicorn will die but not trigger KM - - setChoice(playerA, "Yes"); // accept the drawing of a card from the single trigger (Elvish Archers "elf type") - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Elvish Archers", 1); - assertGraveyardCount(playerA, "Pearled Unicorn", 1); - - // successful fire due to dead Elvish Archers (elf) so playerA draws a card - assertHandCount(playerA, 1); - - } - -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.cards.triggers; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author jeffwadsworth + */ +public class KardurDoomscourgeAndKithkinMourncallerTest extends CardTestPlayerBase { + + @Test + public void testKDRemovedFromCombatViaRegenerateAbility() { + setStrictChooseMode(true); + // Kardur, Doomscourge: if an attacking creature dies, each opponent loses 1 life and you gain 1 life + addCard(Zone.BATTLEFIELD, playerA, "Kardur, Doomscourge"); + addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); + addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); + addCard(Zone.HAND, playerA, "Regenerate"); + addCard(Zone.HAND, playerA, "Terror"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Regenerate", "Elvish Archers"); + + attack(1, playerA, "Elvish Archers"); + block(1, playerB, "Serra Angel", "Elvish Archers"); // regeneration shield used up and EA is removed from combat + + castSpell(1, PhaseStep.END_COMBAT, playerA, "Terror", "Elvish Archers"); // still within the combat phase, the EA is destroyed/dies + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Elvish Archers", 1); + + // does not fire due to the Elvish Archers not in an attacking state + assertLife(playerA, 20); + assertLife(playerB, 20); + + } + + @Test + public void testSuccessfulKDTrigger() { + setStrictChooseMode(true); + // Kardur, Doomscourge: if an attacking creature dies, each opponent loses 1 life and you gain 1 life + addCard(Zone.BATTLEFIELD, playerA, "Kardur, Doomscourge"); + addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); // 2/2 first strike + addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4 vigilance + + attack(1, playerA, "Elvish Archers"); + block(1, playerB, "Serra Angel", "Elvish Archers"); // Elvish Archer dies causing KD to trigger + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Elvish Archers", 1); + + // successful fire so playerA gains 1 life and playerB loses 1 life + assertLife(playerA, 21); + assertLife(playerB, 19); + + } + + @Test + public void testKMTrigger() { + setStrictChooseMode(true); + // Kithkin Mourncaller: if an elf or kithkin dies, you may draw a card + addCard(Zone.BATTLEFIELD, playerA, "Kithkin Mourncaller"); + addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); // 2/1 first strike + addCard(Zone.BATTLEFIELD, playerA, "Pearled Unicorn"); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4 vigilance + addCard(Zone.BATTLEFIELD, playerB, "Runeclaw Bear"); // 2/2 + addCard(Zone.LIBRARY, playerA, "Island", 2); // used for draw trigger + + attack(1, playerA, "Elvish Archers"); + attack(1, playerA, "Pearled Unicorn"); + block(1, playerB, "Serra Angel", "Elvish Archers"); // Elvish Achers will die and trigger KM + block(1, playerB, "Runeclaw Bear", "Pearled Unicorn"); // Pearled Unicorn will die but not trigger KM + + setChoice(playerA, "Yes"); // accept the drawing of a card from the single trigger (Elvish Archers "elf type") + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Elvish Archers", 1); + assertGraveyardCount(playerA, "Pearled Unicorn", 1); + + // successful fire due to dead Elvish Archers (elf) so playerA draws a card + assertHandCount(playerA, 1); + + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/AshenRiderTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/AshenRiderTest.java index ed02d06706f..9de90e04f46 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/AshenRiderTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/AshenRiderTest.java @@ -1,68 +1,68 @@ -package org.mage.test.cards.triggers.dies; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class AshenRiderTest extends CardTestPlayerBase { - - /* - * Volrath, the Shapestealer and Ashen Rider, Ashen Rider has a counter on it: - Turn Volrath into Ashen Rider: - Destroy the Volrath (who's the Ashen Rider) with Putrefy: - The death trigger for the Volrath copying Ashen Rider did not trigger. - */ - @Test - public void cartelAristrocraftInteractionOpponentDoesNotPayLife() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - addCard(Zone.BATTLEFIELD, playerA, "Island", 2); - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); - - // Flying - // When Ashen Rider enters the battlefield or dies, exile target permanent. - addCard(Zone.BATTLEFIELD, playerA, "Ashen Rider"); // Creature {4}{W}{W}{B}{B} - - // At the beginning of combat on your turn, put a -1/-1 counter on up to one target creature. - // {1}: Until your next turn, Volrath, the Shapestealer becomes a copy of target creature with a counter on it, except it's 7/5 and it has this ability. - addCard(Zone.HAND, playerA, "Volrath, the Shapestealer"); // Creature {2}{B}{G}{U} - addTarget(playerA, "Ashen Rider"); - - // Destroy target artifact or creature. It can't be regenerated. - addCard(Zone.HAND, playerA, "Putrefy"); // Instant {1}{B}{G} - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Volrath, the Shapestealer"); - - activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}: Until your next turn"); - addTarget(playerA, "Ashen Rider"); - - waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); - - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Putrefy", "Ashen Rider[only copy]"); - - addTarget(playerA, "Silvercoat Lion"); // Dies trigger of Volrath, the Shapestealer copied from Ashen Rider - - setStopAt(3, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, "Ashen Rider", 4,4); - - assertGraveyardCount(playerA, "Putrefy", 1); - assertGraveyardCount(playerA, "Volrath, the Shapestealer", 1); - - assertExileCount(playerB, "Silvercoat Lion", 1); - - } -} +package org.mage.test.cards.triggers.dies; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class AshenRiderTest extends CardTestPlayerBase { + + /* + * Volrath, the Shapestealer and Ashen Rider, Ashen Rider has a counter on it: + Turn Volrath into Ashen Rider: + Destroy the Volrath (who's the Ashen Rider) with Putrefy: + The death trigger for the Volrath copying Ashen Rider did not trigger. + */ + @Test + public void cartelAristrocraftInteractionOpponentDoesNotPayLife() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + + // Flying + // When Ashen Rider enters the battlefield or dies, exile target permanent. + addCard(Zone.BATTLEFIELD, playerA, "Ashen Rider"); // Creature {4}{W}{W}{B}{B} + + // At the beginning of combat on your turn, put a -1/-1 counter on up to one target creature. + // {1}: Until your next turn, Volrath, the Shapestealer becomes a copy of target creature with a counter on it, except it's 7/5 and it has this ability. + addCard(Zone.HAND, playerA, "Volrath, the Shapestealer"); // Creature {2}{B}{G}{U} + addTarget(playerA, "Ashen Rider"); + + // Destroy target artifact or creature. It can't be regenerated. + addCard(Zone.HAND, playerA, "Putrefy"); // Instant {1}{B}{G} + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Volrath, the Shapestealer"); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}: Until your next turn"); + addTarget(playerA, "Ashen Rider"); + + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Putrefy", "Ashen Rider[only copy]"); + + addTarget(playerA, "Silvercoat Lion"); // Dies trigger of Volrath, the Shapestealer copied from Ashen Rider + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Ashen Rider", 4,4); + + assertGraveyardCount(playerA, "Putrefy", 1); + assertGraveyardCount(playerA, "Volrath, the Shapestealer", 1); + + assertExileCount(playerB, "Silvercoat Lion", 1); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/commander/duel/CommanderColorChangeTest.java b/Mage.Tests/src/test/java/org/mage/test/commander/duel/CommanderColorChangeTest.java index e3cf24ae1ef..c4f23c1ebf7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/commander/duel/CommanderColorChangeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/commander/duel/CommanderColorChangeTest.java @@ -1,122 +1,122 @@ -package org.mage.test.commander.duel; - -import java.io.FileNotFoundException; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.GameException; -import mage.game.permanent.Permanent; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestCommanderDuelBase; - -/** - * - * @author LevelX2 - */ - -public class CommanderColorChangeTest extends CardTestCommanderDuelBase { - - @Override - protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - // When a player casts a spell or a creature attacks, exile Norin the Wary. Return it to the battlefield under its owner's control at the beginning of the next end step. - setDecknamePlayerA("CMDNorinTheWary.dck"); // Commander = Norin the Wary {R} - return super.createNewGameAndPlayers(); - } - - @Test - public void castCommanderWithAddedBlueColor() { - setStrictChooseMode(true); - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - - // As Painter's Servant enters the battlefield, choose a color. - // All cards that aren't on the battlefield, spells, and permanents are the chosen color in addition to their other colors. - addCard(Zone.HAND, playerA, "Painter's Servant", 1); // Artifact Creature {2} - - // Whenever a player casts a blue spell, you may gain 1 life. - addCard(Zone.BATTLEFIELD, playerA, "Kraken's Eye", 1); - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Painter's Servant"); - setChoice(playerA, "Blue"); - - // When a player casts a spell or a creature attacks, exile Norin the Wary. - // Return it to the battlefield under its owner's control at the beginning of the next end step. - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Norin the Wary"); - setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Norin the Wary", 1); - - Permanent norin = getPermanent("Norin the Wary", playerA); - Assert.assertEquals(true, norin.getColor(currentGame).isBlue()); - Assert.assertEquals(true, norin.getColor(currentGame).isRed()); - - Permanent kraken = getPermanent("Kraken's Eye", playerA); - Assert.assertEquals(true, kraken.getColor(currentGame).isBlue()); - - assertLife(playerA, 41); - assertLife(playerB, 40); - - } - - - /** - * I played a Painter's Servant, named black, but the other commanders get a extra colors - * Later it got removed but the commanders and some cards still have the extra color - * I played it again later, named green, and the previously affected cards get the extra color - * so now they have 2 extra colors and the commander get and additional color on top of that - * And finally I got the empty hand error #6738 on my turn for what I assume is the Painter's Servant + Grindstone combo I have, - * but nonetheless manage to tie the game so it go into a second game and the issue carry over, - * all the commanders have all the extra colors they gain from the first game - */ - - @Test - public void castCommanderWithoutAddedBlueColor() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - - // As Painter's Servant enters the battlefield, choose a color. - // All cards that aren't on the battlefield, spells, and permanents are the chosen color in addition to their other colors. - addCard(Zone.HAND, playerA, "Painter's Servant", 1); // Artifact Creature {2} - - // Whenever a player casts a blue spell, you may gain 1 life. - addCard(Zone.BATTLEFIELD, playerA, "Kraken's Eye", 1); - - - // Exile target artifact or enchantment. - addCard(Zone.HAND, playerB, "Altar's Light", 1); // Instant {2}{W}{W} - addCard(Zone.BATTLEFIELD, playerB, "Plains", 4); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Painter's Servant"); - setChoice(playerA, "Blue"); - - // When a player casts a spell or a creature attacks, exile Norin the Wary. - // Return it to the battlefield under its owner's control at the beginning of the next end step. - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Norin the Wary"); - setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Altar's Light", "Painter's Servant", "Norin the Wary"); - setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertPermanentCount(playerA, "Norin the Wary", 1); - assertAllCommandsUsed(); - - Permanent norin = getPermanent("Norin the Wary", playerA); - Assert.assertEquals(false, norin.getColor(currentGame).isBlue()); - Assert.assertEquals(true, norin.getColor(currentGame).isRed()); - - Permanent kraken = getPermanent("Kraken's Eye", playerA); - Assert.assertEquals(false, kraken.getColor(currentGame).isBlue()); - - assertLife(playerA, 42); - assertLife(playerB, 40); - - } -} +package org.mage.test.commander.duel; + +import java.io.FileNotFoundException; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.GameException; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommanderDuelBase; + +/** + * + * @author LevelX2 + */ + +public class CommanderColorChangeTest extends CardTestCommanderDuelBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + // When a player casts a spell or a creature attacks, exile Norin the Wary. Return it to the battlefield under its owner's control at the beginning of the next end step. + setDecknamePlayerA("CMDNorinTheWary.dck"); // Commander = Norin the Wary {R} + return super.createNewGameAndPlayers(); + } + + @Test + public void castCommanderWithAddedBlueColor() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + // As Painter's Servant enters the battlefield, choose a color. + // All cards that aren't on the battlefield, spells, and permanents are the chosen color in addition to their other colors. + addCard(Zone.HAND, playerA, "Painter's Servant", 1); // Artifact Creature {2} + + // Whenever a player casts a blue spell, you may gain 1 life. + addCard(Zone.BATTLEFIELD, playerA, "Kraken's Eye", 1); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Painter's Servant"); + setChoice(playerA, "Blue"); + + // When a player casts a spell or a creature attacks, exile Norin the Wary. + // Return it to the battlefield under its owner's control at the beginning of the next end step. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Norin the Wary"); + setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Norin the Wary", 1); + + Permanent norin = getPermanent("Norin the Wary", playerA); + Assert.assertEquals(true, norin.getColor(currentGame).isBlue()); + Assert.assertEquals(true, norin.getColor(currentGame).isRed()); + + Permanent kraken = getPermanent("Kraken's Eye", playerA); + Assert.assertEquals(true, kraken.getColor(currentGame).isBlue()); + + assertLife(playerA, 41); + assertLife(playerB, 40); + + } + + + /** + * I played a Painter's Servant, named black, but the other commanders get a extra colors + * Later it got removed but the commanders and some cards still have the extra color + * I played it again later, named green, and the previously affected cards get the extra color + * so now they have 2 extra colors and the commander get and additional color on top of that + * And finally I got the empty hand error #6738 on my turn for what I assume is the Painter's Servant + Grindstone combo I have, + * but nonetheless manage to tie the game so it go into a second game and the issue carry over, + * all the commanders have all the extra colors they gain from the first game + */ + + @Test + public void castCommanderWithoutAddedBlueColor() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + // As Painter's Servant enters the battlefield, choose a color. + // All cards that aren't on the battlefield, spells, and permanents are the chosen color in addition to their other colors. + addCard(Zone.HAND, playerA, "Painter's Servant", 1); // Artifact Creature {2} + + // Whenever a player casts a blue spell, you may gain 1 life. + addCard(Zone.BATTLEFIELD, playerA, "Kraken's Eye", 1); + + + // Exile target artifact or enchantment. + addCard(Zone.HAND, playerB, "Altar's Light", 1); // Instant {2}{W}{W} + addCard(Zone.BATTLEFIELD, playerB, "Plains", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Painter's Servant"); + setChoice(playerA, "Blue"); + + // When a player casts a spell or a creature attacks, exile Norin the Wary. + // Return it to the battlefield under its owner's control at the beginning of the next end step. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Norin the Wary"); + setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Altar's Light", "Painter's Servant", "Norin the Wary"); + setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Norin the Wary", 1); + assertAllCommandsUsed(); + + Permanent norin = getPermanent("Norin the Wary", playerA); + Assert.assertEquals(false, norin.getColor(currentGame).isBlue()); + Assert.assertEquals(true, norin.getColor(currentGame).isRed()); + + Permanent kraken = getPermanent("Kraken's Eye", playerA); + Assert.assertEquals(false, kraken.getColor(currentGame).isBlue()); + + assertLife(playerA, 42); + assertLife(playerB, 40); + + } +} diff --git a/Mage/src/main/java/mage/ApprovingObject.java b/Mage/src/main/java/mage/ApprovingObject.java index 3701880d715..9468aad1577 100644 --- a/Mage/src/main/java/mage/ApprovingObject.java +++ b/Mage/src/main/java/mage/ApprovingObject.java @@ -1,28 +1,28 @@ -package mage; - -import mage.abilities.Ability; -import mage.game.Game; - -/** - * - * @author LevelX2 - */ -public class ApprovingObject { - - private final Ability approvingAbility; - private final MageObjectReference approvingMageObjectReference; - - public ApprovingObject(Ability source, Game game) { - this.approvingAbility = source; - this.approvingMageObjectReference = new MageObjectReference(source.getSourceId(), game); - } - - public Ability getApprovingAbility() { - return approvingAbility; - } - - public MageObjectReference getApprovingMageObjectReference() { - return approvingMageObjectReference; - } - -} +package mage; + +import mage.abilities.Ability; +import mage.game.Game; + +/** + * + * @author LevelX2 + */ +public class ApprovingObject { + + private final Ability approvingAbility; + private final MageObjectReference approvingMageObjectReference; + + public ApprovingObject(Ability source, Game game) { + this.approvingAbility = source; + this.approvingMageObjectReference = new MageObjectReference(source.getSourceId(), game); + } + + public Ability getApprovingAbility() { + return approvingAbility; + } + + public MageObjectReference getApprovingMageObjectReference() { + return approvingMageObjectReference; + } + +} diff --git a/Mage/src/main/java/mage/abilities/common/AttackingCreaturePutIntoGraveyardTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/AttackingCreaturePutIntoGraveyardTriggeredAbility.java index 58f656fdd8f..8f83c2834cf 100644 --- a/Mage/src/main/java/mage/abilities/common/AttackingCreaturePutIntoGraveyardTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/AttackingCreaturePutIntoGraveyardTriggeredAbility.java @@ -1,124 +1,124 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package mage.abilities.common; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; -import mage.constants.Zone; -import mage.filter.FilterPermanent; -import mage.game.Game; -import mage.game.events.GameEvent; -import static mage.game.events.GameEvent.EventType.END_COMBAT_STEP_POST; -import static mage.game.events.GameEvent.EventType.REMOVED_FROM_COMBAT; -import static mage.game.events.GameEvent.EventType.ZONE_CHANGE; -import mage.game.events.ZoneChangeEvent; -import mage.game.permanent.Permanent; - -/** - * - * @author weirddan455 and jeffwadsworth - */ -public class AttackingCreaturePutIntoGraveyardTriggeredAbility extends TriggeredAbilityImpl { - - protected FilterPermanent filterPermanent; - private final boolean onlyToControllerGraveyard; - private final boolean itDies; - - public AttackingCreaturePutIntoGraveyardTriggeredAbility(Effect effect, FilterPermanent filterPermanent, Boolean onlyToControllerGraveyard, Boolean itDies, Boolean optional) { - super(Zone.BATTLEFIELD, effect, optional); - this.filterPermanent = filterPermanent; - this.onlyToControllerGraveyard = onlyToControllerGraveyard; - this.itDies = itDies; - } - - private AttackingCreaturePutIntoGraveyardTriggeredAbility(final AttackingCreaturePutIntoGraveyardTriggeredAbility ability) { - super(ability); - this.filterPermanent = ability.filterPermanent; - this.onlyToControllerGraveyard = ability.onlyToControllerGraveyard; - this.itDies = ability.itDies; - } - - @Override - public AttackingCreaturePutIntoGraveyardTriggeredAbility copy() { - return new AttackingCreaturePutIntoGraveyardTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - switch (event.getType()) { - case ATTACKER_DECLARED: - case END_COMBAT_STEP_POST: - case ZONE_CHANGE: - case REMOVED_FROM_COMBAT: - return true; - default: - return false; - } - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - switch (event.getType()) { - case ATTACKER_DECLARED: - Permanent permanent = game.getPermanent(event.getSourceId()); - if (permanent != null - && !filterPermanent.match(permanent, game)) { - return false; - } - List attackersList = new ArrayList<>(); - List attackersListCopy = (List) game.getState().getValue(this.getSourceId() + "Attackers"); - if (attackersListCopy == null) { - attackersListCopy = attackersList; - } - attackersListCopy.add(event.getSourceId()); // add the filtered creature to the list - game.getState().setValue(this.getSourceId() + "Attackers", attackersListCopy); - return false; - case END_COMBAT_STEP_POST: - game.getState().setValue(this.getSourceId() + "Attackers", null); - return false; - case ZONE_CHANGE: - ZoneChangeEvent zEvent = (ZoneChangeEvent) event; - if (zEvent.getFromZone() == Zone.BATTLEFIELD - && zEvent.getToZone() == Zone.GRAVEYARD) { - if (onlyToControllerGraveyard - && !this.isControlledBy(game.getOwnerId(zEvent.getTargetId()))) { - return false; - } - if (itDies - && !zEvent.isDiesEvent()) { - return false; - } - List attackers = (List) game.getState().getValue(this.getSourceId() + "Attackers"); - return attackers != null - && attackers.contains(zEvent.getTargetId()); - } - case REMOVED_FROM_COMBAT: - // a card removed from combat is no longer an attacker or blocker so remove it from the list - List attackersListRFC = (List) game.getState().getValue(this.getSourceId() + "Attackers"); - if (attackersListRFC != null - && attackersListRFC.contains(event.getTargetId())) { - attackersListRFC.remove(event.getTargetId()); - game.getState().setValue(this.getSourceId() + "Attackers", attackersListRFC); - } - - default: - return false; - } - } - - @Override - public String getTriggerPhrase() { - if (itDies) { - return "Whenever " + filterPermanent.getMessage() + " dies, "; - } - return "Whenever " + filterPermanent.getMessage() + " is put into " + (onlyToControllerGraveyard ? "your" : "a") - + " graveyard from the battlefield, "; - } - -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package mage.abilities.common; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import static mage.game.events.GameEvent.EventType.END_COMBAT_STEP_POST; +import static mage.game.events.GameEvent.EventType.REMOVED_FROM_COMBAT; +import static mage.game.events.GameEvent.EventType.ZONE_CHANGE; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; + +/** + * + * @author weirddan455 and jeffwadsworth + */ +public class AttackingCreaturePutIntoGraveyardTriggeredAbility extends TriggeredAbilityImpl { + + protected FilterPermanent filterPermanent; + private final boolean onlyToControllerGraveyard; + private final boolean itDies; + + public AttackingCreaturePutIntoGraveyardTriggeredAbility(Effect effect, FilterPermanent filterPermanent, Boolean onlyToControllerGraveyard, Boolean itDies, Boolean optional) { + super(Zone.BATTLEFIELD, effect, optional); + this.filterPermanent = filterPermanent; + this.onlyToControllerGraveyard = onlyToControllerGraveyard; + this.itDies = itDies; + } + + private AttackingCreaturePutIntoGraveyardTriggeredAbility(final AttackingCreaturePutIntoGraveyardTriggeredAbility ability) { + super(ability); + this.filterPermanent = ability.filterPermanent; + this.onlyToControllerGraveyard = ability.onlyToControllerGraveyard; + this.itDies = ability.itDies; + } + + @Override + public AttackingCreaturePutIntoGraveyardTriggeredAbility copy() { + return new AttackingCreaturePutIntoGraveyardTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + switch (event.getType()) { + case ATTACKER_DECLARED: + case END_COMBAT_STEP_POST: + case ZONE_CHANGE: + case REMOVED_FROM_COMBAT: + return true; + default: + return false; + } + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + switch (event.getType()) { + case ATTACKER_DECLARED: + Permanent permanent = game.getPermanent(event.getSourceId()); + if (permanent != null + && !filterPermanent.match(permanent, game)) { + return false; + } + List attackersList = new ArrayList<>(); + List attackersListCopy = (List) game.getState().getValue(this.getSourceId() + "Attackers"); + if (attackersListCopy == null) { + attackersListCopy = attackersList; + } + attackersListCopy.add(event.getSourceId()); // add the filtered creature to the list + game.getState().setValue(this.getSourceId() + "Attackers", attackersListCopy); + return false; + case END_COMBAT_STEP_POST: + game.getState().setValue(this.getSourceId() + "Attackers", null); + return false; + case ZONE_CHANGE: + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (zEvent.getFromZone() == Zone.BATTLEFIELD + && zEvent.getToZone() == Zone.GRAVEYARD) { + if (onlyToControllerGraveyard + && !this.isControlledBy(game.getOwnerId(zEvent.getTargetId()))) { + return false; + } + if (itDies + && !zEvent.isDiesEvent()) { + return false; + } + List attackers = (List) game.getState().getValue(this.getSourceId() + "Attackers"); + return attackers != null + && attackers.contains(zEvent.getTargetId()); + } + case REMOVED_FROM_COMBAT: + // a card removed from combat is no longer an attacker or blocker so remove it from the list + List attackersListRFC = (List) game.getState().getValue(this.getSourceId() + "Attackers"); + if (attackersListRFC != null + && attackersListRFC.contains(event.getTargetId())) { + attackersListRFC.remove(event.getTargetId()); + game.getState().setValue(this.getSourceId() + "Attackers", attackersListRFC); + } + + default: + return false; + } + } + + @Override + public String getTriggerPhrase() { + if (itDies) { + return "Whenever " + filterPermanent.getMessage() + " dies, "; + } + return "Whenever " + filterPermanent.getMessage() + " is put into " + (onlyToControllerGraveyard ? "your" : "a") + + " graveyard from the battlefield, "; + } + +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/DidNotAttackThisTurnEnchantedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/DidNotAttackThisTurnEnchantedCondition.java index 80fc43a4527..a45b5d5eaf7 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/DidNotAttackThisTurnEnchantedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/DidNotAttackThisTurnEnchantedCondition.java @@ -1,31 +1,31 @@ -package mage.abilities.condition.common; - -import mage.MageObjectReference; -import mage.abilities.Ability; -import mage.abilities.condition.Condition; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.watchers.common.AttackedThisTurnWatcher; - -/** - * - * @author jeffwadsworth - */ -public enum DidNotAttackThisTurnEnchantedCondition implements Condition { - - instance; - - @Override - public boolean apply(Game game, Ability source) { - Permanent auraPermanent = game.getPermanentOrLKIBattlefield(source.getSourceId()); - if (auraPermanent != null) { - Permanent enchantedPermanent = game.getPermanent(auraPermanent.getAttachedTo()); - AttackedThisTurnWatcher watcher = game.getState().getWatcher(AttackedThisTurnWatcher.class); - return enchantedPermanent != null - && watcher != null - && !watcher.getAttackedThisTurnCreatures().contains( - new MageObjectReference(enchantedPermanent, game)); - } - return false; - } -} +package mage.abilities.condition.common; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.watchers.common.AttackedThisTurnWatcher; + +/** + * + * @author jeffwadsworth + */ +public enum DidNotAttackThisTurnEnchantedCondition implements Condition { + + instance; + + @Override + public boolean apply(Game game, Ability source) { + Permanent auraPermanent = game.getPermanentOrLKIBattlefield(source.getSourceId()); + if (auraPermanent != null) { + Permanent enchantedPermanent = game.getPermanent(auraPermanent.getAttachedTo()); + AttackedThisTurnWatcher watcher = game.getState().getWatcher(AttackedThisTurnWatcher.class); + return enchantedPermanent != null + && watcher != null + && !watcher.getAttackedThisTurnCreatures().contains( + new MageObjectReference(enchantedPermanent, game)); + } + return false; + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerLifeDividedValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerLifeDividedValue.java index d6a3138440b..351dcea28bf 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerLifeDividedValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerLifeDividedValue.java @@ -1,48 +1,48 @@ -package mage.abilities.dynamicvalue.common; - -import mage.abilities.Ability; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.effects.Effect; -import mage.game.Game; -import mage.players.Player; - -/** - * - * @author LevelX2 - */ -public class ControllerLifeDividedValue implements DynamicValue { - - private final Integer divider; - - public ControllerLifeDividedValue(Integer divider) { - this.divider = divider; - } - - public ControllerLifeDividedValue(final ControllerLifeDividedValue dynamicValue) { - this.divider = dynamicValue.divider; - } - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - Player p = game.getPlayer(sourceAbility.getControllerId()); - if (p != null) { - return p.getLife() / divider; - } - return 0; - } - - @Override - public ControllerLifeDividedValue copy() { - return new ControllerLifeDividedValue(this); - } - - @Override - public String toString() { - return "X"; - } - - @Override - public String getMessage() { - return ""; - } -} +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.game.Game; +import mage.players.Player; + +/** + * + * @author LevelX2 + */ +public class ControllerLifeDividedValue implements DynamicValue { + + private final Integer divider; + + public ControllerLifeDividedValue(Integer divider) { + this.divider = divider; + } + + public ControllerLifeDividedValue(final ControllerLifeDividedValue dynamicValue) { + this.divider = dynamicValue.divider; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + Player p = game.getPlayer(sourceAbility.getControllerId()); + if (p != null) { + return p.getLife() / divider; + } + return 0; + } + + @Override + public ControllerLifeDividedValue copy() { + return new ControllerLifeDividedValue(this); + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return ""; + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/HighestCMCOfPermanentValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/HighestCMCOfPermanentValue.java index 338e4b9b51f..65bac591598 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/HighestCMCOfPermanentValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/HighestCMCOfPermanentValue.java @@ -1,62 +1,62 @@ -package mage.abilities.dynamicvalue.common; - -import mage.abilities.Ability; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.effects.Effect; -import mage.filter.FilterPermanent; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.players.Player; - -/** - * - * @author LevelX2 - */ -public class HighestCMCOfPermanentValue implements DynamicValue { - - private final FilterPermanent filter; - private final boolean onlyIfCanBeSacrificed; - - public HighestCMCOfPermanentValue(FilterPermanent filter, boolean onlyIfCanBeSacrificed) { - super(); - this.filter = filter; - this.onlyIfCanBeSacrificed = onlyIfCanBeSacrificed; - } - - public HighestCMCOfPermanentValue(final HighestCMCOfPermanentValue dynamicValue) { - this.filter = dynamicValue.filter; - this.onlyIfCanBeSacrificed = dynamicValue.onlyIfCanBeSacrificed; - } - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - int value = 0; - Player controller = game.getPlayer(sourceAbility.getControllerId()); - if (controller != null) { - for (Permanent permanent : game.getBattlefield() - .getActivePermanents(filter, sourceAbility.getControllerId(), sourceAbility.getSourceId(), game)) { - if ((!onlyIfCanBeSacrificed || controller.canPaySacrificeCost(permanent, sourceAbility, sourceAbility.getControllerId(), game)) - && permanent.getManaValue() > value) { - value = permanent.getManaValue(); - } - - } - } - return value; - } - - @Override - public HighestCMCOfPermanentValue copy() { - return new HighestCMCOfPermanentValue(this); - } - - @Override - public String toString() { - return "X"; - } - - @Override - public String getMessage() { - return filter.getMessage(); - } -} +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +/** + * + * @author LevelX2 + */ +public class HighestCMCOfPermanentValue implements DynamicValue { + + private final FilterPermanent filter; + private final boolean onlyIfCanBeSacrificed; + + public HighestCMCOfPermanentValue(FilterPermanent filter, boolean onlyIfCanBeSacrificed) { + super(); + this.filter = filter; + this.onlyIfCanBeSacrificed = onlyIfCanBeSacrificed; + } + + public HighestCMCOfPermanentValue(final HighestCMCOfPermanentValue dynamicValue) { + this.filter = dynamicValue.filter; + this.onlyIfCanBeSacrificed = dynamicValue.onlyIfCanBeSacrificed; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + int value = 0; + Player controller = game.getPlayer(sourceAbility.getControllerId()); + if (controller != null) { + for (Permanent permanent : game.getBattlefield() + .getActivePermanents(filter, sourceAbility.getControllerId(), sourceAbility.getSourceId(), game)) { + if ((!onlyIfCanBeSacrificed || controller.canPaySacrificeCost(permanent, sourceAbility, sourceAbility.getControllerId(), game)) + && permanent.getManaValue() > value) { + value = permanent.getManaValue(); + } + + } + } + return value; + } + + @Override + public HighestCMCOfPermanentValue copy() { + return new HighestCMCOfPermanentValue(this); + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return filter.getMessage(); + } +} diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index d6c8c2a37b5..6a2eb625d3b 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1,5084 +1,5084 @@ -package mage.players; - -import com.google.common.collect.ImmutableMap; -import mage.*; -import mage.abilities.*; -import mage.abilities.ActivatedAbility.ActivationStatus; -import mage.abilities.common.PassAbility; -import mage.abilities.common.PlayLandAsCommanderAbility; -import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; -import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; -import mage.abilities.costs.*; -import mage.abilities.costs.mana.AlternateManaPaymentAbility; -import mage.abilities.costs.mana.ManaCost; -import mage.abilities.costs.mana.ManaCosts; -import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.RestrictionEffect; -import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; -import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; -import mage.abilities.keyword.*; -import mage.abilities.mana.ActivatedManaAbilityImpl; -import mage.abilities.mana.ManaOptions; -import mage.actions.MageDrawAction; -import mage.cards.*; -import mage.cards.decks.Deck; -import mage.choices.Choice; -import mage.choices.ChoiceImpl; -import mage.constants.*; -import mage.counters.Counter; -import mage.counters.CounterType; -import mage.counters.Counters; -import mage.designations.Designation; -import mage.designations.DesignationType; -import mage.filter.FilterCard; -import mage.filter.FilterMana; -import mage.filter.FilterPermanent; -import mage.filter.StaticFilters; -import mage.filter.common.FilterControlledPermanent; -import mage.filter.common.FilterCreatureForCombat; -import mage.filter.common.FilterCreatureForCombatBlock; -import mage.filter.predicate.Predicates; -import mage.filter.predicate.permanent.PermanentIdPredicate; -import mage.game.*; -import mage.game.combat.CombatGroup; -import mage.game.command.CommandObject; -import mage.game.events.*; -import mage.game.match.MatchPlayer; -import mage.game.permanent.Permanent; -import mage.game.permanent.PermanentCard; -import mage.game.permanent.PermanentToken; -import mage.game.permanent.token.SquirrelToken; -import mage.game.stack.Spell; -import mage.game.stack.StackAbility; -import mage.game.stack.StackObject; -import mage.game.turn.Step; -import mage.players.net.UserData; -import mage.target.Target; -import mage.target.TargetAmount; -import mage.target.TargetCard; -import mage.target.TargetPermanent; -import mage.target.common.TargetCardInLibrary; -import mage.target.common.TargetDiscard; -import mage.util.CardUtil; -import mage.util.GameLog; -import mage.util.RandomUtil; -import org.apache.log4j.Logger; - -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -public abstract class PlayerImpl implements Player, Serializable { - - private static final Logger logger = Logger.getLogger(PlayerImpl.class); - - /** - * Used to cancel waiting requests send to the player - */ - protected boolean abort; - - protected final UUID playerId; - protected String name; - protected boolean human; - protected int life; - protected boolean wins; - protected boolean draws; - protected boolean loses; - protected Library library; - protected Cards sideboard; - protected Cards hand; - protected Graveyard graveyard; - protected Set commandersIds = new HashSet<>(0); - protected Abilities abilities; - protected Counters counters; - protected int landsPlayed; - protected int landsPerTurn = 1; - protected int loyaltyUsePerTurn = 1; - protected int maxHandSize = 7; - protected int maxAttackedBy = Integer.MAX_VALUE; - protected ManaPool manaPool; - // priority control - protected boolean passed; // player passed priority - protected boolean passedTurn; // F4 - protected boolean passedTurnSkipStack; // F6 // TODO: research - protected boolean passedUntilEndOfTurn; // F5 - protected boolean passedUntilNextMain; // F7 - protected boolean passedUntilStackResolved; // F10 - protected Date dateLastAddedToStack; - protected boolean passedUntilEndStepBeforeMyTurn; // F11 - protected boolean skippedAtLeastOnce; // used to track if passed started in specific phase - /** - * This indicates that player passed all turns until their own turn starts - * (F9). Note! This differs from passedTurn as it doesn't care about spells - * and abilities in the stack and will pass them as well. - */ - protected boolean passedAllTurns; // F9 - protected AbilityType justActivatedType; // used to check if priority can be passed automatically - - protected int turns; - protected int storedBookmark = -1; - protected int priorityTimeLeft = Integer.MAX_VALUE; - - // conceded or connection lost game - protected boolean left; - // set if the player quits the complete match - protected boolean quit; - // set if the player lost match because of priority timeout - protected boolean timerTimeout; - // set if the player lost match because of idle timeout - protected boolean idleTimeout; - - protected RangeOfInfluence range; - protected Set inRange = new HashSet<>(); // players list in current range of influence (updates each turn) - - protected boolean isTestMode = false; - protected boolean canGainLife = true; - protected boolean canLoseLife = true; - protected boolean canPayLifeCost = true; - protected boolean loseByZeroOrLessLife = true; - protected boolean canPlayCardsFromGraveyard = true; - protected boolean drawsOnOpponentsTurn = false; - - protected FilterPermanent sacrificeCostFilter; - - protected final List alternativeSourceCosts = new ArrayList<>(); - - protected boolean isGameUnderControl = true; - protected UUID turnController; - protected List turnControllers = new ArrayList<>(); - protected Set playersUnderYourControl = new HashSet<>(); - - protected Set usersAllowedToSeeHandCards = new HashSet<>(); - - protected List attachments = new ArrayList<>(); - - protected boolean topCardRevealed = false; - - // 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn - // or until a specific point in that turn will last until that turn would have begun. - // They neither expire immediately nor last indefinitely. - protected boolean reachedNextTurnAfterLeaving = false; - - // indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs) - // support multiple cards with alternative mana cost - protected Set castSourceIdWithAlternateMana = new HashSet<>(); - protected Map> castSourceIdManaCosts = new HashMap<>(); - protected Map> castSourceIdCosts = new HashMap<>(); - - // indicates that the player is in mana payment phase - protected boolean payManaMode = false; - - protected UserData userData; - protected MatchPlayer matchPlayer; - - protected List designations = new ArrayList<>(); - - // mana colors the player can handle like Phyrexian mana - protected FilterMana phyrexianColors; - - // Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy) - protected final List> availableTriggeredManaList = new ArrayList<>(); - - /** - * During some steps we can't play anything - */ - protected final Map silentPhaseSteps = ImmutableMap.builder(). - put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build(); - - public PlayerImpl(String name, RangeOfInfluence range) { - this(UUID.randomUUID()); - this.name = name; - this.range = range; - hand = new CardsImpl(); - graveyard = new Graveyard(); - abilities = new AbilitiesImpl<>(); - counters = new Counters(); - manaPool = new ManaPool(playerId); - library = new Library(playerId); - sideboard = new CardsImpl(); - phyrexianColors = null; - } - - protected PlayerImpl(UUID id) { - this.playerId = id; - } - - public PlayerImpl(final PlayerImpl player) { - this.abort = player.abort; - this.playerId = player.playerId; - - this.name = player.name; - this.human = player.human; - this.life = player.life; - this.wins = player.wins; - this.draws = player.draws; - this.loses = player.loses; - - this.library = player.library.copy(); - this.sideboard = player.sideboard.copy(); - this.hand = player.hand.copy(); - this.graveyard = player.graveyard.copy(); - this.commandersIds = player.commandersIds; - this.abilities = player.abilities.copy(); - this.counters = player.counters.copy(); - - this.landsPlayed = player.landsPlayed; - this.landsPerTurn = player.landsPerTurn; - this.loyaltyUsePerTurn = player.loyaltyUsePerTurn; - this.maxHandSize = player.maxHandSize; - this.maxAttackedBy = player.maxAttackedBy; - this.manaPool = player.manaPool.copy(); - this.turns = player.turns; - - this.left = player.left; - this.quit = player.quit; - this.timerTimeout = player.timerTimeout; - this.idleTimeout = player.idleTimeout; - this.range = player.range; - this.canGainLife = player.canGainLife; - this.canLoseLife = player.canLoseLife; - this.loseByZeroOrLessLife = player.loseByZeroOrLessLife; - this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard; - this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn; - - this.attachments.addAll(player.attachments); - - this.inRange.addAll(player.inRange); - this.userData = player.userData; - this.matchPlayer = player.matchPlayer; - - this.canPayLifeCost = player.canPayLifeCost; - this.sacrificeCostFilter = player.sacrificeCostFilter; - this.alternativeSourceCosts.addAll(player.alternativeSourceCosts); - this.storedBookmark = player.storedBookmark; - - this.topCardRevealed = player.topCardRevealed; - this.playersUnderYourControl.addAll(player.playersUnderYourControl); - this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); - - this.isTestMode = player.isTestMode; - this.isGameUnderControl = player.isGameUnderControl; - - this.turnController = player.turnController; - this.turnControllers.addAll(player.turnControllers); - - this.passed = player.passed; - this.passedTurn = player.passedTurn; - this.passedTurnSkipStack = player.passedTurnSkipStack; - this.passedUntilEndOfTurn = player.passedUntilEndOfTurn; - this.passedUntilNextMain = player.passedUntilNextMain; - this.passedUntilStackResolved = player.passedUntilStackResolved; - this.dateLastAddedToStack = player.dateLastAddedToStack; - this.passedUntilEndStepBeforeMyTurn = player.passedUntilEndStepBeforeMyTurn; - this.skippedAtLeastOnce = player.skippedAtLeastOnce; - this.passedAllTurns = player.passedAllTurns; - this.justActivatedType = player.justActivatedType; - - this.priorityTimeLeft = player.getPriorityTimeLeft(); - this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; - - this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); - for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { - this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { - this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - this.payManaMode = player.payManaMode; - this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; - for (Designation object : player.designations) { - this.designations.add(object.copy()); - } - } - - @Override - public void restore(Player player) { - this.name = player.getName(); - this.human = player.isHuman(); - this.life = player.getLife(); - - this.passed = player.isPassed(); - - // Don't restore more global states. If restored they are probably cause for unintended draws (https://github.com/magefree/mage/issues/1205). -// this.wins = player.hasWon(); -// this.loses = player.hasLost(); -// this.left = player.hasLeft(); -// this.quit = player.hasQuit(); - // Makes no sense to restore -// this.priorityTimeLeft = player.getPriorityTimeLeft(); -// this.idleTimeout = player.hasIdleTimeout(); -// this.timerTimeout = player.hasTimerTimeout(); - // can't change so no need to restore -// this.isTestMode = player.isTestMode(); - // This is meta data and should'nt be restored by rollback -// this.userData = player.getUserData(); - this.library = player.getLibrary().copy(); - this.sideboard = player.getSideboard().copy(); - this.hand = player.getHand().copy(); - this.graveyard = player.getGraveyard().copy(); - - //noinspection deprecation - it's ok to use it in inner methods - this.commandersIds = new HashSet<>(player.getCommandersIds()); - - this.abilities = player.getAbilities().copy(); - this.counters = player.getCounters().copy(); - - this.landsPlayed = player.getLandsPlayed(); - this.landsPerTurn = player.getLandsPerTurn(); - this.loyaltyUsePerTurn = player.getLoyaltyUsePerTurn(); - this.maxHandSize = player.getMaxHandSize(); - this.maxAttackedBy = player.getMaxAttackedBy(); - this.manaPool = player.getManaPool().copy(); - // Restore user specific settings in case changed since state save - this.manaPool.setAutoPayment(this.getUserData().isManaPoolAutomatic()); - this.manaPool.setAutoPaymentRestricted(this.getUserData().isManaPoolAutomaticRestricted()); - - this.turns = player.getTurns(); - - this.range = player.getRange(); - this.canGainLife = player.isCanGainLife(); - this.canLoseLife = player.isCanLoseLife(); - this.attachments.clear(); - this.attachments.addAll(player.getAttachments()); - - this.inRange.clear(); - this.inRange.addAll(player.getInRange()); - this.canPayLifeCost = player.getCanPayLifeCost(); - this.sacrificeCostFilter = player.getSacrificeCostFilter() != null - ? player.getSacrificeCostFilter().copy() : null; - this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); - this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard(); - this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn(); - this.alternativeSourceCosts.clear(); - this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); - - this.topCardRevealed = player.isTopCardRevealed(); - this.playersUnderYourControl.clear(); - this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl()); - this.isGameUnderControl = player.isGameUnderControl(); - - this.turnController = player.getTurnControlledBy(); - this.turnControllers.clear(); - this.turnControllers.addAll(player.getTurnControllers()); - this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); - - this.clearCastSourceIdManaCosts(); - this.castSourceIdWithAlternateMana.clear(); - this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); - for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { - this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { - this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - - this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null; - - this.designations.clear(); - for (Designation object : player.getDesignations()) { - this.designations.add(object.copy()); - } - - // Don't restore! - // this.storedBookmark - // this.usersAllowedToSeeHandCards - } - - @Override - public void useDeck(Deck deck, Game game) { - library.clear(); - library.addAll(deck.getCards(), game); - sideboard.clear(); - for (Card card : deck.getSideboard()) { - sideboard.add(card); - } - } - - /** - * Cast e.g. from Karn Liberated to restart the current game - * - * @param game - */ - @Override - public void init(Game game) { - init(game, false); - } - - @Override - public void init(Game game, boolean testMode) { - this.abort = false; - if (!testMode) { - this.hand.clear(); - this.graveyard.clear(); - } - this.library.reset(); - this.abilities.clear(); - this.counters.clear(); - this.wins = false; - this.draws = false; - this.loses = false; - this.left = false; - // reset is necessary because in tournament player will be used for each round - this.quit = false; - this.timerTimeout = false; - this.idleTimeout = false; - - this.turns = 0; - this.isGameUnderControl = true; - this.turnController = this.getId(); - this.turnControllers.clear(); - this.playersUnderYourControl.clear(); - - this.passed = false; - this.passedTurn = false; - this.passedTurnSkipStack = false; - this.passedUntilEndOfTurn = false; - this.passedUntilNextMain = false; - this.passedUntilStackResolved = false; - this.dateLastAddedToStack = null; - this.passedUntilEndStepBeforeMyTurn = false; - this.skippedAtLeastOnce = false; - this.passedAllTurns = false; - this.justActivatedType = null; - - this.canGainLife = true; - this.canLoseLife = true; - this.topCardRevealed = false; - this.payManaMode = false; - this.setLife(game.getStartingLife(), game, null); - this.setReachedNextTurnAfterLeaving(false); - - this.clearCastSourceIdManaCosts(); - - this.getManaPool().init(); // needed to remove mana that not empties on step change from previous game if left - this.phyrexianColors = null; - - this.designations.clear(); - } - - /** - * called before apply effects - */ - @Override - public void reset() { - this.abilities.clear(); - this.landsPerTurn = 1; - this.loyaltyUsePerTurn = 1; - this.maxHandSize = 7; - this.maxAttackedBy = Integer.MAX_VALUE; - this.canGainLife = true; - this.canLoseLife = true; - this.canPayLifeCost = true; - this.sacrificeCostFilter = null; - this.loseByZeroOrLessLife = true; - this.canPlayCardsFromGraveyard = false; - this.drawsOnOpponentsTurn = false; - this.topCardRevealed = false; - this.alternativeSourceCosts.clear(); - this.clearCastSourceIdManaCosts(); - this.getManaPool().clearEmptyManaPoolRules(); - this.phyrexianColors = null; - } - - @Override - public Counters getCounters() { - return counters; - } - - @Override - public void beginTurn(Game game) { - this.landsPlayed = 0; - updateRange(game); - } - - @Override - public RangeOfInfluence getRange() { - return range; - } - - @Override - public void updateRange(Game game) { - // 20100423 - 801.2c - // 801.2c The particular players within each player’s range of influence are determined as each turn begins. - // BUT it also uses before game start to fill game and card data in starting game events - inRange.clear(); - inRange.add(this.playerId); - inRange.addAll(getAllNearPlayers(game, true)); - inRange.addAll(getAllNearPlayers(game, false)); - } - - private Set getAllNearPlayers(Game game, boolean needPrevious) { - // find all near players (search from current player position) - Set foundedList = new HashSet<>(); - PlayerList players = game.getState().getPlayerList(this.playerId); - int needAmount = this.getRange().getRange(); // distance to search (0 - ALL range) - int foundedAmount = 0; - while (needAmount == 0 || foundedAmount < needAmount) { - Player foundedPlayer = needPrevious ? players.getPrevious(game) : players.getNext(game, false); - - // PlayerList is inifine, so stops on repeats - if (foundedPlayer == null || foundedPlayer.getId().equals(this.playerId) || foundedList.contains(foundedPlayer.getId())) { - break; - } - // skip leaved player (no needs cause next/previous code already checks it) - - foundedList.add(foundedPlayer.getId()); - foundedAmount++; - } - return foundedList; - } - - @Override - public Set getInRange() { - if (inRange.isEmpty()) { - // runtime check: inRange filled on beginTurn, but unit tests adds cards by cheat engine before game starting, - // so inRange will be empty and some ETB effects can be broken (example: Spark Double puts direct to battlefield). - // Cheat engine already have a workaround, so that error must not be visible in normal situation. - throw new IllegalStateException("Wrong code usage (game is not started, but you call getInRange in some effects)."); - } - - return inRange; - } - - @Override - public Set getPlayersUnderYourControl() { - return this.playersUnderYourControl; - } - - @Override - public void controlPlayersTurn(Game game, UUID playerId) { - Player player = game.getPlayer(playerId); - player.setTurnControlledBy(this.getId()); - game.informPlayers(getLogName() + " controls the turn of " + player.getLogName()); - if (!playerId.equals(this.getId())) { - this.playersUnderYourControl.add(playerId); - if (!player.hasLeft() && !player.hasLost()) { - player.setGameUnderYourControl(false); - } - DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility( - new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), player.getLogName())); - ability.setSourceId(getId()); - ability.setControllerId(getId()); - game.addDelayedTriggeredAbility(ability, null); - } - } - - @Override - public void setTurnControlledBy(UUID playerId) { - this.turnController = playerId; - this.turnControllers.add(playerId); - } - - @Override - public List getTurnControllers() { - return this.turnControllers; - } - - @Override - public UUID getTurnControlledBy() { - return this.turnController; - } - - @Override - public void resetOtherTurnsControlled() { - playersUnderYourControl.clear(); - } - - /** - * returns true if the player has the control itself - false if the player - * is controlled by another player - * - * @return - */ - @Override - public boolean isGameUnderControl() { - return isGameUnderControl; - } - - @Override - public void setGameUnderYourControl(boolean value) { - setGameUnderYourControl(value, true); - } - - @Override - public void setGameUnderYourControl(boolean value, boolean fullRestore) { - this.isGameUnderControl = value; - if (isGameUnderControl) { - if (fullRestore) { - this.turnControllers.clear(); - this.turnController = getId(); - } else { - if (turnControllers.size() > 0) { - this.turnControllers.remove(turnControllers.size() - 1); - } - if (turnControllers.isEmpty()) { - this.turnController = getId(); - } else { - this.turnController = turnControllers.get(turnControllers.size() - 1); - isGameUnderControl = false; - } - } - } - } - - @Override - public void endOfTurn(Game game) { - this.passedTurn = false; - this.passedTurnSkipStack = false; - } - - @Override - public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) { - if (this.hasLost() || this.hasLeft()) { - return false; - } - if (source != null) { - // there is only variant of shroud, so check the instance and any asthougheffects that would ignore it - if (abilities.containsKey(ShroudAbility.getInstance().getId()) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game) == null) { - return false; - } - // check for all variants of hexproof and any asthougheffects that would ignore it - // TODO there may be "prevented by rule-modification" effects, so add them if known - for (Ability a : abilities) { - if (a instanceof HexproofBaseAbility - && ((HexproofBaseAbility) a).checkObject(source, game) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { - return false; - } - } - return !hasProtectionFrom(source, game); - } - return true; - } - - @Override - public boolean hasProtectionFrom(MageObject source, Game game) { - for (ProtectionAbility ability : abilities.getProtectionAbilities()) { - if (!ability.canTarget(source, game)) { - return true; - } - } - return false; - } - - @Override - public int drawCards(int num, Ability source, Game game) { - if (num > 0) { - return game.doAction(source, new MageDrawAction(this, num, null)); - } - return 0; - } - - @Override - public int drawCards(int num, Ability source, Game game, GameEvent event) { - return game.doAction(source, new MageDrawAction(this, num, event)); - } - - @Override - public void discardToMax(Game game) { - if (hand.size() > this.maxHandSize) { - if (!game.isSimulation()) { - game.informPlayers(getLogName() + " discards down to " - + this.maxHandSize - + (this.maxHandSize == 1 - ? " hand card" : " hand cards")); - } - discard(hand.size() - this.maxHandSize, false, false, null, game); - } - } - - /** - * Don't use this in normal card code, it's for more internal use. Always - * use the [Player].moveCards methods if possible for card movement of card - * code. - * - * @param card - * @param game - * @return - */ - @Override - public boolean putInHand(Card card, Game game) { - if (card.isOwnedBy(playerId)) { - card.setZone(Zone.HAND, game); - this.hand.add(card); - } else { - return game.getPlayer(card.getOwnerId()).putInHand(card, game); - } - return true; - } - - @Override - public boolean removeFromHand(Card card, Game game) { - return hand.remove(card.getId()); - } - - @Override - public boolean removeFromLibrary(Card card, Game game) { - if (card == null) { - return false; - } - library.remove(card.getId(), game); - // must return true all the time (some cards can be removed directly from library, see getLibrary().removeFromTop) - // TODO: replace removeFromTop logic to normal with moveToZone - return true; - } - - @Override - public Card discardOne(boolean random, boolean payForCost, Ability source, Game game) { - return discard(1, random, payForCost, source, game).getRandom(game); - } - - @Override - public Cards discard(int amount, boolean random, boolean payForCost, Ability source, Game game) { - if (random) { - return discard(getRandomToDiscard(amount, source, game), payForCost, source, game); - } - return discard(amount, amount, payForCost, source, game); - } - - @Override - public Cards discard(int minAmount, int maxAmount, boolean payForCost, Ability source, Game game) { - return discard(getToDiscard(minAmount, maxAmount, source, game), payForCost, source, game); - } - - @Override - public Cards discard(Cards cards, boolean payForCost, Ability source, Game game) { - Cards discardedCards = new CardsImpl(); - if (cards == null) { - return discardedCards; - } - for (Card card : cards.getCards(game)) { - if (doDiscard(card, source, game, payForCost, false)) { - discardedCards.add(card); - } - } - if (!discardedCards.isEmpty()) { - game.fireEvent(new DiscardedCardsEvent(source, playerId, discardedCards.size(), discardedCards)); - } - return discardedCards; - } - - @Override - public boolean discard(Card card, boolean payForCost, Ability source, Game game) { - return doDiscard(card, source, game, payForCost, true); - } - - private Cards getToDiscard(int minAmount, int maxAmount, Ability source, Game game) { - Cards toDiscard = new CardsImpl(); - if (minAmount > maxAmount) { - return getToDiscard(maxAmount, minAmount, source, game); - } - if (maxAmount < 1) { - return toDiscard; - } - if (getHand().size() <= minAmount) { - toDiscard.addAll(getHand()); - return toDiscard; - } - TargetDiscard target = new TargetDiscard(minAmount, maxAmount, StaticFilters.FILTER_CARD, getId()); - choose(Outcome.Discard, target, source != null ? source.getSourceId() : null, game); - toDiscard.addAll(target.getTargets()); - return toDiscard; - } - - private Cards getRandomToDiscard(int amount, Ability source, Game game) { - Cards toDiscard = new CardsImpl(); - Cards hand = getHand().copy(); - for (int i = 0; i < amount; i++) { - if (hand.isEmpty()) { - break; - } - Card card = hand.getRandom(game); - hand.remove(card); - toDiscard.add(card); - } - return toDiscard; - } - - private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) { - //20100716 - 701.7 - /* 701.7. Discard # - 701.7a To discard a card, move it from its owner’s hand to that player’s graveyard. - 701.7b By default, effects that cause a player to discard a card allow the affected - player to choose which card to discard. Some effects, however, require a random - discard or allow another player to choose which card is discarded. - 701.7c If a card is discarded, but an effect causes it to be put into a hidden zone - instead of into its owner’s graveyard without being revealed, all values of that - card’s characteristics are considered to be undefined. - TODO: - If a card is discarded this way to pay a cost that specifies a characteristic - about the discarded card, that cost payment is illegal; the game returns to - the moment before the cost was paid (see rule 717, "Handling Illegal Actions"). - */ - if (card == null) { - return false; - } - GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, card.getId(), source, playerId); - gameEvent.setFlag(!payForCost); // event from effect (1) or from cost (0) - if (game.replaceEvent(gameEvent, source)) { - return false; - } - // write info to game log first so game log infos from triggered or replacement effects follow in the game log - if (!game.isSimulation()) { - game.informPlayers(getLogName() + " discards " + card.getLogName() + CardUtil.getSourceLogName(game, source)); - } - /* If a card is discarded while Rest in Peace is on the battlefield, abilities that function - * when a card is discarded (such as madness) still work, even though that card never reaches - * a graveyard. In addition, spells or abilities that check the characteristics of a discarded - * card (such as Chandra Ablaze's first ability) can find that card in exile. */ - card.moveToZone(Zone.GRAVEYARD, source, game, false); - // So discard is also successful if card is moved to another zone by replacement effect! - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DISCARDED_CARD, card.getId(), source, playerId)); - - if (fireFinalEvent) { - game.fireEvent(new DiscardedCardsEvent(source, playerId, 1, new CardsImpl(card))); - } - return true; - } - - @Override - public List getAttachments() { - return attachments; - } - - @Override - public boolean addAttachment(UUID permanentId, Ability source, Game game) { - if (!this.attachments.contains(permanentId)) { - Permanent aura = game.getPermanent(permanentId); - if (aura == null) { - aura = game.getPermanentEntering(permanentId); - } - if (aura != null) { - if (!game.replaceEvent(new EnchantPlayerEvent(playerId, aura, source))) { - this.attachments.add(permanentId); - aura.attachTo(playerId, source, game); - game.fireEvent(new EnchantedPlayerEvent(playerId, aura, source)); - return true; - } - } - } - return false; - } - - @Override - public boolean removeAttachment(Permanent attachment, Ability source, Game game) { - if (this.attachments.contains(attachment.getId())) { - if (!game.replaceEvent(new UnattachEvent(playerId, attachment.getId(), attachment, source))) { - this.attachments.remove(attachment.getId()); - attachment.attachTo(null, source, game); - game.fireEvent(new UnattachedEvent(playerId, attachment.getId(), attachment, source)); - return true; - } - } - return false; - } - - @Override - public boolean removeFromBattlefield(Permanent permanent, Ability source, Game game) { - permanent.removeFromCombat(game, false); - game.getBattlefield().removePermanent(permanent.getId()); - if (permanent.getAttachedTo() != null) { - Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); - if (attachedTo != null) { - attachedTo.removeAttachment(permanent.getId(), source, game); - } else { - Player attachedToPlayer = game.getPlayer(permanent.getAttachedTo()); - if (attachedToPlayer != null) { - attachedToPlayer.removeAttachment(permanent, source, game); - } else { - Card attachedToCard = game.getCard(permanent.getAttachedTo()); - if (attachedToCard != null) { - attachedToCard.removeAttachment(permanent.getId(), source, game); - } - } - } - - } - if (permanent.getPairedCard() != null) { - Permanent pairedCard = permanent.getPairedCard().getPermanent(game); - if (pairedCard != null) { - pairedCard.clearPairedCard(); - } - } - if (permanent.getBandedCards() != null && !permanent.getBandedCards().isEmpty()) { - for (UUID bandedId : permanent.getBandedCards()) { - Permanent banded = game.getPermanent(bandedId); - if (banded != null) { - banded.removeBandedCard(permanent.getId()); - } - } - } - return true; - } - - @Override - public boolean putInGraveyard(Card card, Game game) { - if (card.isOwnedBy(playerId)) { - this.graveyard.add(card); - } else { - return game.getPlayer(card.getOwnerId()).putInGraveyard(card, game); - } - return true; - } - - @Override - public boolean removeFromGraveyard(Card card, Game game) { - return this.graveyard.remove(card); - } - - @Override - public boolean putCardsOnBottomOfLibrary(Card card, Game game, Ability source, boolean anyOrder) { - return putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, anyOrder); - } - - @Override - public boolean putCardsOnBottomOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { - if (!cardsToLibrary.isEmpty()) { - Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException - if (!anyOrder) { - // random order - List ids = new ArrayList<>(cards); - Collections.shuffle(ids); - for (UUID id : ids) { - moveObjectToLibrary(id, source, game, false, false); - } - } else { - // user defined order - TargetCard target = new TargetCard(Zone.ALL, - new FilterCard("card ORDER to put on the BOTTOM of your library (last one chosen will be bottommost)")); - target.setRequired(true); - while (cards.size() > 1 && this.canRespond() - && this.choose(Outcome.Neutral, cards, target, game)) { - UUID targetObjectId = target.getFirstTarget(); - if (targetObjectId == null) { - break; - } - cards.remove(targetObjectId); - moveObjectToLibrary(targetObjectId, source, game, false, false); - target.clearChosen(); - } - for (UUID c : cards) { - moveObjectToLibrary(c, source, game, false, false); - } - } - } - return true; - } - - @Override - public boolean shuffleCardsToLibrary(Cards cards, Game game, Ability source) { - if (cards.isEmpty()) { - return true; - } - game.informPlayers(getLogName() + " shuffles " + CardUtil.numberToText(cards.size(), "a") - + " card" + (cards.size() == 1 ? "" : "s") - + " into their library" + CardUtil.getSourceLogName(game, source)); - boolean status = moveCards(cards, Zone.LIBRARY, source, game); - shuffleLibrary(source, game); - return status; - } - - @Override - public boolean shuffleCardsToLibrary(Card card, Game game, Ability source) { - if (card == null) { - return true; - } - return shuffleCardsToLibrary(new CardsImpl(card), game, source); - } - - @Override - public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int xFromTheTop, boolean withName) { - if (card.isOwnedBy(getId())) { - if (library.size() + 1 < xFromTheTop) { - putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, true); - } else { - if (card.moveToZone(Zone.LIBRARY, source, game, true) - && !(card instanceof PermanentToken) && !card.isCopy()) { - Card cardInLib = getLibrary().getFromTop(game); - if (cardInLib != null && cardInLib.getId().equals(card.getId())) { // check needed because e.g. commander can go to command zone - cardInLib = getLibrary().removeFromTop(game); - getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game); - game.informPlayers(withName ? cardInLib.getLogName() : "A card" - + " is put into " - + getLogName() - + "'s library " - + CardUtil.numberToOrdinalText(xFromTheTop) - + " from the top" + CardUtil.getSourceLogName(game, source, cardInLib.getId())); - } - } else { - return false; - } - } - } else { - return game.getPlayer(card.getOwnerId()).putCardOnTopXOfLibrary(card, game, source, xFromTheTop, withName); - } - return true; - } - - /** - * Can be cards or permanents that go to library - * - * @param cardsToLibrary - * @param game - * @param source - * @param anyOrder - * @return - */ - @Override - public boolean putCardsOnTopOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { - if (cardsToLibrary != null && !cardsToLibrary.isEmpty()) { - Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException - if (!anyOrder) { - // random order - List ids = new ArrayList<>(cards); - Collections.shuffle(ids); - for (UUID id : ids) { - moveObjectToLibrary(id, source, game, true, false); - } - } else { - // user defined order - TargetCard target = new TargetCard(Zone.ALL, - new FilterCard("card ORDER to put on the TOP of your library (last one chosen will be topmost)")); - target.setRequired(true); - while (cards.size() > 1 - && this.canRespond() - && this.choose(Outcome.Neutral, cards, target, game)) { - UUID targetObjectId = target.getFirstTarget(); - if (targetObjectId == null) { - break; - } - cards.remove(targetObjectId); - moveObjectToLibrary(targetObjectId, source, game, true, false); - target.clearChosen(); - } - for (UUID c : cards) { - moveObjectToLibrary(c, source, game, true, false); - } - } - } - return true; - } - - @Override - public boolean putCardsOnTopOfLibrary(Card cardToLibrary, Game game, Ability source, boolean anyOrder) { - if (cardToLibrary != null) { - return putCardsOnTopOfLibrary(new CardsImpl(cardToLibrary), game, source, anyOrder); - } - return true; - } - - private boolean moveObjectToLibrary(UUID objectId, Ability source, Game game, boolean toTop, boolean withName) { - MageObject mageObject = game.getObject(objectId); - if (mageObject instanceof Spell && mageObject.isCopy()) { - // Spell copies are not moved as cards, so here the no copy spell has to be selected to move - // (but because copy and original have the same objectId the wrong sepell can be selected from stack). - // So let's check if the original spell is on the stack and has to be selected. // TODO: Better handling so each spell could be selected by a unique id - Spell spellNoCopy = game.getStack().getSpell(source.getSourceId(), false); - if (spellNoCopy != null) { - mageObject = spellNoCopy; - } - } - if (mageObject != null) { - Zone fromZone = game.getState().getZone(objectId); - if ((mageObject instanceof Permanent)) { - return this.moveCardToLibraryWithInfo((Permanent) mageObject, source, game, fromZone, toTop, withName); - } else if (mageObject instanceof Card) { - return this.moveCardToLibraryWithInfo((Card) mageObject, source, game, fromZone, toTop, withName); - } - } - return false; - } - - @Override - public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { - // cost must be copied for data consistence between game simulations - castSourceIdWithAlternateMana.add(sourceId); - castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null); - castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null); - } - - @Override - public Set getCastSourceIdWithAlternateMana() { - return castSourceIdWithAlternateMana; - } - - @Override - public Map> getCastSourceIdCosts() { - return castSourceIdCosts; - } - - @Override - public Map> getCastSourceIdManaCosts() { - return castSourceIdManaCosts; - } - - @Override - public void clearCastSourceIdManaCosts() { - this.castSourceIdCosts.clear(); - this.castSourceIdManaCosts.clear(); - this.castSourceIdWithAlternateMana.clear(); - } - - @Override - public void setPayManaMode(boolean payManaMode) { - this.payManaMode = payManaMode; - } - - @Override - public boolean isInPayManaMode() { - return payManaMode; - } - - @Override - public boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject) { - if (card == null) { - return false; - } - - // play without timing and from any zone - boolean result; - if (card.isLand(game)) { - result = playLand(card, game, true); - } else { - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); - result = cast(this.chooseAbilityForCast(card, game, noMana), game, noMana, approvingObject); - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); - } - - if (!result) { - game.informPlayer(this, "You can't play " + card.getIdName() + '.'); - } - return result; - } - - /** - * @param originalAbility - * @param game - * @param noMana cast it without paying mana costs - * @param approvingObject which object approved the cast - * @return - */ - @Override - public boolean cast(SpellAbility originalAbility, Game game, boolean noMana, ApprovingObject approvingObject) { - if (game == null || originalAbility == null) { - return false; - } - - // Use ability copy to avoid problems with targets and costs on recast (issue https://github.com/magefree/mage/issues/5189). - SpellAbility ability = originalAbility.copy(); - ability.setControllerId(getId()); - ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); - - //20091005 - 601.2a - if (ability.getSourceId() == null) { - logger.error("Ability without sourceId turn " + game.getTurnNum() + ". Ability: " + ability.getRule()); - return false; - } - Card card = game.getCard(ability.getSourceId()); - if (card != null) { - Zone fromZone = game.getState().getZone(card.getMainCard().getId()); - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, - ability.getId(), ability, playerId, approvingObject); - castEvent.setZone(fromZone); - if (!game.replaceEvent(castEvent, ability)) { - int bookmark = game.bookmarkState(); - setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) - card.cast(game, fromZone, ability, playerId); - Spell spell = game.getStack().getSpell(ability.getId()); - if (spell == null) { - logger.error("Got no spell from stack. ability: " + ability.getRule()); - return false; - } - if (card.isCopy()) { - spell.setCopy(true, null); - } - // Update the zcc to the stack - ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); - - // ALTERNATIVE COST from dynamic effects - // some effects set sourceId to cast without paying mana costs or other costs - if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) { - Ability spellAbility = spell.getSpellAbility(); - ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()); - Costs costs = getCastSourceIdCosts().get(ability.getSourceId()); - if (alternateCosts == null) { - noMana = true; - } else { - spellAbility.getManaCosts().clear(); - spellAbility.getManaCostsToPay().clear(); - spellAbility.getManaCosts().add(alternateCosts.copy()); - spellAbility.getManaCostsToPay().add(alternateCosts.copy()); - } - spellAbility.getCosts().clear(); - if (costs != null) { - spellAbility.getCosts().addAll(costs); - } - } - clearCastSourceIdManaCosts(); // TODO: test multiple alternative cost for different cards as same time - - castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, - spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); - castEvent.setZone(fromZone); - game.fireEvent(castEvent); - if (spell.activate(game, noMana)) { - GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, - spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); - castedEvent.setZone(fromZone); - game.fireEvent(castedEvent); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + spell.getActivatedMessage(game)); - } - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - restoreState(bookmark, ability.getRule(), game); - } - } - return false; - } - - @Override - public boolean playLand(Card card, Game game, boolean ignoreTiming) { - // Check for alternate casting possibilities: e.g. land with Morph - if (card == null) { - return false; - } - ActivatedAbility playLandAbility = null; - boolean foundAlternative = false; - for (Ability ability : card.getAbilities(game)) { - // if cast for noMana no Alternative costs are allowed - if ((ability instanceof AlternativeSourceCosts) - || (ability instanceof OptionalAdditionalSourceCosts)) { - foundAlternative = true; - } - if (ability instanceof PlayLandAbility) { - playLandAbility = (ActivatedAbility) ability; - } - } - - // try alternative cast (face down) - if (foundAlternative) { - SpellAbility spellAbility = new SpellAbility(null, "", - game.getState().getZone(card.getId()), SpellAbilityType.FACE_DOWN_CREATURE); - spellAbility.setControllerId(this.getId()); - spellAbility.setSourceId(card.getId()); - if (cast(spellAbility, game, false, null)) { - return true; - } - } - - if (playLandAbility == null) { - return false; - } - - //20091005 - 114.2a - ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game); - if (ignoreTiming) { - if (!canPlayLand()) { - return false; // ignore timing does not mean that more lands than normal can be played - } - } else { - if (!activationStatus.canActivate()) { - return false; - } - } - - //20091005 - 305.1 - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()))) { - // int bookmark = game.bookmarkState(); - // land events must return original zone (uses for commander watcher) - Zone cardZoneBefore = game.getState().getZone(card.getId()); - GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); - landEventBefore.setZone(cardZoneBefore); - game.fireEvent(landEventBefore); - - if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { - landsPlayed++; - GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); - landEventAfter.setZone(cardZoneBefore); - game.fireEvent(landEventAfter); - - String playText = getLogName() + " plays " + card.getLogName(); - if (card instanceof ModalDoubleFacesCardHalf) { - ModalDoubleFacesCard mdfCard = (ModalDoubleFacesCard) card.getMainCard(); - playText = getLogName() + " plays " + GameLog.replaceNameByColoredName(card, card.getName(), mdfCard) - + " as MDF side of " + GameLog.getColoredObjectIdName(mdfCard); - } - game.fireInformEvent(playText); - // game.removeBookmark(bookmark); - resetStoredBookmark(game); // prevent undo after playing a land - return true; - } - // putOntoBattlefield returned false if putOntoBattlefield was replaced by replacement effect (e.g. Kjeldoran Outpost). - // But that would undo the effect completely, - // what makes no real sense. So it makes no sense to generally do a restoreState here. - // restoreState(bookmark, card.getName(), game); - } - // if the to play the land is replaced (e.g. Kjeldoran Outpost and don't sacrificing a Plains) it's a valid state so returning true here - return true; - } - - protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) { - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, - ability.getId(), ability, playerId))) { - int bookmark = game.bookmarkState(); - if (ability.activate(game, false)) { - if (ability.resolve(game)) { - if (ability.isUndoPossible()) { - if (storedBookmark == -1 || storedBookmark > bookmark) { // e.g. useful for undo Nykthos, Shrine to Nyx - setStoredBookmark(bookmark); - } - } else { - resetStoredBookmark(game); - } - return true; - } - } - restoreState(bookmark, ability.getRule(), game); - } - return false; - } - - protected boolean playAbility(ActivatedAbility ability, Game game) { - //20091005 - 602.2a - if (ability.isUsesStack()) { - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, - ability.getId(), ability, playerId))) { - int bookmark = game.bookmarkState(); - setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) - ability.newId(); - ability.setControllerId(playerId); - game.getStack().push(new StackAbility(ability, playerId)); - if (ability.activate(game, false)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATED_ABILITY, - ability.getId(), ability, playerId)); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + ability.getGameLogMessage(game)); - } - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - restoreState(bookmark, ability.getRule(), game); - } - } else { - int bookmark = game.bookmarkState(); - if (ability.activate(game, false)) { - ability.resolve(game); - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - restoreState(bookmark, ability.getRule(), game); - } - return false; - } - - protected boolean specialAction(SpecialAction action, Game game) { - //20091005 - 114 - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_ACTION, - action.getId(), action, getId()))) { - int bookmark = game.bookmarkState(); - if (action.activate(game, false)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_ACTION, - action.getId(), action, getId())); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + action.getGameLogMessage(game)); - } - if (action.resolve(game)) { - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - } - restoreState(bookmark, action.getRule(), game); - } - return false; - } - - protected boolean specialManaPayment(SpecialAction action, Game game) { - //20091005 - 114 - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_MANA_PAYMENT, - action.getId(), action, getId()))) { - int bookmark = game.bookmarkState(); - if (action.activate(game, false)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_MANA_PAYMENT, - action.getId(), action, getId())); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + action.getGameLogMessage(game)); - } - if (action.resolve(game)) { - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - } - restoreState(bookmark, action.getRule(), game); - } - return false; - } - - @Override - public boolean activateAbility(ActivatedAbility ability, Game game) { - if (ability == null) { - return false; - } - boolean result; - if (ability instanceof PassAbility) { - pass(game); - return true; - } - Card card = game.getCard(ability.getSourceId()); - if (ability instanceof PlayLandAsCommanderAbility) { - - // LAND as commander: play land with cost, but without stack - ActivationStatus activationStatus = ability.canActivate(this.playerId, game); - if (!activationStatus.canActivate() || !this.canPlayLand()) { - return false; - } - if (card == null) { - return false; - } - - // as copy, tries to applie cost effects and pays - Ability activatingAbility = ability.copy(); - if (activatingAbility.activate(game, false)) { - result = playLand(card, game, false); - } else { - result = false; - } - - } else if (ability instanceof PlayLandAbility) { - - // LAND as normal card: without cost and stack - result = playLand(card, game, false); - - } else { - - // ABILITY - ActivationStatus activationStatus = ability.canActivate(this.playerId, game); - if (!activationStatus.canActivate()) { - return false; - } - - switch (ability.getAbilityType()) { - case SPECIAL_ACTION: - result = specialAction((SpecialAction) ability.copy(), game); - break; - case SPECIAL_MANA_PAYMENT: - result = specialManaPayment((SpecialAction) ability.copy(), game); - break; - case MANA: - result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game); - break; - case SPELL: - result = cast((SpellAbility) ability, game, false, activationStatus.getApprovingObject()); - break; - default: - result = playAbility(ability.copy(), game); - break; - } - } - - //if player has taken an action then reset all player passed flags - justActivatedType = null; - if (result) { - if (isHuman() - && (ability.getAbilityType() == AbilityType.SPELL - || ability.getAbilityType() == AbilityType.ACTIVATED)) { - if (ability.isUsesStack()) { // if the ability does not use the stack (e.g. Suspend) auto pass would go to next phase unintended - setJustActivatedType(ability.getAbilityType()); - } - } - game.getPlayers().resetPassed(); - } - return result; - } - - @Override - public boolean triggerAbility(TriggeredAbility triggeredAbility, Game game) { - if (triggeredAbility == null) { - logger.warn("Null source in triggerAbility method"); - throw new IllegalArgumentException("source TriggeredAbility must not be null"); - } - //20091005 - 603.3c, 603.3d - int bookmark = game.bookmarkState(); - TriggeredAbility ability = triggeredAbility.copy(); - MageObject sourceObject = ability.getSourceObject(game); - if (sourceObject != null) { - sourceObject.adjustTargets(ability, game); - } - UUID triggerId = null; - if (ability.canChooseTarget(game, playerId)) { - if (ability.isUsesStack()) { - game.getStack().push(new StackAbility(ability, playerId)); - } - if (ability.activate(game, false)) { - if ((ability.isUsesStack() - || ability.getRuleVisible()) - && !game.isSimulation()) { - game.informPlayers(getLogName() + " - " + ability.getGameLogMessage(game)); - } - if (!ability.isUsesStack()) { - ability.resolve(game); - } else { - game.fireEvent(new GameEvent( - GameEvent.EventType.TRIGGERED_ABILITY, - ability.getId(), ability, ability.getControllerId() - )); - triggerId = ability.getId(); - } - game.removeBookmark(bookmark); - return true; - } - } - restoreState(bookmark, triggeredAbility.getRule(), game); // why restore is needed here? (to remove the triggered ability from the stack because of no possible targets) - GameEvent event = new GameEvent( - GameEvent.EventType.ABILITY_TRIGGERED, - triggerId, ability, ability.getControllerId() - ); - game.getState().setValue(event.getId().toString(), ability.getTriggerEvent()); - game.fireEvent(event); - return false; - } - - /** - * Return spells for possible cast Uses in GUI to show only playable spells - * for choosing from the card (example: effect allow to cast card and player - * must choose the spell ability) - * - * @param game - * @param playerId - * @param object - * @param zone - * @param noMana - * @return - */ - public static LinkedHashMap getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) { - // it uses simple check from spellCanBeActivatedRegularlyNow - // reason: no approved info here (e.g. forced to choose spell ability from cast card) - LinkedHashMap useable = new LinkedHashMap<>(); - Abilities allAbilities; - if (object instanceof Card) { - allAbilities = ((Card) object).getAbilities(game); - } else { - allAbilities = object.getAbilities(); - } - - for (Ability ability : allAbilities) { - if (ability instanceof SpellAbility) { - SpellAbility spellAbility = (SpellAbility) ability; - - switch (spellAbility.getSpellAbilityType()) { - case BASE_ALTERNATE: - // rules: - // If you cast a spell “without paying its mana cost,” you can’t choose to cast it for - // any alternative costs. You can, however, pay additional costs, such as kicker costs. - // If the card has any mandatory additional costs, those must be paid to cast the spell. - // (2021-02-05) - if (!noMana) { - if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { - useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability - } - return useable; - } - break; - case SPLIT_FUSED: - // rules: - // If you cast a split card with fuse from your hand without paying its mana cost, - // you can choose to use its fuse ability and cast both halves without paying their mana costs. - if (zone == Zone.HAND) { - if (spellAbility.canChooseTarget(game, playerId)) { - useable.put(spellAbility.getId(), spellAbility); - } - } - case SPLIT: - if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getLeftHalfCard().getSpellAbility()); - } - if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getRightHalfCard().getSpellAbility()); - } - return useable; - case SPLIT_AFTERMATH: - if (zone == Zone.GRAVEYARD) { - if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getRightHalfCard().getSpellAbility()); - } - } else { - if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getLeftHalfCard().getSpellAbility()); - } - } - return useable; - default: - if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { - useable.put(spellAbility.getId(), spellAbility); - } - } - } - } - return useable; - } - - @Override - public LinkedHashMap getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) { - LinkedHashMap useable = new LinkedHashMap<>(); - // stack abilities - can't activate anything - // spell ability - can activate additional abilities (example: "Lightning Storm") - if (object instanceof StackAbility || object == null) { - return useable; - } - boolean previousState = game.inCheckPlayableState(); - game.setCheckPlayableState(true); - try { - // collect and filter playable activated abilities - // GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right) - Set needIds = CardUtil.getObjectParts(object); - - // workaround to find all abilities first and filter it for one object - List allPlayable = getPlayable(game, true, zone, false); - for (ActivatedAbility ability : allPlayable) { - if (needIds.contains(ability.getSourceId())) { - useable.putIfAbsent(ability.getId(), ability); - } - } - } finally { - game.setCheckPlayableState(previousState); - } - return useable; - } - - protected LinkedHashMap getUseableManaAbilities(MageObject object, Zone zone, Game game) { - LinkedHashMap useable = new LinkedHashMap<>(); - boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); - for (ActivatedManaAbilityImpl ability : object.getAbilities().getActivatedManaAbilities(zone)) { - if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { - if (ability.canActivate(playerId, game).canActivate()) { - useable.put(ability.getId(), ability); - } - } - } - return useable; - } - - @Override - public int getLandsPlayed() { - return landsPlayed; - } - - @Override - public boolean canPlayLand() { - //20091005 - 114.2a - return landsPlayed < landsPerTurn; - } - - protected boolean isActivePlayer(Game game) { - return game.isActivePlayer(this.playerId); - } - - @Override - public void shuffleLibrary(Ability source, Game game) { - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SHUFFLE_LIBRARY, playerId, source, playerId))) { - this.library.shuffle(); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + "'s library is shuffled" + CardUtil.getSourceLogName(game, source)); - } - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SHUFFLED, playerId, source, playerId)); - } - } - - @Override - public void revealCards(Ability source, Cards cards, Game game) { - revealCards(source, null, cards, game, true); - } - - @Override - public void revealCards(String titleSuffix, Cards cards, Game game) { - revealCards(titleSuffix, cards, game, true); - } - - @Override - public void revealCards(String titleSuffix, Cards cards, Game game, boolean postToLog) { - revealCards(null, titleSuffix, cards, game, postToLog); - } - - @Override - public void revealCards(Ability source, String titleSuffix, Cards cards, Game game) { - revealCards(source, titleSuffix, cards, game, true); - } - - @Override - public void revealCards(Ability source, String titleSuffix, Cards cards, Game game, boolean postToLog) { - if (cards == null || cards.isEmpty()) { - return; - } - if (postToLog) { - game.getState().getRevealed().add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); - } else { - game.getState().getRevealed().update(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); - } - if (postToLog && !game.isSimulation()) { - StringBuilder sb = new StringBuilder(getLogName()).append(" reveals "); - int current = 0, last = cards.size(); - for (Card card : cards.getCards(game)) { - current++; - sb.append(GameLog.getColoredObjectName(card)); - if (current < last) { - sb.append(", "); - } - } - sb.append(CardUtil.getSourceLogName(game, source)); - game.informPlayers(sb.toString()); - } - } - - @Override - public void lookAtCards(String titleSuffix, Card card, Game game) { - game.getState().getLookedAt(this.playerId).add(titleSuffix, card); - game.fireUpdatePlayersEvent(); - } - - @Override - public void lookAtCards(String titleSuffix, Cards cards, Game game) { - this.lookAtCards(null, titleSuffix, cards, game); - } - - @Override - public void lookAtCards(Ability source, String titleSuffix, Cards cards, Game game) { - game.getState().getLookedAt(this.playerId).add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); - game.fireUpdatePlayersEvent(); - } - - @Override - public void phasing(Game game) { - //20091005 - 502.1 - List phasedOut = game.getBattlefield().getPhasedOut(game, playerId); - for (Permanent permanent : game.getBattlefield().getPhasedIn(game, playerId)) { - // 502.15i When a permanent phases out, any local enchantments or Equipment - // attached to that permanent phase out at the same time. This alternate way of - // phasing out is known as phasing out "indirectly." An enchantment or Equipment - // that phased out indirectly won't phase in by itself, but instead phases in - // along with the card it's attached to. - Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); - if (!(attachedTo != null && attachedTo.isControlledBy(this.getId()))) { - permanent.phaseOut(game, false); - } - } - for (Permanent permanent : phasedOut) { - if (!permanent.isPhasedOutIndirectly()) { - permanent.phaseIn(game); - } - } - } - - @Override - public void untap(Game game) { - // create list of all "notMoreThan" effects to track which one are consumed - Map>, Integer> notMoreThanEffectsUsage = new HashMap<>(); - for (Entry> restrictionEffect - : game.getContinuousEffects().getApplicableRestrictionUntapNotMoreThanEffects(this, game).entrySet()) { - notMoreThanEffectsUsage.put(restrictionEffect, restrictionEffect.getKey().getNumber()); - } - - if (!notMoreThanEffectsUsage.isEmpty()) { - // create list of all permanents that can be untapped generally - List canBeUntapped = new ArrayList<>(); - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { - boolean untap = true; - for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { - untap &= effect.canBeUntapped(permanent, null, game, true); - } - if (untap) { - canBeUntapped.add(permanent); - } - } - // selected permanents to untap - List selectedToUntap = new ArrayList<>(); - - // player can cancel the selection of an effect to use a preferred order of restriction effects - boolean playerCanceledSelection; - do { - playerCanceledSelection = false; - // select permanents to untap to consume the "notMoreThan" effects - for (Map.Entry>, Integer> handledEntry : notMoreThanEffectsUsage.entrySet()) { - // select a permanent to untap for this entry - int numberToUntap = handledEntry.getValue(); - if (numberToUntap > 0) { - - List leftForUntap = getPermanentsThatCanBeUntapped(game, - canBeUntapped, - handledEntry.getKey().getKey(), - notMoreThanEffectsUsage); - - FilterControlledPermanent filter = handledEntry.getKey().getKey().getFilter().copy(); - String message = filter.getMessage(); - // omit already from other untap effects selected permanents - for (Permanent permanent : selectedToUntap) { - filter.add(Predicates.not(new PermanentIdPredicate(permanent.getId()))); - } - // while targets left and there is still allowed to untap - while (canRespond() && !leftForUntap.isEmpty() && numberToUntap > 0) { - // player has to select the permanent they want to untap for this restriction - Ability ability = handledEntry.getKey().getValue().iterator().next(); - if (ability != null) { - StringBuilder sb = new StringBuilder(message).append(" to untap").append(" (").append(Math.min(leftForUntap.size(), - numberToUntap)).append(" in total"); - MageObject effectSource = game.getObject(ability.getSourceId()); - if (effectSource != null) { - sb.append(" from ").append(effectSource.getLogName()); - } - sb.append(')'); - filter.setMessage(sb.toString()); - Target target = new TargetPermanent(1, 1, filter, true); - if (!this.chooseTarget(Outcome.Untap, target, ability, game)) { - // player canceled, go on with the next effect (if no other effect available, this effect will be active again) - playerCanceledSelection = true; - break; - } - Permanent selectedPermanent = game.getPermanent(target.getFirstTarget()); - if (leftForUntap.contains(selectedPermanent)) { - selectedToUntap.add(selectedPermanent); - numberToUntap--; - // don't allow to select same permanent twice - filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); - // reduce available untap numbers from other "UntapNotMoreThan" effects if selected permanent applies to their filter too - for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { - if (notMoreThanEffect.getValue() > 0 - && notMoreThanEffect.getKey().getKey().getFilter().match(selectedPermanent, game)) { - notMoreThanEffect.setValue(notMoreThanEffect.getValue() - 1); - } - } - // update the left for untap list - leftForUntap = getPermanentsThatCanBeUntapped(game, - canBeUntapped, - handledEntry.getKey().getKey(), - notMoreThanEffectsUsage); - // remove already selected permanents - for (Permanent permanent : selectedToUntap) { - leftForUntap.remove(permanent); - } - - } else { - // player selected an permanent that is restricted by another effect, disallow it (so AI can select another one) - filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); - if (this.isHuman() && !game.isSimulation()) { - game.informPlayer(this, "This permanent can't be untapped because of other restricting effect."); - } - } - } - } - } - } - - } while (canRespond() && playerCanceledSelection); - - if (!game.isSimulation()) { - // show in log which permanents were selected to untap - for (Permanent permanent : selectedToUntap) { - game.informPlayers(this.getLogName() + " untapped " + permanent.getLogName()); - } - } - // untap if permanent is not concerned by notMoreThan effects or is included in the selectedToUntapList - for (Permanent permanent : canBeUntapped) { - boolean doUntap = true; - if (!selectedToUntap.contains(permanent)) { - // if the permanent is covered by one of the restriction effects, don't untap it - for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { - if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game)) { - doUntap = false; - break; - } - } - } - if (permanent != null && doUntap) { - permanent.untap(game); - } - - } - - } else { - //20091005 - 502.2 - - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { - boolean untap = true; - for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { - untap &= effect.canBeUntapped(permanent, null, game, true); - } - if (untap) { - permanent.untap(game); - } - } - } - } - - private List getPermanentsThatCanBeUntapped(Game game, List canBeUntapped, RestrictionUntapNotMoreThanEffect handledEffect, Map>, Integer> notMoreThanEffectsUsage) { - List leftForUntap = new ArrayList<>(); - // select permanents that can still be untapped - for (Permanent permanent : canBeUntapped) { - if (handledEffect.getFilter().match(permanent, game)) { // matches the restricted permanents of handled entry - boolean canBeSelected = true; - // check if the permanent is restricted by another restriction that has left no permanent - for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { - if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game) - && notMoreThanEffect.getValue() == 0) { - canBeSelected = false; - break; - } - } - if (canBeSelected) { - leftForUntap.add(permanent); - } - } - } - return leftForUntap; - } - - @Override - public UUID getId() { - return playerId; - } - - @Override - public Cards getHand() { - return hand; - } - - @Override - public Graveyard getGraveyard() { - return graveyard; - } - - @Override - public ManaPool getManaPool() { - return this.manaPool; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getLogName() { - return GameLog.getColoredPlayerName(name); - } - - @Override - public boolean isHuman() { - return human; - } - - @Override - public Library getLibrary() { - return library; - } - - @Override - public Cards getSideboard() { - return sideboard; - } - - @Override - public int getLife() { - return life; - } - - @Override - public void initLife(int life) { - this.life = life; - } - - @Override - public void setLife(int life, Game game, Ability source) { - // rule 118.5 - if (life > this.life) { - gainLife(life - this.life, game, source); - } else if (life < this.life) { - loseLife(this.life - life, game, source, false); - } - } - - @Override - public void setLifeTotalCanChange(boolean lifeTotalCanChange) { - this.canGainLife = lifeTotalCanChange; - this.canLoseLife = lifeTotalCanChange; - } - - @Override - public boolean isLifeTotalCanChange() { - return canGainLife || canLoseLife; - } - - @Override - public List getAlternativeSourceCosts() { - return alternativeSourceCosts; - } - - @Override - public boolean isCanLoseLife() { - return canLoseLife; - } - - @Override - public void setCanLoseLife(boolean canLoseLife) { - this.canLoseLife = canLoseLife; - } - - @Override - public int loseLife(int amount, Game game, Ability source, boolean atCombat, UUID attackerId) { - if (!canLoseLife || !this.isInGame()) { - return 0; - } - GameEvent event = new GameEvent(GameEvent.EventType.LOSE_LIFE, - playerId, source, playerId, amount, atCombat); - if (!game.replaceEvent(event)) { - this.life = CardUtil.overflowDec(this.life, event.getAmount()); - if (!game.isSimulation()) { - UUID needId = attackerId; - if (needId == null) { - needId = source == null ? null : source.getSourceId(); - } - game.informPlayers(this.getLogName() + " loses " + event.getAmount() + " life" - + (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", "")); - } - if (amount > 0) { - game.fireEvent(new GameEvent(GameEvent.EventType.LOST_LIFE, - playerId, source, playerId, amount, atCombat)); - } - return amount; - } - return 0; - } - - @Override - public int loseLife(int amount, Game game, Ability source, boolean atCombat) { - return loseLife(amount, game, source, atCombat, null); - } - - @Override - public boolean isCanGainLife() { - return canGainLife; - } - - @Override - public void setCanGainLife(boolean canGainLife) { - this.canGainLife = canGainLife; - } - - @Override - public int gainLife(int amount, Game game, Ability source) { - if (!canGainLife || amount <= 0) { - return 0; - } - GameEvent event = new GameEvent(GameEvent.EventType.GAIN_LIFE, - playerId, source, playerId, amount, false); - if (!game.replaceEvent(event)) { - // TODO: lock life at Integer.MAX_VALUE if reached, until it's set to a different amount - // (https://magic.wizards.com/en/articles/archive/news/unstable-faqawaslfaqpaftidawabiajtbt-2017-12-06 - "infinite" life total stays infinite no matter how much is gained or lost) - // this.life += event.getAmount(); - this.life = CardUtil.overflowInc(this.life, event.getAmount()); - if (!game.isSimulation()) { - game.informPlayers(this.getLogName() + " gains " + event.getAmount() + " life" + CardUtil.getSourceLogName(game, source)); - } - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.GAINED_LIFE, - playerId, source, playerId, event.getAmount())); - return event.getAmount(); - } - return 0; - } - - @Override - public void exchangeLife(Player player, Ability source, Game game) { - int lifePlayer1 = getLife(); - int lifePlayer2 = player.getLife(); - if ((lifePlayer1 != lifePlayer2 && this.isLifeTotalCanChange() && player.isLifeTotalCanChange()) - && (lifePlayer1 >= lifePlayer2 || (this.isCanGainLife() && player.isCanLoseLife())) - && (lifePlayer1 <= lifePlayer2 || (this.isCanLoseLife() && player.isCanGainLife()))) { - this.setLife(lifePlayer2, game, source); - player.setLife(lifePlayer1, game, source); - } - } - - @Override - public int damage(int damage, UUID attackerId, Ability source, Game game) { - return doDamage(damage, attackerId, source, game, false, true, null); - } - - @Override - public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable) { - return doDamage(damage, attackerId, source, game, combatDamage, preventable, null); - } - - @Override - public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { - return doDamage(damage, attackerId, source, game, combatDamage, preventable, appliedEffects); - } - - private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { - if (!this.isInGame()) { - return 0; - } - - if (damage < 1) { - return 0; - } - if (!canDamage(game.getObject(attackerId), game)) { - MageObject sourceObject = game.getObject(attackerId); - game.informPlayers(damage + " damage " - + (sourceObject == null ? "" : "from " + sourceObject.getLogName()) - + " to " + getLogName() - + (damage > 1 ? " were" : "was") + " prevented because of protection"); - return 0; - } - DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combatDamage); - event.setAppliedEffects(appliedEffects); - if (game.replaceEvent(event)) { - return 0; - } - int actualDamage = event.getAmount(); - if (actualDamage < 1) { - return 0; - } - UUID sourceControllerId = null; - Abilities sourceAbilities = null; - MageObject attacker = game.getPermanentOrLKIBattlefield(attackerId); - if (attacker == null) { - StackObject stackObject = game.getStack().getStackObject(attackerId); - if (stackObject != null) { - attacker = stackObject.getStackAbility().getSourceObject(game); - } else { - attacker = game.getObject(attackerId); - } - if (attacker instanceof Spell) { - sourceAbilities = ((Spell) attacker).getAbilities(game); - sourceControllerId = ((Spell) attacker).getControllerId(); - } else if (attacker instanceof Card) { - sourceAbilities = ((Card) attacker).getAbilities(game); - sourceControllerId = ((Card) attacker).getOwnerId(); - } else if (attacker instanceof CommandObject) { - sourceControllerId = ((CommandObject) attacker).getControllerId(); - sourceAbilities = attacker.getAbilities(); - } - } else { - sourceAbilities = ((Permanent) attacker).getAbilities(game); - sourceControllerId = ((Permanent) attacker).getControllerId(); - } - if (event.isAsThoughInfect() || (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId()))) { - addCounters(CounterType.POISON.createInstance(actualDamage), sourceControllerId, source, game); - } else { - GameEvent damageToLifeLossEvent = new GameEvent(GameEvent.EventType.DAMAGE_CAUSES_LIFE_LOSS, - playerId, source, playerId, actualDamage, combatDamage); - if (!game.replaceEvent(damageToLifeLossEvent)) { - this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combatDamage, attackerId); - } - } - if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) { - if (combatDamage) { - game.getPermanent(attackerId).markLifelink(actualDamage); - } else { - Player player = game.getPlayer(sourceControllerId); - player.gainLife(actualDamage, game, source); - } - } - // Unstable ability - Earl of Squirrel - if (sourceAbilities != null && sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) { - Player player = game.getPlayer(sourceControllerId); - new SquirrelToken().putOntoBattlefield(actualDamage, game, source, player.getId()); - } - DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combatDamage); - game.fireEvent(damagedEvent); - game.getState().addSimultaneousDamage(damagedEvent, game); - return actualDamage; - } - - @Override - public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) { - boolean returnCode = true; - GameEvent addingAllEvent = GameEvent.getEvent( - GameEvent.EventType.ADD_COUNTERS, playerId, source, - playerAddingCounters, counter.getName(), counter.getCount() - ); - if (!game.replaceEvent(addingAllEvent)) { - int amount = addingAllEvent.getAmount(); - int finalAmount = amount; - boolean isEffectFlag = addingAllEvent.getFlag(); - for (int i = 0; i < amount; i++) { - Counter eventCounter = counter.copy(); - eventCounter.remove(eventCounter.getCount() - 1); - GameEvent addingOneEvent = GameEvent.getEvent( - GameEvent.EventType.ADD_COUNTER, playerId, source, - playerAddingCounters, counter.getName(), 1 - ); - addingOneEvent.setFlag(isEffectFlag); - if (!game.replaceEvent(addingOneEvent)) { - getCounters().addCounter(eventCounter); - GameEvent addedOneEvent = GameEvent.getEvent( - GameEvent.EventType.COUNTER_ADDED, playerId, source, - playerAddingCounters, counter.getName(), 1 - ); - addedOneEvent.setFlag(addingOneEvent.getFlag()); - game.fireEvent(addedOneEvent); - } else { - finalAmount--; - returnCode = false; - } - } - if (finalAmount > 0) { - GameEvent addedAllEvent = GameEvent.getEvent( - GameEvent.EventType.COUNTERS_ADDED, playerId, source, - playerAddingCounters, counter.getName(), amount - ); - addedAllEvent.setFlag(addingAllEvent.getFlag()); - game.fireEvent(addedAllEvent); - } - } else { - returnCode = false; - } - return returnCode; - } - - @Override - public void removeCounters(String name, int amount, Ability source, Game game) { - int finalAmount = 0; - for (int i = 0; i < amount; i++) { - if (!counters.removeCounter(name, 1)) { - break; - } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, - getId(), source, (source == null ? null : source.getControllerId())); - event.setData(name); - event.setAmount(1); - game.fireEvent(event); - finalAmount++; - } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, - getId(), source, (source == null ? null : source.getControllerId())); - event.setData(name); - event.setAmount(finalAmount); - game.fireEvent(event); - } - - protected boolean canDamage(MageObject source, Game game) { - for (ProtectionAbility ability : abilities.getProtectionAbilities()) { - if (!ability.canTarget(source, game)) { - return false; - } - } - return true; - } - - @Override - public Abilities getAbilities() { - return this.abilities; - } - - @Override - public void addAbility(Ability ability) { - ability.setSourceId(playerId); - this.abilities.add(ability); - } - - @Override - public int getLandsPerTurn() { - return this.landsPerTurn; - } - - @Override - public void setLandsPerTurn(int landsPerTurn) { - this.landsPerTurn = landsPerTurn; - } - - @Override - public int getLoyaltyUsePerTurn() { - return this.loyaltyUsePerTurn; - } - - @Override - public void setLoyaltyUsePerTurn(int loyaltyUsePerTurn) { - this.loyaltyUsePerTurn = loyaltyUsePerTurn; - } - - @Override - public int getMaxHandSize() { - return maxHandSize; - } - - @Override - public void setMaxHandSize(int maxHandSize) { - this.maxHandSize = maxHandSize; - } - - @Override - public void setMaxAttackedBy(int maxAttackedBy) { - this.maxAttackedBy = maxAttackedBy; - } - - @Override - public int getMaxAttackedBy() { - return maxAttackedBy; - } - - @Override - public void setResponseString(String responseString) { - } - - @Override - public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) { - } - - @Override - public void setResponseUUID(UUID responseUUID) { - } - - @Override - public void setResponseBoolean(Boolean responseBoolean) { - } - - @Override - public void setResponseInteger(Integer responseInteger) { - } - - @Override - public boolean isPassed() { - return passed; - } - - @Override - public void pass(Game game) { - this.passed = true; - resetStoredBookmark(game); - } - - @Override - public void resetPassed() { - this.passed = this.loses || this.hasLeft(); - } - - @Override - public void resetPlayerPassedActions() { - this.passed = false; - this.passedTurn = false; - this.passedTurnSkipStack = false; - this.passedUntilEndOfTurn = false; - this.passedUntilNextMain = false; - this.passedUntilStackResolved = false; - this.dateLastAddedToStack = null; - this.passedUntilEndStepBeforeMyTurn = false; - this.skippedAtLeastOnce = false; - this.passedAllTurns = false; - this.justActivatedType = null; - } - - @Override - public void quit(Game game) { - quit = true; - this.concede(game); - logger.debug(getName() + " quits the match."); - game.informPlayers(getLogName() + " quits the match."); - } - - @Override - public void timerTimeout(Game game) { - quit = true; - timerTimeout = true; - this.concede(game); - game.informPlayers(getLogName() + " has run out of time, losing the match."); - } - - @Override - public void idleTimeout(Game game) { - quit = true; - idleTimeout = true; - this.concede(game); - game.informPlayers(getLogName() + " was idle for too long, losing the Match."); - } - - @Override - public void concede(Game game) { - game.setConcedingPlayer(playerId); - lost(game); -// this.left = true; - } - - @Override - public void sendPlayerAction(PlayerAction playerAction, Game game, Object data) { - switch (playerAction) { - case PASS_PRIORITY_UNTIL_MY_NEXT_TURN: // F9 - resetPlayerPassedActions(); - passedAllTurns = true; - this.skip(); - break; - case PASS_PRIORITY_UNTIL_TURN_END_STEP: // F5 - resetPlayerPassedActions(); - passedUntilEndOfTurn = true; - skippedAtLeastOnce = PhaseStep.END_TURN != game.getTurn().getStepType(); - this.skip(); - break; - case PASS_PRIORITY_UNTIL_NEXT_TURN: // F4 - resetPlayerPassedActions(); - passedTurn = true; - this.skip(); - break; - case PASS_PRIORITY_UNTIL_NEXT_TURN_SKIP_STACK: // F6 - resetPlayerPassedActions(); - passedTurnSkipStack = true; - this.skip(); - break; - case PASS_PRIORITY_UNTIL_NEXT_MAIN_PHASE: //F7 - resetPlayerPassedActions(); - passedUntilNextMain = true; - skippedAtLeastOnce = !(game.getTurn().getStepType() == PhaseStep.POSTCOMBAT_MAIN - || game.getTurn().getStepType() == PhaseStep.PRECOMBAT_MAIN); - this.skip(); - break; - case PASS_PRIORITY_UNTIL_STACK_RESOLVED: // Default F10 - Skips until the current stack is resolved - if (!game.getStack().isEmpty()) { // If stack is empty do nothing - resetPlayerPassedActions(); - passedUntilStackResolved = true; - dateLastAddedToStack = game.getStack().getDateLastAdded(); - this.skip(); - } - break; - case PASS_PRIORITY_UNTIL_END_STEP_BEFORE_MY_NEXT_TURN: //F11 - resetPlayerPassedActions(); - passedUntilEndStepBeforeMyTurn = true; - this.skip(); - break; - case PASS_PRIORITY_CANCEL_ALL_ACTIONS: - resetPlayerPassedActions(); - break; - case PERMISSION_REQUESTS_ALLOWED_OFF: - userData.setAllowRequestShowHandCards(false); - break; - case PERMISSION_REQUESTS_ALLOWED_ON: - userData.setAllowRequestShowHandCards(true); - userData.resetRequestedHandPlayersList(game.getId()); // users can send request again - break; - } - logger.trace("PASS Priority: " + playerAction); - } - - @Override - public void leave() { - this.passed = true; - this.loses = true; - this.left = true; - this.abort(); - //20100423 - 800.4a - this.hand.clear(); - this.graveyard.clear(); - this.library.clear(); - } - - @Override - public boolean hasLeft() { - return this.left; - } - - @Override - public void lost(Game game) { - if (canLose(game)) { - lostForced(game); - } - } - - @Override - public void lostForced(Game game) { - logger.debug(this.getName() + " has lost gameId: " + game.getId()); - //20100423 - 603.9 - if (!this.wins) { - this.loses = true; - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LOST, null, null, playerId)); - game.informPlayers(this.getLogName() + " has lost the game."); - } else { - logger.debug(this.getName() + " has already won - stop lost"); - } - // for draw - first all players that have lost have to be set to lost - if (!hasLeft()) { - logger.debug("Game over playerId: " + playerId); - game.setConcedingPlayer(playerId); - } - } - - @Override - public boolean canLose(Game game) { - return hasLeft() // If a player concedes or has left the match they lose also if effect would say otherwise - || !game.replaceEvent(new GameEvent(GameEvent.EventType.LOSES, null, null, playerId)); - } - - @Override - public void won(Game game) { - boolean opponentInGame = false; - for (UUID opponentId : game.getOpponents(playerId)) { - Player opponent = game.getPlayer(opponentId); - - if (opponent != null && opponent.isInGame()) { - opponentInGame = true; - break; - } - } - if (!opponentInGame - || // if no more opponent is in game the wins event may no longer be replaced - !game.replaceEvent(new GameEvent(GameEvent.EventType.WINS, null, null, playerId))) { - logger.debug("player won -> start: " + this.getName()); - if (!this.loses) { - //20130501 - 800.7, 801.16 - // all opponents in range loose the game - for (UUID opponentId : game.getOpponents(playerId)) { - Player opponent = game.getPlayer(opponentId); - if (opponent != null && !opponent.hasLost()) { - logger.debug("player won -> calling opponent lost: " - + this.getName() + " opponent: " + opponent.getName()); - opponent.lostForced(game); - } - } - // if no more opponents alive (independant from range), you win and the game ends - int opponentsAlive = 0; - for (UUID playerIdToCheck : game.getPlayerList()) { - if (game.isOpponent(this, playerIdToCheck)) { // Check without range - Player opponent = game.getPlayer(playerIdToCheck); - if (opponent != null && !opponent.hasLost()) { - opponentsAlive++; - } - } - } - if (opponentsAlive == 0 && !hasWon()) { - logger.debug("player won -> No more opponents alive game won: " + this.getName()); - game.informPlayers(this.getLogName() + " has won the game"); - this.wins = true; - game.end(); - } - } else { - logger.debug("player won -> but already lost before or other players still alive: " + this.getName()); - } - } - } - - @Override - public void drew(Game game) { - if (!hasLost()) { - this.draws = true; - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DRAW_PLAYER, null, null, playerId)); - game.informPlayers("For " + this.getLogName() + " the game is a draw."); - game.setConcedingPlayer(playerId); - } - } - - @Override - public boolean hasLost() { - return this.loses; - } - - @Override - public boolean isInGame() { - return !hasQuit() && !hasLost() && !hasWon() && !hasDrew() && !hasLeft(); - } - - @Override - public boolean canRespond() { // abort is checked here to get out of player requests (as example: after disconnect) - return isInGame() && !abort; - } - - @Override - public boolean hasWon() { - return !this.loses && this.wins; - } - - @Override - public boolean hasDrew() { - return this.draws; - } - - @Override - public void declareAttacker(UUID attackerId, UUID defenderId, Game game, boolean allowUndo) { - if (allowUndo) { - setStoredBookmark(game.bookmarkState()); // makes it possible to UNDO a declared attacker with costs from e.g. Propaganda - } - Permanent attacker = game.getPermanent(attackerId); - if (attacker != null - && attacker.canAttack(defenderId, game) - && attacker.isControlledBy(playerId)) { - if (!game.getCombat().declareAttacker(attackerId, defenderId, playerId, game)) { - game.undo(playerId); - } - } - } - - @Override - public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game) { - declareBlocker(defenderId, blockerId, attackerId, game, true); - } - - @Override - public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game, boolean allowUndo) { - if (isHuman() && allowUndo) { - setStoredBookmark(game.bookmarkState()); - } - Permanent blocker = game.getPermanent(blockerId); - CombatGroup group = game.getCombat().findGroup(attackerId); - if (blocker != null && group != null && group.canBlock(blocker, game)) { - group.addBlocker(blockerId, playerId, game); - game.getCombat().addBlockingGroup(blockerId, attackerId, playerId, game); - } else if (this.isHuman() && !game.isSimulation()) { - game.informPlayer(this, "You can't block this creature."); - } - } - - @Override - public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game) { - return searchLibrary(target, source, game, playerId); - } - - @Override - public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) { - //20091005 - 701.14c - - // searching control can be intercepted by another player, see Opposition Agent - SearchLibraryEvent searchEvent = new SearchLibraryEvent(targetPlayerId, source, playerId, Integer.MAX_VALUE); - if (game.replaceEvent(searchEvent)) { - return false; - } - - Player targetPlayer = game.getPlayer(targetPlayerId); - Player searchingPlayer = this; - Player searchingController = game.getPlayer(searchEvent.getSearchingControllerId()); - if (targetPlayer == null || searchingController == null) { - return false; - } - - String searchInfo = searchingPlayer.getLogName(); - if (!searchingPlayer.getId().equals(searchingController.getId())) { - searchInfo = searchInfo + " under control of " + searchingPlayer.getLogName(); - } - if (targetPlayer.getId().equals(searchingPlayer.getId())) { - searchInfo = searchInfo + " searches their library"; - } else { - searchInfo = searchInfo + " searches the library of " + targetPlayer.getLogName(); - } - - if (!game.isSimulation()) { - game.informPlayers(searchInfo + CardUtil.getSourceLogName(game, source)); - } - - // https://www.reddit.com/r/magicTCG/comments/jj8gh9/opposition_agent_and_panglacial_wurm_interaction/ - // You must take full player control while searching, e.g. you can cast opponent's cards by Panglacial Wurm effect: - // * While you’re searching your library, you may cast Panglacial Wurm from your library. - // So use here same code as Word of Command - // P.S. no needs in searchingController, but it helps with unit tests, see TakeControlWhileSearchingLibraryTest - boolean takeControl = false; - if (!searchingPlayer.getId().equals(searchingController.getId())) { - CardUtil.takeControlUnderPlayerStart(game, searchingController, searchingPlayer, true); - takeControl = true; - } - - Library searchingLibrary = targetPlayer.getLibrary(); - TargetCardInLibrary newTarget = target.copy(); - int count; - int librarySearchLimit = searchEvent.getAmount(); - List cardsFromTop = null; - do { - // TODO: prevent shuffling from moving the visualized cards - if (librarySearchLimit == Integer.MAX_VALUE) { - count = searchingLibrary.count(target.getFilter(), game); - } else { - if (cardsFromTop == null) { - cardsFromTop = new ArrayList<>(searchingLibrary.getTopCards(game, librarySearchLimit)); - } else { - cardsFromTop.retainAll(searchingLibrary.getCards(game)); - } - newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size())); - count = Math.min(searchingLibrary.count(target.getFilter(), game), librarySearchLimit); - } - - if (count < target.getNumberOfTargets()) { - newTarget.setMinNumberOfTargets(count); - } - - // handling Panglacial Wurm - cast cards while searching from own library - if (targetPlayer.getId().equals(searchingPlayer.getId())) { - if (handleCastableCardsWhileLibrarySearching(library, game, targetPlayer)) { - // clear all choices to start from scratch (casted cards must be removed from library) - newTarget.clearChosen(); - continue; - } - } - - if (newTarget.choose(Outcome.Neutral, searchingController.getId(), targetPlayer.getId(), game)) { - target.getTargets().clear(); - for (UUID targetId : newTarget.getTargets()) { - target.add(targetId, game); - } - } - - // END SEARCH - if (takeControl) { - CardUtil.takeControlUnderPlayerEnd(game, searchingController, searchingPlayer); - game.informPlayers("Control of " + searchingPlayer.getLogName() + " is back" + CardUtil.getSourceLogName(game, source)); - } - - LibrarySearchedEvent searchedEvent = new LibrarySearchedEvent(targetPlayer.getId(), source, searchingPlayer.getId(), target); - if (!game.replaceEvent(searchedEvent)) { - game.fireEvent(searchedEvent); - } - break; - } while (true); - - return true; - } - - @Override - public boolean seekCard(FilterCard filter, Ability source, Game game) { - Set cards = this.getLibrary() - .getCards(game) - .stream() - .filter(card -> filter.match(card, source.getSourceId(), getId(), game)) - .collect(Collectors.toSet()); - Card card = RandomUtil.randomFromCollection(cards); - if (card == null) { - return false; - } - game.informPlayers(this.getLogName() + " seeks a card from their library"); - this.moveCards(card, Zone.HAND, source, game); - return true; - } - - @Override - public void lookAtAllLibraries(Ability source, Game game) { - for (UUID playerId : game.getState().getPlayersInRange(this.getId(), game)) { - Player player = game.getPlayer(playerId); - String playerName = this.getName().equals(player.getName()) ? "Your " : player.getName() + "'s "; - playerName += "library"; - Cards cardsInLibrary = new CardsImpl(player.getLibrary().getTopCards(game, player.getLibrary().size())); - lookAtCards(playerName, cardsInLibrary, game); - } - } - - private boolean handleCastableCardsWhileLibrarySearching(Library library, Game game, Player targetPlayer) { - // must return true after cast try (to restart searching process without casted cards) - // uses for handling Panglacial Wurm: - // * While you're searching your library, you may cast Panglacial Wurm from your library. - - List castableCards = library.getCards(game).stream() - .filter(card -> card.getAbilities(game).containsClass(WhileSearchingPlayFromLibraryAbility.class)) - .map(MageItem::getId) - .collect(Collectors.toList()); - if (castableCards.size() == 0) { - return false; - } - - // only humans can use it - if (targetPlayer.isComputer()) { - return false; - } - - if (!targetPlayer.chooseUse(Outcome.AIDontUseIt, "There are " + castableCards.size() + " cards you can cast while searching your library. Cast any of them?", null, game)) { - return false; - } - - boolean casted = false; - TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, StaticFilters.FILTER_CARD); - targetCard.setTargetName("card to cast from library"); - targetCard.setNotTarget(true); - while (castableCards.size() > 0) { - targetCard.clearChosen(); - if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, game)) { - break; - } - - Card card = game.getCard(targetCard.getFirstTarget()); - if (card == null) { - break; - } - - // AI NOTICE: if you want AI implement here then remove selected card from castable after each - // choice (otherwise you catch infinite freeze on uncastable use case) - // casting selected card - // TODO: fix costs (why is Panglacial Wurm automatically accepting payment?) - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); - targetPlayer.cast(targetPlayer.chooseAbilityForCast(card, game, false), game, false, null); - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); - castableCards.remove(card.getId()); - casted = true; - } - return casted; - } - - /** - * @param source - * @param game - * @param winnable - * @return if winnable, true if player won the toss, if not winnable, true - * for heads and false for tails - */ - @Override - public boolean flipCoin(Ability source, Game game, boolean winnable) { - boolean chosen = false; - if (winnable) { - chosen = this.chooseUse(Outcome.Benefit, "Heads or tails?", "", "Heads", "Tails", source, game); - game.informPlayers(getLogName() + " chose " + CardUtil.booleanToFlipName(chosen)); - } - boolean result = this.flipCoinResult(game); - FlipCoinEvent event = new FlipCoinEvent(playerId, source, result, chosen, winnable); - game.replaceEvent(event); - game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult()) - + CardUtil.getSourceLogName(game, source)); - if (event.getFlipCount() > 1) { - boolean canChooseHeads = event.getResult(); - boolean canChooseTails = !event.getResult(); - for (int i = 1; i < event.getFlipCount(); i++) { - boolean tempFlip = this.flipCoinResult(game); - canChooseHeads = canChooseHeads || tempFlip; - canChooseTails = canChooseTails || !tempFlip; - game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(tempFlip)); - } - if (canChooseHeads && canChooseTails) { - event.setResult(chooseUse(Outcome.Benefit, "Choose which flip to keep", - (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), - "Heads", "Tails", source, game - )); - } else { - event.setResult(canChooseHeads); - } - game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); - } - if (event.isWinnable()) { - game.informPlayers(getLogName() + " " + (event.getResult() == event.getChosen() ? "won" : "lost") + " the flip" - + CardUtil.getSourceLogName(game, source)); - } - game.fireEvent(event.createFlippedEvent()); - if (event.isWinnable()) { - return event.getResult() == event.getChosen(); - } - return event.getResult(); - } - - /** - * Return result for next flip coint try (can be contolled in tests) - * - * @return - */ - @Override - public boolean flipCoinResult(Game game) { - return RandomUtil.nextBoolean(); - } - - private static final class RollDieResult { - - // 706.2. - // After the roll, the number indicated on the top face of the die before any modifiers is - // the natural result. The instruction may include modifiers to the roll which add to or - // subtract from the natural result. Modifiers may also come from other sources. After - // considering all applicable modifiers, the final number is the result of the die roll. - private final int naturalResult; - private final int modifier; - private final PlanarDieRollResult planarResult; - - RollDieResult(int naturalResult, int modifier, PlanarDieRollResult planarResult) { - this.naturalResult = naturalResult; - this.modifier = modifier; - this.planarResult = planarResult; - } - - public int getResult() { - return this.naturalResult + this.modifier; - } - - public PlanarDieRollResult getPlanarResult() { - return this.planarResult; - } - } - - @Override - public int rollDieResult(int sides, Game game) { - return RandomUtil.nextInt(sides) + 1; - } - - /** - * Roll single die. Support both die types: planar and numerical. - * - * @param outcome - * @param game - * @param source - * @param rollDieType - * @param sidesAmount - * @param chaosSidesAmount - * @param planarSidesAmount - * @param rollsAmount - * @return - */ - private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { - if (rollsAmount == 1) { - return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount); - } - Set choices = new HashSet<>(); - for (int j = 0; j < rollsAmount; j++) { - choices.add(rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount)); - } - if (choices.size() == 1) { - return choices.stream().findFirst().orElse(0); - } - - // AI hint - use max/min values - if (this.isComputer()) { - if (rollDieType == RollDieType.NUMERICAL) { - // numerical - if (outcome.isGood()) { - return choices.stream() - .map(Integer.class::cast) - .max(Comparator.naturalOrder()) - .orElse(null); - } else { - return choices.stream() - .map(Integer.class::cast) - .min(Comparator.naturalOrder()) - .orElse(null); - } - } else { - // planar - // priority: chaos -> planar -> blank - return choices.stream() - .map(PlanarDieRollResult.class::cast) - .max(Comparator.comparingInt(PlanarDieRollResult::getAIPriority)) - .orElse(null); - } - } - - Choice choice = new ChoiceImpl(true); - choice.setMessage("Choose which die roll result to keep (the rest will be ignored)"); - choice.setChoices(choices.stream().sorted().map(Object::toString).collect(Collectors.toSet())); - - this.choose(Outcome.Neutral, choice, game); - Object defaultChoice = choices.iterator().next(); - return choices.stream() - .filter(o -> o.toString().equals(choice.getChoice())) - .findFirst() - .orElse(defaultChoice); - } - - private Object rollDieInnerWithReplacement(Game game, Ability source, RollDieType rollDieType, int numSides, int numChaosSides, int numPlanarSides) { - switch (rollDieType) { - - case NUMERICAL: { - int result = rollDieResult(numSides, game); - // Clam-I-Am workaround: - // If you roll a 3 on a six-sided die, you may reroll that die. - if (numSides == 6 - && result == 3 - && game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.REPLACE_ROLLED_DIE, source.getControllerId(), source, source.getControllerId())) - && chooseUse(Outcome.Neutral, "Re-roll the 3?", source, game)) { - result = rollDieResult(numSides, game); - } - return result; - } - - case PLANAR: { - if (numChaosSides + numPlanarSides > numSides) { - numChaosSides = GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES; - numPlanarSides = GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES; - } - // for 9 sides: - // 1..2 - chaos - // 3..7 - blank - // 8..9 - planar - int result = this.rollDieResult(numSides, game); - PlanarDieRollResult roll; - if (result <= numChaosSides) { - roll = PlanarDieRollResult.CHAOS_ROLL; - } else if (result > numSides - numPlanarSides) { - roll = PlanarDieRollResult.PLANAR_ROLL; - } else { - roll = PlanarDieRollResult.BLANK_ROLL; - } - return roll; - } - - default: { - throw new IllegalArgumentException("Unknown roll die type " + rollDieType); - } - } - } - - /** - * @param outcome - * @param source - * @param game - * @param sidesAmount number of sides the dice has - * @param rollsAmount number of tries to roll the dice - * @param ignoreLowestAmount remove the lowest rolls from the results - * @return the number that the player rolled - */ - @Override - public List rollDice(Outcome outcome, Ability source, Game game, int sidesAmount, int rollsAmount, int ignoreLowestAmount) { - return rollDiceInner(outcome, source, game, RollDieType.NUMERICAL, sidesAmount, 0, 0, rollsAmount, ignoreLowestAmount) - .stream() - .map(Integer.class::cast) - .collect(Collectors.toList()); - } - - /** - * Inner code to roll a dice. Support normal and planar types. - * - * @param outcome - * @param source - * @param game - * @param rollDieType die type to roll, e.g. planar or numerical - * @param sidesAmount sides per die - * @param chaosSidesAmount for planar die: chaos sides - * @param planarSidesAmount for planar die: planar sides - * @param rollsAmount rolls - * @param ignoreLowestAmount for numerical die: ignore multiple rolls with - * the lowest values - * @return - */ - private List rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, - int rollsAmount, int ignoreLowestAmount) { - RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount); - if (ignoreLowestAmount > 0) { - rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount); - } - game.replaceEvent(rollDiceEvent); - - // 706.6. - // In a Planechase game, rolling the planar die will cause any ability that triggers whenever a - // player rolls one or more dice to trigger. However, any effect that refers to a numerical - // result of a die roll, including ones that compare the results of that roll to other rolls - // or to a given number, ignores the rolling of the planar die. See rule 901, “Planechase.” - // ROLL MULTIPLE dies - // results amount can be less than a rolls amount (example: The Big Idea allows rolling 2x instead 1x) - List dieResults = new ArrayList<>(); - List dieRolls = new ArrayList<>(); - for (int i = 0; i < rollDiceEvent.getAmount(); i++) { - // ROLL SINGLE die - RollDieEvent rollDieEvent = new RollDieEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides()); - game.replaceEvent(rollDieEvent); - - Object rollResult; - // big idea logic for numerical rolls only - if (rollDieEvent.getRollDieType() == RollDieType.NUMERICAL && rollDieEvent.getBigIdeaRollsAmount() > 0) { - // rolls 2x + sum results - // The Big Idea: roll two six-sided dice and use the total of those results - int totalSum = 0; - for (int j = 0; j < rollDieEvent.getBigIdeaRollsAmount() + 1; j++) { - int singleResult = (Integer) rollDieInner( - outcome, - game, - source, - rollDieEvent.getRollDieType(), - rollDieEvent.getSides(), - chaosSidesAmount, - planarSidesAmount, - rollDieEvent.getRollsAmount()); - totalSum += singleResult; - dieRolls.add(new RollDieResult(singleResult, rollDieEvent.getResultModifier(), null)); - } - rollResult = totalSum; - } else { - // rolls 1x - switch (rollDieEvent.getRollDieType()) { - default: - case NUMERICAL: { - int naturalResult = (Integer) rollDieInner( - outcome, - game, - source, - rollDieEvent.getRollDieType(), - rollDieEvent.getSides(), - chaosSidesAmount, - planarSidesAmount, - rollDieEvent.getRollsAmount() - ); - dieRolls.add(new RollDieResult(naturalResult, rollDieEvent.getResultModifier(), null)); - rollResult = naturalResult; - break; - } - - case PLANAR: { - PlanarDieRollResult planarResult = (PlanarDieRollResult) rollDieInner( - outcome, - game, - source, - rollDieEvent.getRollDieType(), - rollDieEvent.getSides(), - chaosSidesAmount, - planarSidesAmount, - rollDieEvent.getRollsAmount() - ); - dieRolls.add(new RollDieResult(0, 0, planarResult)); - rollResult = planarResult; - break; - } - } - } - dieResults.add(rollResult); - } - - // ignore the lowest results - // planar dies: due to 706.6. planar die results must be fully ignored - // - // 706.5. - // If a player is instructed to roll two or more dice and ignore the lowest roll, the roll - // that yielded the lowest result is considered to have never happened. No abilities trigger - // because of the ignored roll, and no effects apply to that roll. If multiple results are tied - // for the lowest, the player chooses one of those rolls to be ignored. - int diceRolledTotal = dieRolls.size(); - String ignoreMessage; - if (rollDiceEvent.getRollDieType() == RollDieType.NUMERICAL && rollDiceEvent.getIgnoreLowestAmount() > 0) { - // find ignored values - List ignoredResults = new ArrayList<>(); - for (int i = 0; i < rollDiceEvent.getIgnoreLowestAmount(); i++) { - int min = dieResults.stream().map(Integer.class::cast).mapToInt(Integer::intValue).min().orElse(0); - dieResults.remove(Integer.valueOf(min)); - ignoredResults.add(min); - } - ignoreMessage = String.format( - ignoredResults.size() > 1 ? ", ignoring [%s]" : ", ignoring %s", - ignoredResults - .stream() - .map(x -> "" + x) - .collect(Collectors.joining(", ")) - ); - // remove ignored rolls (they not exist anymore) - List newRolls = new ArrayList<>(); - for (RollDieResult rollDieResult : dieRolls) { - if (ignoredResults.contains(rollDieResult.getResult())) { - ignoredResults.remove((Integer) rollDieResult.getResult()); - } else { - newRolls.add(rollDieResult); - } - } - dieRolls.clear(); - dieRolls.addAll(newRolls); - } else { - ignoreMessage = ""; - } - - // raise affected roll events - for (RollDieResult result : dieRolls) { - game.fireEvent(new DieRolledEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides(), result.naturalResult, result.modifier, result.planarResult)); - } - game.fireEvent(new DiceRolledEvent(rollDiceEvent.getSides(), dieResults, source)); - - String resultString = dieResults - .stream() - .map(Object::toString) - .collect(Collectors.joining(", ")); - String message; - switch (rollDiceEvent.getRollDieType()) { - default: - case NUMERICAL: - // [Roll a die] user rolled 4d6, results: [4, 6], ignoring [1, 3] (source: xxx) - message = String.format("[Roll a die] %s rolled %sd%s, result%s: %s%s%s", - getLogName(), - diceRolledTotal > 1 ? diceRolledTotal : "a ", - rollDiceEvent.getSides(), - dieResults.size() > 1 ? 's' : "", - dieResults.size() > 1 ? '[' + resultString + ']' : resultString, - ignoreMessage, - CardUtil.getSourceLogName(game, source)); - break; - case PLANAR: - // [Roll a planar die] user rolled CHAOS (source: xxx) - message = String.format("[Roll a planar die] %s rolled %s%s", - getLogName(), - dieResults.size() > 1 ? '[' + resultString + ']' : resultString, - CardUtil.getSourceLogName(game, source)); - break; - } - game.informPlayers(message); - return dieResults; - } - - /** - * @param source - * @param game - * @param chaosSidesAmount The number of chaos sides the planar die - * currently has (normally 1 but can be 5) - * @param planarSidesAmount The number of chaos sides the planar die - * currently has (normally 1) - * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll - * or BlankRoll - */ - @Override - public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int chaosSidesAmount, int planarSidesAmount) { - return rollDiceInner(outcome, source, game, RollDieType.PLANAR, GameOptions.PLANECHASE_PLANAR_DIE_TOTAL_SIDES, chaosSidesAmount, planarSidesAmount, 1, 0) - .stream() - .map(o -> (PlanarDieRollResult) o) - .findFirst() - .orElse(PlanarDieRollResult.BLANK_ROLL); - } - - @Override - public List getAvailableAttackers(Game game) { - // TODO: get available opponents and their planeswalkers, check for each if permanent can attack one - return getAvailableAttackers(null, game); - } - - @Override - public List getAvailableAttackers(UUID defenderId, Game game) { - FilterCreatureForCombat filter = new FilterCreatureForCombat(); - List attackers = game.getBattlefield().getAllActivePermanents(filter, playerId, game); - attackers.removeIf(entry -> !entry.canAttack(defenderId, game)); - return attackers; - } - - @Override - public List getAvailableBlockers(Game game) { - FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock(); - return game.getBattlefield().getAllActivePermanents(blockFilter, playerId, game); - } - - /** - * Returns the mana options the player currently has. That means which - * combinations of mana are available to cast spells or activate abilities - * etc. - * - * @param game - * @return - */ - @Override - public ManaOptions getManaAvailable(Game game) { - boolean oldState = game.inCheckPlayableState(); - game.setCheckPlayableState(true); - - ManaOptions availableMana = new ManaOptions(); - availableMana.addMana(manaPool.getMana()); - // conditional mana - for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { - availableMana.addMana(conditionalMana); - } - - List> sourceWithoutManaCosts = new ArrayList<>(); - List> sourceWithCosts = new ArrayList<>(); - for (Card card : getHand().getCards(game)) { - Abilities manaAbilities - = card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game); - for (Iterator it = manaAbilities.iterator(); it.hasNext();) { - ActivatedManaAbilityImpl ability = it.next(); - Abilities noTapAbilities = new AbilitiesImpl<>(ability); - if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { - sourceWithoutManaCosts.add(noTapAbilities); - } else { - sourceWithCosts.add(noTapAbilities); - } - } - } - - for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range - Boolean canUse = null; - boolean canAdd = false; - boolean useLater = false; // sources with mana costs or mana pool dependency - Abilities manaAbilities - = permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true - for (Iterator it = manaAbilities.iterator(); it.hasNext();) { - ActivatedManaAbilityImpl ability = it.next(); - if (canUse == null) { - canUse = permanent.canUseActivatedAbilities(game); - } - if (canUse) { - // abilities without Tap costs have to be handled as separate sources, because they can be used also - if (!ability.hasTapCost()) { - it.remove(); - Abilities noTapAbilities = new AbilitiesImpl<>(ability); - if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { - sourceWithoutManaCosts.add(noTapAbilities); - } else { - sourceWithCosts.add(noTapAbilities); - } - continue; - } - - canAdd = true; - if (!ability.getManaCosts().isEmpty() || ability.isPoolDependant()) { - useLater = true; - break; - } - } - } - if (canAdd) { - if (useLater) { - sourceWithCosts.add(manaAbilities); - } else { - sourceWithoutManaCosts.add(manaAbilities); - } - } - } - - for (Abilities manaAbilities : sourceWithoutManaCosts) { - availableMana.addMana(manaAbilities, game); - } - - boolean anAbilityWasUsed = true; - boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production - while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) { - anAbilityWasUsed = false; - for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext();) { - Abilities manaAbilities = iterator.next(); - if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) { - boolean used; - if (manaAbilities.hasPoolDependantAbilities()) { - used = availableMana.addManaPoolDependant(manaAbilities, game); - } else { - used = availableMana.addManaWithCost(manaAbilities, game); - } - if (used) { - iterator.remove(); - availableMana.removeDuplicated(); - anAbilityWasUsed = true; - } - } - } - if (!anAbilityWasUsed && !usePoolDependantAbilities) { - usePoolDependantAbilities = true; - anAbilityWasUsed = true; - } - } - - // remove duplicated variants (see ManaOptionsTest for info - when that rises) - availableMana.removeDuplicated(); - - game.setCheckPlayableState(oldState); - return availableMana; - } - - /** - * Used during calculation of available mana to gather the amount of - * producable triggered mana caused by using mana sources. So the set value - * is only used during the calculation of the mana produced by one source - * and cleared thereafter - * - * @param netManaAvailable the net mana produced by the triggered mana - * abaility - */ - @Override - public void addAvailableTriggeredMana(List netManaAvailable - ) { - this.availableTriggeredManaList.add(netManaAvailable); - } - - /** - * Used during calculation of available mana to get the amount of producable - * triggered mana caused by using mana sources. The list is cleared as soon - * the value is retrieved during available mana calculation. - * - * @return - */ - @Override - public List> getAvailableTriggeredMana() { - return availableTriggeredManaList; - } - // returns only mana producers that don't require mana payment - - protected List getAvailableManaProducers(Game game) { - List result = new ArrayList<>(); - for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range - Boolean canUse = null; - boolean canAdd = false; - for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { - if (!ability.getManaCosts().isEmpty()) { - canAdd = false; - break; - } - if (canUse == null) { - canUse = permanent.canUseActivatedAbilities(game); - } - if (canUse && ability.canActivate(playerId, game).canActivate()) { - canAdd = true; - } - } - if (canAdd) { - result.add(permanent); - } - } - for (Card card : getHand().getCards(game)) { - boolean canAdd = false; - for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.HAND)) { - if (!ability.getManaCosts().isEmpty()) { - canAdd = false; - break; - } - if (ability.canActivate(playerId, game).canActivate()) { - canAdd = true; - } - } - if (canAdd) { - result.add(card); - } - } - return result; - } - - // returns only mana producers that require mana payment - public List getAvailableManaProducersWithCost(Game game) { - List result = new ArrayList<>(); - for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { - Boolean canUse = null; - for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { - if (canUse == null) { - canUse = permanent.canUseActivatedAbilities(game); - } - if (canUse && ability.canActivate(playerId, game).canActivate() - && !ability.getManaCosts().isEmpty()) { - result.add(permanent); - break; - } - } - } - return result; - } - - /** - * @param ability - * @param availableMana if null, it won't be checked if enough mana is - * available - * @param sourceObject - * @param game - * @return - */ - protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { - if (!(ability instanceof ActivatedManaAbilityImpl)) { - ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability - if (!copy.canActivate(playerId, game).canActivate()) { - return false; - } - if (availableMana != null) { - sourceObject.adjustCosts(copy, game); - game.getContinuousEffects().costModification(copy, game); - } - boolean canBeCastRegularly = true; - if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { - // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... - // 117.6a (...) If an alternative cost is applied to an unpayable cost, - // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. - canBeCastRegularly = false; - } - if (canBeCastRegularly) { - if (canPayMinimumManaCost(copy, availableMana, game)) { - return true; - } - } - - // ALTERNATIVE COST FROM dynamic effects - if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) { - ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()); - Costs costs = getCastSourceIdCosts().get(copy.getSourceId()); - - boolean canPutToPlay = true; - if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { - canPutToPlay = false; - } - if (costs != null && !costs.canPay(copy, copy, playerId, game)) { - canPutToPlay = false; - } - - if (canPutToPlay) { - return true; - } - } - - // ALTERNATIVE COST from source card (any AlternativeSourceCosts) - if (AbilityType.SPELL.equals(ability.getAbilityType())) { - return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); - } - } - return false; - } - - protected boolean canPayMinimumManaCost(ActivatedAbility ability, ManaOptions availableMana, Game game) { - ManaOptions abilityOptions = ability.getMinimumCostToActivate(playerId, game); - if (abilityOptions.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - // Check for pay option with like phyrexian mana - if (getPhyrexianColors() != null) { - addPhyrexianLikePayOptions(abilityOptions, availableMana, game); - } - - ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(), - AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); - for (Mana mana : abilityOptions) { - if (mana.count() == 0) { - return true; - } - for (Mana avail : availableMana) { - // TODO: SPEND_OTHER_MANA effects with getAsThoughManaType can change mana type to pay, - // but that code processing it as any color, need to test and fix another use cases - // (example: Sunglasses of Urza - may spend white mana as though it were red mana) - - // - // add tests for non any color like Sunglasses of Urza - if (approvingObject != null && mana.count() <= avail.count()) { - return true; - } - if (avail instanceof ConditionalMana && !((ConditionalMana) avail).apply(ability, game, getId(), ability.getManaCosts())) { - continue; - } - if (mana.enough(avail)) { // here we need to check if spend mana as though allow to pay the mana cost - return true; - } - } - } - } - return false; - } - - private void addPhyrexianLikePayOptions(ManaOptions abilityOptions, ManaOptions availableMana, Game game) { - int maxLifeMana = getLife() / 2; - if (maxLifeMana > 0) { - Set phyrexianOptions = new HashSet<>(); - for (Mana mana : abilityOptions) { - int availableLifeMana = maxLifeMana; - if (getPhyrexianColors().isBlack()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLACK); - } - if (getPhyrexianColors().isBlue()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLUE); - } - if (getPhyrexianColors().isRed()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.RED); - } - if (getPhyrexianColors().isGreen()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.GREEN); - } - if (getPhyrexianColors().isWhite()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.WHITE); - } - } - abilityOptions.addAll(phyrexianOptions); - } - } - - private int createReducedManaPayOption(int availableLifeMana, Mana oldPayOption, Set phyrexianOptions, ManaType manaType) { - if (oldPayOption.get(manaType) > 0) { - Mana manaCopy = oldPayOption.copy(); - int restVal; - if (availableLifeMana > oldPayOption.get(manaType)) { - restVal = 0; - availableLifeMana -= oldPayOption.get(manaType); - } else { - restVal = CardUtil.overflowDec(oldPayOption.get(manaType), availableLifeMana); - availableLifeMana = 0; - } - manaCopy.set(manaType, restVal); - phyrexianOptions.add(manaCopy); - } - return availableLifeMana; - } - - protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) { - if (sourceObject != null && !(sourceObject instanceof Permanent)) { - Ability copyAbility; // for alternative cost and reduce tries - for (Ability alternateSourceCostsAbility : sourceObject.getAbilities()) { - // if cast for noMana no Alternative costs are allowed - if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) { - if (((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) { - if (alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl(); - for (Cost cost : alternateSourceCostsAbility.getCosts()) { - // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost2) { - if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); - } - } else { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - } - - if (manaCosts.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - - // alternative cost reduce - copyAbility = ability.copy(); - copyAbility.getManaCostsToPay().clear(); - copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); - sourceObject.adjustCosts(copyAbility, game); - game.getContinuousEffects().costModification(copyAbility, game); - - // reduced all cost - if (copyAbility.getManaCostsToPay().isEmpty()) { - return true; - } - - for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - if (availableMana.enough(mana)) { - return true; - } - } - } - } - } - } - } - - // controller specific alternate spell costs - for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) { - if (alternateSourceCosts instanceof Ability) { - if (alternateSourceCosts.isAvailable(ability, game)) { - if (((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl(); - for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { - // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost2) { - if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); - } - } else { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - } - - if (manaCosts.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - - // alternative cost reduce - copyAbility = ability.copy(); - copyAbility.getManaCostsToPay().clear(); - copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); - sourceObject.adjustCosts(copyAbility, game); - game.getContinuousEffects().costModification(copyAbility, game); - - // reduced all cost - if (copyAbility.getManaCostsToPay().isEmpty()) { - return true; - } - - for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - if (availableMana.enough(mana)) { - return true; - } - } - } - } - } - } - } - } - return false; - } - - protected ActivatedAbility findActivatedAbilityFromPlayable(MageObject object, ManaOptions availableMana, Ability ability, Game game) { - - // special mana to pay spell cost - ManaOptions manaFull = availableMana.copy(); - if (ability instanceof SpellAbility) { - for (AlternateManaPaymentAbility altAbility : CardUtil.getAbilities(object, game).stream() - .filter(a -> a instanceof AlternateManaPaymentAbility) - .map(a -> (AlternateManaPaymentAbility) a) - .collect(Collectors.toList())) { - ManaOptions manaSpecial = altAbility.getManaOptions(ability, game, ability.getManaCostsToPay()); - manaFull.addMana(manaSpecial); - } - } - - // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) - if (ability instanceof ActivatedManaAbilityImpl) { - // mana ability - if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) { - return (ActivatedManaAbilityImpl) ability; - } - } else if (ability instanceof AlternativeSourceCosts) { - // alternative cost must be replaced by real play ability - return findActivatedAbilityFromAlternativeSourceCost(object, manaFull, ability, game); - } else if (ability instanceof ActivatedAbility) { - // all other activated ability - if (canPlay((ActivatedAbility) ability, manaFull, object, game)) { - return (ActivatedAbility) ability; - } - } - - // non playable abilities like static - return null; - } - - protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions availableMana, Ability ability, Game game) { - // return play ability that can activate AlternativeSourceCosts - if (ability instanceof AlternativeSourceCosts && object != null && !(object instanceof Permanent)) { - ActivatedAbility playAbility = null; - if (object.isLand(game)) { - playAbility = (PlayLandAbility) CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); - } else if (object instanceof Card) { - playAbility = ((Card) object).getSpellAbility(); - } - if (playAbility == null) { - return null; - } - - // 707.4.Objects that are cast face down are turned face down before they are put onto the stack - // E.g. no lands per turn limit, no cast restrictions, cost reduce, etc - // Even mana cost can't be checked here without lookahead - // So make it available all the time - boolean canUse; - if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId()) - || (null != game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game)))) { - canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game); - } else { - canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions - } - - if (canUse) { - return playAbility; - } - } - return null; - } - - private void getPlayableFromObjectAll(Game game, Zone fromZone, MageObject object, ManaOptions availableMana, List output) { - if (fromZone == null || object == null) { - return; - } - - // BASIC abilities - if (object instanceof SplitCard) { - SplitCard mainCard = (SplitCard) object; - getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof ModalDoubleFacesCard) { - ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) object; - getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof AdventureCard) { - // adventure must use different card characteristics for different spells (main or adventure) - AdventureCard adventureCard = (AdventureCard) object; - getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof Card) { - getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output); - } else if (object instanceof StackObject) { - // spells on stack are processing by Card above, other stack objects must be ignored - } else { - // other things like CommandObject - getPlayableFromObjectSingle(game, fromZone, object, object.getAbilities(), availableMana, output); - } - - // DYNAMIC ADDED abilities are adds in getAbilities(game) - } - - private void getPlayableFromObjectSingle(Game game, Zone fromZone, MageObject object, Abilities candidateAbilities, ManaOptions availableMana, List output) { - // check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller) - // must check all abilities, not activated only - for (Ability ability : candidateAbilities) { - if (!(ability instanceof ActivatedAbility)) { - continue; - } - boolean isPlaySpell = (ability instanceof SpellAbility); - boolean isPlayLand = (ability instanceof PlayLandAbility); - - // as original controller - // play land restrictions - if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( - GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), - ability, this.getId()), ability, game, true)) { - continue; - } - // cast spell restrictions 1 - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability, this.getId()); - castEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castEvent, ability, game, true)) { - continue; - } - // cast spell restrictions 2 - GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, - ability.getId(), ability, this.getId()); - castLateEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castLateEvent, ability, game, true)) { - continue; - } - - ApprovingObject approvingObject; - if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) { - // play hand from non hand zone (except battlefield - you can't play already played permanents) - approvingObject = game.getContinuousEffects().asThough(object.getId(), - AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); - } else { - // other abilities from direct zones - approvingObject = null; - } - - boolean canActivateAsHandZone = approvingObject != null - || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); - boolean possibleToPlay = canActivateAsHandZone - && ability.getZone().match(Zone.HAND) - && (isPlaySpell || isPlayLand); - - // spell/hand abilities (play from all zones) - // need permitingObject or canPlayCardsFromGraveyard - // zone's abilities (play from specific zone) - // no need in permitingObject - if (fromZone != Zone.ALL && ability.getZone().match(fromZone)) { - possibleToPlay = true; - } - - if (!possibleToPlay) { - continue; - } - - // direct mode (with original controller) - ActivatedAbility playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); - if (playAbility != null && !output.contains(playAbility)) { - output.add(playAbility); - continue; - } - - // from non hand mode (with affected controller) - if (canActivateAsHandZone && ability.getControllerId() != this.getId()) { - UUID savedControllerId = ability.getControllerId(); - ability.setControllerId(this.getId()); - try { - playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); - if (playAbility != null && !output.contains(playAbility)) { - output.add(playAbility); - } - } finally { - ability.setControllerId(savedControllerId); - } - } - } - } - - @Override - public List getPlayable(Game game, boolean hidden) { - return getPlayable(game, hidden, Zone.ALL, true); - } - - /** - * Returns a list of all available spells and abilities the player can - * currently cast/activate with his available resources - * - * @param game - * @param hidden also from hidden objects (e.g. turned face down cards ?) - * @param fromZone of objects from which zone (ALL = from all zones) - * @param hideDuplicatedAbilities if equal abilities exist return only the - * first instance - * @return - */ - public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { - List playable = new ArrayList<>(); - if (shouldSkipGettingPlayable(game)) { - return playable; - } - - boolean previousState = game.inCheckPlayableState(); - game.setCheckPlayableState(true); - try { - ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition)) - boolean fromAll = fromZone.equals(Zone.ALL); - if (hidden && (fromAll || fromZone == Zone.HAND)) { - for (Card card : hand.getCards(game)) { - for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) - if (ability.getZone().match(Zone.HAND)) { - boolean isPlaySpell = (ability instanceof SpellAbility); - boolean isPlayLand = (ability instanceof PlayLandAbility); - - // play land restrictions - if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( - GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), - ability, this.getId()), ability, game, true)) { - continue; - } - // cast spell restrictions 1 - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, - ability.getId(), ability, this.getId()); - castEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castEvent, ability, game, true)) { - continue; - } - // cast spell restrictions 2 - GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, - ability.getId(), ability, this.getId()); - castLateEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castLateEvent, ability, game, true)) { - continue; - } - - ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); - if (playAbility != null && !playable.contains(playAbility)) { - playable.add(playAbility); - } - } - } - } - } - - if (fromAll || fromZone == Zone.GRAVEYARD) { - for (UUID playerId : game.getState().getPlayersInRange(getId(), game)) { - Player player = game.getPlayer(playerId); - if (player == null) { - continue; - } - for (Card card : player.getGraveyard().getCards(game)) { - getPlayableFromObjectAll(game, Zone.GRAVEYARD, card, availableMana, playable); - } - } - } - - if (fromAll || fromZone == Zone.EXILED) { - for (ExileZone exile : game.getExile().getExileZones()) { - for (Card card : exile.getCards(game)) { - getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable); - } - } - } - - // check to play revealed cards - if (fromAll) { - for (Cards revealedCards : game.getState().getRevealed().values()) { - for (Card card : revealedCards.getCards(game)) { - // revealed cards can be from any zones - getPlayableFromObjectAll(game, game.getState().getZone(card.getId()), card, availableMana, playable); - } - } - } - - // outside cards - if (fromAll || fromZone == Zone.OUTSIDE) { - // companion cards - for (Cards companionCards : game.getState().getCompanion().values()) { - for (Card card : companionCards.getCards(game)) { - getPlayableFromObjectAll(game, Zone.OUTSIDE, card, availableMana, playable); - } - } - - // sideboard cards (example: Wish) - for (UUID sideboardCardId : this.getSideboard()) { - Card sideboardCard = game.getCard(sideboardCardId); - if (sideboardCard != null) { - getPlayableFromObjectAll(game, Zone.OUTSIDE, sideboardCard, availableMana, playable); - } - } - } - - // check if it's possible to play the top card of a library - if (fromAll || fromZone == Zone.LIBRARY) { - for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { - Player player = game.getPlayer(playerInRangeId); - if (player != null && player.getLibrary().hasCards()) { - Card card = player.getLibrary().getFromTop(game); - if (card != null) { - getPlayableFromObjectAll(game, Zone.LIBRARY, card, availableMana, playable); - } - } - } - } - - // check the hand zone (Sen Triplets) - // TODO: remove direct hand check (reveal fix in Sen Triplets)? - // human games: cards from opponent's hand must be revealed before play - // AI games: computer can see and play cards from opponent's hand without reveal - if (fromAll || fromZone == Zone.HAND) { - for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { - Player player = game.getPlayer(playerInRangeId); - if (player != null && !player.getHand().isEmpty()) { - for (Card card : player.getHand().getCards(game)) { - if (card != null) { - getPlayableFromObjectAll(game, Zone.HAND, card, availableMana, playable); - } - } - } - } - } - - // eliminate duplicate activated abilities (uses for AI plays) - Map activatedUnique = new HashMap<>(); - List activatedAll = new ArrayList<>(); - - // activated abilities from battlefield objects - if (fromAll || fromZone == Zone.BATTLEFIELD) { - for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { - boolean canUseActivated = permanent.canUseActivatedAbilities(game); - List currentPlayable = new ArrayList<>(); - getPlayableFromObjectAll(game, Zone.BATTLEFIELD, permanent, availableMana, currentPlayable); - for (ActivatedAbility ability : currentPlayable) { - if (ability instanceof SpecialAction || canUseActivated) { - activatedUnique.putIfAbsent(ability.toString(), ability); - activatedAll.add(ability); - } - } - } - } - - // activated abilities from stack objects - if (fromAll || fromZone == Zone.STACK) { - for (StackObject stackObject : game.getState().getStack()) { - List currentPlayable = new ArrayList<>(); - getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable); - for (ActivatedAbility ability : currentPlayable) { - activatedUnique.put(ability.toString(), ability); - activatedAll.add(ability); - } - } - } - - // activated abilities from objects in the command zone (emblems or commanders) - if (fromAll || fromZone == Zone.COMMAND) { - for (CommandObject commandObject : game.getState().getCommand()) { - List currentPlayable = new ArrayList<>(); - getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable); - for (ActivatedAbility ability : currentPlayable) { - activatedUnique.put(ability.toString(), ability); - activatedAll.add(ability); - } - } - } - - if (hideDuplicatedAbilities) { - playable.addAll(activatedUnique.values()); - } else { - playable.addAll(activatedAll); - } - } finally { - game.setCheckPlayableState(previousState); - } - - return playable; - } - - /** - * Creates a list of card ids that are currently playable.
- * Used to mark the playable cards in GameView Also contains number of - * playable abilities for that object (it's just info, server decides to - * show choose dialog or not) - * - * @param game - * @return A Set of cardIds that are playable and amount of playable - * abilities - */ - @Override - public PlayableObjectsList getPlayableObjects(Game game, Zone zone) { - // collect abilities per object - List playableAbilities = getPlayable(game, true, zone, false); // do not hide duplicated abilities/cards - Map> playableObjects = new HashMap<>(); - for (ActivatedAbility ability : playableAbilities) { - if (ability.getSourceId() != null) { - - // normal card - putToPlayableObjects(playableObjects, ability.getSourceId(), ability); - - // main card - must be marked playable in GUI - Card card = game.getCard(ability.getSourceId()); - if (card != null && card.getMainCard().getId() != card.getId()) { - putToPlayableObjects(playableObjects, card.getMainCard().getId(), ability); - } - - // spell on stack - can have activated abilities, - // so mark it as playable too (users must able to clicks on stack objects) - // example: Lightning Storm - Spell spell = game.getSpell(ability.getSourceId()); - if (spell != null) { - putToPlayableObjects(playableObjects, spell.getId(), ability); - } - } - } - - // collect stats - PlayableObjectsList playableObjectsList = new PlayableObjectsList(playableObjects); - return playableObjectsList; - } - - private void putToPlayableObjects(Map> playableObjects, UUID objectId, ActivatedAbility ability) { - if (!playableObjects.containsKey(objectId)) { - playableObjects.put(objectId, new ArrayList<>()); - } - playableObjects.get(objectId).add(ability); - } - - /** - * Skip "silent" phase step when players are not allowed to cast anything. - * E.g. players can't play or cast anything during declaring attackers. - * - * @param game - * @return - */ - private boolean shouldSkipGettingPlayable(Game game) { - if (game.getStep() == null) { // happens at the start of the game - return true; - } - for (Entry phaseStep : silentPhaseSteps.entrySet()) { - if (game.getPhase() != null - && game.getPhase().getStep() != null - && phaseStep.getKey() == game.getPhase().getStep().getType()) { - if (phaseStep.getValue() == null - || phaseStep.getValue() == game.getPhase().getStep().getStepPart()) { - return true; - } - } - } - return false; - } - - /** - * Only used for AIs - * - * @param ability - * @param game - * @return - */ - @Override - public List getPlayableOptions(Ability ability, Game game) { - List options = new ArrayList<>(); - if (ability.isModal()) { - addModeOptions(options, ability, game); - } else if (!ability.getTargets().getUnchosen().isEmpty()) { - // TODO: Handle other variable costs than mana costs - if (!ability.getManaCosts().getVariableCosts().isEmpty()) { - addVariableXOptions(options, ability, 0, game); - } else { - addTargetOptions(options, ability, 0, game); - } - } else if (!ability.getCosts().getTargets().getUnchosen().isEmpty()) { - addCostTargetOptions(options, ability, 0, game); - } - - return options; - } - - private void addModeOptions(List options, Ability option, Game game) { - // TODO: Support modal spells with more than one selectable mode - for (Mode mode : option.getModes().values()) { - Ability newOption = option.copy(); - newOption.getModes().clearSelectedModes(); - newOption.getModes().addSelectedMode(mode.getId()); - newOption.getModes().setActiveMode(mode); - if (!newOption.getTargets().getUnchosen().isEmpty()) { - if (!newOption.getManaCosts().getVariableCosts().isEmpty()) { - addVariableXOptions(options, newOption, 0, game); - } else { - addTargetOptions(options, newOption, 0, game); - } - } else if (!newOption.getCosts().getTargets().getUnchosen().isEmpty()) { - addCostTargetOptions(options, newOption, 0, game); - } else { - options.add(newOption); - } - } - } - - protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) { - addTargetOptions(options, option, targetNum, game); - } - - protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { - for (Target target : option.getTargets().getUnchosen().get(targetNum).getTargetOptions(option, game)) { - Ability newOption = option.copy(); - if (target instanceof TargetAmount) { - for (UUID targetId : target.getTargets()) { - int amount = target.getTargetAmount(targetId); - newOption.getTargets().get(targetNum).addTarget(targetId, amount, newOption, game, true); - } - } else { - for (UUID targetId : target.getTargets()) { - newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); - } - } - if (targetNum < option.getTargets().size() - 2) { - addTargetOptions(options, newOption, targetNum + 1, game); - } else if (!option.getCosts().getTargets().isEmpty()) { - addCostTargetOptions(options, newOption, 0, game); - } else { - options.add(newOption); - } - } - } - - private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) { - for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(option.getSourceId(), playerId, game)) { - Ability newOption = option.copy(); - newOption.getCosts().getTargets().get(targetNum).addTarget(targetId, option, game, true); - if (targetNum < option.getCosts().getTargets().size() - 1) { - addCostTargetOptions(options, newOption, targetNum + 1, game); - } else { - options.add(newOption); - } - } - } - - @Override - public boolean isTestsMode() { - return isTestMode; - } - - @Override - public void setTestMode(boolean value) { - this.isTestMode = value; - } - - @Override - public boolean isTopCardRevealed() { - return topCardRevealed; - } - - @Override - public void setTopCardRevealed(boolean topCardRevealed) { - this.topCardRevealed = topCardRevealed; - } - - @Override - public UserData getUserData() { - return this.userData; - } - - public UserData getControllingPlayersUserData(Game game) { - if (!isGameUnderControl()) { - Player player = game.getPlayer(getTurnControlledBy()); - if (player.isHuman()) { - return player.getUserData(); - } - } - return this.userData; - } - - @Override - public void setUserData(UserData userData) { - this.userData = userData; - getManaPool().setAutoPayment(userData.isManaPoolAutomatic()); - getManaPool().setAutoPaymentRestricted(userData.isManaPoolAutomaticRestricted()); - } - - @Override - public void addAction(String action - ) { - // do nothing - } - - @Override - public int getActionCount() { - return 0; - } - - @Override - public void setAllowBadMoves(boolean allowBadMoves) { - // do nothing - } - - @Override - public boolean canPayLifeCost(Ability ability) { - if (!canPayLifeCost - && (AbilityType.ACTIVATED.equals(ability.getAbilityType()) - || AbilityType.SPELL.equals(ability.getAbilityType()))) { - return false; - } - return isLifeTotalCanChange(); - } - - @Override - public boolean getCanPayLifeCost() { - return canPayLifeCost; - } - - @Override - public void setCanPayLifeCost(boolean canPayLifeCost) { - this.canPayLifeCost = canPayLifeCost; - } - - @Override - public boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game) { - return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, source.getSourceId(), controllerId, game); - } - - @Override - public void setCanPaySacrificeCostFilter(FilterPermanent filter - ) { - this.sacrificeCostFilter = filter; - } - - @Override - public FilterPermanent getSacrificeCostFilter() { - return sacrificeCostFilter; - } - - @Override - public boolean canLoseByZeroOrLessLife() { - return loseByZeroOrLessLife; - } - - @Override - public void setLoseByZeroOrLessLife(boolean loseByZeroOrLessLife) { - this.loseByZeroOrLessLife = loseByZeroOrLessLife; - } - - @Override - public boolean canPlayCardsFromGraveyard() { - return canPlayCardsFromGraveyard; - } - - @Override - public void setPlayCardsFromGraveyard(boolean playCardsFromGraveyard) { - this.canPlayCardsFromGraveyard = playCardsFromGraveyard; - } - - @Override - public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) { - this.drawsOnOpponentsTurn = drawsOnOpponentsTurn; - } - - @Override - public boolean isDrawsOnOpponentsTurn() { - return drawsOnOpponentsTurn; - } - - @Override - public boolean autoLoseGame() { - return false; - } - - @Override - public void becomesActivePlayer() { - this.passedAllTurns = false; - this.passedUntilEndStepBeforeMyTurn = false; - this.turns++; - } - - @Override - public int getTurns() { - return turns; - } - - @Override - public int getStoredBookmark() { - return storedBookmark; - } - - @Override - public void setStoredBookmark(int storedBookmark) { - this.storedBookmark = storedBookmark; - } - - @Override - public synchronized void resetStoredBookmark(Game game) { - if (this.storedBookmark != -1) { - game.removeBookmark(this.storedBookmark); - } - setStoredBookmark(-1); - } - - @Override - public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { - if (null != game.getContinuousEffects().asThough(card.getId(), - AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) { - // two modes: look at the card or do not look and activate other abilities - String lookMessage = "Look at " + card.getIdName(); - String lookYes = "Yes, look at the card"; - String lookNo = "No, play/activate the card/ability"; - if (chooseUse(Outcome.Benefit, lookMessage, "", lookYes, lookNo, null, game)) { - Cards cards = new CardsImpl(card); - this.lookAtCards(getName() + " - " + card.getIdName() + " - " - + CardUtil.sdf.format(System.currentTimeMillis()), cards, game); - return true; - } - } - return false; - } - - @Override - public void setPriorityTimeLeft(int timeLeft - ) { - priorityTimeLeft = timeLeft; - } - - @Override - public int getPriorityTimeLeft() { - return priorityTimeLeft; - } - - @Override - public boolean hasQuit() { - return quit; - } - - @Override - public boolean hasTimerTimeout() { - return timerTimeout; - } - - @Override - public boolean hasIdleTimeout() { - return idleTimeout; - } - - @Override - public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving) { - this.reachedNextTurnAfterLeaving = reachedNextTurnAfterLeaving; - } - - @Override - public boolean hasReachedNextTurnAfterLeaving() { - return reachedNextTurnAfterLeaving; - } - - @Override - public boolean canJoinTable(Table table - ) { - return !table.userIsBanned(name); - } - - @Override - public void addCommanderId(UUID commanderId - ) { - this.commandersIds.add(commanderId); - } - - @Override - public Set getCommandersIds() { - return this.commandersIds; - } - - @Override - public boolean moveCards(Card card, Zone toZone, Ability source, Game game) { - return moveCards(card, toZone, source, game, false, false, false, null); - } - - @Override - public boolean moveCards(Card card, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { - Set cardList = new HashSet<>(); - if (card != null) { - cardList.add(card); - } - return moveCards(cardList, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); - } - - @Override - public boolean moveCards(Cards cards, Zone toZone, Ability source, Game game) { - return moveCards(cards.getCards(game), toZone, source, game); - } - - @Override - public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game - ) { - return moveCards(cards, toZone, source, game, false, false, false, null); - } - - @Override - public boolean moveCards(Set cards, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { - if (cards.isEmpty()) { - return true; - } - Set successfulMovedCards = new LinkedHashSet<>(); - Zone fromZone = null; - switch (toZone) { - case GRAVEYARD: - fromZone = game.getState().getZone(cards.iterator().next().getId()); - successfulMovedCards = moveCardsToGraveyardWithInfo(cards, source, game, fromZone); - return !successfulMovedCards.isEmpty(); - case BATTLEFIELD: // new logic that does not yet add the permanents to battlefield while replacement effects are handled - List infoList = new ArrayList<>(); - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - ZoneChangeEvent event = new ZoneChangeEvent(card.getId(), source, - byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects); - infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped, source)); - } - infoList = ZonesHandler.moveCards(infoList, game, source); - for (ZoneChangeInfo info : infoList) { - Permanent permanent = game.getPermanent(info.event.getTargetId()); - if (permanent != null) { - successfulMovedCards.add(permanent); - if (!game.isSimulation()) { - Player eventPlayer = game.getPlayer(info.event.getPlayerId()); - if (eventPlayer != null && fromZone != null) { - game.informPlayers(eventPlayer.getLogName() + " puts " - + (info.faceDown ? "a card face down " : permanent.getLogName()) + " from " - + fromZone.toString().toLowerCase(Locale.ENGLISH) + " onto the Battlefield" - + CardUtil.getSourceLogName(game, source, permanent.getId())); - } - } - } - } - // TODO: must be replaced by game.getState().processAction(game), see isInUseableZoneDiesTrigger comments - // about short living LKI problem - //game.getState().processAction(game); - game.applyEffects(); - break; - case HAND: - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - boolean hideCard = fromZone == Zone.LIBRARY - || (card.isFaceDown(game) - && fromZone != Zone.STACK - && fromZone != Zone.BATTLEFIELD); - if (moveCardToHandWithInfo(card, source, game, !hideCard)) { - successfulMovedCards.add(card); - } - } - break; - case EXILED: - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - boolean withName = (fromZone == Zone.BATTLEFIELD - || fromZone == Zone.STACK) - || !card.isFaceDown(game); - if (moveCardToExileWithInfo(card, null, "", source, game, fromZone, withName)) { - successfulMovedCards.add(card); - } - } - break; - case LIBRARY: - for (Card card : cards) { - if (card instanceof Spell) { - fromZone = game.getState().getZone(((Spell) card).getSourceId()); - } else { - fromZone = game.getState().getZone(card.getId()); - } - boolean hideCard = fromZone == Zone.HAND || fromZone == Zone.LIBRARY; - if (moveCardToLibraryWithInfo(card, source, game, fromZone, true, !hideCard)) { - successfulMovedCards.add(card); - } - } - break; - case COMMAND: - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - if (moveCardToCommandWithInfo(card, source, game, fromZone)) { - successfulMovedCards.add(card); - } - } - break; - case OUTSIDE: - for (Card card : cards) { - if (card instanceof Permanent) { - game.getBattlefield().removePermanent(card.getId()); - ZoneChangeEvent event = new ZoneChangeEvent((Permanent) card, source, - byOwner ? card.getOwnerId() : getId(), Zone.BATTLEFIELD, Zone.OUTSIDE, appliedEffects); - game.fireEvent(event); - } - } - break; - default: - throw new UnsupportedOperationException("to Zone" + toZone + " not supported yet"); - } - return !successfulMovedCards.isEmpty(); - } - - @Override - public boolean moveCardsToExile(Card card, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { - Set cards = new HashSet<>(); - if (card != null) { - cards.add(card); - } - return moveCardsToExile(cards, source, game, withName, exileId, exileZoneName); - } - - @Override - public boolean moveCardsToExile(Set cards, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { - if (cards.isEmpty()) { - return true; - } - boolean result = false; - for (Card card : cards) { - Zone fromZone = game.getState().getZone(card.getId()); - result |= moveCardToExileWithInfo(card, exileId, exileZoneName, source, game, fromZone, withName); - } - return result; - } - - @Override - public boolean moveCardToHandWithInfo(Card card, Ability source, Game game, boolean withName) { - boolean result = false; - Zone fromZone = game.getState().getZone(card.getId()); - if (fromZone == Zone.BATTLEFIELD && !(card instanceof Permanent)) { - card = game.getPermanent(card.getId()); - } - if (card.moveToZone(Zone.HAND, source, game, false)) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - if (!game.isSimulation()) { - game.informPlayers(getLogName() + " puts " - + (withName ? card.getLogName() : (card.isFaceDown(game) ? "a face down card" : "a card")) - + " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' - + (card.isOwnedBy(this.getId()) ? "into their hand" : "into its owner's hand" - + CardUtil.getSourceLogName(game, source, card.getId())) - ); - } - result = true; - } - return result; - } - - @Override - public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, Game game, Zone fromZone) { - Set movedCards = new LinkedHashSet<>(); - while (!allCards.isEmpty()) { - // identify cards from one owner - Cards cards = new CardsImpl(); - UUID ownerId = null; - for (Iterator it = allCards.iterator(); it.hasNext();) { - Card card = it.next(); - if (cards.isEmpty()) { - ownerId = card.getOwnerId(); - } - if (card.isOwnedBy(ownerId)) { - it.remove(); - cards.add(card); - } - } - // move cards to graveyard in order the owner decides - if (!cards.isEmpty()) { - Player choosingPlayer = this; - if (!Objects.equals(ownerId, this.getId())) { - choosingPlayer = game.getPlayer(ownerId); - } - if (choosingPlayer == null) { - continue; - } - boolean chooseOrder = false; - if (userData.askMoveToGraveOrder()) { - if (cards.size() > 1) { - chooseOrder = choosingPlayer.chooseUse(Outcome.Neutral, - "Choose the order in which the cards go to the graveyard?", source, game); - } - } - if (chooseOrder) { - TargetCard target = new TargetCard(fromZone, - new FilterCard("card to put on the top of your graveyard (last one chosen will be topmost)")); - target.setRequired(true); - while (choosingPlayer.canRespond() && cards.size() > 1) { - choosingPlayer.chooseTarget(Outcome.Neutral, cards, target, source, game); - UUID targetObjectId = target.getFirstTarget(); - Card card = cards.get(targetObjectId, game); - cards.remove(targetObjectId); - if (card != null) { - fromZone = game.getState().getZone(card.getId()); - if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { - movedCards.add(card); - } - } - target.clearChosen(); - } - if (cards.size() == 1) { - Card card = cards.getCards(game).iterator().next(); - if (card != null && choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { - movedCards.add(card); - } - } - } else { - for (Card card : cards.getCards(game)) { - if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { - movedCards.add(card); - } - } - } - } - } - return movedCards; - } - - @Override - public boolean moveCardToGraveyardWithInfo(Card card, Ability source, Game game, Zone fromZone) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToZone(Zone.GRAVEYARD, source, game, false)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - StringBuilder sb = new StringBuilder(this.getLogName()) - .append(" puts ").append(card.getLogName()).append(' ').append(card.isCopy() ? "(Copy) " : "") - .append(fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' : ""); - if (card.isOwnedBy(getId())) { - sb.append("into their graveyard"); - } else { - sb.append("it into its owner's graveyard"); - } - sb.append(CardUtil.getSourceLogName(game, source, card.getId())); - game.informPlayers(sb.toString()); - } - result = true; - } - return result; - } - - @Override - public boolean moveCardToLibraryWithInfo(Card card, Ability source, Game game, Zone fromZone, boolean toTop, boolean withName) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToZone(Zone.LIBRARY, source, game, toTop)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - StringBuilder sb = new StringBuilder(this.getLogName()) - .append(" puts ").append(withName ? card.getLogName() : "a card").append(' '); - if (fromZone != null) { - sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); - } - sb.append("to the ").append(toTop ? "top" : "bottom"); - if (card.isOwnedBy(getId())) { - sb.append(" of their library"); - } else { - Player player = game.getPlayer(card.getOwnerId()); - if (player != null) { - sb.append(" of ").append(player.getLogName()).append("'s library"); - } - } - sb.append(CardUtil.getSourceLogName(game, source, card.getId())); - game.informPlayers(sb.toString()); - } - result = true; - } - return result; - } - - @Override - public boolean moveCardToCommandWithInfo(Card card, Ability source, Game game, Zone fromZone) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToZone(Zone.COMMAND, source, game, true)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - StringBuilder sb = new StringBuilder(this.getLogName()) - .append(" puts ").append(card.getLogName()).append(' '); - if (fromZone != null) { - sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); - } - if (card.isOwnedBy(getId())) { - sb.append(" to their command zone"); - } else { - Player player = game.getPlayer(card.getOwnerId()); - if (player != null) { - sb.append(" to ").append(player.getLogName()).append("'s command zone"); - } - } - sb.append(CardUtil.getSourceLogName(game, source, card.getId())); - game.informPlayers(sb.toString()); - } - result = true; - } - return result; - } - - @Override - public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, Ability source, Game game, Zone fromZone, boolean withName) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToExile(exileId, exileName, source, game)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard) { - // in case it's face down or name was changed by copying from other permanent - Card basicCard = game.getCard(card.getId()); - if (basicCard != null) { - card = basicCard; - } - } else if (card instanceof Spell) { - final Spell spell = (Spell) card; - if (spell.isCopy()) { - // copied spell, only remove from stack - game.getStack().remove(spell, game); - } - } - if (Zone.EXILED.equals(game.getState().getZone(card.getId()))) { // only if target zone was not replaced - game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() - + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' - + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) - + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); - } - - } - result = true; - } - return result; - } - - @Override - public Cards millCards(int toMill, Ability source, Game game) { - GameEvent event = GameEvent.getEvent(GameEvent.EventType.MILL_CARDS, getId(), source, getId(), toMill); - if (game.replaceEvent(event)) { - return new CardsImpl(); - } - Cards cards = new CardsImpl(this.getLibrary().getTopCards(game, event.getAmount())); - this.moveCards(cards, Zone.GRAVEYARD, source, game); - for (Card card : cards.getCards(game)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.MILLED_CARD, card.getId(), source, getId())); - } - return cards; - } - - @Override - public boolean hasOpponent(UUID playerToCheckId, Game game) { - return !this.getId().equals(playerToCheckId) - && game.isOpponent(this, playerToCheckId) - && getInRange().contains(playerToCheckId); - } - - @Override - public void cleanUpOnMatchEnd() { - - } - - @Override - public boolean getPassedAllTurns() { - return passedAllTurns; - } - - @Override - public boolean getPassedUntilNextMain() { - return passedUntilNextMain; - } - - @Override - public boolean getPassedUntilEndOfTurn() { - return passedUntilEndOfTurn; - } - - @Override - public boolean getPassedTurn() { - return passedTurn; - } - - @Override - public boolean getPassedUntilStackResolved() { - return passedUntilStackResolved; - } - - @Override - public boolean getPassedUntilEndStepBeforeMyTurn() { - return passedUntilEndStepBeforeMyTurn; - } - - @Override - public AbilityType getJustActivatedType() { - return justActivatedType; - } - - @Override - public void setJustActivatedType(AbilityType justActivatedType - ) { - this.justActivatedType = justActivatedType; - } - - @Override - public void revokePermissionToSeeHandCards() { - usersAllowedToSeeHandCards.clear(); - } - - @Override - public void addPermissionToShowHandCards(UUID watcherUserId - ) { - usersAllowedToSeeHandCards.add(watcherUserId); - } - - @Override - public boolean isPlayerAllowedToRequestHand(UUID gameId, UUID requesterPlayerId) { - return userData.isAllowRequestHandToPlayer(gameId, requesterPlayerId); - } - - @Override - public void addPlayerToRequestedHandList(UUID gameId, UUID requesterPlayerId) { - userData.addPlayerToRequestedHandList(gameId, requesterPlayerId); - } - - @Override - public boolean hasUserPermissionToSeeHand(UUID userId - ) { - return usersAllowedToSeeHandCards.contains(userId); - } - - @Override - public Set getUsersAllowedToSeeHandCards() { - return usersAllowedToSeeHandCards; - } - - @Override - public void setMatchPlayer(MatchPlayer matchPlayer - ) { - this.matchPlayer = matchPlayer; - } - - @Override - public MatchPlayer getMatchPlayer() { - return matchPlayer; - } - - @Override - public void abortReset() { - abort = false; - } - - @Override - public void signalPlayerConcede() { - - } - - @Override - public boolean scry(int value, Ability source, Game game) { - GameEvent event = new GameEvent(GameEvent.EventType.SCRY, getId(), source, getId(), value, true); - if (game.replaceEvent(event)) { - return false; - } - game.informPlayers(getLogName() + " scries " + event.getAmount() + CardUtil.getSourceLogName(game, source)); - Cards cards = new CardsImpl(); - cards.addAll(getLibrary().getTopCards(game, event.getAmount())); - if (!cards.isEmpty()) { - TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, - new FilterCard("card" + (cards.size() == 1 ? "" : "s") - + " to PUT on the BOTTOM of your library (Scry)")); - chooseTarget(Outcome.Benefit, cards, target, source, game); - putCardsOnBottomOfLibrary(new CardsImpl(target.getTargets()), game, source, true); - cards.removeAll(target.getTargets()); - putCardsOnTopOfLibrary(cards, game, source, true); - } - game.fireEvent(new GameEvent(GameEvent.EventType.SCRIED, getId(), source, getId(), event.getAmount(), true)); - return true; - } - - @Override - public boolean surveil(int value, Ability source, Game game) { - GameEvent event = new GameEvent(GameEvent.EventType.SURVEIL, getId(), source, getId(), value, true); - if (game.replaceEvent(event)) { - return false; - } - game.informPlayers(getLogName() + " surveils " + event.getAmount() + CardUtil.getSourceLogName(game, source)); - Cards cards = new CardsImpl(); - cards.addAll(getLibrary().getTopCards(game, event.getAmount())); - if (!cards.isEmpty()) { - TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, - new FilterCard("cards to PUT into your GRAVEYARD (Surveil)")); - chooseTarget(Outcome.Benefit, cards, target, source, game); - moveCards(new CardsImpl(target.getTargets()), Zone.GRAVEYARD, source, game); - cards.removeAll(target.getTargets()); - putCardsOnTopOfLibrary(cards, game, source, true); - } - game.fireEvent(new GameEvent(GameEvent.EventType.SURVEILED, getId(), source, getId(), event.getAmount(), true)); - return true; - } - - @Override - public boolean addTargets(Ability ability, Game game - ) { - // only used for TestPlayer to preSet Targets - return true; - } - - @Override - public String getHistory() { - return "no available"; - } - - @Override - public boolean hasDesignation(DesignationType designationName) { - for (Designation designation : designations) { - if (designation.getDesignationType().equals(designationName)) { - return true; - } - } - return false; - } - - @Override - public void addDesignation(Designation designation) { - if (!designation.isUnique() || !this.hasDesignation(designation.getDesignationType())) { - designations.add(designation); - } - } - - @Override - public List getDesignations() { - return designations; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - Player obj = (Player) o; - if (this.getId() == null || obj.getId() == null) { - return false; - } - - return this.getId().equals(obj.getId()); - } - - @Override - public int hashCode() { - int hash = 7; - hash = 89 * hash + Objects.hashCode(this.playerId); - return hash; - } - - @Override - public void addPhyrexianToColors(FilterMana colors) { - if (phyrexianColors == null) { - phyrexianColors = colors.copy(); - } else { - if (colors.isWhite()) { - this.phyrexianColors.setWhite(true); - } - if (colors.isBlue()) { - this.phyrexianColors.setBlue(true); - } - if (colors.isBlack()) { - this.phyrexianColors.setBlack(true); - } - if (colors.isRed()) { - this.phyrexianColors.setRed(true); - } - if (colors.isGreen()) { - this.phyrexianColors.setGreen(true); - } - } - } - - @Override - public FilterMana getPhyrexianColors() { - return this.phyrexianColors; - } - - @Override - public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { - return card.getSpellAbility(); - } - - @Override - public String toString() { - return getName() + " (" + super.getClass().getSimpleName() + ")"; - } -} +package mage.players; + +import com.google.common.collect.ImmutableMap; +import mage.*; +import mage.abilities.*; +import mage.abilities.ActivatedAbility.ActivationStatus; +import mage.abilities.common.PassAbility; +import mage.abilities.common.PlayLandAsCommanderAbility; +import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; +import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; +import mage.abilities.costs.*; +import mage.abilities.costs.mana.AlternateManaPaymentAbility; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; +import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; +import mage.abilities.keyword.*; +import mage.abilities.mana.ActivatedManaAbilityImpl; +import mage.abilities.mana.ManaOptions; +import mage.actions.MageDrawAction; +import mage.cards.*; +import mage.cards.decks.Deck; +import mage.choices.Choice; +import mage.choices.ChoiceImpl; +import mage.constants.*; +import mage.counters.Counter; +import mage.counters.CounterType; +import mage.counters.Counters; +import mage.designations.Designation; +import mage.designations.DesignationType; +import mage.filter.FilterCard; +import mage.filter.FilterMana; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.common.FilterCreatureForCombat; +import mage.filter.common.FilterCreatureForCombatBlock; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.PermanentIdPredicate; +import mage.game.*; +import mage.game.combat.CombatGroup; +import mage.game.command.CommandObject; +import mage.game.events.*; +import mage.game.match.MatchPlayer; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentCard; +import mage.game.permanent.PermanentToken; +import mage.game.permanent.token.SquirrelToken; +import mage.game.stack.Spell; +import mage.game.stack.StackAbility; +import mage.game.stack.StackObject; +import mage.game.turn.Step; +import mage.players.net.UserData; +import mage.target.Target; +import mage.target.TargetAmount; +import mage.target.TargetCard; +import mage.target.TargetPermanent; +import mage.target.common.TargetCardInLibrary; +import mage.target.common.TargetDiscard; +import mage.util.CardUtil; +import mage.util.GameLog; +import mage.util.RandomUtil; +import org.apache.log4j.Logger; + +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public abstract class PlayerImpl implements Player, Serializable { + + private static final Logger logger = Logger.getLogger(PlayerImpl.class); + + /** + * Used to cancel waiting requests send to the player + */ + protected boolean abort; + + protected final UUID playerId; + protected String name; + protected boolean human; + protected int life; + protected boolean wins; + protected boolean draws; + protected boolean loses; + protected Library library; + protected Cards sideboard; + protected Cards hand; + protected Graveyard graveyard; + protected Set commandersIds = new HashSet<>(0); + protected Abilities abilities; + protected Counters counters; + protected int landsPlayed; + protected int landsPerTurn = 1; + protected int loyaltyUsePerTurn = 1; + protected int maxHandSize = 7; + protected int maxAttackedBy = Integer.MAX_VALUE; + protected ManaPool manaPool; + // priority control + protected boolean passed; // player passed priority + protected boolean passedTurn; // F4 + protected boolean passedTurnSkipStack; // F6 // TODO: research + protected boolean passedUntilEndOfTurn; // F5 + protected boolean passedUntilNextMain; // F7 + protected boolean passedUntilStackResolved; // F10 + protected Date dateLastAddedToStack; + protected boolean passedUntilEndStepBeforeMyTurn; // F11 + protected boolean skippedAtLeastOnce; // used to track if passed started in specific phase + /** + * This indicates that player passed all turns until their own turn starts + * (F9). Note! This differs from passedTurn as it doesn't care about spells + * and abilities in the stack and will pass them as well. + */ + protected boolean passedAllTurns; // F9 + protected AbilityType justActivatedType; // used to check if priority can be passed automatically + + protected int turns; + protected int storedBookmark = -1; + protected int priorityTimeLeft = Integer.MAX_VALUE; + + // conceded or connection lost game + protected boolean left; + // set if the player quits the complete match + protected boolean quit; + // set if the player lost match because of priority timeout + protected boolean timerTimeout; + // set if the player lost match because of idle timeout + protected boolean idleTimeout; + + protected RangeOfInfluence range; + protected Set inRange = new HashSet<>(); // players list in current range of influence (updates each turn) + + protected boolean isTestMode = false; + protected boolean canGainLife = true; + protected boolean canLoseLife = true; + protected boolean canPayLifeCost = true; + protected boolean loseByZeroOrLessLife = true; + protected boolean canPlayCardsFromGraveyard = true; + protected boolean drawsOnOpponentsTurn = false; + + protected FilterPermanent sacrificeCostFilter; + + protected final List alternativeSourceCosts = new ArrayList<>(); + + protected boolean isGameUnderControl = true; + protected UUID turnController; + protected List turnControllers = new ArrayList<>(); + protected Set playersUnderYourControl = new HashSet<>(); + + protected Set usersAllowedToSeeHandCards = new HashSet<>(); + + protected List attachments = new ArrayList<>(); + + protected boolean topCardRevealed = false; + + // 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn + // or until a specific point in that turn will last until that turn would have begun. + // They neither expire immediately nor last indefinitely. + protected boolean reachedNextTurnAfterLeaving = false; + + // indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs) + // support multiple cards with alternative mana cost + protected Set castSourceIdWithAlternateMana = new HashSet<>(); + protected Map> castSourceIdManaCosts = new HashMap<>(); + protected Map> castSourceIdCosts = new HashMap<>(); + + // indicates that the player is in mana payment phase + protected boolean payManaMode = false; + + protected UserData userData; + protected MatchPlayer matchPlayer; + + protected List designations = new ArrayList<>(); + + // mana colors the player can handle like Phyrexian mana + protected FilterMana phyrexianColors; + + // Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy) + protected final List> availableTriggeredManaList = new ArrayList<>(); + + /** + * During some steps we can't play anything + */ + protected final Map silentPhaseSteps = ImmutableMap.builder(). + put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build(); + + public PlayerImpl(String name, RangeOfInfluence range) { + this(UUID.randomUUID()); + this.name = name; + this.range = range; + hand = new CardsImpl(); + graveyard = new Graveyard(); + abilities = new AbilitiesImpl<>(); + counters = new Counters(); + manaPool = new ManaPool(playerId); + library = new Library(playerId); + sideboard = new CardsImpl(); + phyrexianColors = null; + } + + protected PlayerImpl(UUID id) { + this.playerId = id; + } + + public PlayerImpl(final PlayerImpl player) { + this.abort = player.abort; + this.playerId = player.playerId; + + this.name = player.name; + this.human = player.human; + this.life = player.life; + this.wins = player.wins; + this.draws = player.draws; + this.loses = player.loses; + + this.library = player.library.copy(); + this.sideboard = player.sideboard.copy(); + this.hand = player.hand.copy(); + this.graveyard = player.graveyard.copy(); + this.commandersIds = player.commandersIds; + this.abilities = player.abilities.copy(); + this.counters = player.counters.copy(); + + this.landsPlayed = player.landsPlayed; + this.landsPerTurn = player.landsPerTurn; + this.loyaltyUsePerTurn = player.loyaltyUsePerTurn; + this.maxHandSize = player.maxHandSize; + this.maxAttackedBy = player.maxAttackedBy; + this.manaPool = player.manaPool.copy(); + this.turns = player.turns; + + this.left = player.left; + this.quit = player.quit; + this.timerTimeout = player.timerTimeout; + this.idleTimeout = player.idleTimeout; + this.range = player.range; + this.canGainLife = player.canGainLife; + this.canLoseLife = player.canLoseLife; + this.loseByZeroOrLessLife = player.loseByZeroOrLessLife; + this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard; + this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn; + + this.attachments.addAll(player.attachments); + + this.inRange.addAll(player.inRange); + this.userData = player.userData; + this.matchPlayer = player.matchPlayer; + + this.canPayLifeCost = player.canPayLifeCost; + this.sacrificeCostFilter = player.sacrificeCostFilter; + this.alternativeSourceCosts.addAll(player.alternativeSourceCosts); + this.storedBookmark = player.storedBookmark; + + this.topCardRevealed = player.topCardRevealed; + this.playersUnderYourControl.addAll(player.playersUnderYourControl); + this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); + + this.isTestMode = player.isTestMode; + this.isGameUnderControl = player.isGameUnderControl; + + this.turnController = player.turnController; + this.turnControllers.addAll(player.turnControllers); + + this.passed = player.passed; + this.passedTurn = player.passedTurn; + this.passedTurnSkipStack = player.passedTurnSkipStack; + this.passedUntilEndOfTurn = player.passedUntilEndOfTurn; + this.passedUntilNextMain = player.passedUntilNextMain; + this.passedUntilStackResolved = player.passedUntilStackResolved; + this.dateLastAddedToStack = player.dateLastAddedToStack; + this.passedUntilEndStepBeforeMyTurn = player.passedUntilEndStepBeforeMyTurn; + this.skippedAtLeastOnce = player.skippedAtLeastOnce; + this.passedAllTurns = player.passedAllTurns; + this.justActivatedType = player.justActivatedType; + + this.priorityTimeLeft = player.getPriorityTimeLeft(); + this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; + + this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); + for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { + this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { + this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + this.payManaMode = player.payManaMode; + this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; + for (Designation object : player.designations) { + this.designations.add(object.copy()); + } + } + + @Override + public void restore(Player player) { + this.name = player.getName(); + this.human = player.isHuman(); + this.life = player.getLife(); + + this.passed = player.isPassed(); + + // Don't restore more global states. If restored they are probably cause for unintended draws (https://github.com/magefree/mage/issues/1205). +// this.wins = player.hasWon(); +// this.loses = player.hasLost(); +// this.left = player.hasLeft(); +// this.quit = player.hasQuit(); + // Makes no sense to restore +// this.priorityTimeLeft = player.getPriorityTimeLeft(); +// this.idleTimeout = player.hasIdleTimeout(); +// this.timerTimeout = player.hasTimerTimeout(); + // can't change so no need to restore +// this.isTestMode = player.isTestMode(); + // This is meta data and should'nt be restored by rollback +// this.userData = player.getUserData(); + this.library = player.getLibrary().copy(); + this.sideboard = player.getSideboard().copy(); + this.hand = player.getHand().copy(); + this.graveyard = player.getGraveyard().copy(); + + //noinspection deprecation - it's ok to use it in inner methods + this.commandersIds = new HashSet<>(player.getCommandersIds()); + + this.abilities = player.getAbilities().copy(); + this.counters = player.getCounters().copy(); + + this.landsPlayed = player.getLandsPlayed(); + this.landsPerTurn = player.getLandsPerTurn(); + this.loyaltyUsePerTurn = player.getLoyaltyUsePerTurn(); + this.maxHandSize = player.getMaxHandSize(); + this.maxAttackedBy = player.getMaxAttackedBy(); + this.manaPool = player.getManaPool().copy(); + // Restore user specific settings in case changed since state save + this.manaPool.setAutoPayment(this.getUserData().isManaPoolAutomatic()); + this.manaPool.setAutoPaymentRestricted(this.getUserData().isManaPoolAutomaticRestricted()); + + this.turns = player.getTurns(); + + this.range = player.getRange(); + this.canGainLife = player.isCanGainLife(); + this.canLoseLife = player.isCanLoseLife(); + this.attachments.clear(); + this.attachments.addAll(player.getAttachments()); + + this.inRange.clear(); + this.inRange.addAll(player.getInRange()); + this.canPayLifeCost = player.getCanPayLifeCost(); + this.sacrificeCostFilter = player.getSacrificeCostFilter() != null + ? player.getSacrificeCostFilter().copy() : null; + this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); + this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard(); + this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn(); + this.alternativeSourceCosts.clear(); + this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); + + this.topCardRevealed = player.isTopCardRevealed(); + this.playersUnderYourControl.clear(); + this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl()); + this.isGameUnderControl = player.isGameUnderControl(); + + this.turnController = player.getTurnControlledBy(); + this.turnControllers.clear(); + this.turnControllers.addAll(player.getTurnControllers()); + this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); + + this.clearCastSourceIdManaCosts(); + this.castSourceIdWithAlternateMana.clear(); + this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); + for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { + this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { + this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + + this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null; + + this.designations.clear(); + for (Designation object : player.getDesignations()) { + this.designations.add(object.copy()); + } + + // Don't restore! + // this.storedBookmark + // this.usersAllowedToSeeHandCards + } + + @Override + public void useDeck(Deck deck, Game game) { + library.clear(); + library.addAll(deck.getCards(), game); + sideboard.clear(); + for (Card card : deck.getSideboard()) { + sideboard.add(card); + } + } + + /** + * Cast e.g. from Karn Liberated to restart the current game + * + * @param game + */ + @Override + public void init(Game game) { + init(game, false); + } + + @Override + public void init(Game game, boolean testMode) { + this.abort = false; + if (!testMode) { + this.hand.clear(); + this.graveyard.clear(); + } + this.library.reset(); + this.abilities.clear(); + this.counters.clear(); + this.wins = false; + this.draws = false; + this.loses = false; + this.left = false; + // reset is necessary because in tournament player will be used for each round + this.quit = false; + this.timerTimeout = false; + this.idleTimeout = false; + + this.turns = 0; + this.isGameUnderControl = true; + this.turnController = this.getId(); + this.turnControllers.clear(); + this.playersUnderYourControl.clear(); + + this.passed = false; + this.passedTurn = false; + this.passedTurnSkipStack = false; + this.passedUntilEndOfTurn = false; + this.passedUntilNextMain = false; + this.passedUntilStackResolved = false; + this.dateLastAddedToStack = null; + this.passedUntilEndStepBeforeMyTurn = false; + this.skippedAtLeastOnce = false; + this.passedAllTurns = false; + this.justActivatedType = null; + + this.canGainLife = true; + this.canLoseLife = true; + this.topCardRevealed = false; + this.payManaMode = false; + this.setLife(game.getStartingLife(), game, null); + this.setReachedNextTurnAfterLeaving(false); + + this.clearCastSourceIdManaCosts(); + + this.getManaPool().init(); // needed to remove mana that not empties on step change from previous game if left + this.phyrexianColors = null; + + this.designations.clear(); + } + + /** + * called before apply effects + */ + @Override + public void reset() { + this.abilities.clear(); + this.landsPerTurn = 1; + this.loyaltyUsePerTurn = 1; + this.maxHandSize = 7; + this.maxAttackedBy = Integer.MAX_VALUE; + this.canGainLife = true; + this.canLoseLife = true; + this.canPayLifeCost = true; + this.sacrificeCostFilter = null; + this.loseByZeroOrLessLife = true; + this.canPlayCardsFromGraveyard = false; + this.drawsOnOpponentsTurn = false; + this.topCardRevealed = false; + this.alternativeSourceCosts.clear(); + this.clearCastSourceIdManaCosts(); + this.getManaPool().clearEmptyManaPoolRules(); + this.phyrexianColors = null; + } + + @Override + public Counters getCounters() { + return counters; + } + + @Override + public void beginTurn(Game game) { + this.landsPlayed = 0; + updateRange(game); + } + + @Override + public RangeOfInfluence getRange() { + return range; + } + + @Override + public void updateRange(Game game) { + // 20100423 - 801.2c + // 801.2c The particular players within each player’s range of influence are determined as each turn begins. + // BUT it also uses before game start to fill game and card data in starting game events + inRange.clear(); + inRange.add(this.playerId); + inRange.addAll(getAllNearPlayers(game, true)); + inRange.addAll(getAllNearPlayers(game, false)); + } + + private Set getAllNearPlayers(Game game, boolean needPrevious) { + // find all near players (search from current player position) + Set foundedList = new HashSet<>(); + PlayerList players = game.getState().getPlayerList(this.playerId); + int needAmount = this.getRange().getRange(); // distance to search (0 - ALL range) + int foundedAmount = 0; + while (needAmount == 0 || foundedAmount < needAmount) { + Player foundedPlayer = needPrevious ? players.getPrevious(game) : players.getNext(game, false); + + // PlayerList is inifine, so stops on repeats + if (foundedPlayer == null || foundedPlayer.getId().equals(this.playerId) || foundedList.contains(foundedPlayer.getId())) { + break; + } + // skip leaved player (no needs cause next/previous code already checks it) + + foundedList.add(foundedPlayer.getId()); + foundedAmount++; + } + return foundedList; + } + + @Override + public Set getInRange() { + if (inRange.isEmpty()) { + // runtime check: inRange filled on beginTurn, but unit tests adds cards by cheat engine before game starting, + // so inRange will be empty and some ETB effects can be broken (example: Spark Double puts direct to battlefield). + // Cheat engine already have a workaround, so that error must not be visible in normal situation. + throw new IllegalStateException("Wrong code usage (game is not started, but you call getInRange in some effects)."); + } + + return inRange; + } + + @Override + public Set getPlayersUnderYourControl() { + return this.playersUnderYourControl; + } + + @Override + public void controlPlayersTurn(Game game, UUID playerId) { + Player player = game.getPlayer(playerId); + player.setTurnControlledBy(this.getId()); + game.informPlayers(getLogName() + " controls the turn of " + player.getLogName()); + if (!playerId.equals(this.getId())) { + this.playersUnderYourControl.add(playerId); + if (!player.hasLeft() && !player.hasLost()) { + player.setGameUnderYourControl(false); + } + DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility( + new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), player.getLogName())); + ability.setSourceId(getId()); + ability.setControllerId(getId()); + game.addDelayedTriggeredAbility(ability, null); + } + } + + @Override + public void setTurnControlledBy(UUID playerId) { + this.turnController = playerId; + this.turnControllers.add(playerId); + } + + @Override + public List getTurnControllers() { + return this.turnControllers; + } + + @Override + public UUID getTurnControlledBy() { + return this.turnController; + } + + @Override + public void resetOtherTurnsControlled() { + playersUnderYourControl.clear(); + } + + /** + * returns true if the player has the control itself - false if the player + * is controlled by another player + * + * @return + */ + @Override + public boolean isGameUnderControl() { + return isGameUnderControl; + } + + @Override + public void setGameUnderYourControl(boolean value) { + setGameUnderYourControl(value, true); + } + + @Override + public void setGameUnderYourControl(boolean value, boolean fullRestore) { + this.isGameUnderControl = value; + if (isGameUnderControl) { + if (fullRestore) { + this.turnControllers.clear(); + this.turnController = getId(); + } else { + if (turnControllers.size() > 0) { + this.turnControllers.remove(turnControllers.size() - 1); + } + if (turnControllers.isEmpty()) { + this.turnController = getId(); + } else { + this.turnController = turnControllers.get(turnControllers.size() - 1); + isGameUnderControl = false; + } + } + } + } + + @Override + public void endOfTurn(Game game) { + this.passedTurn = false; + this.passedTurnSkipStack = false; + } + + @Override + public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) { + if (this.hasLost() || this.hasLeft()) { + return false; + } + if (source != null) { + // there is only variant of shroud, so check the instance and any asthougheffects that would ignore it + if (abilities.containsKey(ShroudAbility.getInstance().getId()) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game) == null) { + return false; + } + // check for all variants of hexproof and any asthougheffects that would ignore it + // TODO there may be "prevented by rule-modification" effects, so add them if known + for (Ability a : abilities) { + if (a instanceof HexproofBaseAbility + && ((HexproofBaseAbility) a).checkObject(source, game) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { + return false; + } + } + return !hasProtectionFrom(source, game); + } + return true; + } + + @Override + public boolean hasProtectionFrom(MageObject source, Game game) { + for (ProtectionAbility ability : abilities.getProtectionAbilities()) { + if (!ability.canTarget(source, game)) { + return true; + } + } + return false; + } + + @Override + public int drawCards(int num, Ability source, Game game) { + if (num > 0) { + return game.doAction(source, new MageDrawAction(this, num, null)); + } + return 0; + } + + @Override + public int drawCards(int num, Ability source, Game game, GameEvent event) { + return game.doAction(source, new MageDrawAction(this, num, event)); + } + + @Override + public void discardToMax(Game game) { + if (hand.size() > this.maxHandSize) { + if (!game.isSimulation()) { + game.informPlayers(getLogName() + " discards down to " + + this.maxHandSize + + (this.maxHandSize == 1 + ? " hand card" : " hand cards")); + } + discard(hand.size() - this.maxHandSize, false, false, null, game); + } + } + + /** + * Don't use this in normal card code, it's for more internal use. Always + * use the [Player].moveCards methods if possible for card movement of card + * code. + * + * @param card + * @param game + * @return + */ + @Override + public boolean putInHand(Card card, Game game) { + if (card.isOwnedBy(playerId)) { + card.setZone(Zone.HAND, game); + this.hand.add(card); + } else { + return game.getPlayer(card.getOwnerId()).putInHand(card, game); + } + return true; + } + + @Override + public boolean removeFromHand(Card card, Game game) { + return hand.remove(card.getId()); + } + + @Override + public boolean removeFromLibrary(Card card, Game game) { + if (card == null) { + return false; + } + library.remove(card.getId(), game); + // must return true all the time (some cards can be removed directly from library, see getLibrary().removeFromTop) + // TODO: replace removeFromTop logic to normal with moveToZone + return true; + } + + @Override + public Card discardOne(boolean random, boolean payForCost, Ability source, Game game) { + return discard(1, random, payForCost, source, game).getRandom(game); + } + + @Override + public Cards discard(int amount, boolean random, boolean payForCost, Ability source, Game game) { + if (random) { + return discard(getRandomToDiscard(amount, source, game), payForCost, source, game); + } + return discard(amount, amount, payForCost, source, game); + } + + @Override + public Cards discard(int minAmount, int maxAmount, boolean payForCost, Ability source, Game game) { + return discard(getToDiscard(minAmount, maxAmount, source, game), payForCost, source, game); + } + + @Override + public Cards discard(Cards cards, boolean payForCost, Ability source, Game game) { + Cards discardedCards = new CardsImpl(); + if (cards == null) { + return discardedCards; + } + for (Card card : cards.getCards(game)) { + if (doDiscard(card, source, game, payForCost, false)) { + discardedCards.add(card); + } + } + if (!discardedCards.isEmpty()) { + game.fireEvent(new DiscardedCardsEvent(source, playerId, discardedCards.size(), discardedCards)); + } + return discardedCards; + } + + @Override + public boolean discard(Card card, boolean payForCost, Ability source, Game game) { + return doDiscard(card, source, game, payForCost, true); + } + + private Cards getToDiscard(int minAmount, int maxAmount, Ability source, Game game) { + Cards toDiscard = new CardsImpl(); + if (minAmount > maxAmount) { + return getToDiscard(maxAmount, minAmount, source, game); + } + if (maxAmount < 1) { + return toDiscard; + } + if (getHand().size() <= minAmount) { + toDiscard.addAll(getHand()); + return toDiscard; + } + TargetDiscard target = new TargetDiscard(minAmount, maxAmount, StaticFilters.FILTER_CARD, getId()); + choose(Outcome.Discard, target, source != null ? source.getSourceId() : null, game); + toDiscard.addAll(target.getTargets()); + return toDiscard; + } + + private Cards getRandomToDiscard(int amount, Ability source, Game game) { + Cards toDiscard = new CardsImpl(); + Cards hand = getHand().copy(); + for (int i = 0; i < amount; i++) { + if (hand.isEmpty()) { + break; + } + Card card = hand.getRandom(game); + hand.remove(card); + toDiscard.add(card); + } + return toDiscard; + } + + private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) { + //20100716 - 701.7 + /* 701.7. Discard # + 701.7a To discard a card, move it from its owner’s hand to that player’s graveyard. + 701.7b By default, effects that cause a player to discard a card allow the affected + player to choose which card to discard. Some effects, however, require a random + discard or allow another player to choose which card is discarded. + 701.7c If a card is discarded, but an effect causes it to be put into a hidden zone + instead of into its owner’s graveyard without being revealed, all values of that + card’s characteristics are considered to be undefined. + TODO: + If a card is discarded this way to pay a cost that specifies a characteristic + about the discarded card, that cost payment is illegal; the game returns to + the moment before the cost was paid (see rule 717, "Handling Illegal Actions"). + */ + if (card == null) { + return false; + } + GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, card.getId(), source, playerId); + gameEvent.setFlag(!payForCost); // event from effect (1) or from cost (0) + if (game.replaceEvent(gameEvent, source)) { + return false; + } + // write info to game log first so game log infos from triggered or replacement effects follow in the game log + if (!game.isSimulation()) { + game.informPlayers(getLogName() + " discards " + card.getLogName() + CardUtil.getSourceLogName(game, source)); + } + /* If a card is discarded while Rest in Peace is on the battlefield, abilities that function + * when a card is discarded (such as madness) still work, even though that card never reaches + * a graveyard. In addition, spells or abilities that check the characteristics of a discarded + * card (such as Chandra Ablaze's first ability) can find that card in exile. */ + card.moveToZone(Zone.GRAVEYARD, source, game, false); + // So discard is also successful if card is moved to another zone by replacement effect! + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DISCARDED_CARD, card.getId(), source, playerId)); + + if (fireFinalEvent) { + game.fireEvent(new DiscardedCardsEvent(source, playerId, 1, new CardsImpl(card))); + } + return true; + } + + @Override + public List getAttachments() { + return attachments; + } + + @Override + public boolean addAttachment(UUID permanentId, Ability source, Game game) { + if (!this.attachments.contains(permanentId)) { + Permanent aura = game.getPermanent(permanentId); + if (aura == null) { + aura = game.getPermanentEntering(permanentId); + } + if (aura != null) { + if (!game.replaceEvent(new EnchantPlayerEvent(playerId, aura, source))) { + this.attachments.add(permanentId); + aura.attachTo(playerId, source, game); + game.fireEvent(new EnchantedPlayerEvent(playerId, aura, source)); + return true; + } + } + } + return false; + } + + @Override + public boolean removeAttachment(Permanent attachment, Ability source, Game game) { + if (this.attachments.contains(attachment.getId())) { + if (!game.replaceEvent(new UnattachEvent(playerId, attachment.getId(), attachment, source))) { + this.attachments.remove(attachment.getId()); + attachment.attachTo(null, source, game); + game.fireEvent(new UnattachedEvent(playerId, attachment.getId(), attachment, source)); + return true; + } + } + return false; + } + + @Override + public boolean removeFromBattlefield(Permanent permanent, Ability source, Game game) { + permanent.removeFromCombat(game, false); + game.getBattlefield().removePermanent(permanent.getId()); + if (permanent.getAttachedTo() != null) { + Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); + if (attachedTo != null) { + attachedTo.removeAttachment(permanent.getId(), source, game); + } else { + Player attachedToPlayer = game.getPlayer(permanent.getAttachedTo()); + if (attachedToPlayer != null) { + attachedToPlayer.removeAttachment(permanent, source, game); + } else { + Card attachedToCard = game.getCard(permanent.getAttachedTo()); + if (attachedToCard != null) { + attachedToCard.removeAttachment(permanent.getId(), source, game); + } + } + } + + } + if (permanent.getPairedCard() != null) { + Permanent pairedCard = permanent.getPairedCard().getPermanent(game); + if (pairedCard != null) { + pairedCard.clearPairedCard(); + } + } + if (permanent.getBandedCards() != null && !permanent.getBandedCards().isEmpty()) { + for (UUID bandedId : permanent.getBandedCards()) { + Permanent banded = game.getPermanent(bandedId); + if (banded != null) { + banded.removeBandedCard(permanent.getId()); + } + } + } + return true; + } + + @Override + public boolean putInGraveyard(Card card, Game game) { + if (card.isOwnedBy(playerId)) { + this.graveyard.add(card); + } else { + return game.getPlayer(card.getOwnerId()).putInGraveyard(card, game); + } + return true; + } + + @Override + public boolean removeFromGraveyard(Card card, Game game) { + return this.graveyard.remove(card); + } + + @Override + public boolean putCardsOnBottomOfLibrary(Card card, Game game, Ability source, boolean anyOrder) { + return putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, anyOrder); + } + + @Override + public boolean putCardsOnBottomOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { + if (!cardsToLibrary.isEmpty()) { + Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException + if (!anyOrder) { + // random order + List ids = new ArrayList<>(cards); + Collections.shuffle(ids); + for (UUID id : ids) { + moveObjectToLibrary(id, source, game, false, false); + } + } else { + // user defined order + TargetCard target = new TargetCard(Zone.ALL, + new FilterCard("card ORDER to put on the BOTTOM of your library (last one chosen will be bottommost)")); + target.setRequired(true); + while (cards.size() > 1 && this.canRespond() + && this.choose(Outcome.Neutral, cards, target, game)) { + UUID targetObjectId = target.getFirstTarget(); + if (targetObjectId == null) { + break; + } + cards.remove(targetObjectId); + moveObjectToLibrary(targetObjectId, source, game, false, false); + target.clearChosen(); + } + for (UUID c : cards) { + moveObjectToLibrary(c, source, game, false, false); + } + } + } + return true; + } + + @Override + public boolean shuffleCardsToLibrary(Cards cards, Game game, Ability source) { + if (cards.isEmpty()) { + return true; + } + game.informPlayers(getLogName() + " shuffles " + CardUtil.numberToText(cards.size(), "a") + + " card" + (cards.size() == 1 ? "" : "s") + + " into their library" + CardUtil.getSourceLogName(game, source)); + boolean status = moveCards(cards, Zone.LIBRARY, source, game); + shuffleLibrary(source, game); + return status; + } + + @Override + public boolean shuffleCardsToLibrary(Card card, Game game, Ability source) { + if (card == null) { + return true; + } + return shuffleCardsToLibrary(new CardsImpl(card), game, source); + } + + @Override + public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int xFromTheTop, boolean withName) { + if (card.isOwnedBy(getId())) { + if (library.size() + 1 < xFromTheTop) { + putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, true); + } else { + if (card.moveToZone(Zone.LIBRARY, source, game, true) + && !(card instanceof PermanentToken) && !card.isCopy()) { + Card cardInLib = getLibrary().getFromTop(game); + if (cardInLib != null && cardInLib.getId().equals(card.getId())) { // check needed because e.g. commander can go to command zone + cardInLib = getLibrary().removeFromTop(game); + getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game); + game.informPlayers(withName ? cardInLib.getLogName() : "A card" + + " is put into " + + getLogName() + + "'s library " + + CardUtil.numberToOrdinalText(xFromTheTop) + + " from the top" + CardUtil.getSourceLogName(game, source, cardInLib.getId())); + } + } else { + return false; + } + } + } else { + return game.getPlayer(card.getOwnerId()).putCardOnTopXOfLibrary(card, game, source, xFromTheTop, withName); + } + return true; + } + + /** + * Can be cards or permanents that go to library + * + * @param cardsToLibrary + * @param game + * @param source + * @param anyOrder + * @return + */ + @Override + public boolean putCardsOnTopOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { + if (cardsToLibrary != null && !cardsToLibrary.isEmpty()) { + Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException + if (!anyOrder) { + // random order + List ids = new ArrayList<>(cards); + Collections.shuffle(ids); + for (UUID id : ids) { + moveObjectToLibrary(id, source, game, true, false); + } + } else { + // user defined order + TargetCard target = new TargetCard(Zone.ALL, + new FilterCard("card ORDER to put on the TOP of your library (last one chosen will be topmost)")); + target.setRequired(true); + while (cards.size() > 1 + && this.canRespond() + && this.choose(Outcome.Neutral, cards, target, game)) { + UUID targetObjectId = target.getFirstTarget(); + if (targetObjectId == null) { + break; + } + cards.remove(targetObjectId); + moveObjectToLibrary(targetObjectId, source, game, true, false); + target.clearChosen(); + } + for (UUID c : cards) { + moveObjectToLibrary(c, source, game, true, false); + } + } + } + return true; + } + + @Override + public boolean putCardsOnTopOfLibrary(Card cardToLibrary, Game game, Ability source, boolean anyOrder) { + if (cardToLibrary != null) { + return putCardsOnTopOfLibrary(new CardsImpl(cardToLibrary), game, source, anyOrder); + } + return true; + } + + private boolean moveObjectToLibrary(UUID objectId, Ability source, Game game, boolean toTop, boolean withName) { + MageObject mageObject = game.getObject(objectId); + if (mageObject instanceof Spell && mageObject.isCopy()) { + // Spell copies are not moved as cards, so here the no copy spell has to be selected to move + // (but because copy and original have the same objectId the wrong sepell can be selected from stack). + // So let's check if the original spell is on the stack and has to be selected. // TODO: Better handling so each spell could be selected by a unique id + Spell spellNoCopy = game.getStack().getSpell(source.getSourceId(), false); + if (spellNoCopy != null) { + mageObject = spellNoCopy; + } + } + if (mageObject != null) { + Zone fromZone = game.getState().getZone(objectId); + if ((mageObject instanceof Permanent)) { + return this.moveCardToLibraryWithInfo((Permanent) mageObject, source, game, fromZone, toTop, withName); + } else if (mageObject instanceof Card) { + return this.moveCardToLibraryWithInfo((Card) mageObject, source, game, fromZone, toTop, withName); + } + } + return false; + } + + @Override + public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { + // cost must be copied for data consistence between game simulations + castSourceIdWithAlternateMana.add(sourceId); + castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null); + castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null); + } + + @Override + public Set getCastSourceIdWithAlternateMana() { + return castSourceIdWithAlternateMana; + } + + @Override + public Map> getCastSourceIdCosts() { + return castSourceIdCosts; + } + + @Override + public Map> getCastSourceIdManaCosts() { + return castSourceIdManaCosts; + } + + @Override + public void clearCastSourceIdManaCosts() { + this.castSourceIdCosts.clear(); + this.castSourceIdManaCosts.clear(); + this.castSourceIdWithAlternateMana.clear(); + } + + @Override + public void setPayManaMode(boolean payManaMode) { + this.payManaMode = payManaMode; + } + + @Override + public boolean isInPayManaMode() { + return payManaMode; + } + + @Override + public boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject) { + if (card == null) { + return false; + } + + // play without timing and from any zone + boolean result; + if (card.isLand(game)) { + result = playLand(card, game, true); + } else { + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + result = cast(this.chooseAbilityForCast(card, game, noMana), game, noMana, approvingObject); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + } + + if (!result) { + game.informPlayer(this, "You can't play " + card.getIdName() + '.'); + } + return result; + } + + /** + * @param originalAbility + * @param game + * @param noMana cast it without paying mana costs + * @param approvingObject which object approved the cast + * @return + */ + @Override + public boolean cast(SpellAbility originalAbility, Game game, boolean noMana, ApprovingObject approvingObject) { + if (game == null || originalAbility == null) { + return false; + } + + // Use ability copy to avoid problems with targets and costs on recast (issue https://github.com/magefree/mage/issues/5189). + SpellAbility ability = originalAbility.copy(); + ability.setControllerId(getId()); + ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); + + //20091005 - 601.2a + if (ability.getSourceId() == null) { + logger.error("Ability without sourceId turn " + game.getTurnNum() + ". Ability: " + ability.getRule()); + return false; + } + Card card = game.getCard(ability.getSourceId()); + if (card != null) { + Zone fromZone = game.getState().getZone(card.getMainCard().getId()); + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, + ability.getId(), ability, playerId, approvingObject); + castEvent.setZone(fromZone); + if (!game.replaceEvent(castEvent, ability)) { + int bookmark = game.bookmarkState(); + setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) + card.cast(game, fromZone, ability, playerId); + Spell spell = game.getStack().getSpell(ability.getId()); + if (spell == null) { + logger.error("Got no spell from stack. ability: " + ability.getRule()); + return false; + } + if (card.isCopy()) { + spell.setCopy(true, null); + } + // Update the zcc to the stack + ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); + + // ALTERNATIVE COST from dynamic effects + // some effects set sourceId to cast without paying mana costs or other costs + if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) { + Ability spellAbility = spell.getSpellAbility(); + ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()); + Costs costs = getCastSourceIdCosts().get(ability.getSourceId()); + if (alternateCosts == null) { + noMana = true; + } else { + spellAbility.getManaCosts().clear(); + spellAbility.getManaCostsToPay().clear(); + spellAbility.getManaCosts().add(alternateCosts.copy()); + spellAbility.getManaCostsToPay().add(alternateCosts.copy()); + } + spellAbility.getCosts().clear(); + if (costs != null) { + spellAbility.getCosts().addAll(costs); + } + } + clearCastSourceIdManaCosts(); // TODO: test multiple alternative cost for different cards as same time + + castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, + spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); + castEvent.setZone(fromZone); + game.fireEvent(castEvent); + if (spell.activate(game, noMana)) { + GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, + spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); + castedEvent.setZone(fromZone); + game.fireEvent(castedEvent); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + spell.getActivatedMessage(game)); + } + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + restoreState(bookmark, ability.getRule(), game); + } + } + return false; + } + + @Override + public boolean playLand(Card card, Game game, boolean ignoreTiming) { + // Check for alternate casting possibilities: e.g. land with Morph + if (card == null) { + return false; + } + ActivatedAbility playLandAbility = null; + boolean foundAlternative = false; + for (Ability ability : card.getAbilities(game)) { + // if cast for noMana no Alternative costs are allowed + if ((ability instanceof AlternativeSourceCosts) + || (ability instanceof OptionalAdditionalSourceCosts)) { + foundAlternative = true; + } + if (ability instanceof PlayLandAbility) { + playLandAbility = (ActivatedAbility) ability; + } + } + + // try alternative cast (face down) + if (foundAlternative) { + SpellAbility spellAbility = new SpellAbility(null, "", + game.getState().getZone(card.getId()), SpellAbilityType.FACE_DOWN_CREATURE); + spellAbility.setControllerId(this.getId()); + spellAbility.setSourceId(card.getId()); + if (cast(spellAbility, game, false, null)) { + return true; + } + } + + if (playLandAbility == null) { + return false; + } + + //20091005 - 114.2a + ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game); + if (ignoreTiming) { + if (!canPlayLand()) { + return false; // ignore timing does not mean that more lands than normal can be played + } + } else { + if (!activationStatus.canActivate()) { + return false; + } + } + + //20091005 - 305.1 + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, + card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()))) { + // int bookmark = game.bookmarkState(); + // land events must return original zone (uses for commander watcher) + Zone cardZoneBefore = game.getState().getZone(card.getId()); + GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, + card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); + landEventBefore.setZone(cardZoneBefore); + game.fireEvent(landEventBefore); + + if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { + landsPlayed++; + GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, + card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); + landEventAfter.setZone(cardZoneBefore); + game.fireEvent(landEventAfter); + + String playText = getLogName() + " plays " + card.getLogName(); + if (card instanceof ModalDoubleFacesCardHalf) { + ModalDoubleFacesCard mdfCard = (ModalDoubleFacesCard) card.getMainCard(); + playText = getLogName() + " plays " + GameLog.replaceNameByColoredName(card, card.getName(), mdfCard) + + " as MDF side of " + GameLog.getColoredObjectIdName(mdfCard); + } + game.fireInformEvent(playText); + // game.removeBookmark(bookmark); + resetStoredBookmark(game); // prevent undo after playing a land + return true; + } + // putOntoBattlefield returned false if putOntoBattlefield was replaced by replacement effect (e.g. Kjeldoran Outpost). + // But that would undo the effect completely, + // what makes no real sense. So it makes no sense to generally do a restoreState here. + // restoreState(bookmark, card.getName(), game); + } + // if the to play the land is replaced (e.g. Kjeldoran Outpost and don't sacrificing a Plains) it's a valid state so returning true here + return true; + } + + protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) { + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, + ability.getId(), ability, playerId))) { + int bookmark = game.bookmarkState(); + if (ability.activate(game, false)) { + if (ability.resolve(game)) { + if (ability.isUndoPossible()) { + if (storedBookmark == -1 || storedBookmark > bookmark) { // e.g. useful for undo Nykthos, Shrine to Nyx + setStoredBookmark(bookmark); + } + } else { + resetStoredBookmark(game); + } + return true; + } + } + restoreState(bookmark, ability.getRule(), game); + } + return false; + } + + protected boolean playAbility(ActivatedAbility ability, Game game) { + //20091005 - 602.2a + if (ability.isUsesStack()) { + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, + ability.getId(), ability, playerId))) { + int bookmark = game.bookmarkState(); + setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) + ability.newId(); + ability.setControllerId(playerId); + game.getStack().push(new StackAbility(ability, playerId)); + if (ability.activate(game, false)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATED_ABILITY, + ability.getId(), ability, playerId)); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + ability.getGameLogMessage(game)); + } + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + restoreState(bookmark, ability.getRule(), game); + } + } else { + int bookmark = game.bookmarkState(); + if (ability.activate(game, false)) { + ability.resolve(game); + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + restoreState(bookmark, ability.getRule(), game); + } + return false; + } + + protected boolean specialAction(SpecialAction action, Game game) { + //20091005 - 114 + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_ACTION, + action.getId(), action, getId()))) { + int bookmark = game.bookmarkState(); + if (action.activate(game, false)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_ACTION, + action.getId(), action, getId())); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + action.getGameLogMessage(game)); + } + if (action.resolve(game)) { + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + } + restoreState(bookmark, action.getRule(), game); + } + return false; + } + + protected boolean specialManaPayment(SpecialAction action, Game game) { + //20091005 - 114 + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_MANA_PAYMENT, + action.getId(), action, getId()))) { + int bookmark = game.bookmarkState(); + if (action.activate(game, false)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_MANA_PAYMENT, + action.getId(), action, getId())); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + action.getGameLogMessage(game)); + } + if (action.resolve(game)) { + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + } + restoreState(bookmark, action.getRule(), game); + } + return false; + } + + @Override + public boolean activateAbility(ActivatedAbility ability, Game game) { + if (ability == null) { + return false; + } + boolean result; + if (ability instanceof PassAbility) { + pass(game); + return true; + } + Card card = game.getCard(ability.getSourceId()); + if (ability instanceof PlayLandAsCommanderAbility) { + + // LAND as commander: play land with cost, but without stack + ActivationStatus activationStatus = ability.canActivate(this.playerId, game); + if (!activationStatus.canActivate() || !this.canPlayLand()) { + return false; + } + if (card == null) { + return false; + } + + // as copy, tries to applie cost effects and pays + Ability activatingAbility = ability.copy(); + if (activatingAbility.activate(game, false)) { + result = playLand(card, game, false); + } else { + result = false; + } + + } else if (ability instanceof PlayLandAbility) { + + // LAND as normal card: without cost and stack + result = playLand(card, game, false); + + } else { + + // ABILITY + ActivationStatus activationStatus = ability.canActivate(this.playerId, game); + if (!activationStatus.canActivate()) { + return false; + } + + switch (ability.getAbilityType()) { + case SPECIAL_ACTION: + result = specialAction((SpecialAction) ability.copy(), game); + break; + case SPECIAL_MANA_PAYMENT: + result = specialManaPayment((SpecialAction) ability.copy(), game); + break; + case MANA: + result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game); + break; + case SPELL: + result = cast((SpellAbility) ability, game, false, activationStatus.getApprovingObject()); + break; + default: + result = playAbility(ability.copy(), game); + break; + } + } + + //if player has taken an action then reset all player passed flags + justActivatedType = null; + if (result) { + if (isHuman() + && (ability.getAbilityType() == AbilityType.SPELL + || ability.getAbilityType() == AbilityType.ACTIVATED)) { + if (ability.isUsesStack()) { // if the ability does not use the stack (e.g. Suspend) auto pass would go to next phase unintended + setJustActivatedType(ability.getAbilityType()); + } + } + game.getPlayers().resetPassed(); + } + return result; + } + + @Override + public boolean triggerAbility(TriggeredAbility triggeredAbility, Game game) { + if (triggeredAbility == null) { + logger.warn("Null source in triggerAbility method"); + throw new IllegalArgumentException("source TriggeredAbility must not be null"); + } + //20091005 - 603.3c, 603.3d + int bookmark = game.bookmarkState(); + TriggeredAbility ability = triggeredAbility.copy(); + MageObject sourceObject = ability.getSourceObject(game); + if (sourceObject != null) { + sourceObject.adjustTargets(ability, game); + } + UUID triggerId = null; + if (ability.canChooseTarget(game, playerId)) { + if (ability.isUsesStack()) { + game.getStack().push(new StackAbility(ability, playerId)); + } + if (ability.activate(game, false)) { + if ((ability.isUsesStack() + || ability.getRuleVisible()) + && !game.isSimulation()) { + game.informPlayers(getLogName() + " - " + ability.getGameLogMessage(game)); + } + if (!ability.isUsesStack()) { + ability.resolve(game); + } else { + game.fireEvent(new GameEvent( + GameEvent.EventType.TRIGGERED_ABILITY, + ability.getId(), ability, ability.getControllerId() + )); + triggerId = ability.getId(); + } + game.removeBookmark(bookmark); + return true; + } + } + restoreState(bookmark, triggeredAbility.getRule(), game); // why restore is needed here? (to remove the triggered ability from the stack because of no possible targets) + GameEvent event = new GameEvent( + GameEvent.EventType.ABILITY_TRIGGERED, + triggerId, ability, ability.getControllerId() + ); + game.getState().setValue(event.getId().toString(), ability.getTriggerEvent()); + game.fireEvent(event); + return false; + } + + /** + * Return spells for possible cast Uses in GUI to show only playable spells + * for choosing from the card (example: effect allow to cast card and player + * must choose the spell ability) + * + * @param game + * @param playerId + * @param object + * @param zone + * @param noMana + * @return + */ + public static LinkedHashMap getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) { + // it uses simple check from spellCanBeActivatedRegularlyNow + // reason: no approved info here (e.g. forced to choose spell ability from cast card) + LinkedHashMap useable = new LinkedHashMap<>(); + Abilities allAbilities; + if (object instanceof Card) { + allAbilities = ((Card) object).getAbilities(game); + } else { + allAbilities = object.getAbilities(); + } + + for (Ability ability : allAbilities) { + if (ability instanceof SpellAbility) { + SpellAbility spellAbility = (SpellAbility) ability; + + switch (spellAbility.getSpellAbilityType()) { + case BASE_ALTERNATE: + // rules: + // If you cast a spell “without paying its mana cost,” you can’t choose to cast it for + // any alternative costs. You can, however, pay additional costs, such as kicker costs. + // If the card has any mandatory additional costs, those must be paid to cast the spell. + // (2021-02-05) + if (!noMana) { + if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability + } + return useable; + } + break; + case SPLIT_FUSED: + // rules: + // If you cast a split card with fuse from your hand without paying its mana cost, + // you can choose to use its fuse ability and cast both halves without paying their mana costs. + if (zone == Zone.HAND) { + if (spellAbility.canChooseTarget(game, playerId)) { + useable.put(spellAbility.getId(), spellAbility); + } + } + case SPLIT: + if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getLeftHalfCard().getSpellAbility()); + } + if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getRightHalfCard().getSpellAbility()); + } + return useable; + case SPLIT_AFTERMATH: + if (zone == Zone.GRAVEYARD) { + if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getRightHalfCard().getSpellAbility()); + } + } else { + if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getLeftHalfCard().getSpellAbility()); + } + } + return useable; + default: + if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + useable.put(spellAbility.getId(), spellAbility); + } + } + } + } + return useable; + } + + @Override + public LinkedHashMap getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) { + LinkedHashMap useable = new LinkedHashMap<>(); + // stack abilities - can't activate anything + // spell ability - can activate additional abilities (example: "Lightning Storm") + if (object instanceof StackAbility || object == null) { + return useable; + } + boolean previousState = game.inCheckPlayableState(); + game.setCheckPlayableState(true); + try { + // collect and filter playable activated abilities + // GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right) + Set needIds = CardUtil.getObjectParts(object); + + // workaround to find all abilities first and filter it for one object + List allPlayable = getPlayable(game, true, zone, false); + for (ActivatedAbility ability : allPlayable) { + if (needIds.contains(ability.getSourceId())) { + useable.putIfAbsent(ability.getId(), ability); + } + } + } finally { + game.setCheckPlayableState(previousState); + } + return useable; + } + + protected LinkedHashMap getUseableManaAbilities(MageObject object, Zone zone, Game game) { + LinkedHashMap useable = new LinkedHashMap<>(); + boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); + for (ActivatedManaAbilityImpl ability : object.getAbilities().getActivatedManaAbilities(zone)) { + if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { + if (ability.canActivate(playerId, game).canActivate()) { + useable.put(ability.getId(), ability); + } + } + } + return useable; + } + + @Override + public int getLandsPlayed() { + return landsPlayed; + } + + @Override + public boolean canPlayLand() { + //20091005 - 114.2a + return landsPlayed < landsPerTurn; + } + + protected boolean isActivePlayer(Game game) { + return game.isActivePlayer(this.playerId); + } + + @Override + public void shuffleLibrary(Ability source, Game game) { + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SHUFFLE_LIBRARY, playerId, source, playerId))) { + this.library.shuffle(); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + "'s library is shuffled" + CardUtil.getSourceLogName(game, source)); + } + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SHUFFLED, playerId, source, playerId)); + } + } + + @Override + public void revealCards(Ability source, Cards cards, Game game) { + revealCards(source, null, cards, game, true); + } + + @Override + public void revealCards(String titleSuffix, Cards cards, Game game) { + revealCards(titleSuffix, cards, game, true); + } + + @Override + public void revealCards(String titleSuffix, Cards cards, Game game, boolean postToLog) { + revealCards(null, titleSuffix, cards, game, postToLog); + } + + @Override + public void revealCards(Ability source, String titleSuffix, Cards cards, Game game) { + revealCards(source, titleSuffix, cards, game, true); + } + + @Override + public void revealCards(Ability source, String titleSuffix, Cards cards, Game game, boolean postToLog) { + if (cards == null || cards.isEmpty()) { + return; + } + if (postToLog) { + game.getState().getRevealed().add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); + } else { + game.getState().getRevealed().update(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); + } + if (postToLog && !game.isSimulation()) { + StringBuilder sb = new StringBuilder(getLogName()).append(" reveals "); + int current = 0, last = cards.size(); + for (Card card : cards.getCards(game)) { + current++; + sb.append(GameLog.getColoredObjectName(card)); + if (current < last) { + sb.append(", "); + } + } + sb.append(CardUtil.getSourceLogName(game, source)); + game.informPlayers(sb.toString()); + } + } + + @Override + public void lookAtCards(String titleSuffix, Card card, Game game) { + game.getState().getLookedAt(this.playerId).add(titleSuffix, card); + game.fireUpdatePlayersEvent(); + } + + @Override + public void lookAtCards(String titleSuffix, Cards cards, Game game) { + this.lookAtCards(null, titleSuffix, cards, game); + } + + @Override + public void lookAtCards(Ability source, String titleSuffix, Cards cards, Game game) { + game.getState().getLookedAt(this.playerId).add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); + game.fireUpdatePlayersEvent(); + } + + @Override + public void phasing(Game game) { + //20091005 - 502.1 + List phasedOut = game.getBattlefield().getPhasedOut(game, playerId); + for (Permanent permanent : game.getBattlefield().getPhasedIn(game, playerId)) { + // 502.15i When a permanent phases out, any local enchantments or Equipment + // attached to that permanent phase out at the same time. This alternate way of + // phasing out is known as phasing out "indirectly." An enchantment or Equipment + // that phased out indirectly won't phase in by itself, but instead phases in + // along with the card it's attached to. + Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); + if (!(attachedTo != null && attachedTo.isControlledBy(this.getId()))) { + permanent.phaseOut(game, false); + } + } + for (Permanent permanent : phasedOut) { + if (!permanent.isPhasedOutIndirectly()) { + permanent.phaseIn(game); + } + } + } + + @Override + public void untap(Game game) { + // create list of all "notMoreThan" effects to track which one are consumed + Map>, Integer> notMoreThanEffectsUsage = new HashMap<>(); + for (Entry> restrictionEffect + : game.getContinuousEffects().getApplicableRestrictionUntapNotMoreThanEffects(this, game).entrySet()) { + notMoreThanEffectsUsage.put(restrictionEffect, restrictionEffect.getKey().getNumber()); + } + + if (!notMoreThanEffectsUsage.isEmpty()) { + // create list of all permanents that can be untapped generally + List canBeUntapped = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { + boolean untap = true; + for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { + untap &= effect.canBeUntapped(permanent, null, game, true); + } + if (untap) { + canBeUntapped.add(permanent); + } + } + // selected permanents to untap + List selectedToUntap = new ArrayList<>(); + + // player can cancel the selection of an effect to use a preferred order of restriction effects + boolean playerCanceledSelection; + do { + playerCanceledSelection = false; + // select permanents to untap to consume the "notMoreThan" effects + for (Map.Entry>, Integer> handledEntry : notMoreThanEffectsUsage.entrySet()) { + // select a permanent to untap for this entry + int numberToUntap = handledEntry.getValue(); + if (numberToUntap > 0) { + + List leftForUntap = getPermanentsThatCanBeUntapped(game, + canBeUntapped, + handledEntry.getKey().getKey(), + notMoreThanEffectsUsage); + + FilterControlledPermanent filter = handledEntry.getKey().getKey().getFilter().copy(); + String message = filter.getMessage(); + // omit already from other untap effects selected permanents + for (Permanent permanent : selectedToUntap) { + filter.add(Predicates.not(new PermanentIdPredicate(permanent.getId()))); + } + // while targets left and there is still allowed to untap + while (canRespond() && !leftForUntap.isEmpty() && numberToUntap > 0) { + // player has to select the permanent they want to untap for this restriction + Ability ability = handledEntry.getKey().getValue().iterator().next(); + if (ability != null) { + StringBuilder sb = new StringBuilder(message).append(" to untap").append(" (").append(Math.min(leftForUntap.size(), + numberToUntap)).append(" in total"); + MageObject effectSource = game.getObject(ability.getSourceId()); + if (effectSource != null) { + sb.append(" from ").append(effectSource.getLogName()); + } + sb.append(')'); + filter.setMessage(sb.toString()); + Target target = new TargetPermanent(1, 1, filter, true); + if (!this.chooseTarget(Outcome.Untap, target, ability, game)) { + // player canceled, go on with the next effect (if no other effect available, this effect will be active again) + playerCanceledSelection = true; + break; + } + Permanent selectedPermanent = game.getPermanent(target.getFirstTarget()); + if (leftForUntap.contains(selectedPermanent)) { + selectedToUntap.add(selectedPermanent); + numberToUntap--; + // don't allow to select same permanent twice + filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); + // reduce available untap numbers from other "UntapNotMoreThan" effects if selected permanent applies to their filter too + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getValue() > 0 + && notMoreThanEffect.getKey().getKey().getFilter().match(selectedPermanent, game)) { + notMoreThanEffect.setValue(notMoreThanEffect.getValue() - 1); + } + } + // update the left for untap list + leftForUntap = getPermanentsThatCanBeUntapped(game, + canBeUntapped, + handledEntry.getKey().getKey(), + notMoreThanEffectsUsage); + // remove already selected permanents + for (Permanent permanent : selectedToUntap) { + leftForUntap.remove(permanent); + } + + } else { + // player selected an permanent that is restricted by another effect, disallow it (so AI can select another one) + filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); + if (this.isHuman() && !game.isSimulation()) { + game.informPlayer(this, "This permanent can't be untapped because of other restricting effect."); + } + } + } + } + } + } + + } while (canRespond() && playerCanceledSelection); + + if (!game.isSimulation()) { + // show in log which permanents were selected to untap + for (Permanent permanent : selectedToUntap) { + game.informPlayers(this.getLogName() + " untapped " + permanent.getLogName()); + } + } + // untap if permanent is not concerned by notMoreThan effects or is included in the selectedToUntapList + for (Permanent permanent : canBeUntapped) { + boolean doUntap = true; + if (!selectedToUntap.contains(permanent)) { + // if the permanent is covered by one of the restriction effects, don't untap it + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game)) { + doUntap = false; + break; + } + } + } + if (permanent != null && doUntap) { + permanent.untap(game); + } + + } + + } else { + //20091005 - 502.2 + + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { + boolean untap = true; + for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { + untap &= effect.canBeUntapped(permanent, null, game, true); + } + if (untap) { + permanent.untap(game); + } + } + } + } + + private List getPermanentsThatCanBeUntapped(Game game, List canBeUntapped, RestrictionUntapNotMoreThanEffect handledEffect, Map>, Integer> notMoreThanEffectsUsage) { + List leftForUntap = new ArrayList<>(); + // select permanents that can still be untapped + for (Permanent permanent : canBeUntapped) { + if (handledEffect.getFilter().match(permanent, game)) { // matches the restricted permanents of handled entry + boolean canBeSelected = true; + // check if the permanent is restricted by another restriction that has left no permanent + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game) + && notMoreThanEffect.getValue() == 0) { + canBeSelected = false; + break; + } + } + if (canBeSelected) { + leftForUntap.add(permanent); + } + } + } + return leftForUntap; + } + + @Override + public UUID getId() { + return playerId; + } + + @Override + public Cards getHand() { + return hand; + } + + @Override + public Graveyard getGraveyard() { + return graveyard; + } + + @Override + public ManaPool getManaPool() { + return this.manaPool; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getLogName() { + return GameLog.getColoredPlayerName(name); + } + + @Override + public boolean isHuman() { + return human; + } + + @Override + public Library getLibrary() { + return library; + } + + @Override + public Cards getSideboard() { + return sideboard; + } + + @Override + public int getLife() { + return life; + } + + @Override + public void initLife(int life) { + this.life = life; + } + + @Override + public void setLife(int life, Game game, Ability source) { + // rule 118.5 + if (life > this.life) { + gainLife(life - this.life, game, source); + } else if (life < this.life) { + loseLife(this.life - life, game, source, false); + } + } + + @Override + public void setLifeTotalCanChange(boolean lifeTotalCanChange) { + this.canGainLife = lifeTotalCanChange; + this.canLoseLife = lifeTotalCanChange; + } + + @Override + public boolean isLifeTotalCanChange() { + return canGainLife || canLoseLife; + } + + @Override + public List getAlternativeSourceCosts() { + return alternativeSourceCosts; + } + + @Override + public boolean isCanLoseLife() { + return canLoseLife; + } + + @Override + public void setCanLoseLife(boolean canLoseLife) { + this.canLoseLife = canLoseLife; + } + + @Override + public int loseLife(int amount, Game game, Ability source, boolean atCombat, UUID attackerId) { + if (!canLoseLife || !this.isInGame()) { + return 0; + } + GameEvent event = new GameEvent(GameEvent.EventType.LOSE_LIFE, + playerId, source, playerId, amount, atCombat); + if (!game.replaceEvent(event)) { + this.life = CardUtil.overflowDec(this.life, event.getAmount()); + if (!game.isSimulation()) { + UUID needId = attackerId; + if (needId == null) { + needId = source == null ? null : source.getSourceId(); + } + game.informPlayers(this.getLogName() + " loses " + event.getAmount() + " life" + + (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", "")); + } + if (amount > 0) { + game.fireEvent(new GameEvent(GameEvent.EventType.LOST_LIFE, + playerId, source, playerId, amount, atCombat)); + } + return amount; + } + return 0; + } + + @Override + public int loseLife(int amount, Game game, Ability source, boolean atCombat) { + return loseLife(amount, game, source, atCombat, null); + } + + @Override + public boolean isCanGainLife() { + return canGainLife; + } + + @Override + public void setCanGainLife(boolean canGainLife) { + this.canGainLife = canGainLife; + } + + @Override + public int gainLife(int amount, Game game, Ability source) { + if (!canGainLife || amount <= 0) { + return 0; + } + GameEvent event = new GameEvent(GameEvent.EventType.GAIN_LIFE, + playerId, source, playerId, amount, false); + if (!game.replaceEvent(event)) { + // TODO: lock life at Integer.MAX_VALUE if reached, until it's set to a different amount + // (https://magic.wizards.com/en/articles/archive/news/unstable-faqawaslfaqpaftidawabiajtbt-2017-12-06 - "infinite" life total stays infinite no matter how much is gained or lost) + // this.life += event.getAmount(); + this.life = CardUtil.overflowInc(this.life, event.getAmount()); + if (!game.isSimulation()) { + game.informPlayers(this.getLogName() + " gains " + event.getAmount() + " life" + CardUtil.getSourceLogName(game, source)); + } + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.GAINED_LIFE, + playerId, source, playerId, event.getAmount())); + return event.getAmount(); + } + return 0; + } + + @Override + public void exchangeLife(Player player, Ability source, Game game) { + int lifePlayer1 = getLife(); + int lifePlayer2 = player.getLife(); + if ((lifePlayer1 != lifePlayer2 && this.isLifeTotalCanChange() && player.isLifeTotalCanChange()) + && (lifePlayer1 >= lifePlayer2 || (this.isCanGainLife() && player.isCanLoseLife())) + && (lifePlayer1 <= lifePlayer2 || (this.isCanLoseLife() && player.isCanGainLife()))) { + this.setLife(lifePlayer2, game, source); + player.setLife(lifePlayer1, game, source); + } + } + + @Override + public int damage(int damage, UUID attackerId, Ability source, Game game) { + return doDamage(damage, attackerId, source, game, false, true, null); + } + + @Override + public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable) { + return doDamage(damage, attackerId, source, game, combatDamage, preventable, null); + } + + @Override + public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { + return doDamage(damage, attackerId, source, game, combatDamage, preventable, appliedEffects); + } + + private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { + if (!this.isInGame()) { + return 0; + } + + if (damage < 1) { + return 0; + } + if (!canDamage(game.getObject(attackerId), game)) { + MageObject sourceObject = game.getObject(attackerId); + game.informPlayers(damage + " damage " + + (sourceObject == null ? "" : "from " + sourceObject.getLogName()) + + " to " + getLogName() + + (damage > 1 ? " were" : "was") + " prevented because of protection"); + return 0; + } + DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combatDamage); + event.setAppliedEffects(appliedEffects); + if (game.replaceEvent(event)) { + return 0; + } + int actualDamage = event.getAmount(); + if (actualDamage < 1) { + return 0; + } + UUID sourceControllerId = null; + Abilities sourceAbilities = null; + MageObject attacker = game.getPermanentOrLKIBattlefield(attackerId); + if (attacker == null) { + StackObject stackObject = game.getStack().getStackObject(attackerId); + if (stackObject != null) { + attacker = stackObject.getStackAbility().getSourceObject(game); + } else { + attacker = game.getObject(attackerId); + } + if (attacker instanceof Spell) { + sourceAbilities = ((Spell) attacker).getAbilities(game); + sourceControllerId = ((Spell) attacker).getControllerId(); + } else if (attacker instanceof Card) { + sourceAbilities = ((Card) attacker).getAbilities(game); + sourceControllerId = ((Card) attacker).getOwnerId(); + } else if (attacker instanceof CommandObject) { + sourceControllerId = ((CommandObject) attacker).getControllerId(); + sourceAbilities = attacker.getAbilities(); + } + } else { + sourceAbilities = ((Permanent) attacker).getAbilities(game); + sourceControllerId = ((Permanent) attacker).getControllerId(); + } + if (event.isAsThoughInfect() || (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId()))) { + addCounters(CounterType.POISON.createInstance(actualDamage), sourceControllerId, source, game); + } else { + GameEvent damageToLifeLossEvent = new GameEvent(GameEvent.EventType.DAMAGE_CAUSES_LIFE_LOSS, + playerId, source, playerId, actualDamage, combatDamage); + if (!game.replaceEvent(damageToLifeLossEvent)) { + this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combatDamage, attackerId); + } + } + if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) { + if (combatDamage) { + game.getPermanent(attackerId).markLifelink(actualDamage); + } else { + Player player = game.getPlayer(sourceControllerId); + player.gainLife(actualDamage, game, source); + } + } + // Unstable ability - Earl of Squirrel + if (sourceAbilities != null && sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) { + Player player = game.getPlayer(sourceControllerId); + new SquirrelToken().putOntoBattlefield(actualDamage, game, source, player.getId()); + } + DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combatDamage); + game.fireEvent(damagedEvent); + game.getState().addSimultaneousDamage(damagedEvent, game); + return actualDamage; + } + + @Override + public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) { + boolean returnCode = true; + GameEvent addingAllEvent = GameEvent.getEvent( + GameEvent.EventType.ADD_COUNTERS, playerId, source, + playerAddingCounters, counter.getName(), counter.getCount() + ); + if (!game.replaceEvent(addingAllEvent)) { + int amount = addingAllEvent.getAmount(); + int finalAmount = amount; + boolean isEffectFlag = addingAllEvent.getFlag(); + for (int i = 0; i < amount; i++) { + Counter eventCounter = counter.copy(); + eventCounter.remove(eventCounter.getCount() - 1); + GameEvent addingOneEvent = GameEvent.getEvent( + GameEvent.EventType.ADD_COUNTER, playerId, source, + playerAddingCounters, counter.getName(), 1 + ); + addingOneEvent.setFlag(isEffectFlag); + if (!game.replaceEvent(addingOneEvent)) { + getCounters().addCounter(eventCounter); + GameEvent addedOneEvent = GameEvent.getEvent( + GameEvent.EventType.COUNTER_ADDED, playerId, source, + playerAddingCounters, counter.getName(), 1 + ); + addedOneEvent.setFlag(addingOneEvent.getFlag()); + game.fireEvent(addedOneEvent); + } else { + finalAmount--; + returnCode = false; + } + } + if (finalAmount > 0) { + GameEvent addedAllEvent = GameEvent.getEvent( + GameEvent.EventType.COUNTERS_ADDED, playerId, source, + playerAddingCounters, counter.getName(), amount + ); + addedAllEvent.setFlag(addingAllEvent.getFlag()); + game.fireEvent(addedAllEvent); + } + } else { + returnCode = false; + } + return returnCode; + } + + @Override + public void removeCounters(String name, int amount, Ability source, Game game) { + int finalAmount = 0; + for (int i = 0; i < amount; i++) { + if (!counters.removeCounter(name, 1)) { + break; + } + GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, + getId(), source, (source == null ? null : source.getControllerId())); + event.setData(name); + event.setAmount(1); + game.fireEvent(event); + finalAmount++; + } + GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, + getId(), source, (source == null ? null : source.getControllerId())); + event.setData(name); + event.setAmount(finalAmount); + game.fireEvent(event); + } + + protected boolean canDamage(MageObject source, Game game) { + for (ProtectionAbility ability : abilities.getProtectionAbilities()) { + if (!ability.canTarget(source, game)) { + return false; + } + } + return true; + } + + @Override + public Abilities getAbilities() { + return this.abilities; + } + + @Override + public void addAbility(Ability ability) { + ability.setSourceId(playerId); + this.abilities.add(ability); + } + + @Override + public int getLandsPerTurn() { + return this.landsPerTurn; + } + + @Override + public void setLandsPerTurn(int landsPerTurn) { + this.landsPerTurn = landsPerTurn; + } + + @Override + public int getLoyaltyUsePerTurn() { + return this.loyaltyUsePerTurn; + } + + @Override + public void setLoyaltyUsePerTurn(int loyaltyUsePerTurn) { + this.loyaltyUsePerTurn = loyaltyUsePerTurn; + } + + @Override + public int getMaxHandSize() { + return maxHandSize; + } + + @Override + public void setMaxHandSize(int maxHandSize) { + this.maxHandSize = maxHandSize; + } + + @Override + public void setMaxAttackedBy(int maxAttackedBy) { + this.maxAttackedBy = maxAttackedBy; + } + + @Override + public int getMaxAttackedBy() { + return maxAttackedBy; + } + + @Override + public void setResponseString(String responseString) { + } + + @Override + public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) { + } + + @Override + public void setResponseUUID(UUID responseUUID) { + } + + @Override + public void setResponseBoolean(Boolean responseBoolean) { + } + + @Override + public void setResponseInteger(Integer responseInteger) { + } + + @Override + public boolean isPassed() { + return passed; + } + + @Override + public void pass(Game game) { + this.passed = true; + resetStoredBookmark(game); + } + + @Override + public void resetPassed() { + this.passed = this.loses || this.hasLeft(); + } + + @Override + public void resetPlayerPassedActions() { + this.passed = false; + this.passedTurn = false; + this.passedTurnSkipStack = false; + this.passedUntilEndOfTurn = false; + this.passedUntilNextMain = false; + this.passedUntilStackResolved = false; + this.dateLastAddedToStack = null; + this.passedUntilEndStepBeforeMyTurn = false; + this.skippedAtLeastOnce = false; + this.passedAllTurns = false; + this.justActivatedType = null; + } + + @Override + public void quit(Game game) { + quit = true; + this.concede(game); + logger.debug(getName() + " quits the match."); + game.informPlayers(getLogName() + " quits the match."); + } + + @Override + public void timerTimeout(Game game) { + quit = true; + timerTimeout = true; + this.concede(game); + game.informPlayers(getLogName() + " has run out of time, losing the match."); + } + + @Override + public void idleTimeout(Game game) { + quit = true; + idleTimeout = true; + this.concede(game); + game.informPlayers(getLogName() + " was idle for too long, losing the Match."); + } + + @Override + public void concede(Game game) { + game.setConcedingPlayer(playerId); + lost(game); +// this.left = true; + } + + @Override + public void sendPlayerAction(PlayerAction playerAction, Game game, Object data) { + switch (playerAction) { + case PASS_PRIORITY_UNTIL_MY_NEXT_TURN: // F9 + resetPlayerPassedActions(); + passedAllTurns = true; + this.skip(); + break; + case PASS_PRIORITY_UNTIL_TURN_END_STEP: // F5 + resetPlayerPassedActions(); + passedUntilEndOfTurn = true; + skippedAtLeastOnce = PhaseStep.END_TURN != game.getTurn().getStepType(); + this.skip(); + break; + case PASS_PRIORITY_UNTIL_NEXT_TURN: // F4 + resetPlayerPassedActions(); + passedTurn = true; + this.skip(); + break; + case PASS_PRIORITY_UNTIL_NEXT_TURN_SKIP_STACK: // F6 + resetPlayerPassedActions(); + passedTurnSkipStack = true; + this.skip(); + break; + case PASS_PRIORITY_UNTIL_NEXT_MAIN_PHASE: //F7 + resetPlayerPassedActions(); + passedUntilNextMain = true; + skippedAtLeastOnce = !(game.getTurn().getStepType() == PhaseStep.POSTCOMBAT_MAIN + || game.getTurn().getStepType() == PhaseStep.PRECOMBAT_MAIN); + this.skip(); + break; + case PASS_PRIORITY_UNTIL_STACK_RESOLVED: // Default F10 - Skips until the current stack is resolved + if (!game.getStack().isEmpty()) { // If stack is empty do nothing + resetPlayerPassedActions(); + passedUntilStackResolved = true; + dateLastAddedToStack = game.getStack().getDateLastAdded(); + this.skip(); + } + break; + case PASS_PRIORITY_UNTIL_END_STEP_BEFORE_MY_NEXT_TURN: //F11 + resetPlayerPassedActions(); + passedUntilEndStepBeforeMyTurn = true; + this.skip(); + break; + case PASS_PRIORITY_CANCEL_ALL_ACTIONS: + resetPlayerPassedActions(); + break; + case PERMISSION_REQUESTS_ALLOWED_OFF: + userData.setAllowRequestShowHandCards(false); + break; + case PERMISSION_REQUESTS_ALLOWED_ON: + userData.setAllowRequestShowHandCards(true); + userData.resetRequestedHandPlayersList(game.getId()); // users can send request again + break; + } + logger.trace("PASS Priority: " + playerAction); + } + + @Override + public void leave() { + this.passed = true; + this.loses = true; + this.left = true; + this.abort(); + //20100423 - 800.4a + this.hand.clear(); + this.graveyard.clear(); + this.library.clear(); + } + + @Override + public boolean hasLeft() { + return this.left; + } + + @Override + public void lost(Game game) { + if (canLose(game)) { + lostForced(game); + } + } + + @Override + public void lostForced(Game game) { + logger.debug(this.getName() + " has lost gameId: " + game.getId()); + //20100423 - 603.9 + if (!this.wins) { + this.loses = true; + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LOST, null, null, playerId)); + game.informPlayers(this.getLogName() + " has lost the game."); + } else { + logger.debug(this.getName() + " has already won - stop lost"); + } + // for draw - first all players that have lost have to be set to lost + if (!hasLeft()) { + logger.debug("Game over playerId: " + playerId); + game.setConcedingPlayer(playerId); + } + } + + @Override + public boolean canLose(Game game) { + return hasLeft() // If a player concedes or has left the match they lose also if effect would say otherwise + || !game.replaceEvent(new GameEvent(GameEvent.EventType.LOSES, null, null, playerId)); + } + + @Override + public void won(Game game) { + boolean opponentInGame = false; + for (UUID opponentId : game.getOpponents(playerId)) { + Player opponent = game.getPlayer(opponentId); + + if (opponent != null && opponent.isInGame()) { + opponentInGame = true; + break; + } + } + if (!opponentInGame + || // if no more opponent is in game the wins event may no longer be replaced + !game.replaceEvent(new GameEvent(GameEvent.EventType.WINS, null, null, playerId))) { + logger.debug("player won -> start: " + this.getName()); + if (!this.loses) { + //20130501 - 800.7, 801.16 + // all opponents in range loose the game + for (UUID opponentId : game.getOpponents(playerId)) { + Player opponent = game.getPlayer(opponentId); + if (opponent != null && !opponent.hasLost()) { + logger.debug("player won -> calling opponent lost: " + + this.getName() + " opponent: " + opponent.getName()); + opponent.lostForced(game); + } + } + // if no more opponents alive (independant from range), you win and the game ends + int opponentsAlive = 0; + for (UUID playerIdToCheck : game.getPlayerList()) { + if (game.isOpponent(this, playerIdToCheck)) { // Check without range + Player opponent = game.getPlayer(playerIdToCheck); + if (opponent != null && !opponent.hasLost()) { + opponentsAlive++; + } + } + } + if (opponentsAlive == 0 && !hasWon()) { + logger.debug("player won -> No more opponents alive game won: " + this.getName()); + game.informPlayers(this.getLogName() + " has won the game"); + this.wins = true; + game.end(); + } + } else { + logger.debug("player won -> but already lost before or other players still alive: " + this.getName()); + } + } + } + + @Override + public void drew(Game game) { + if (!hasLost()) { + this.draws = true; + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DRAW_PLAYER, null, null, playerId)); + game.informPlayers("For " + this.getLogName() + " the game is a draw."); + game.setConcedingPlayer(playerId); + } + } + + @Override + public boolean hasLost() { + return this.loses; + } + + @Override + public boolean isInGame() { + return !hasQuit() && !hasLost() && !hasWon() && !hasDrew() && !hasLeft(); + } + + @Override + public boolean canRespond() { // abort is checked here to get out of player requests (as example: after disconnect) + return isInGame() && !abort; + } + + @Override + public boolean hasWon() { + return !this.loses && this.wins; + } + + @Override + public boolean hasDrew() { + return this.draws; + } + + @Override + public void declareAttacker(UUID attackerId, UUID defenderId, Game game, boolean allowUndo) { + if (allowUndo) { + setStoredBookmark(game.bookmarkState()); // makes it possible to UNDO a declared attacker with costs from e.g. Propaganda + } + Permanent attacker = game.getPermanent(attackerId); + if (attacker != null + && attacker.canAttack(defenderId, game) + && attacker.isControlledBy(playerId)) { + if (!game.getCombat().declareAttacker(attackerId, defenderId, playerId, game)) { + game.undo(playerId); + } + } + } + + @Override + public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game) { + declareBlocker(defenderId, blockerId, attackerId, game, true); + } + + @Override + public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game, boolean allowUndo) { + if (isHuman() && allowUndo) { + setStoredBookmark(game.bookmarkState()); + } + Permanent blocker = game.getPermanent(blockerId); + CombatGroup group = game.getCombat().findGroup(attackerId); + if (blocker != null && group != null && group.canBlock(blocker, game)) { + group.addBlocker(blockerId, playerId, game); + game.getCombat().addBlockingGroup(blockerId, attackerId, playerId, game); + } else if (this.isHuman() && !game.isSimulation()) { + game.informPlayer(this, "You can't block this creature."); + } + } + + @Override + public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game) { + return searchLibrary(target, source, game, playerId); + } + + @Override + public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) { + //20091005 - 701.14c + + // searching control can be intercepted by another player, see Opposition Agent + SearchLibraryEvent searchEvent = new SearchLibraryEvent(targetPlayerId, source, playerId, Integer.MAX_VALUE); + if (game.replaceEvent(searchEvent)) { + return false; + } + + Player targetPlayer = game.getPlayer(targetPlayerId); + Player searchingPlayer = this; + Player searchingController = game.getPlayer(searchEvent.getSearchingControllerId()); + if (targetPlayer == null || searchingController == null) { + return false; + } + + String searchInfo = searchingPlayer.getLogName(); + if (!searchingPlayer.getId().equals(searchingController.getId())) { + searchInfo = searchInfo + " under control of " + searchingPlayer.getLogName(); + } + if (targetPlayer.getId().equals(searchingPlayer.getId())) { + searchInfo = searchInfo + " searches their library"; + } else { + searchInfo = searchInfo + " searches the library of " + targetPlayer.getLogName(); + } + + if (!game.isSimulation()) { + game.informPlayers(searchInfo + CardUtil.getSourceLogName(game, source)); + } + + // https://www.reddit.com/r/magicTCG/comments/jj8gh9/opposition_agent_and_panglacial_wurm_interaction/ + // You must take full player control while searching, e.g. you can cast opponent's cards by Panglacial Wurm effect: + // * While you’re searching your library, you may cast Panglacial Wurm from your library. + // So use here same code as Word of Command + // P.S. no needs in searchingController, but it helps with unit tests, see TakeControlWhileSearchingLibraryTest + boolean takeControl = false; + if (!searchingPlayer.getId().equals(searchingController.getId())) { + CardUtil.takeControlUnderPlayerStart(game, searchingController, searchingPlayer, true); + takeControl = true; + } + + Library searchingLibrary = targetPlayer.getLibrary(); + TargetCardInLibrary newTarget = target.copy(); + int count; + int librarySearchLimit = searchEvent.getAmount(); + List cardsFromTop = null; + do { + // TODO: prevent shuffling from moving the visualized cards + if (librarySearchLimit == Integer.MAX_VALUE) { + count = searchingLibrary.count(target.getFilter(), game); + } else { + if (cardsFromTop == null) { + cardsFromTop = new ArrayList<>(searchingLibrary.getTopCards(game, librarySearchLimit)); + } else { + cardsFromTop.retainAll(searchingLibrary.getCards(game)); + } + newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size())); + count = Math.min(searchingLibrary.count(target.getFilter(), game), librarySearchLimit); + } + + if (count < target.getNumberOfTargets()) { + newTarget.setMinNumberOfTargets(count); + } + + // handling Panglacial Wurm - cast cards while searching from own library + if (targetPlayer.getId().equals(searchingPlayer.getId())) { + if (handleCastableCardsWhileLibrarySearching(library, game, targetPlayer)) { + // clear all choices to start from scratch (casted cards must be removed from library) + newTarget.clearChosen(); + continue; + } + } + + if (newTarget.choose(Outcome.Neutral, searchingController.getId(), targetPlayer.getId(), game)) { + target.getTargets().clear(); + for (UUID targetId : newTarget.getTargets()) { + target.add(targetId, game); + } + } + + // END SEARCH + if (takeControl) { + CardUtil.takeControlUnderPlayerEnd(game, searchingController, searchingPlayer); + game.informPlayers("Control of " + searchingPlayer.getLogName() + " is back" + CardUtil.getSourceLogName(game, source)); + } + + LibrarySearchedEvent searchedEvent = new LibrarySearchedEvent(targetPlayer.getId(), source, searchingPlayer.getId(), target); + if (!game.replaceEvent(searchedEvent)) { + game.fireEvent(searchedEvent); + } + break; + } while (true); + + return true; + } + + @Override + public boolean seekCard(FilterCard filter, Ability source, Game game) { + Set cards = this.getLibrary() + .getCards(game) + .stream() + .filter(card -> filter.match(card, source.getSourceId(), getId(), game)) + .collect(Collectors.toSet()); + Card card = RandomUtil.randomFromCollection(cards); + if (card == null) { + return false; + } + game.informPlayers(this.getLogName() + " seeks a card from their library"); + this.moveCards(card, Zone.HAND, source, game); + return true; + } + + @Override + public void lookAtAllLibraries(Ability source, Game game) { + for (UUID playerId : game.getState().getPlayersInRange(this.getId(), game)) { + Player player = game.getPlayer(playerId); + String playerName = this.getName().equals(player.getName()) ? "Your " : player.getName() + "'s "; + playerName += "library"; + Cards cardsInLibrary = new CardsImpl(player.getLibrary().getTopCards(game, player.getLibrary().size())); + lookAtCards(playerName, cardsInLibrary, game); + } + } + + private boolean handleCastableCardsWhileLibrarySearching(Library library, Game game, Player targetPlayer) { + // must return true after cast try (to restart searching process without casted cards) + // uses for handling Panglacial Wurm: + // * While you're searching your library, you may cast Panglacial Wurm from your library. + + List castableCards = library.getCards(game).stream() + .filter(card -> card.getAbilities(game).containsClass(WhileSearchingPlayFromLibraryAbility.class)) + .map(MageItem::getId) + .collect(Collectors.toList()); + if (castableCards.size() == 0) { + return false; + } + + // only humans can use it + if (targetPlayer.isComputer()) { + return false; + } + + if (!targetPlayer.chooseUse(Outcome.AIDontUseIt, "There are " + castableCards.size() + " cards you can cast while searching your library. Cast any of them?", null, game)) { + return false; + } + + boolean casted = false; + TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, StaticFilters.FILTER_CARD); + targetCard.setTargetName("card to cast from library"); + targetCard.setNotTarget(true); + while (castableCards.size() > 0) { + targetCard.clearChosen(); + if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, game)) { + break; + } + + Card card = game.getCard(targetCard.getFirstTarget()); + if (card == null) { + break; + } + + // AI NOTICE: if you want AI implement here then remove selected card from castable after each + // choice (otherwise you catch infinite freeze on uncastable use case) + // casting selected card + // TODO: fix costs (why is Panglacial Wurm automatically accepting payment?) + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + targetPlayer.cast(targetPlayer.chooseAbilityForCast(card, game, false), game, false, null); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + castableCards.remove(card.getId()); + casted = true; + } + return casted; + } + + /** + * @param source + * @param game + * @param winnable + * @return if winnable, true if player won the toss, if not winnable, true + * for heads and false for tails + */ + @Override + public boolean flipCoin(Ability source, Game game, boolean winnable) { + boolean chosen = false; + if (winnable) { + chosen = this.chooseUse(Outcome.Benefit, "Heads or tails?", "", "Heads", "Tails", source, game); + game.informPlayers(getLogName() + " chose " + CardUtil.booleanToFlipName(chosen)); + } + boolean result = this.flipCoinResult(game); + FlipCoinEvent event = new FlipCoinEvent(playerId, source, result, chosen, winnable); + game.replaceEvent(event); + game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult()) + + CardUtil.getSourceLogName(game, source)); + if (event.getFlipCount() > 1) { + boolean canChooseHeads = event.getResult(); + boolean canChooseTails = !event.getResult(); + for (int i = 1; i < event.getFlipCount(); i++) { + boolean tempFlip = this.flipCoinResult(game); + canChooseHeads = canChooseHeads || tempFlip; + canChooseTails = canChooseTails || !tempFlip; + game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(tempFlip)); + } + if (canChooseHeads && canChooseTails) { + event.setResult(chooseUse(Outcome.Benefit, "Choose which flip to keep", + (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), + "Heads", "Tails", source, game + )); + } else { + event.setResult(canChooseHeads); + } + game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); + } + if (event.isWinnable()) { + game.informPlayers(getLogName() + " " + (event.getResult() == event.getChosen() ? "won" : "lost") + " the flip" + + CardUtil.getSourceLogName(game, source)); + } + game.fireEvent(event.createFlippedEvent()); + if (event.isWinnable()) { + return event.getResult() == event.getChosen(); + } + return event.getResult(); + } + + /** + * Return result for next flip coint try (can be contolled in tests) + * + * @return + */ + @Override + public boolean flipCoinResult(Game game) { + return RandomUtil.nextBoolean(); + } + + private static final class RollDieResult { + + // 706.2. + // After the roll, the number indicated on the top face of the die before any modifiers is + // the natural result. The instruction may include modifiers to the roll which add to or + // subtract from the natural result. Modifiers may also come from other sources. After + // considering all applicable modifiers, the final number is the result of the die roll. + private final int naturalResult; + private final int modifier; + private final PlanarDieRollResult planarResult; + + RollDieResult(int naturalResult, int modifier, PlanarDieRollResult planarResult) { + this.naturalResult = naturalResult; + this.modifier = modifier; + this.planarResult = planarResult; + } + + public int getResult() { + return this.naturalResult + this.modifier; + } + + public PlanarDieRollResult getPlanarResult() { + return this.planarResult; + } + } + + @Override + public int rollDieResult(int sides, Game game) { + return RandomUtil.nextInt(sides) + 1; + } + + /** + * Roll single die. Support both die types: planar and numerical. + * + * @param outcome + * @param game + * @param source + * @param rollDieType + * @param sidesAmount + * @param chaosSidesAmount + * @param planarSidesAmount + * @param rollsAmount + * @return + */ + private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType, + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { + if (rollsAmount == 1) { + return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount); + } + Set choices = new HashSet<>(); + for (int j = 0; j < rollsAmount; j++) { + choices.add(rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount)); + } + if (choices.size() == 1) { + return choices.stream().findFirst().orElse(0); + } + + // AI hint - use max/min values + if (this.isComputer()) { + if (rollDieType == RollDieType.NUMERICAL) { + // numerical + if (outcome.isGood()) { + return choices.stream() + .map(Integer.class::cast) + .max(Comparator.naturalOrder()) + .orElse(null); + } else { + return choices.stream() + .map(Integer.class::cast) + .min(Comparator.naturalOrder()) + .orElse(null); + } + } else { + // planar + // priority: chaos -> planar -> blank + return choices.stream() + .map(PlanarDieRollResult.class::cast) + .max(Comparator.comparingInt(PlanarDieRollResult::getAIPriority)) + .orElse(null); + } + } + + Choice choice = new ChoiceImpl(true); + choice.setMessage("Choose which die roll result to keep (the rest will be ignored)"); + choice.setChoices(choices.stream().sorted().map(Object::toString).collect(Collectors.toSet())); + + this.choose(Outcome.Neutral, choice, game); + Object defaultChoice = choices.iterator().next(); + return choices.stream() + .filter(o -> o.toString().equals(choice.getChoice())) + .findFirst() + .orElse(defaultChoice); + } + + private Object rollDieInnerWithReplacement(Game game, Ability source, RollDieType rollDieType, int numSides, int numChaosSides, int numPlanarSides) { + switch (rollDieType) { + + case NUMERICAL: { + int result = rollDieResult(numSides, game); + // Clam-I-Am workaround: + // If you roll a 3 on a six-sided die, you may reroll that die. + if (numSides == 6 + && result == 3 + && game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.REPLACE_ROLLED_DIE, source.getControllerId(), source, source.getControllerId())) + && chooseUse(Outcome.Neutral, "Re-roll the 3?", source, game)) { + result = rollDieResult(numSides, game); + } + return result; + } + + case PLANAR: { + if (numChaosSides + numPlanarSides > numSides) { + numChaosSides = GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES; + numPlanarSides = GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES; + } + // for 9 sides: + // 1..2 - chaos + // 3..7 - blank + // 8..9 - planar + int result = this.rollDieResult(numSides, game); + PlanarDieRollResult roll; + if (result <= numChaosSides) { + roll = PlanarDieRollResult.CHAOS_ROLL; + } else if (result > numSides - numPlanarSides) { + roll = PlanarDieRollResult.PLANAR_ROLL; + } else { + roll = PlanarDieRollResult.BLANK_ROLL; + } + return roll; + } + + default: { + throw new IllegalArgumentException("Unknown roll die type " + rollDieType); + } + } + } + + /** + * @param outcome + * @param source + * @param game + * @param sidesAmount number of sides the dice has + * @param rollsAmount number of tries to roll the dice + * @param ignoreLowestAmount remove the lowest rolls from the results + * @return the number that the player rolled + */ + @Override + public List rollDice(Outcome outcome, Ability source, Game game, int sidesAmount, int rollsAmount, int ignoreLowestAmount) { + return rollDiceInner(outcome, source, game, RollDieType.NUMERICAL, sidesAmount, 0, 0, rollsAmount, ignoreLowestAmount) + .stream() + .map(Integer.class::cast) + .collect(Collectors.toList()); + } + + /** + * Inner code to roll a dice. Support normal and planar types. + * + * @param outcome + * @param source + * @param game + * @param rollDieType die type to roll, e.g. planar or numerical + * @param sidesAmount sides per die + * @param chaosSidesAmount for planar die: chaos sides + * @param planarSidesAmount for planar die: planar sides + * @param rollsAmount rolls + * @param ignoreLowestAmount for numerical die: ignore multiple rolls with + * the lowest values + * @return + */ + private List rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType, + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, + int rollsAmount, int ignoreLowestAmount) { + RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount); + if (ignoreLowestAmount > 0) { + rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount); + } + game.replaceEvent(rollDiceEvent); + + // 706.6. + // In a Planechase game, rolling the planar die will cause any ability that triggers whenever a + // player rolls one or more dice to trigger. However, any effect that refers to a numerical + // result of a die roll, including ones that compare the results of that roll to other rolls + // or to a given number, ignores the rolling of the planar die. See rule 901, “Planechase.” + // ROLL MULTIPLE dies + // results amount can be less than a rolls amount (example: The Big Idea allows rolling 2x instead 1x) + List dieResults = new ArrayList<>(); + List dieRolls = new ArrayList<>(); + for (int i = 0; i < rollDiceEvent.getAmount(); i++) { + // ROLL SINGLE die + RollDieEvent rollDieEvent = new RollDieEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides()); + game.replaceEvent(rollDieEvent); + + Object rollResult; + // big idea logic for numerical rolls only + if (rollDieEvent.getRollDieType() == RollDieType.NUMERICAL && rollDieEvent.getBigIdeaRollsAmount() > 0) { + // rolls 2x + sum results + // The Big Idea: roll two six-sided dice and use the total of those results + int totalSum = 0; + for (int j = 0; j < rollDieEvent.getBigIdeaRollsAmount() + 1; j++) { + int singleResult = (Integer) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount()); + totalSum += singleResult; + dieRolls.add(new RollDieResult(singleResult, rollDieEvent.getResultModifier(), null)); + } + rollResult = totalSum; + } else { + // rolls 1x + switch (rollDieEvent.getRollDieType()) { + default: + case NUMERICAL: { + int naturalResult = (Integer) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount() + ); + dieRolls.add(new RollDieResult(naturalResult, rollDieEvent.getResultModifier(), null)); + rollResult = naturalResult; + break; + } + + case PLANAR: { + PlanarDieRollResult planarResult = (PlanarDieRollResult) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount() + ); + dieRolls.add(new RollDieResult(0, 0, planarResult)); + rollResult = planarResult; + break; + } + } + } + dieResults.add(rollResult); + } + + // ignore the lowest results + // planar dies: due to 706.6. planar die results must be fully ignored + // + // 706.5. + // If a player is instructed to roll two or more dice and ignore the lowest roll, the roll + // that yielded the lowest result is considered to have never happened. No abilities trigger + // because of the ignored roll, and no effects apply to that roll. If multiple results are tied + // for the lowest, the player chooses one of those rolls to be ignored. + int diceRolledTotal = dieRolls.size(); + String ignoreMessage; + if (rollDiceEvent.getRollDieType() == RollDieType.NUMERICAL && rollDiceEvent.getIgnoreLowestAmount() > 0) { + // find ignored values + List ignoredResults = new ArrayList<>(); + for (int i = 0; i < rollDiceEvent.getIgnoreLowestAmount(); i++) { + int min = dieResults.stream().map(Integer.class::cast).mapToInt(Integer::intValue).min().orElse(0); + dieResults.remove(Integer.valueOf(min)); + ignoredResults.add(min); + } + ignoreMessage = String.format( + ignoredResults.size() > 1 ? ", ignoring [%s]" : ", ignoring %s", + ignoredResults + .stream() + .map(x -> "" + x) + .collect(Collectors.joining(", ")) + ); + // remove ignored rolls (they not exist anymore) + List newRolls = new ArrayList<>(); + for (RollDieResult rollDieResult : dieRolls) { + if (ignoredResults.contains(rollDieResult.getResult())) { + ignoredResults.remove((Integer) rollDieResult.getResult()); + } else { + newRolls.add(rollDieResult); + } + } + dieRolls.clear(); + dieRolls.addAll(newRolls); + } else { + ignoreMessage = ""; + } + + // raise affected roll events + for (RollDieResult result : dieRolls) { + game.fireEvent(new DieRolledEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides(), result.naturalResult, result.modifier, result.planarResult)); + } + game.fireEvent(new DiceRolledEvent(rollDiceEvent.getSides(), dieResults, source)); + + String resultString = dieResults + .stream() + .map(Object::toString) + .collect(Collectors.joining(", ")); + String message; + switch (rollDiceEvent.getRollDieType()) { + default: + case NUMERICAL: + // [Roll a die] user rolled 4d6, results: [4, 6], ignoring [1, 3] (source: xxx) + message = String.format("[Roll a die] %s rolled %sd%s, result%s: %s%s%s", + getLogName(), + diceRolledTotal > 1 ? diceRolledTotal : "a ", + rollDiceEvent.getSides(), + dieResults.size() > 1 ? 's' : "", + dieResults.size() > 1 ? '[' + resultString + ']' : resultString, + ignoreMessage, + CardUtil.getSourceLogName(game, source)); + break; + case PLANAR: + // [Roll a planar die] user rolled CHAOS (source: xxx) + message = String.format("[Roll a planar die] %s rolled %s%s", + getLogName(), + dieResults.size() > 1 ? '[' + resultString + ']' : resultString, + CardUtil.getSourceLogName(game, source)); + break; + } + game.informPlayers(message); + return dieResults; + } + + /** + * @param source + * @param game + * @param chaosSidesAmount The number of chaos sides the planar die + * currently has (normally 1 but can be 5) + * @param planarSidesAmount The number of chaos sides the planar die + * currently has (normally 1) + * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll + * or BlankRoll + */ + @Override + public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int chaosSidesAmount, int planarSidesAmount) { + return rollDiceInner(outcome, source, game, RollDieType.PLANAR, GameOptions.PLANECHASE_PLANAR_DIE_TOTAL_SIDES, chaosSidesAmount, planarSidesAmount, 1, 0) + .stream() + .map(o -> (PlanarDieRollResult) o) + .findFirst() + .orElse(PlanarDieRollResult.BLANK_ROLL); + } + + @Override + public List getAvailableAttackers(Game game) { + // TODO: get available opponents and their planeswalkers, check for each if permanent can attack one + return getAvailableAttackers(null, game); + } + + @Override + public List getAvailableAttackers(UUID defenderId, Game game) { + FilterCreatureForCombat filter = new FilterCreatureForCombat(); + List attackers = game.getBattlefield().getAllActivePermanents(filter, playerId, game); + attackers.removeIf(entry -> !entry.canAttack(defenderId, game)); + return attackers; + } + + @Override + public List getAvailableBlockers(Game game) { + FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock(); + return game.getBattlefield().getAllActivePermanents(blockFilter, playerId, game); + } + + /** + * Returns the mana options the player currently has. That means which + * combinations of mana are available to cast spells or activate abilities + * etc. + * + * @param game + * @return + */ + @Override + public ManaOptions getManaAvailable(Game game) { + boolean oldState = game.inCheckPlayableState(); + game.setCheckPlayableState(true); + + ManaOptions availableMana = new ManaOptions(); + availableMana.addMana(manaPool.getMana()); + // conditional mana + for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { + availableMana.addMana(conditionalMana); + } + + List> sourceWithoutManaCosts = new ArrayList<>(); + List> sourceWithCosts = new ArrayList<>(); + for (Card card : getHand().getCards(game)) { + Abilities manaAbilities + = card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game); + for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + ActivatedManaAbilityImpl ability = it.next(); + Abilities noTapAbilities = new AbilitiesImpl<>(ability); + if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { + sourceWithoutManaCosts.add(noTapAbilities); + } else { + sourceWithCosts.add(noTapAbilities); + } + } + } + + for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range + Boolean canUse = null; + boolean canAdd = false; + boolean useLater = false; // sources with mana costs or mana pool dependency + Abilities manaAbilities + = permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true + for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + ActivatedManaAbilityImpl ability = it.next(); + if (canUse == null) { + canUse = permanent.canUseActivatedAbilities(game); + } + if (canUse) { + // abilities without Tap costs have to be handled as separate sources, because they can be used also + if (!ability.hasTapCost()) { + it.remove(); + Abilities noTapAbilities = new AbilitiesImpl<>(ability); + if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { + sourceWithoutManaCosts.add(noTapAbilities); + } else { + sourceWithCosts.add(noTapAbilities); + } + continue; + } + + canAdd = true; + if (!ability.getManaCosts().isEmpty() || ability.isPoolDependant()) { + useLater = true; + break; + } + } + } + if (canAdd) { + if (useLater) { + sourceWithCosts.add(manaAbilities); + } else { + sourceWithoutManaCosts.add(manaAbilities); + } + } + } + + for (Abilities manaAbilities : sourceWithoutManaCosts) { + availableMana.addMana(manaAbilities, game); + } + + boolean anAbilityWasUsed = true; + boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production + while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) { + anAbilityWasUsed = false; + for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext();) { + Abilities manaAbilities = iterator.next(); + if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) { + boolean used; + if (manaAbilities.hasPoolDependantAbilities()) { + used = availableMana.addManaPoolDependant(manaAbilities, game); + } else { + used = availableMana.addManaWithCost(manaAbilities, game); + } + if (used) { + iterator.remove(); + availableMana.removeDuplicated(); + anAbilityWasUsed = true; + } + } + } + if (!anAbilityWasUsed && !usePoolDependantAbilities) { + usePoolDependantAbilities = true; + anAbilityWasUsed = true; + } + } + + // remove duplicated variants (see ManaOptionsTest for info - when that rises) + availableMana.removeDuplicated(); + + game.setCheckPlayableState(oldState); + return availableMana; + } + + /** + * Used during calculation of available mana to gather the amount of + * producable triggered mana caused by using mana sources. So the set value + * is only used during the calculation of the mana produced by one source + * and cleared thereafter + * + * @param netManaAvailable the net mana produced by the triggered mana + * abaility + */ + @Override + public void addAvailableTriggeredMana(List netManaAvailable + ) { + this.availableTriggeredManaList.add(netManaAvailable); + } + + /** + * Used during calculation of available mana to get the amount of producable + * triggered mana caused by using mana sources. The list is cleared as soon + * the value is retrieved during available mana calculation. + * + * @return + */ + @Override + public List> getAvailableTriggeredMana() { + return availableTriggeredManaList; + } + // returns only mana producers that don't require mana payment + + protected List getAvailableManaProducers(Game game) { + List result = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range + Boolean canUse = null; + boolean canAdd = false; + for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { + if (!ability.getManaCosts().isEmpty()) { + canAdd = false; + break; + } + if (canUse == null) { + canUse = permanent.canUseActivatedAbilities(game); + } + if (canUse && ability.canActivate(playerId, game).canActivate()) { + canAdd = true; + } + } + if (canAdd) { + result.add(permanent); + } + } + for (Card card : getHand().getCards(game)) { + boolean canAdd = false; + for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.HAND)) { + if (!ability.getManaCosts().isEmpty()) { + canAdd = false; + break; + } + if (ability.canActivate(playerId, game).canActivate()) { + canAdd = true; + } + } + if (canAdd) { + result.add(card); + } + } + return result; + } + + // returns only mana producers that require mana payment + public List getAvailableManaProducersWithCost(Game game) { + List result = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { + Boolean canUse = null; + for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { + if (canUse == null) { + canUse = permanent.canUseActivatedAbilities(game); + } + if (canUse && ability.canActivate(playerId, game).canActivate() + && !ability.getManaCosts().isEmpty()) { + result.add(permanent); + break; + } + } + } + return result; + } + + /** + * @param ability + * @param availableMana if null, it won't be checked if enough mana is + * available + * @param sourceObject + * @param game + * @return + */ + protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { + if (!(ability instanceof ActivatedManaAbilityImpl)) { + ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability + if (!copy.canActivate(playerId, game).canActivate()) { + return false; + } + if (availableMana != null) { + sourceObject.adjustCosts(copy, game); + game.getContinuousEffects().costModification(copy, game); + } + boolean canBeCastRegularly = true; + if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { + // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... + // 117.6a (...) If an alternative cost is applied to an unpayable cost, + // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. + canBeCastRegularly = false; + } + if (canBeCastRegularly) { + if (canPayMinimumManaCost(copy, availableMana, game)) { + return true; + } + } + + // ALTERNATIVE COST FROM dynamic effects + if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) { + ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()); + Costs costs = getCastSourceIdCosts().get(copy.getSourceId()); + + boolean canPutToPlay = true; + if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { + canPutToPlay = false; + } + if (costs != null && !costs.canPay(copy, copy, playerId, game)) { + canPutToPlay = false; + } + + if (canPutToPlay) { + return true; + } + } + + // ALTERNATIVE COST from source card (any AlternativeSourceCosts) + if (AbilityType.SPELL.equals(ability.getAbilityType())) { + return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); + } + } + return false; + } + + protected boolean canPayMinimumManaCost(ActivatedAbility ability, ManaOptions availableMana, Game game) { + ManaOptions abilityOptions = ability.getMinimumCostToActivate(playerId, game); + if (abilityOptions.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + // Check for pay option with like phyrexian mana + if (getPhyrexianColors() != null) { + addPhyrexianLikePayOptions(abilityOptions, availableMana, game); + } + + ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(), + AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); + for (Mana mana : abilityOptions) { + if (mana.count() == 0) { + return true; + } + for (Mana avail : availableMana) { + // TODO: SPEND_OTHER_MANA effects with getAsThoughManaType can change mana type to pay, + // but that code processing it as any color, need to test and fix another use cases + // (example: Sunglasses of Urza - may spend white mana as though it were red mana) + + // + // add tests for non any color like Sunglasses of Urza + if (approvingObject != null && mana.count() <= avail.count()) { + return true; + } + if (avail instanceof ConditionalMana && !((ConditionalMana) avail).apply(ability, game, getId(), ability.getManaCosts())) { + continue; + } + if (mana.enough(avail)) { // here we need to check if spend mana as though allow to pay the mana cost + return true; + } + } + } + } + return false; + } + + private void addPhyrexianLikePayOptions(ManaOptions abilityOptions, ManaOptions availableMana, Game game) { + int maxLifeMana = getLife() / 2; + if (maxLifeMana > 0) { + Set phyrexianOptions = new HashSet<>(); + for (Mana mana : abilityOptions) { + int availableLifeMana = maxLifeMana; + if (getPhyrexianColors().isBlack()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLACK); + } + if (getPhyrexianColors().isBlue()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLUE); + } + if (getPhyrexianColors().isRed()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.RED); + } + if (getPhyrexianColors().isGreen()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.GREEN); + } + if (getPhyrexianColors().isWhite()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.WHITE); + } + } + abilityOptions.addAll(phyrexianOptions); + } + } + + private int createReducedManaPayOption(int availableLifeMana, Mana oldPayOption, Set phyrexianOptions, ManaType manaType) { + if (oldPayOption.get(manaType) > 0) { + Mana manaCopy = oldPayOption.copy(); + int restVal; + if (availableLifeMana > oldPayOption.get(manaType)) { + restVal = 0; + availableLifeMana -= oldPayOption.get(manaType); + } else { + restVal = CardUtil.overflowDec(oldPayOption.get(manaType), availableLifeMana); + availableLifeMana = 0; + } + manaCopy.set(manaType, restVal); + phyrexianOptions.add(manaCopy); + } + return availableLifeMana; + } + + protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) { + if (sourceObject != null && !(sourceObject instanceof Permanent)) { + Ability copyAbility; // for alternative cost and reduce tries + for (Ability alternateSourceCostsAbility : sourceObject.getAbilities()) { + // if cast for noMana no Alternative costs are allowed + if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) { + if (((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) { + if (alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) { + ManaCostsImpl manaCosts = new ManaCostsImpl(); + for (Cost cost : alternateSourceCostsAbility.getCosts()) { + // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here + if (cost instanceof AlternativeCost2) { + if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); + } + } else { + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); + } + } + } + + if (manaCosts.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + + // alternative cost reduce + copyAbility = ability.copy(); + copyAbility.getManaCostsToPay().clear(); + copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); + sourceObject.adjustCosts(copyAbility, game); + game.getContinuousEffects().costModification(copyAbility, game); + + // reduced all cost + if (copyAbility.getManaCostsToPay().isEmpty()) { + return true; + } + + for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { + if (availableMana.enough(mana)) { + return true; + } + } + } + } + } + } + } + + // controller specific alternate spell costs + for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) { + if (alternateSourceCosts instanceof Ability) { + if (alternateSourceCosts.isAvailable(ability, game)) { + if (((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) { + ManaCostsImpl manaCosts = new ManaCostsImpl(); + for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { + // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here + if (cost instanceof AlternativeCost2) { + if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); + } + } else { + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); + } + } + } + + if (manaCosts.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + + // alternative cost reduce + copyAbility = ability.copy(); + copyAbility.getManaCostsToPay().clear(); + copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); + sourceObject.adjustCosts(copyAbility, game); + game.getContinuousEffects().costModification(copyAbility, game); + + // reduced all cost + if (copyAbility.getManaCostsToPay().isEmpty()) { + return true; + } + + for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { + if (availableMana.enough(mana)) { + return true; + } + } + } + } + } + } + } + } + return false; + } + + protected ActivatedAbility findActivatedAbilityFromPlayable(MageObject object, ManaOptions availableMana, Ability ability, Game game) { + + // special mana to pay spell cost + ManaOptions manaFull = availableMana.copy(); + if (ability instanceof SpellAbility) { + for (AlternateManaPaymentAbility altAbility : CardUtil.getAbilities(object, game).stream() + .filter(a -> a instanceof AlternateManaPaymentAbility) + .map(a -> (AlternateManaPaymentAbility) a) + .collect(Collectors.toList())) { + ManaOptions manaSpecial = altAbility.getManaOptions(ability, game, ability.getManaCostsToPay()); + manaFull.addMana(manaSpecial); + } + } + + // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) + if (ability instanceof ActivatedManaAbilityImpl) { + // mana ability + if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) { + return (ActivatedManaAbilityImpl) ability; + } + } else if (ability instanceof AlternativeSourceCosts) { + // alternative cost must be replaced by real play ability + return findActivatedAbilityFromAlternativeSourceCost(object, manaFull, ability, game); + } else if (ability instanceof ActivatedAbility) { + // all other activated ability + if (canPlay((ActivatedAbility) ability, manaFull, object, game)) { + return (ActivatedAbility) ability; + } + } + + // non playable abilities like static + return null; + } + + protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions availableMana, Ability ability, Game game) { + // return play ability that can activate AlternativeSourceCosts + if (ability instanceof AlternativeSourceCosts && object != null && !(object instanceof Permanent)) { + ActivatedAbility playAbility = null; + if (object.isLand(game)) { + playAbility = (PlayLandAbility) CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); + } else if (object instanceof Card) { + playAbility = ((Card) object).getSpellAbility(); + } + if (playAbility == null) { + return null; + } + + // 707.4.Objects that are cast face down are turned face down before they are put onto the stack + // E.g. no lands per turn limit, no cast restrictions, cost reduce, etc + // Even mana cost can't be checked here without lookahead + // So make it available all the time + boolean canUse; + if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId()) + || (null != game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game)))) { + canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game); + } else { + canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions + } + + if (canUse) { + return playAbility; + } + } + return null; + } + + private void getPlayableFromObjectAll(Game game, Zone fromZone, MageObject object, ManaOptions availableMana, List output) { + if (fromZone == null || object == null) { + return; + } + + // BASIC abilities + if (object instanceof SplitCard) { + SplitCard mainCard = (SplitCard) object; + getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); + } else if (object instanceof ModalDoubleFacesCard) { + ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) object; + getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); + } else if (object instanceof AdventureCard) { + // adventure must use different card characteristics for different spells (main or adventure) + AdventureCard adventureCard = (AdventureCard) object; + getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); + } else if (object instanceof Card) { + getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output); + } else if (object instanceof StackObject) { + // spells on stack are processing by Card above, other stack objects must be ignored + } else { + // other things like CommandObject + getPlayableFromObjectSingle(game, fromZone, object, object.getAbilities(), availableMana, output); + } + + // DYNAMIC ADDED abilities are adds in getAbilities(game) + } + + private void getPlayableFromObjectSingle(Game game, Zone fromZone, MageObject object, Abilities candidateAbilities, ManaOptions availableMana, List output) { + // check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller) + // must check all abilities, not activated only + for (Ability ability : candidateAbilities) { + if (!(ability instanceof ActivatedAbility)) { + continue; + } + boolean isPlaySpell = (ability instanceof SpellAbility); + boolean isPlayLand = (ability instanceof PlayLandAbility); + + // as original controller + // play land restrictions + if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( + GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), + ability, this.getId()), ability, game, true)) { + continue; + } + // cast spell restrictions 1 + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability, this.getId()); + castEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castEvent, ability, game, true)) { + continue; + } + // cast spell restrictions 2 + GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, + ability.getId(), ability, this.getId()); + castLateEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castLateEvent, ability, game, true)) { + continue; + } + + ApprovingObject approvingObject; + if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) { + // play hand from non hand zone (except battlefield - you can't play already played permanents) + approvingObject = game.getContinuousEffects().asThough(object.getId(), + AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); + } else { + // other abilities from direct zones + approvingObject = null; + } + + boolean canActivateAsHandZone = approvingObject != null + || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); + boolean possibleToPlay = canActivateAsHandZone + && ability.getZone().match(Zone.HAND) + && (isPlaySpell || isPlayLand); + + // spell/hand abilities (play from all zones) + // need permitingObject or canPlayCardsFromGraveyard + // zone's abilities (play from specific zone) + // no need in permitingObject + if (fromZone != Zone.ALL && ability.getZone().match(fromZone)) { + possibleToPlay = true; + } + + if (!possibleToPlay) { + continue; + } + + // direct mode (with original controller) + ActivatedAbility playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); + if (playAbility != null && !output.contains(playAbility)) { + output.add(playAbility); + continue; + } + + // from non hand mode (with affected controller) + if (canActivateAsHandZone && ability.getControllerId() != this.getId()) { + UUID savedControllerId = ability.getControllerId(); + ability.setControllerId(this.getId()); + try { + playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); + if (playAbility != null && !output.contains(playAbility)) { + output.add(playAbility); + } + } finally { + ability.setControllerId(savedControllerId); + } + } + } + } + + @Override + public List getPlayable(Game game, boolean hidden) { + return getPlayable(game, hidden, Zone.ALL, true); + } + + /** + * Returns a list of all available spells and abilities the player can + * currently cast/activate with his available resources + * + * @param game + * @param hidden also from hidden objects (e.g. turned face down cards ?) + * @param fromZone of objects from which zone (ALL = from all zones) + * @param hideDuplicatedAbilities if equal abilities exist return only the + * first instance + * @return + */ + public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { + List playable = new ArrayList<>(); + if (shouldSkipGettingPlayable(game)) { + return playable; + } + + boolean previousState = game.inCheckPlayableState(); + game.setCheckPlayableState(true); + try { + ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition)) + boolean fromAll = fromZone.equals(Zone.ALL); + if (hidden && (fromAll || fromZone == Zone.HAND)) { + for (Card card : hand.getCards(game)) { + for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) + if (ability.getZone().match(Zone.HAND)) { + boolean isPlaySpell = (ability instanceof SpellAbility); + boolean isPlayLand = (ability instanceof PlayLandAbility); + + // play land restrictions + if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( + GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), + ability, this.getId()), ability, game, true)) { + continue; + } + // cast spell restrictions 1 + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, + ability.getId(), ability, this.getId()); + castEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castEvent, ability, game, true)) { + continue; + } + // cast spell restrictions 2 + GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, + ability.getId(), ability, this.getId()); + castLateEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castLateEvent, ability, game, true)) { + continue; + } + + ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); + if (playAbility != null && !playable.contains(playAbility)) { + playable.add(playAbility); + } + } + } + } + } + + if (fromAll || fromZone == Zone.GRAVEYARD) { + for (UUID playerId : game.getState().getPlayersInRange(getId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + for (Card card : player.getGraveyard().getCards(game)) { + getPlayableFromObjectAll(game, Zone.GRAVEYARD, card, availableMana, playable); + } + } + } + + if (fromAll || fromZone == Zone.EXILED) { + for (ExileZone exile : game.getExile().getExileZones()) { + for (Card card : exile.getCards(game)) { + getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable); + } + } + } + + // check to play revealed cards + if (fromAll) { + for (Cards revealedCards : game.getState().getRevealed().values()) { + for (Card card : revealedCards.getCards(game)) { + // revealed cards can be from any zones + getPlayableFromObjectAll(game, game.getState().getZone(card.getId()), card, availableMana, playable); + } + } + } + + // outside cards + if (fromAll || fromZone == Zone.OUTSIDE) { + // companion cards + for (Cards companionCards : game.getState().getCompanion().values()) { + for (Card card : companionCards.getCards(game)) { + getPlayableFromObjectAll(game, Zone.OUTSIDE, card, availableMana, playable); + } + } + + // sideboard cards (example: Wish) + for (UUID sideboardCardId : this.getSideboard()) { + Card sideboardCard = game.getCard(sideboardCardId); + if (sideboardCard != null) { + getPlayableFromObjectAll(game, Zone.OUTSIDE, sideboardCard, availableMana, playable); + } + } + } + + // check if it's possible to play the top card of a library + if (fromAll || fromZone == Zone.LIBRARY) { + for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { + Player player = game.getPlayer(playerInRangeId); + if (player != null && player.getLibrary().hasCards()) { + Card card = player.getLibrary().getFromTop(game); + if (card != null) { + getPlayableFromObjectAll(game, Zone.LIBRARY, card, availableMana, playable); + } + } + } + } + + // check the hand zone (Sen Triplets) + // TODO: remove direct hand check (reveal fix in Sen Triplets)? + // human games: cards from opponent's hand must be revealed before play + // AI games: computer can see and play cards from opponent's hand without reveal + if (fromAll || fromZone == Zone.HAND) { + for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { + Player player = game.getPlayer(playerInRangeId); + if (player != null && !player.getHand().isEmpty()) { + for (Card card : player.getHand().getCards(game)) { + if (card != null) { + getPlayableFromObjectAll(game, Zone.HAND, card, availableMana, playable); + } + } + } + } + } + + // eliminate duplicate activated abilities (uses for AI plays) + Map activatedUnique = new HashMap<>(); + List activatedAll = new ArrayList<>(); + + // activated abilities from battlefield objects + if (fromAll || fromZone == Zone.BATTLEFIELD) { + for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { + boolean canUseActivated = permanent.canUseActivatedAbilities(game); + List currentPlayable = new ArrayList<>(); + getPlayableFromObjectAll(game, Zone.BATTLEFIELD, permanent, availableMana, currentPlayable); + for (ActivatedAbility ability : currentPlayable) { + if (ability instanceof SpecialAction || canUseActivated) { + activatedUnique.putIfAbsent(ability.toString(), ability); + activatedAll.add(ability); + } + } + } + } + + // activated abilities from stack objects + if (fromAll || fromZone == Zone.STACK) { + for (StackObject stackObject : game.getState().getStack()) { + List currentPlayable = new ArrayList<>(); + getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable); + for (ActivatedAbility ability : currentPlayable) { + activatedUnique.put(ability.toString(), ability); + activatedAll.add(ability); + } + } + } + + // activated abilities from objects in the command zone (emblems or commanders) + if (fromAll || fromZone == Zone.COMMAND) { + for (CommandObject commandObject : game.getState().getCommand()) { + List currentPlayable = new ArrayList<>(); + getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable); + for (ActivatedAbility ability : currentPlayable) { + activatedUnique.put(ability.toString(), ability); + activatedAll.add(ability); + } + } + } + + if (hideDuplicatedAbilities) { + playable.addAll(activatedUnique.values()); + } else { + playable.addAll(activatedAll); + } + } finally { + game.setCheckPlayableState(previousState); + } + + return playable; + } + + /** + * Creates a list of card ids that are currently playable.
+ * Used to mark the playable cards in GameView Also contains number of + * playable abilities for that object (it's just info, server decides to + * show choose dialog or not) + * + * @param game + * @return A Set of cardIds that are playable and amount of playable + * abilities + */ + @Override + public PlayableObjectsList getPlayableObjects(Game game, Zone zone) { + // collect abilities per object + List playableAbilities = getPlayable(game, true, zone, false); // do not hide duplicated abilities/cards + Map> playableObjects = new HashMap<>(); + for (ActivatedAbility ability : playableAbilities) { + if (ability.getSourceId() != null) { + + // normal card + putToPlayableObjects(playableObjects, ability.getSourceId(), ability); + + // main card - must be marked playable in GUI + Card card = game.getCard(ability.getSourceId()); + if (card != null && card.getMainCard().getId() != card.getId()) { + putToPlayableObjects(playableObjects, card.getMainCard().getId(), ability); + } + + // spell on stack - can have activated abilities, + // so mark it as playable too (users must able to clicks on stack objects) + // example: Lightning Storm + Spell spell = game.getSpell(ability.getSourceId()); + if (spell != null) { + putToPlayableObjects(playableObjects, spell.getId(), ability); + } + } + } + + // collect stats + PlayableObjectsList playableObjectsList = new PlayableObjectsList(playableObjects); + return playableObjectsList; + } + + private void putToPlayableObjects(Map> playableObjects, UUID objectId, ActivatedAbility ability) { + if (!playableObjects.containsKey(objectId)) { + playableObjects.put(objectId, new ArrayList<>()); + } + playableObjects.get(objectId).add(ability); + } + + /** + * Skip "silent" phase step when players are not allowed to cast anything. + * E.g. players can't play or cast anything during declaring attackers. + * + * @param game + * @return + */ + private boolean shouldSkipGettingPlayable(Game game) { + if (game.getStep() == null) { // happens at the start of the game + return true; + } + for (Entry phaseStep : silentPhaseSteps.entrySet()) { + if (game.getPhase() != null + && game.getPhase().getStep() != null + && phaseStep.getKey() == game.getPhase().getStep().getType()) { + if (phaseStep.getValue() == null + || phaseStep.getValue() == game.getPhase().getStep().getStepPart()) { + return true; + } + } + } + return false; + } + + /** + * Only used for AIs + * + * @param ability + * @param game + * @return + */ + @Override + public List getPlayableOptions(Ability ability, Game game) { + List options = new ArrayList<>(); + if (ability.isModal()) { + addModeOptions(options, ability, game); + } else if (!ability.getTargets().getUnchosen().isEmpty()) { + // TODO: Handle other variable costs than mana costs + if (!ability.getManaCosts().getVariableCosts().isEmpty()) { + addVariableXOptions(options, ability, 0, game); + } else { + addTargetOptions(options, ability, 0, game); + } + } else if (!ability.getCosts().getTargets().getUnchosen().isEmpty()) { + addCostTargetOptions(options, ability, 0, game); + } + + return options; + } + + private void addModeOptions(List options, Ability option, Game game) { + // TODO: Support modal spells with more than one selectable mode + for (Mode mode : option.getModes().values()) { + Ability newOption = option.copy(); + newOption.getModes().clearSelectedModes(); + newOption.getModes().addSelectedMode(mode.getId()); + newOption.getModes().setActiveMode(mode); + if (!newOption.getTargets().getUnchosen().isEmpty()) { + if (!newOption.getManaCosts().getVariableCosts().isEmpty()) { + addVariableXOptions(options, newOption, 0, game); + } else { + addTargetOptions(options, newOption, 0, game); + } + } else if (!newOption.getCosts().getTargets().getUnchosen().isEmpty()) { + addCostTargetOptions(options, newOption, 0, game); + } else { + options.add(newOption); + } + } + } + + protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) { + addTargetOptions(options, option, targetNum, game); + } + + protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { + for (Target target : option.getTargets().getUnchosen().get(targetNum).getTargetOptions(option, game)) { + Ability newOption = option.copy(); + if (target instanceof TargetAmount) { + for (UUID targetId : target.getTargets()) { + int amount = target.getTargetAmount(targetId); + newOption.getTargets().get(targetNum).addTarget(targetId, amount, newOption, game, true); + } + } else { + for (UUID targetId : target.getTargets()) { + newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); + } + } + if (targetNum < option.getTargets().size() - 2) { + addTargetOptions(options, newOption, targetNum + 1, game); + } else if (!option.getCosts().getTargets().isEmpty()) { + addCostTargetOptions(options, newOption, 0, game); + } else { + options.add(newOption); + } + } + } + + private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) { + for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(option.getSourceId(), playerId, game)) { + Ability newOption = option.copy(); + newOption.getCosts().getTargets().get(targetNum).addTarget(targetId, option, game, true); + if (targetNum < option.getCosts().getTargets().size() - 1) { + addCostTargetOptions(options, newOption, targetNum + 1, game); + } else { + options.add(newOption); + } + } + } + + @Override + public boolean isTestsMode() { + return isTestMode; + } + + @Override + public void setTestMode(boolean value) { + this.isTestMode = value; + } + + @Override + public boolean isTopCardRevealed() { + return topCardRevealed; + } + + @Override + public void setTopCardRevealed(boolean topCardRevealed) { + this.topCardRevealed = topCardRevealed; + } + + @Override + public UserData getUserData() { + return this.userData; + } + + public UserData getControllingPlayersUserData(Game game) { + if (!isGameUnderControl()) { + Player player = game.getPlayer(getTurnControlledBy()); + if (player.isHuman()) { + return player.getUserData(); + } + } + return this.userData; + } + + @Override + public void setUserData(UserData userData) { + this.userData = userData; + getManaPool().setAutoPayment(userData.isManaPoolAutomatic()); + getManaPool().setAutoPaymentRestricted(userData.isManaPoolAutomaticRestricted()); + } + + @Override + public void addAction(String action + ) { + // do nothing + } + + @Override + public int getActionCount() { + return 0; + } + + @Override + public void setAllowBadMoves(boolean allowBadMoves) { + // do nothing + } + + @Override + public boolean canPayLifeCost(Ability ability) { + if (!canPayLifeCost + && (AbilityType.ACTIVATED.equals(ability.getAbilityType()) + || AbilityType.SPELL.equals(ability.getAbilityType()))) { + return false; + } + return isLifeTotalCanChange(); + } + + @Override + public boolean getCanPayLifeCost() { + return canPayLifeCost; + } + + @Override + public void setCanPayLifeCost(boolean canPayLifeCost) { + this.canPayLifeCost = canPayLifeCost; + } + + @Override + public boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game) { + return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, source.getSourceId(), controllerId, game); + } + + @Override + public void setCanPaySacrificeCostFilter(FilterPermanent filter + ) { + this.sacrificeCostFilter = filter; + } + + @Override + public FilterPermanent getSacrificeCostFilter() { + return sacrificeCostFilter; + } + + @Override + public boolean canLoseByZeroOrLessLife() { + return loseByZeroOrLessLife; + } + + @Override + public void setLoseByZeroOrLessLife(boolean loseByZeroOrLessLife) { + this.loseByZeroOrLessLife = loseByZeroOrLessLife; + } + + @Override + public boolean canPlayCardsFromGraveyard() { + return canPlayCardsFromGraveyard; + } + + @Override + public void setPlayCardsFromGraveyard(boolean playCardsFromGraveyard) { + this.canPlayCardsFromGraveyard = playCardsFromGraveyard; + } + + @Override + public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) { + this.drawsOnOpponentsTurn = drawsOnOpponentsTurn; + } + + @Override + public boolean isDrawsOnOpponentsTurn() { + return drawsOnOpponentsTurn; + } + + @Override + public boolean autoLoseGame() { + return false; + } + + @Override + public void becomesActivePlayer() { + this.passedAllTurns = false; + this.passedUntilEndStepBeforeMyTurn = false; + this.turns++; + } + + @Override + public int getTurns() { + return turns; + } + + @Override + public int getStoredBookmark() { + return storedBookmark; + } + + @Override + public void setStoredBookmark(int storedBookmark) { + this.storedBookmark = storedBookmark; + } + + @Override + public synchronized void resetStoredBookmark(Game game) { + if (this.storedBookmark != -1) { + game.removeBookmark(this.storedBookmark); + } + setStoredBookmark(-1); + } + + @Override + public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { + if (null != game.getContinuousEffects().asThough(card.getId(), + AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) { + // two modes: look at the card or do not look and activate other abilities + String lookMessage = "Look at " + card.getIdName(); + String lookYes = "Yes, look at the card"; + String lookNo = "No, play/activate the card/ability"; + if (chooseUse(Outcome.Benefit, lookMessage, "", lookYes, lookNo, null, game)) { + Cards cards = new CardsImpl(card); + this.lookAtCards(getName() + " - " + card.getIdName() + " - " + + CardUtil.sdf.format(System.currentTimeMillis()), cards, game); + return true; + } + } + return false; + } + + @Override + public void setPriorityTimeLeft(int timeLeft + ) { + priorityTimeLeft = timeLeft; + } + + @Override + public int getPriorityTimeLeft() { + return priorityTimeLeft; + } + + @Override + public boolean hasQuit() { + return quit; + } + + @Override + public boolean hasTimerTimeout() { + return timerTimeout; + } + + @Override + public boolean hasIdleTimeout() { + return idleTimeout; + } + + @Override + public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving) { + this.reachedNextTurnAfterLeaving = reachedNextTurnAfterLeaving; + } + + @Override + public boolean hasReachedNextTurnAfterLeaving() { + return reachedNextTurnAfterLeaving; + } + + @Override + public boolean canJoinTable(Table table + ) { + return !table.userIsBanned(name); + } + + @Override + public void addCommanderId(UUID commanderId + ) { + this.commandersIds.add(commanderId); + } + + @Override + public Set getCommandersIds() { + return this.commandersIds; + } + + @Override + public boolean moveCards(Card card, Zone toZone, Ability source, Game game) { + return moveCards(card, toZone, source, game, false, false, false, null); + } + + @Override + public boolean moveCards(Card card, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { + Set cardList = new HashSet<>(); + if (card != null) { + cardList.add(card); + } + return moveCards(cardList, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); + } + + @Override + public boolean moveCards(Cards cards, Zone toZone, Ability source, Game game) { + return moveCards(cards.getCards(game), toZone, source, game); + } + + @Override + public boolean moveCards(Set cards, Zone toZone, + Ability source, Game game + ) { + return moveCards(cards, toZone, source, game, false, false, false, null); + } + + @Override + public boolean moveCards(Set cards, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { + if (cards.isEmpty()) { + return true; + } + Set successfulMovedCards = new LinkedHashSet<>(); + Zone fromZone = null; + switch (toZone) { + case GRAVEYARD: + fromZone = game.getState().getZone(cards.iterator().next().getId()); + successfulMovedCards = moveCardsToGraveyardWithInfo(cards, source, game, fromZone); + return !successfulMovedCards.isEmpty(); + case BATTLEFIELD: // new logic that does not yet add the permanents to battlefield while replacement effects are handled + List infoList = new ArrayList<>(); + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + ZoneChangeEvent event = new ZoneChangeEvent(card.getId(), source, + byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects); + infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped, source)); + } + infoList = ZonesHandler.moveCards(infoList, game, source); + for (ZoneChangeInfo info : infoList) { + Permanent permanent = game.getPermanent(info.event.getTargetId()); + if (permanent != null) { + successfulMovedCards.add(permanent); + if (!game.isSimulation()) { + Player eventPlayer = game.getPlayer(info.event.getPlayerId()); + if (eventPlayer != null && fromZone != null) { + game.informPlayers(eventPlayer.getLogName() + " puts " + + (info.faceDown ? "a card face down " : permanent.getLogName()) + " from " + + fromZone.toString().toLowerCase(Locale.ENGLISH) + " onto the Battlefield" + + CardUtil.getSourceLogName(game, source, permanent.getId())); + } + } + } + } + // TODO: must be replaced by game.getState().processAction(game), see isInUseableZoneDiesTrigger comments + // about short living LKI problem + //game.getState().processAction(game); + game.applyEffects(); + break; + case HAND: + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + boolean hideCard = fromZone == Zone.LIBRARY + || (card.isFaceDown(game) + && fromZone != Zone.STACK + && fromZone != Zone.BATTLEFIELD); + if (moveCardToHandWithInfo(card, source, game, !hideCard)) { + successfulMovedCards.add(card); + } + } + break; + case EXILED: + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + boolean withName = (fromZone == Zone.BATTLEFIELD + || fromZone == Zone.STACK) + || !card.isFaceDown(game); + if (moveCardToExileWithInfo(card, null, "", source, game, fromZone, withName)) { + successfulMovedCards.add(card); + } + } + break; + case LIBRARY: + for (Card card : cards) { + if (card instanceof Spell) { + fromZone = game.getState().getZone(((Spell) card).getSourceId()); + } else { + fromZone = game.getState().getZone(card.getId()); + } + boolean hideCard = fromZone == Zone.HAND || fromZone == Zone.LIBRARY; + if (moveCardToLibraryWithInfo(card, source, game, fromZone, true, !hideCard)) { + successfulMovedCards.add(card); + } + } + break; + case COMMAND: + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + if (moveCardToCommandWithInfo(card, source, game, fromZone)) { + successfulMovedCards.add(card); + } + } + break; + case OUTSIDE: + for (Card card : cards) { + if (card instanceof Permanent) { + game.getBattlefield().removePermanent(card.getId()); + ZoneChangeEvent event = new ZoneChangeEvent((Permanent) card, source, + byOwner ? card.getOwnerId() : getId(), Zone.BATTLEFIELD, Zone.OUTSIDE, appliedEffects); + game.fireEvent(event); + } + } + break; + default: + throw new UnsupportedOperationException("to Zone" + toZone + " not supported yet"); + } + return !successfulMovedCards.isEmpty(); + } + + @Override + public boolean moveCardsToExile(Card card, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { + Set cards = new HashSet<>(); + if (card != null) { + cards.add(card); + } + return moveCardsToExile(cards, source, game, withName, exileId, exileZoneName); + } + + @Override + public boolean moveCardsToExile(Set cards, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { + if (cards.isEmpty()) { + return true; + } + boolean result = false; + for (Card card : cards) { + Zone fromZone = game.getState().getZone(card.getId()); + result |= moveCardToExileWithInfo(card, exileId, exileZoneName, source, game, fromZone, withName); + } + return result; + } + + @Override + public boolean moveCardToHandWithInfo(Card card, Ability source, Game game, boolean withName) { + boolean result = false; + Zone fromZone = game.getState().getZone(card.getId()); + if (fromZone == Zone.BATTLEFIELD && !(card instanceof Permanent)) { + card = game.getPermanent(card.getId()); + } + if (card.moveToZone(Zone.HAND, source, game, false)) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + if (!game.isSimulation()) { + game.informPlayers(getLogName() + " puts " + + (withName ? card.getLogName() : (card.isFaceDown(game) ? "a face down card" : "a card")) + + " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' + + (card.isOwnedBy(this.getId()) ? "into their hand" : "into its owner's hand" + + CardUtil.getSourceLogName(game, source, card.getId())) + ); + } + result = true; + } + return result; + } + + @Override + public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, Game game, Zone fromZone) { + Set movedCards = new LinkedHashSet<>(); + while (!allCards.isEmpty()) { + // identify cards from one owner + Cards cards = new CardsImpl(); + UUID ownerId = null; + for (Iterator it = allCards.iterator(); it.hasNext();) { + Card card = it.next(); + if (cards.isEmpty()) { + ownerId = card.getOwnerId(); + } + if (card.isOwnedBy(ownerId)) { + it.remove(); + cards.add(card); + } + } + // move cards to graveyard in order the owner decides + if (!cards.isEmpty()) { + Player choosingPlayer = this; + if (!Objects.equals(ownerId, this.getId())) { + choosingPlayer = game.getPlayer(ownerId); + } + if (choosingPlayer == null) { + continue; + } + boolean chooseOrder = false; + if (userData.askMoveToGraveOrder()) { + if (cards.size() > 1) { + chooseOrder = choosingPlayer.chooseUse(Outcome.Neutral, + "Choose the order in which the cards go to the graveyard?", source, game); + } + } + if (chooseOrder) { + TargetCard target = new TargetCard(fromZone, + new FilterCard("card to put on the top of your graveyard (last one chosen will be topmost)")); + target.setRequired(true); + while (choosingPlayer.canRespond() && cards.size() > 1) { + choosingPlayer.chooseTarget(Outcome.Neutral, cards, target, source, game); + UUID targetObjectId = target.getFirstTarget(); + Card card = cards.get(targetObjectId, game); + cards.remove(targetObjectId); + if (card != null) { + fromZone = game.getState().getZone(card.getId()); + if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { + movedCards.add(card); + } + } + target.clearChosen(); + } + if (cards.size() == 1) { + Card card = cards.getCards(game).iterator().next(); + if (card != null && choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { + movedCards.add(card); + } + } + } else { + for (Card card : cards.getCards(game)) { + if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { + movedCards.add(card); + } + } + } + } + } + return movedCards; + } + + @Override + public boolean moveCardToGraveyardWithInfo(Card card, Ability source, Game game, Zone fromZone) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToZone(Zone.GRAVEYARD, source, game, false)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + StringBuilder sb = new StringBuilder(this.getLogName()) + .append(" puts ").append(card.getLogName()).append(' ').append(card.isCopy() ? "(Copy) " : "") + .append(fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' : ""); + if (card.isOwnedBy(getId())) { + sb.append("into their graveyard"); + } else { + sb.append("it into its owner's graveyard"); + } + sb.append(CardUtil.getSourceLogName(game, source, card.getId())); + game.informPlayers(sb.toString()); + } + result = true; + } + return result; + } + + @Override + public boolean moveCardToLibraryWithInfo(Card card, Ability source, Game game, Zone fromZone, boolean toTop, boolean withName) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToZone(Zone.LIBRARY, source, game, toTop)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + StringBuilder sb = new StringBuilder(this.getLogName()) + .append(" puts ").append(withName ? card.getLogName() : "a card").append(' '); + if (fromZone != null) { + sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); + } + sb.append("to the ").append(toTop ? "top" : "bottom"); + if (card.isOwnedBy(getId())) { + sb.append(" of their library"); + } else { + Player player = game.getPlayer(card.getOwnerId()); + if (player != null) { + sb.append(" of ").append(player.getLogName()).append("'s library"); + } + } + sb.append(CardUtil.getSourceLogName(game, source, card.getId())); + game.informPlayers(sb.toString()); + } + result = true; + } + return result; + } + + @Override + public boolean moveCardToCommandWithInfo(Card card, Ability source, Game game, Zone fromZone) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToZone(Zone.COMMAND, source, game, true)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + StringBuilder sb = new StringBuilder(this.getLogName()) + .append(" puts ").append(card.getLogName()).append(' '); + if (fromZone != null) { + sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); + } + if (card.isOwnedBy(getId())) { + sb.append(" to their command zone"); + } else { + Player player = game.getPlayer(card.getOwnerId()); + if (player != null) { + sb.append(" to ").append(player.getLogName()).append("'s command zone"); + } + } + sb.append(CardUtil.getSourceLogName(game, source, card.getId())); + game.informPlayers(sb.toString()); + } + result = true; + } + return result; + } + + @Override + public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, Ability source, Game game, Zone fromZone, boolean withName) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToExile(exileId, exileName, source, game)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard) { + // in case it's face down or name was changed by copying from other permanent + Card basicCard = game.getCard(card.getId()); + if (basicCard != null) { + card = basicCard; + } + } else if (card instanceof Spell) { + final Spell spell = (Spell) card; + if (spell.isCopy()) { + // copied spell, only remove from stack + game.getStack().remove(spell, game); + } + } + if (Zone.EXILED.equals(game.getState().getZone(card.getId()))) { // only if target zone was not replaced + game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() + + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); + } + + } + result = true; + } + return result; + } + + @Override + public Cards millCards(int toMill, Ability source, Game game) { + GameEvent event = GameEvent.getEvent(GameEvent.EventType.MILL_CARDS, getId(), source, getId(), toMill); + if (game.replaceEvent(event)) { + return new CardsImpl(); + } + Cards cards = new CardsImpl(this.getLibrary().getTopCards(game, event.getAmount())); + this.moveCards(cards, Zone.GRAVEYARD, source, game); + for (Card card : cards.getCards(game)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.MILLED_CARD, card.getId(), source, getId())); + } + return cards; + } + + @Override + public boolean hasOpponent(UUID playerToCheckId, Game game) { + return !this.getId().equals(playerToCheckId) + && game.isOpponent(this, playerToCheckId) + && getInRange().contains(playerToCheckId); + } + + @Override + public void cleanUpOnMatchEnd() { + + } + + @Override + public boolean getPassedAllTurns() { + return passedAllTurns; + } + + @Override + public boolean getPassedUntilNextMain() { + return passedUntilNextMain; + } + + @Override + public boolean getPassedUntilEndOfTurn() { + return passedUntilEndOfTurn; + } + + @Override + public boolean getPassedTurn() { + return passedTurn; + } + + @Override + public boolean getPassedUntilStackResolved() { + return passedUntilStackResolved; + } + + @Override + public boolean getPassedUntilEndStepBeforeMyTurn() { + return passedUntilEndStepBeforeMyTurn; + } + + @Override + public AbilityType getJustActivatedType() { + return justActivatedType; + } + + @Override + public void setJustActivatedType(AbilityType justActivatedType + ) { + this.justActivatedType = justActivatedType; + } + + @Override + public void revokePermissionToSeeHandCards() { + usersAllowedToSeeHandCards.clear(); + } + + @Override + public void addPermissionToShowHandCards(UUID watcherUserId + ) { + usersAllowedToSeeHandCards.add(watcherUserId); + } + + @Override + public boolean isPlayerAllowedToRequestHand(UUID gameId, UUID requesterPlayerId) { + return userData.isAllowRequestHandToPlayer(gameId, requesterPlayerId); + } + + @Override + public void addPlayerToRequestedHandList(UUID gameId, UUID requesterPlayerId) { + userData.addPlayerToRequestedHandList(gameId, requesterPlayerId); + } + + @Override + public boolean hasUserPermissionToSeeHand(UUID userId + ) { + return usersAllowedToSeeHandCards.contains(userId); + } + + @Override + public Set getUsersAllowedToSeeHandCards() { + return usersAllowedToSeeHandCards; + } + + @Override + public void setMatchPlayer(MatchPlayer matchPlayer + ) { + this.matchPlayer = matchPlayer; + } + + @Override + public MatchPlayer getMatchPlayer() { + return matchPlayer; + } + + @Override + public void abortReset() { + abort = false; + } + + @Override + public void signalPlayerConcede() { + + } + + @Override + public boolean scry(int value, Ability source, Game game) { + GameEvent event = new GameEvent(GameEvent.EventType.SCRY, getId(), source, getId(), value, true); + if (game.replaceEvent(event)) { + return false; + } + game.informPlayers(getLogName() + " scries " + event.getAmount() + CardUtil.getSourceLogName(game, source)); + Cards cards = new CardsImpl(); + cards.addAll(getLibrary().getTopCards(game, event.getAmount())); + if (!cards.isEmpty()) { + TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, + new FilterCard("card" + (cards.size() == 1 ? "" : "s") + + " to PUT on the BOTTOM of your library (Scry)")); + chooseTarget(Outcome.Benefit, cards, target, source, game); + putCardsOnBottomOfLibrary(new CardsImpl(target.getTargets()), game, source, true); + cards.removeAll(target.getTargets()); + putCardsOnTopOfLibrary(cards, game, source, true); + } + game.fireEvent(new GameEvent(GameEvent.EventType.SCRIED, getId(), source, getId(), event.getAmount(), true)); + return true; + } + + @Override + public boolean surveil(int value, Ability source, Game game) { + GameEvent event = new GameEvent(GameEvent.EventType.SURVEIL, getId(), source, getId(), value, true); + if (game.replaceEvent(event)) { + return false; + } + game.informPlayers(getLogName() + " surveils " + event.getAmount() + CardUtil.getSourceLogName(game, source)); + Cards cards = new CardsImpl(); + cards.addAll(getLibrary().getTopCards(game, event.getAmount())); + if (!cards.isEmpty()) { + TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, + new FilterCard("cards to PUT into your GRAVEYARD (Surveil)")); + chooseTarget(Outcome.Benefit, cards, target, source, game); + moveCards(new CardsImpl(target.getTargets()), Zone.GRAVEYARD, source, game); + cards.removeAll(target.getTargets()); + putCardsOnTopOfLibrary(cards, game, source, true); + } + game.fireEvent(new GameEvent(GameEvent.EventType.SURVEILED, getId(), source, getId(), event.getAmount(), true)); + return true; + } + + @Override + public boolean addTargets(Ability ability, Game game + ) { + // only used for TestPlayer to preSet Targets + return true; + } + + @Override + public String getHistory() { + return "no available"; + } + + @Override + public boolean hasDesignation(DesignationType designationName) { + for (Designation designation : designations) { + if (designation.getDesignationType().equals(designationName)) { + return true; + } + } + return false; + } + + @Override + public void addDesignation(Designation designation) { + if (!designation.isUnique() || !this.hasDesignation(designation.getDesignationType())) { + designations.add(designation); + } + } + + @Override + public List getDesignations() { + return designations; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Player obj = (Player) o; + if (this.getId() == null || obj.getId() == null) { + return false; + } + + return this.getId().equals(obj.getId()); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 89 * hash + Objects.hashCode(this.playerId); + return hash; + } + + @Override + public void addPhyrexianToColors(FilterMana colors) { + if (phyrexianColors == null) { + phyrexianColors = colors.copy(); + } else { + if (colors.isWhite()) { + this.phyrexianColors.setWhite(true); + } + if (colors.isBlue()) { + this.phyrexianColors.setBlue(true); + } + if (colors.isBlack()) { + this.phyrexianColors.setBlack(true); + } + if (colors.isRed()) { + this.phyrexianColors.setRed(true); + } + if (colors.isGreen()) { + this.phyrexianColors.setGreen(true); + } + } + } + + @Override + public FilterMana getPhyrexianColors() { + return this.phyrexianColors; + } + + @Override + public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { + return card.getSpellAbility(); + } + + @Override + public String toString() { + return getName() + " (" + super.getClass().getSimpleName() + ")"; + } +} From b963168f351e586fae9fa57205a876cd6c17ca3f Mon Sep 17 00:00:00 2001 From: "jeff@delmarus.com" <> Date: Thu, 23 Sep 2021 19:26:01 -0500 Subject: [PATCH 182/231] - hopefully line spacing is rectified on my client --- Mage/src/main/java/mage/players/PlayerImpl.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 6a2eb625d3b..94c55f11f4b 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -641,11 +641,14 @@ public abstract class PlayerImpl implements Player, Serializable { } // check for all variants of hexproof and any asthougheffects that would ignore it // TODO there may be "prevented by rule-modification" effects, so add them if known - for (Ability a : abilities) { - if (a instanceof HexproofBaseAbility - && ((HexproofBaseAbility) a).checkObject(source, game) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { - return false; + if (sourceControllerId != null + && this.hasOpponent(sourceControllerId, game)) { + for (Ability a : abilities) { + if (a instanceof HexproofBaseAbility + && ((HexproofBaseAbility) a).checkObject(source, game) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { + return false; + } } } return !hasProtectionFrom(source, game); From 5e1b04aeb85a8f49cb4542226d0d4daa7298402c Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:33:16 -0400 Subject: [PATCH 183/231] Fix Kelpie Guide's activated ability (#8291) Should only be able to untap another target permanent. --- Mage.Sets/src/mage/cards/k/KelpieGuide.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Mage.Sets/src/mage/cards/k/KelpieGuide.java b/Mage.Sets/src/mage/cards/k/KelpieGuide.java index 71dc527d5ee..9cd5913574b 100644 --- a/Mage.Sets/src/mage/cards/k/KelpieGuide.java +++ b/Mage.Sets/src/mage/cards/k/KelpieGuide.java @@ -19,6 +19,7 @@ import mage.constants.Zone; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledLandPermanent; import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; import mage.target.TargetPermanent; import java.util.UUID; @@ -34,6 +35,10 @@ public final class KelpieGuide extends CardImpl { = new FilterControlledPermanent("another target permanent you control"); private static final Condition condition = new PermanentsOnTheBattlefieldCondition(filter, ComparisonType.MORE_THAN, 7); + + static { + filter2.add(AnotherPredicate.instance); + } public KelpieGuide(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}"); From c6e385e321a43f7c8593bf824f06bbf80ba9de07 Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Fri, 24 Sep 2021 09:51:31 -0500 Subject: [PATCH 184/231] - Added test for #8293 --- .../damage/SeraphAndSengirVampireTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/SeraphAndSengirVampireTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/SeraphAndSengirVampireTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/SeraphAndSengirVampireTest.java new file mode 100644 index 00000000000..b87d2c22605 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/SeraphAndSengirVampireTest.java @@ -0,0 +1,38 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.cards.triggers.damage; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author jeffwadsworth + */ +public class SeraphAndSengirVampireTest extends CardTestPlayerBase { + + @Test + public void testBothDieButTriggersStillFire() { + + // https://github.com/magefree/mage/issues/8293 + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Seraph", 1); // 4/4 flying : take control of creature that dies after taking damage + addCard(Zone.BATTLEFIELD, playerB, "Sengir Vampire", 1); // 4/4 flying : gains +1/+1 for any creature that takes damage and dies + + attack(3, playerA, "Seraph"); + block(3, playerB, "Sengir Vampire", "Seraph"); + + setStopAt(4, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Seraph", 1); // Seraph dies + assertGraveyardCount(playerB, "Sengir Vampire", 0); + assertPermanentCount(playerA, "Sengir Vampire", 1); // playerA now controls the Sengir Vampire + } +} From b895611c4d3e9c2c9836c56dc38d01bc822d10fe Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 24 Sep 2021 22:07:12 -0400 Subject: [PATCH 185/231] Updating standard (don't merge until 9/24) (#8297) * removed rotated cards from standard ban list * updated standard legality to handle sets releasing after fall set inn the same year * some more updates to standard * small change --- .../src/mage/deck/Standard.java | 74 +++++++------------ .../main/java/mage/cards/ExpansionSet.java | 13 ++++ 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Standard.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Standard.java index 902dc2e6ca3..9ab606410b4 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Standard.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Standard.java @@ -5,7 +5,11 @@ import mage.cards.Sets; import mage.cards.decks.Constructed; import mage.constants.SetType; -import java.util.*; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.stream.Collectors; /** * @author BetaSteward_at_googlemail.com @@ -17,61 +21,39 @@ public class Standard extends Constructed { setCodes.addAll(makeLegalSets()); - banned.add("Agent of Treachery"); - banned.add("Cauldron Familiar"); - banned.add("Escape to the Wilds"); - banned.add("Field of the Dead"); - banned.add("Fires of Invention"); - banned.add("Growth Spiral"); - banned.add("Lucky Clover"); - banned.add("Oko, Thief of Crowns"); banned.add("Omnath, Locus of Creation"); - banned.add("Once Upon a Time"); - banned.add("Teferi, Time Raveler"); - banned.add("Uro, Titan of Nature's Wrath"); - banned.add("Wilderness Reclamation"); - banned.add("Veil of Summer"); } private static boolean isFallSet(ExpansionSet set) { Calendar cal = Calendar.getInstance(); cal.setTime(set.getReleaseDate()); - // Fall sets are normally released during or after September - return set.getSetType() == SetType.EXPANSION && (cal.get(Calendar.MONTH) > 7); + // Fall sets are normally released during or after September and before November + return set.getSetType() == SetType.EXPANSION + && Calendar.SEPTEMBER <= cal.get(Calendar.MONTH) + && cal.get(Calendar.MONTH) < Calendar.NOVEMBER; } static List makeLegalSets() { - List codes = new ArrayList<>(); GregorianCalendar current = new GregorianCalendar(); - List sets = new ArrayList(Sets.getInstance().values()); - Collections.sort(sets, new Comparator() { - @Override - public int compare(final ExpansionSet lhs, ExpansionSet rhs) { - return lhs.getReleaseDate().after(rhs.getReleaseDate()) ? -1 : 1; - } - }); - int fallSetsAdded = 0; - Date earliestDate = null; // Get the second most recent fall set that's been released. - for (ExpansionSet set : sets) { - if (set.getReleaseDate().after(current.getTime())) { - continue; - } - if (isFallSet(set)) { - fallSetsAdded++; - if (fallSetsAdded == 2) { - earliestDate = set.getReleaseDate(); - break; - } - } - } - - for (ExpansionSet set : sets) { - boolean isDateCompatible = earliestDate != null && !set.getReleaseDate().before(earliestDate) /*!set.getReleaseDate().after(current.getTime())*/; // no after date restrict for early tests and beta - if (set.getSetType().isStandardLegal() && isDateCompatible) { - codes.add(set.getCode()); - } - } - return codes; + Date earliestDate = Sets + .getInstance() + .values() + .stream() + .filter(set -> !set.getReleaseDate().after(current.getTime())) + .filter(Standard::isFallSet) + .sorted(ExpansionSet.getComparator()) + .skip(1) + .findFirst() + .get() + .getReleaseDate(); + return Sets.getInstance() + .values() + .stream() + .filter(set -> set.getSetType().isStandardLegal()) + .filter(set -> !set.getReleaseDate().before(earliestDate)) +// .filter(set -> !set.getReleaseDate().after(current.getTime())) // no after date restrict for early tests and beta + .map(ExpansionSet::getCode) + .collect(Collectors.toList()); } } diff --git a/Mage/src/main/java/mage/cards/ExpansionSet.java b/Mage/src/main/java/mage/cards/ExpansionSet.java index 7e20a391071..68276cc5295 100644 --- a/Mage/src/main/java/mage/cards/ExpansionSet.java +++ b/Mage/src/main/java/mage/cards/ExpansionSet.java @@ -87,6 +87,19 @@ public abstract class ExpansionSet implements Serializable { } } + private static enum ExpansionSetComparator implements Comparator { + instance; + + @Override + public int compare(ExpansionSet lhs, ExpansionSet rhs) { + return lhs.getReleaseDate().after(rhs.getReleaseDate()) ? -1 : 1; + } + } + + public static ExpansionSetComparator getComparator() { + return ExpansionSetComparator.instance; + } + protected final List cards = new ArrayList<>(); protected String name; From 95b782d519de71dfb4039018b98f53722dad3539 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 25 Sep 2021 18:09:21 +0400 Subject: [PATCH 186/231] Tests for #5630 --- Mage.Sets/src/mage/cards/p/PeaceTalks.java | 5 +- .../abilities/keywords/HexproofTest.java | 116 +++++++++++++++++- .../java/mage/game/events/TargetEvent.java | 6 + .../mage/game/permanent/PermanentImpl.java | 14 ++- .../main/java/mage/players/PlayerImpl.java | 91 +++++++------- 5 files changed, 180 insertions(+), 52 deletions(-) diff --git a/Mage.Sets/src/mage/cards/p/PeaceTalks.java b/Mage.Sets/src/mage/cards/p/PeaceTalks.java index 2e4785aaf30..b0b9785f8b2 100644 --- a/Mage.Sets/src/mage/cards/p/PeaceTalks.java +++ b/Mage.Sets/src/mage/cards/p/PeaceTalks.java @@ -24,11 +24,8 @@ public final class PeaceTalks extends CardImpl { public PeaceTalks(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{W}"); - // This turn and next turn, creatures can't attack, - // and players and permanents can't be the targets - // of spells or activated abilities. + // This turn and next turn, creatures can't attack, and players and permanents can't be the targets of spells or activated abilities. this.getSpellAbility().addEffect(new PeaceTalksEffect()); - } private PeaceTalks(final PeaceTalks card) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HexproofTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HexproofTest.java index fafc7c99fd2..2f78ce079f1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HexproofTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HexproofTest.java @@ -3,13 +3,14 @@ package org.mage.test.cards.abilities.keywords; import mage.abilities.keyword.HexproofAbility; import mage.constants.PhaseStep; import mage.constants.Zone; +import org.junit.Assert; import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; /** - * @author LevelX2 + * @author LevelX2, JayDi85 */ -public class HexproofTest extends CardTestPlayerBase { +public class HexproofTest extends CardTestPlayerBaseWithAIHelps { /** * Tests one target gets hexproof @@ -103,4 +104,113 @@ public class HexproofTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Knight of Grace", 0); } + + @Test + public void test_Human_CanTargetValid() { + // +1: Target player discards a card. + addCard(Zone.BATTLEFIELD, playerA, "Liliana Vess", 1); + addCard(Zone.HAND, playerA, "Balduvian Bears", 1); + addCard(Zone.HAND, playerA, "Swamp", 1); + addCard(Zone.HAND, playerB, "Matter Reshaper", 1); + addCard(Zone.HAND, playerB, "Mountain", 1); + // + // You have hexproof. (You can't be the target of spells or abilities your opponents control.) + addCard(Zone.BATTLEFIELD, playerB, "Leyline of Sanctity", 1); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1:"); + addTarget(playerA, playerA); + setChoice(playerA, "Swamp"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Swamp", 1); + } + + @Test + public void test_Human_CantTargetInvalid() { + // +1: Target player discards a card. + addCard(Zone.BATTLEFIELD, playerA, "Liliana Vess", 1); + addCard(Zone.HAND, playerA, "Balduvian Bears", 1); + addCard(Zone.HAND, playerA, "Swamp", 1); + addCard(Zone.HAND, playerB, "Matter Reshaper", 1); + addCard(Zone.HAND, playerB, "Mountain", 1); + // + // You have hexproof. (You can't be the target of spells or abilities your opponents control.) + addCard(Zone.BATTLEFIELD, playerB, "Leyline of Sanctity", 1); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1:"); + addTarget(playerA, playerB); + + try { + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + Assert.fail("must throw exception on execute"); + } catch (Throwable e) { + if (!e.getMessage().contains("setup good targets")) { + Assert.fail("must thow error about bad targets, but got:\n" + e.getMessage()); + } + } + } + + @Test + public void test_AI_MustTargetOnlyValid() { + // +1: Target player discards a card. + addCard(Zone.BATTLEFIELD, playerA, "Liliana Vess", 1); + addCard(Zone.HAND, playerA, "Balduvian Bears", 1); + addCard(Zone.HAND, playerA, "Swamp", 1); + addCard(Zone.HAND, playerB, "Matter Reshaper", 1); + addCard(Zone.HAND, playerB, "Mountain", 1); + // + // You have hexproof. (You can't be the target of spells or abilities your opponents control.) + addCard(Zone.BATTLEFIELD, playerB, "Leyline of Sanctity", 1); + + // ai must not use +1 on itself (due bad score) and must not use on opponent (due hexproof) + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no discarded cards + assertGraveyardCount(playerA, 0); + assertGraveyardCount(playerB, 0); + } + + @Test + public void test_RulesModificationForPlayers() { + // This turn and next turn, creatures can't attack, and players and permanents can't be the targets + // of spells or activated abilities. + addCard(Zone.HAND, playerA, "Peace Talks", 1); // {1}{W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", true); + + // activate restriction + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}", 2); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Peace Talks"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // playable doesn't check illegal targets, so it will be active + // ai can cast on turn 3 only + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + checkLife("after 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, 20); + aiPlayStep(2, PhaseStep.PRECOMBAT_MAIN, playerA); + checkLife("after 2", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, 20); + aiPlayStep(3, PhaseStep.PRECOMBAT_MAIN, playerA); + checkLife("after 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerB, 20 - 3); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } } diff --git a/Mage/src/main/java/mage/game/events/TargetEvent.java b/Mage/src/main/java/mage/game/events/TargetEvent.java index 063d16ac2ab..f2c90ebc87b 100644 --- a/Mage/src/main/java/mage/game/events/TargetEvent.java +++ b/Mage/src/main/java/mage/game/events/TargetEvent.java @@ -2,6 +2,7 @@ package mage.game.events; import mage.abilities.Ability; import mage.cards.Card; +import mage.players.Player; import java.util.UUID; @@ -20,6 +21,11 @@ public class TargetEvent extends GameEvent { this.setSourceId(sourceId); } + public TargetEvent(Player target, UUID sourceId, UUID sourceControllerId) { + super(GameEvent.EventType.TARGET, target.getId(), null, sourceControllerId); + this.setSourceId(sourceId); + } + /** * @param targetId * @param source diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index d2a210af1e0..78dcabfa8be 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -1117,8 +1117,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { return false; } } + if (game.getPlayer(this.getControllerId()).hasOpponent(sourceControllerId, game) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null + && null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) && abilities.stream() .filter(HexproofBaseAbility.class::isInstance) .map(HexproofBaseAbility.class::cast) @@ -1129,9 +1130,14 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { if (hasProtectionFrom(source, game)) { return false; } - // needed to get the correct possible targets if target rule modification effects are active - // e.g. Fiendslayer Paladin tried to target with Ultimate Price - return !game.getContinuousEffects().preventedByRuleModification(new TargetEvent(this, source.getId(), sourceControllerId), null, game, true); + + // example: Fiendslayer Paladin tried to target with Ultimate Price + return !game.getContinuousEffects().preventedByRuleModification( + new TargetEvent(this, source.getId(), sourceControllerId), + null, + game, + true + ); } return true; diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 94c55f11f4b..2feceac5e79 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -634,24 +634,32 @@ public abstract class PlayerImpl implements Player, Serializable { return false; } if (source != null) { - // there is only variant of shroud, so check the instance and any asthougheffects that would ignore it if (abilities.containsKey(ShroudAbility.getInstance().getId()) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game) == null) { + && null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game)) { return false; } - // check for all variants of hexproof and any asthougheffects that would ignore it - // TODO there may be "prevented by rule-modification" effects, so add them if known + if (sourceControllerId != null - && this.hasOpponent(sourceControllerId, game)) { - for (Ability a : abilities) { - if (a instanceof HexproofBaseAbility - && ((HexproofBaseAbility) a).checkObject(source, game) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { - return false; - } - } + && this.hasOpponent(sourceControllerId, game) + && null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) + && abilities.stream() + .filter(HexproofBaseAbility.class::isInstance) + .map(HexproofBaseAbility.class::cast) + .anyMatch(ability -> ability.checkObject(source, game))) { + return false; } - return !hasProtectionFrom(source, game); + + if (hasProtectionFrom(source, game)) { + return false; + } + + // example: Peace Talks + return !game.getContinuousEffects().preventedByRuleModification( + new TargetEvent(this, source.getId(), sourceControllerId), + null, + game, + true + ); } return true; } @@ -686,7 +694,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(getLogName() + " discards down to " + this.maxHandSize + (this.maxHandSize == 1 - ? " hand card" : " hand cards")); + ? " hand card" : " hand cards")); } discard(hand.size() - this.maxHandSize, false, false, null, game); } @@ -1159,7 +1167,7 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param originalAbility * @param game - * @param noMana cast it without paying mana costs + * @param noMana cast it without paying mana costs * @param approvingObject which object approved the cast * @return */ @@ -2921,7 +2929,7 @@ public abstract class PlayerImpl implements Player, Serializable { * @return */ private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { if (rollsAmount == 1) { return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount); } @@ -3017,8 +3025,8 @@ public abstract class PlayerImpl implements Player, Serializable { * @param outcome * @param source * @param game - * @param sidesAmount number of sides the dice has - * @param rollsAmount number of tries to roll the dice + * @param sidesAmount number of sides the dice has + * @param rollsAmount number of tries to roll the dice * @param ignoreLowestAmount remove the lowest rolls from the results * @return the number that the player rolled */ @@ -3036,18 +3044,18 @@ public abstract class PlayerImpl implements Player, Serializable { * @param outcome * @param source * @param game - * @param rollDieType die type to roll, e.g. planar or numerical - * @param sidesAmount sides per die - * @param chaosSidesAmount for planar die: chaos sides - * @param planarSidesAmount for planar die: planar sides - * @param rollsAmount rolls + * @param rollDieType die type to roll, e.g. planar or numerical + * @param sidesAmount sides per die + * @param chaosSidesAmount for planar die: chaos sides + * @param planarSidesAmount for planar die: planar sides + * @param rollsAmount rolls * @param ignoreLowestAmount for numerical die: ignore multiple rolls with - * the lowest values + * the lowest values * @return */ private List rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, - int rollsAmount, int ignoreLowestAmount) { + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, + int rollsAmount, int ignoreLowestAmount) { RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount); if (ignoreLowestAmount > 0) { rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount); @@ -3207,10 +3215,10 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param source * @param game - * @param chaosSidesAmount The number of chaos sides the planar die - * currently has (normally 1 but can be 5) + * @param chaosSidesAmount The number of chaos sides the planar die + * currently has (normally 1 but can be 5) * @param planarSidesAmount The number of chaos sides the planar die - * currently has (normally 1) + * currently has (normally 1) * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll * or BlankRoll */ @@ -3268,7 +3276,7 @@ public abstract class PlayerImpl implements Player, Serializable { for (Card card : getHand().getCards(game)) { Abilities manaAbilities = card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game); - for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + for (Iterator it = manaAbilities.iterator(); it.hasNext(); ) { ActivatedManaAbilityImpl ability = it.next(); Abilities noTapAbilities = new AbilitiesImpl<>(ability); if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { @@ -3285,7 +3293,7 @@ public abstract class PlayerImpl implements Player, Serializable { boolean useLater = false; // sources with mana costs or mana pool dependency Abilities manaAbilities = permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true - for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + for (Iterator it = manaAbilities.iterator(); it.hasNext(); ) { ActivatedManaAbilityImpl ability = it.next(); if (canUse == null) { canUse = permanent.canUseActivatedAbilities(game); @@ -3327,7 +3335,7 @@ public abstract class PlayerImpl implements Player, Serializable { boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) { anAbilityWasUsed = false; - for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext();) { + for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext(); ) { Abilities manaAbilities = iterator.next(); if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) { boolean used; @@ -3363,7 +3371,7 @@ public abstract class PlayerImpl implements Player, Serializable { * and cleared thereafter * * @param netManaAvailable the net mana produced by the triggered mana - * abaility + * abaility */ @Override public void addAvailableTriggeredMana(List netManaAvailable @@ -3445,7 +3453,7 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param ability * @param availableMana if null, it won't be checked if enough mana is - * available + * available * @param sourceObject * @param game * @return @@ -3877,13 +3885,14 @@ public abstract class PlayerImpl implements Player, Serializable { /** * Returns a list of all available spells and abilities the player can - * currently cast/activate with his available resources + * currently cast/activate with his available resources. + * Without target validation. * * @param game - * @param hidden also from hidden objects (e.g. turned face down cards ?) - * @param fromZone of objects from which zone (ALL = from all zones) + * @param hidden also from hidden objects (e.g. turned face down cards ?) + * @param fromZone of objects from which zone (ALL = from all zones) * @param hideDuplicatedAbilities if equal abilities exist return only the - * first instance + * first instance * @return */ public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { @@ -4479,7 +4488,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards, toZone, source, game, false, false, false, null); } @@ -4638,7 +4647,7 @@ public abstract class PlayerImpl implements Player, Serializable { // identify cards from one owner Cards cards = new CardsImpl(); UUID ownerId = null; - for (Iterator it = allCards.iterator(); it.hasNext();) { + for (Iterator it = allCards.iterator(); it.hasNext(); ) { Card card = it.next(); if (cards.isEmpty()) { ownerId = card.getOwnerId(); @@ -4816,7 +4825,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) - + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); + + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); } } From de378577a6e518b08013cbd91a534b2a3e3d0086 Mon Sep 17 00:00:00 2001 From: "jeff@delmarus.com" <> Date: Sat, 25 Sep 2021 09:22:47 -0500 Subject: [PATCH 187/231] - Added test for #5630 --- .../cards/targets/HexproofPlayerTest.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/targets/HexproofPlayerTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/HexproofPlayerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/HexproofPlayerTest.java new file mode 100644 index 00000000000..9ea99bd22f5 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/HexproofPlayerTest.java @@ -0,0 +1,102 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.cards.targets; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author jeffwadsworth + */ +public class HexproofPlayerTest extends CardTestPlayerBase { + + /* + Test hexproof gained via both a static ability from a permanent and from an instant spell + */ + + @Test + public void leyLineOfSanctityOpponentCantTargetTest() { + https://github.com/magefree/mage/issues/5630 + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Leyline of Sanctity"); // controller has hexproof + addCard(Zone.BATTLEFIELD, playerB, "Liliana Vess"); // target player discards a card + addCard(Zone.HAND, playerA, "Memnite"); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, + "+1: Target player discards a card.", playerA); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, 0); + + } + + @Test + public void leyLineOfSanctityControllerCanTargetThemselfTest() { + https://github.com/magefree/mage/issues/5630 + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Leyline of Sanctity"); // controller has hexproof + addCard(Zone.BATTLEFIELD, playerA, "Liliana Vess"); // target player discards a card + addCard(Zone.HAND, playerA, "Memnite", 1); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, + "+1: Target player discards a card.", playerA); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, 1); + + } + + @Test + public void veilOfSummerOpponentCantTargetTest() { + https://github.com/magefree/mage/issues/5630 + setStrictChooseMode(true); + addCard(Zone.HAND, playerA, "Veil of Summer"); // Instant : controller has hexproof + addCard(Zone.BATTLEFIELD, playerB, "Liliana Vess"); // target player discards a card + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); // mana of Veil of Summer + addCard(Zone.HAND, playerA, "Memnite"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Veil of Summer"); // controller has hexproof + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, + "+1: Target player discards a card.", playerA); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, 1); // the Veil of Summer card only, no discarded card + } + + @Test + public void veilOfSummerControllerCanTargetThemselfTest() { + https://github.com/magefree/mage/issues/5630 + setStrictChooseMode(true); + addCard(Zone.HAND, playerA, "Veil of Summer"); // Instant : controller has hexproof + addCard(Zone.BATTLEFIELD, playerA, "Liliana Vess"); // target player discards a card + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); // mana of Veil of Summer + addCard(Zone.HAND, playerA, "Memnite"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Veil of Summer"); // controller has hexproof + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, + "+1: Target player discards a card.", playerA); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, 2); + } +} From f3b8f0a44a25b448cc195eea814d0c73b4581016 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sun, 26 Sep 2021 00:31:17 +0400 Subject: [PATCH 188/231] * Hextproof from color - improved card icon, added details in icon's popup text; --- .../icon/abilities/HexproofAbilityIcon.java | 6 +++++ .../abilities/keyword/HexproofAbility.java | 5 ++++ .../keyword/HexproofBaseAbility.java | 24 ++++++++++++++++--- ...FromArtifactsCreaturesAndEnchantments.java | 5 ++++ .../keyword/HexproofFromBlackAbility.java | 5 ++++ .../keyword/HexproofFromBlueAbility.java | 5 ++++ .../keyword/HexproofFromGreenAbility.java | 5 ++++ .../HexproofFromMonocoloredAbility.java | 5 ++++ .../HexproofFromPlaneswalkersAbility.java | 5 ++++ .../keyword/HexproofFromRedAbility.java | 5 ++++ .../keyword/HexproofFromWhiteAbility.java | 5 ++++ 11 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Mage/src/main/java/mage/abilities/icon/abilities/HexproofAbilityIcon.java b/Mage/src/main/java/mage/abilities/icon/abilities/HexproofAbilityIcon.java index 1872ad3d79e..5f6c29e013f 100644 --- a/Mage/src/main/java/mage/abilities/icon/abilities/HexproofAbilityIcon.java +++ b/Mage/src/main/java/mage/abilities/icon/abilities/HexproofAbilityIcon.java @@ -1,7 +1,9 @@ package mage.abilities.icon.abilities; import mage.abilities.icon.CardIcon; +import mage.abilities.icon.CardIconImpl; import mage.abilities.icon.CardIconType; +import mage.util.CardUtil; /** * @author JayDi85 @@ -28,4 +30,8 @@ public enum HexproofAbilityIcon implements CardIcon { public CardIcon copy() { return instance; } + + public static CardIconImpl createDynamicCardIcon(String hint) { + return new CardIconImpl(CardIconType.ABILITY_HEXPROOF, CardUtil.getTextWithFirstCharUpperCase(hint)); + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofAbility.java index eb42f64c80f..c5b9b0ddb62 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofAbility.java @@ -45,4 +45,9 @@ public class HexproofAbility extends HexproofBaseAbility { public String getRule() { return "hexproof"; } + + @Override + public String getCardIconHint(Game game) { + return "hexproof from all"; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofBaseAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofBaseAbility.java index bf87e48188a..94da34ba717 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofBaseAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofBaseAbility.java @@ -4,12 +4,15 @@ import mage.MageObject; import mage.ObjectColor; import mage.abilities.MageSingleton; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.icon.CardIcon; +import mage.abilities.icon.CardIconImpl; +import mage.abilities.icon.CardIconType; import mage.abilities.icon.abilities.HexproofAbilityIcon; import mage.constants.Zone; import mage.game.Game; +import mage.game.permanent.Permanent; -import java.util.HashSet; -import java.util.Set; +import java.util.*; /** * an abstract base class for hexproof abilities @@ -20,7 +23,6 @@ public abstract class HexproofBaseAbility extends SimpleStaticAbility implements HexproofBaseAbility() { super(Zone.BATTLEFIELD, null); - this.addIcon(HexproofAbilityIcon.instance); } public abstract boolean checkObject(MageObject source, Game game); @@ -60,4 +62,20 @@ public abstract class HexproofBaseAbility extends SimpleStaticAbility implements return null; } } + + public abstract String getCardIconHint(Game game); + + @Override + public List getIcons(Game game) { + if (game == null) { + return new ArrayList<>(Collections.singletonList( + HexproofAbilityIcon.instance + )); + } + + // dynamic icon (example: colored hexproof) + return new ArrayList<>(Collections.singletonList( + HexproofAbilityIcon.createDynamicCardIcon(getCardIconHint(game)) + )); + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromArtifactsCreaturesAndEnchantments.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromArtifactsCreaturesAndEnchantments.java index 2ce0b0eeb5c..0c808248b65 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofFromArtifactsCreaturesAndEnchantments.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromArtifactsCreaturesAndEnchantments.java @@ -44,4 +44,9 @@ public class HexproofFromArtifactsCreaturesAndEnchantments extends HexproofBaseA public String getRule() { return "hexproof from artifacts, creatures, and enchantments"; } + + @Override + public String getCardIconHint(Game game) { + return "hexproof from artifacts, creatures, and enchantments"; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromBlackAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromBlackAbility.java index 82f2275563d..c731cb15e90 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofFromBlackAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromBlackAbility.java @@ -45,4 +45,9 @@ public class HexproofFromBlackAbility extends HexproofBaseAbility { public String getRule() { return "hexproof from black (This creature can't be the target of black spells or abilities your opponents control.)"; } + + @Override + public String getCardIconHint(Game game) { + return "hexproof from black"; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromBlueAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromBlueAbility.java index ac7317f0253..dcf3f449748 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofFromBlueAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromBlueAbility.java @@ -45,4 +45,9 @@ public class HexproofFromBlueAbility extends HexproofBaseAbility { public String getRule() { return "hexproof from blue (This creature can't be the target of blue spells or abilities your opponents control.)"; } + + @Override + public String getCardIconHint(Game game) { + return "hexproof from blue"; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromGreenAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromGreenAbility.java index 3534c4b72d5..12f8768cfd3 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofFromGreenAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromGreenAbility.java @@ -45,4 +45,9 @@ public class HexproofFromGreenAbility extends HexproofBaseAbility { public String getRule() { return "hexproof from green (This creature can't be the target of green spells or abilities your opponents control.)"; } + + @Override + public String getCardIconHint(Game game) { + return "hexproof from green"; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromMonocoloredAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromMonocoloredAbility.java index 27a711692a6..4d5a24b0d70 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofFromMonocoloredAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromMonocoloredAbility.java @@ -45,4 +45,9 @@ public class HexproofFromMonocoloredAbility extends HexproofBaseAbility { public String getRule() { return "hexproof from monocolored (This creature can't be the target of monocolored spells or abilities your opponents control.)"; } + + @Override + public String getCardIconHint(Game game) { + return "hexproof from monocolored"; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromPlaneswalkersAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromPlaneswalkersAbility.java index 0e30e798d90..e7fa893ba13 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofFromPlaneswalkersAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromPlaneswalkersAbility.java @@ -44,4 +44,9 @@ public class HexproofFromPlaneswalkersAbility extends HexproofBaseAbility { public String getRule() { return "hexproof from planeswalkers"; } + + @Override + public String getCardIconHint(Game game) { + return "hexproof from planeswalkers"; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromRedAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromRedAbility.java index b65634b118d..ef41e617983 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofFromRedAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromRedAbility.java @@ -45,4 +45,9 @@ public class HexproofFromRedAbility extends HexproofBaseAbility { public String getRule() { return "hexproof from red (This creature can't be the target of red spells or abilities your opponents control.)"; } + + @Override + public String getCardIconHint(Game game) { + return "hexproof from red"; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HexproofFromWhiteAbility.java b/Mage/src/main/java/mage/abilities/keyword/HexproofFromWhiteAbility.java index ec1d60a0206..d3dc9924b95 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HexproofFromWhiteAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HexproofFromWhiteAbility.java @@ -45,4 +45,9 @@ public class HexproofFromWhiteAbility extends HexproofBaseAbility { public String getRule() { return "hexproof from white (This creature can't be the target of white spells or abilities your opponents control.)"; } + + @Override + public String getCardIconHint(Game game) { + return "hexproof from white"; + } } From 42325c7c2eacd2eb3265ce4bca6fe0b72b3f1e71 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sun, 26 Sep 2021 02:34:50 +0400 Subject: [PATCH 189/231] AI improves: * AI: fixed that AI was able to targeting an invalid player targets in some use cases (#5630); * AI: fixed that AI was able to ignore targeted triggers in some use cases; * AI: improved player targeting for not own chooses (if it's make a choice for another player); --- .../client/remote/CallbackClientImpl.java | 1 - .../src/mage/player/ai/ComputerPlayer6.java | 1 + .../java/mage/player/ai/ComputerPlayer.java | 52 ++++++++++++------- .../src/mage/player/ai/ComputerPlayer2.java | 1 + .../abilities/keywords/HexproofTest.java | 52 ++++++++++++++++++- 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java index 1e062a2396d..59fcb99ce2b 100644 --- a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java +++ b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java @@ -318,7 +318,6 @@ public class CallbackClientImpl implements CallbackClient { // uses for client side only (example: update after scrollbars support) GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { - logger.info("redraw"); panel.updateGame(); } break; diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index 9b13c4809ae..a7dc1d138f2 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -391,6 +391,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { protected void resolve(SimulationNode2 node, int depth, Game game) { StackObject stackObject = game.getStack().getFirst(); if (stackObject instanceof StackAbility) { + // AI hint for search effects (calc all possible cards for best score) SearchEffect effect = getSearchEffect((StackAbility) stackObject); if (effect != null && stackObject.getControllerId().equals(playerId)) { diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 59d2553e7d0..d7deb819b41 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -113,7 +113,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (hand.size() < 6 || isTestsMode() // ignore mulligan in tests || game.getClass().getName().contains("Momir") // ignore mulligan in Momir games - ) { + ) { return false; } Set lands = hand.getCards(new FilterLandCard(), game); @@ -2711,7 +2711,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { } protected void findBestPermanentTargets(Outcome outcome, UUID abilityControllerId, UUID sourceId, FilterPermanent filter, Game game, Target target, - List goodList, List badList, List allList) { + List goodList, List badList, List allList) { // searching for most valuable/powerfull permanents goodList.clear(); badList.clear(); @@ -2893,27 +2893,38 @@ public class ComputerPlayer extends PlayerImpl implements Player { } /** - * Sets a possible target player + * Sets a possible target player. Depends on bad/good outcome + * + * @param source null on choose and non-null on chooseTarget */ private boolean setTargetPlayer(Outcome outcome, Target target, Ability source, UUID sourceId, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) { + Outcome affectedOutcome; + if (abilityControllerId == this.playerId) { + // selects for itself + affectedOutcome = outcome; + } else { + // selects for another player + affectedOutcome = Outcome.inverse(outcome); + } + if (target.getOriginalTarget() instanceof TargetOpponent) { if (source == null) { if (target.canTarget(randomOpponentId, game)) { target.add(randomOpponentId, game); return true; } - } else if (target.canTarget(randomOpponentId, source, game)) { - target.add(randomOpponentId, game); + } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { + target.addTarget(randomOpponentId, source, game); return true; } - for (UUID currentId : game.getOpponents(abilityControllerId)) { + for (UUID possibleOpponentId : game.getOpponents(abilityControllerId)) { if (source == null) { - if (target.canTarget(currentId, game)) { - target.add(currentId, game); + if (target.canTarget(possibleOpponentId, game)) { + target.add(possibleOpponentId, game); return true; } - } else if (target.canTarget(currentId, source, game)) { - target.add(currentId, game); + } else if (target.canTarget(abilityControllerId, possibleOpponentId, source, game)) { + target.addTarget(possibleOpponentId, source, game); return true; } } @@ -2921,8 +2932,9 @@ public class ComputerPlayer extends PlayerImpl implements Player { } if (target.getOriginalTarget() instanceof TargetPlayer) { - if (outcome.isGood()) { + if (affectedOutcome.isGood()) { if (source == null) { + // good if (target.canTarget(abilityControllerId, game)) { target.add(abilityControllerId, game); return true; @@ -2934,19 +2946,20 @@ public class ComputerPlayer extends PlayerImpl implements Player { } } } else { + // good if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { - target.addTarget(playerId, source, game); + target.addTarget(abilityControllerId, source, game); return true; } if (target.isRequired(sourceId, game)) { - if (target.canTarget(randomOpponentId, game)) { - target.add(randomOpponentId, game); + if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { + target.addTarget(randomOpponentId, source, game); return true; } } } - } else if (source == null) { + // bad if (target.canTarget(randomOpponentId, game)) { target.add(randomOpponentId, game); return true; @@ -2958,13 +2971,14 @@ public class ComputerPlayer extends PlayerImpl implements Player { } } } else { - if (target.canTarget(randomOpponentId, game)) { - target.add(randomOpponentId, game); + // bad + if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { + target.addTarget(randomOpponentId, source, game); return true; } if (required) { - if (target.canTarget(abilityControllerId, game)) { - target.add(abilityControllerId, game); + if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { + target.addTarget(abilityControllerId, source, game); return true; } } diff --git a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java index 6feb0d0eeca..097c08ef3b7 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java @@ -351,6 +351,7 @@ public class ComputerPlayer2 extends ComputerPlayer implements Player { } protected int simulatePriority(SimulationNode node, Game game, int alpha, int beta) { + // NOT USED in real AI, see ComputerPlayer6 if (Thread.interrupted()) { Thread.currentThread().interrupt(); logger.debug(indent(node.depth) + "interrupted"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HexproofTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HexproofTest.java index 2f78ce079f1..00c6bf24775 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HexproofTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HexproofTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.abilities.keywords; import mage.abilities.keyword.HexproofAbility; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.counters.CounterType; import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; @@ -127,6 +128,7 @@ public class HexproofTest extends CardTestPlayerBaseWithAIHelps { assertAllCommandsUsed(); assertGraveyardCount(playerA, "Swamp", 1); + assertCounterCount(playerA, "Liliana Vess", CounterType.LOYALTY, 5 + 1); } @Test @@ -158,7 +160,7 @@ public class HexproofTest extends CardTestPlayerBaseWithAIHelps { } @Test - public void test_AI_MustTargetOnlyValid() { + public void test_AI_MustTargetOnlyValid_1() { // +1: Target player discards a card. addCard(Zone.BATTLEFIELD, playerA, "Liliana Vess", 1); addCard(Zone.HAND, playerA, "Balduvian Bears", 1); @@ -180,6 +182,54 @@ public class HexproofTest extends CardTestPlayerBaseWithAIHelps { // no discarded cards assertGraveyardCount(playerA, 0); assertGraveyardCount(playerB, 0); + // no activated abilities + assertCounterCount(playerA, "Liliana Vess", CounterType.LOYALTY, 5 - 2); // search library for -2 + } + + @Test + public void test_AI_MustTargetOnlyValid_2() { + // +1: Target player discards a card. + addCard(Zone.BATTLEFIELD, playerA, "Liliana Vess", 1); + addCard(Zone.HAND, playerA, "Balduvian Bears", 1); + addCard(Zone.HAND, playerA, "Swamp", 1); + addCard(Zone.HAND, playerB, "Matter Reshaper", 1); + addCard(Zone.HAND, playerB, "Mountain", 1); + // + // You and permanents you control gain hexproof from blue and from black until end of turn. + addCard(Zone.HAND, playerB, "Veil of Summer", 1); // instant {G} + addCard(Zone.BATTLEFIELD, playerB, "Forest", 1); + + // prepare hexproof + castSpell(1, PhaseStep.UPKEEP, playerB, "Veil of Summer"); + + // ai must not use +1 on itself (due bad score) and must not use on opponent (due hexproof) + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no discarded cards + assertGraveyardCount(playerA, 0); + assertGraveyardCount(playerB, 1); // Veil of Summer + // no activated abilities + assertCounterCount(playerA, "Liliana Vess", CounterType.LOYALTY, 5 - 2); // search library for -2 + } + + @Test + public void test_AI_Logs() { + addCard(Zone.HAND, playerA, "Lightning Bolt", 3); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerB, 20 - 3 * 3); } @Test From 8418c6a09aac1fbe95d4bba5e4a46237d397f12a Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sun, 26 Sep 2021 18:43:57 +0400 Subject: [PATCH 190/231] AI: game logs improved (docs/wiki, added diff score per command and commands chain) --- .../src/mage/player/ai/ComputerPlayer6.java | 113 ++++++++++++------ 1 file changed, 77 insertions(+), 36 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index a7dc1d138f2..5b347cb2a92 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -162,13 +162,12 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { boolean usedStack = false; while (actions.peek() != null) { Ability ability = actions.poll(); - // log example: ===> Act [PlayerA] Action: Cast Blessings of Nature (target 1; target 2) - logger.info(new StringBuilder("===> Act [") - .append(game.getPlayer(playerId).getName()) - .append("] Action: ") - .append(ability.toString()) - .append(listTargets(game, ability.getTargets(), " (targeting %s)", "")) - .toString()); + // example: ===> SELECTED ACTION for PlayerA: Play Swamp + logger.info(String.format("===> SELECTED ACTION for %s: %s", + getName(), + ability.toString() + + listTargets(game, ability.getTargets(), " (targeting %s)", "") + )); if (!ability.getTargets().isEmpty()) { for (Target target : ability.getTargets()) { for (UUID id : target.getTargets()) { @@ -478,15 +477,24 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { SimulationNode2 bestNode = null; List allActions = currentPlayer.simulatePriority(game); optimize(game, allActions); + int startedScore = GameStateEvaluator2.evaluate(this.getId(), node.getGame()).getTotalScore(); if (logger.isInfoEnabled() && !allActions.isEmpty() && depth == maxDepth) { - logger.info("ADDED ACTIONS (" + allActions.size() + ") " + ' ' + allActions); + logger.info(String.format("POSSIBLE ACTIONS for %s (%d, started score: %d)%s", + getName(), + allActions.size(), + startedScore, + (actions.isEmpty() ? "" : ":") + )); + for (int i = 0; i < allActions.size(); i++) { + logger.info(String.format("-> #%d (%s)", i + 1, allActions.get(i))); + } } - int counter = 0; + int actionNumber = 0; int bestValSubNodes = Integer.MIN_VALUE; for (Ability action : allActions) { - counter++; + actionNumber++; if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.interrupted()) { Thread.currentThread().interrupt(); @@ -504,7 +512,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } if (!sim.checkIfGameIsOver() && (action.isUsesStack() || action instanceof PassAbility)) { - // only pass if the last action uses the stack + // skip priority for opponents before stack resolve UUID nextPlayerId = sim.getPlayerList().get(); do { sim.getPlayer(nextPlayerId).pass(game); @@ -513,48 +521,73 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } SimulationNode2 newNode = new SimulationNode2(node, sim, action, depth, currentPlayer.getId()); sim.checkStateAndTriggered(); - int val; + int actionScore; if (action instanceof PassAbility && sim.getStack().isEmpty()) { - // Stop to simulate deeper if PassAbility and stack is empty - val = GameStateEvaluator2.evaluate(this.getId(), sim).getTotalScore(); + // no more next actions, it's a final score + actionScore = GameStateEvaluator2.evaluate(this.getId(), sim).getTotalScore(); } else { - val = addActions(newNode, depth - 1, alpha, beta); + // resolve current action and calc all next actions to find best score (return max possible score) + actionScore = addActions(newNode, depth - 1, alpha, beta); } - logger.debug("Sim Prio " + BLANKS.substring(0, 2 + (maxDepth - depth) * 3) + '[' + depth + "]#" + counter + " <" + val + "> - (" + action + ") "); + logger.debug("Sim Prio " + BLANKS.substring(0, 2 + (maxDepth - depth) * 3) + '[' + depth + "]#" + actionNumber + " <" + actionScore + "> - (" + action + ") "); + + // Hints on data: + // * node - started game with executed command (pay and put on stack) + // * newNode - resolved game with resolved command (resolve stack) + // * node.children - rewrites to store only best tree (e.g. contains only final data) + // * node.score - rewrites to store max score (e.g. contains only final data) if (logger.isInfoEnabled() && depth >= maxDepth) { - StringBuilder sb = new StringBuilder("Sim Prio [").append(depth).append("] #").append(counter) - .append(" <").append(val).append("> (").append(action) - .append(action.isModal() ? " Mode = " + action.getModes().getMode().toString() : "") - .append(listTargets(game, action.getTargets(), " (targeting %s)", "")).append(')') - .append(logger.isTraceEnabled() ? " #" + newNode.hashCode() : ""); + // show calculated actions and score + // example: Sim Prio [6] #1 <605> (Play Swamp) + int currentActionScore = GameStateEvaluator2.evaluate(this.getId(), newNode.getGame()).getTotalScore(); + int diffCurrentAction = currentActionScore - startedScore; + int diffNextActions = actionScore - startedScore - diffCurrentAction; + logger.info(String.format("Sim Prio [%d] #%d (%s)", + depth, + actionNumber, + printDiffScore(diffCurrentAction), + printDiffScore(diffNextActions), + action + + (action.isModal() ? " Mode = " + action.getModes().getMode().toString() : "") + + listTargets(game, action.getTargets(), " (targeting %s)", "") + + (logger.isTraceEnabled() ? " #" + newNode.hashCode() : "") + )); + // collect childs info (next actions chain) SimulationNode2 logNode = newNode; while (logNode.getChildren() != null && !logNode.getChildren().isEmpty()) { logNode = logNode.getChildren().get(0); if (logNode.getAbilities() != null && !logNode.getAbilities().isEmpty()) { - sb.append(" -> [").append(logNode.getDepth()).append(']').append(logNode.getAbilities().toString()).append('<').append(logNode.getScore()).append('>'); + int logCurrentScore = GameStateEvaluator2.evaluate(this.getId(), logNode.getGame()).getTotalScore(); + int logPrevScore = GameStateEvaluator2.evaluate(this.getId(), logNode.getParent().getGame()).getTotalScore(); + logger.info(String.format("Sim Prio [%d] -> next action: [%d]%s ", + depth, + logNode.getDepth(), + logNode.getAbilities().toString(), + printDiffScore(logCurrentScore - logPrevScore), + printDiffScore(actionScore - logCurrentScore) + )); } } - logger.info(sb); } if (currentPlayer.getId().equals(playerId)) { - if (val > bestValSubNodes) { - bestValSubNodes = val; + if (actionScore > bestValSubNodes) { + bestValSubNodes = actionScore; } if (depth == maxDepth && action instanceof PassAbility) { - val = val - PASSIVITY_PENALTY; // passivity penalty + actionScore = actionScore - PASSIVITY_PENALTY; // passivity penalty } - if (val > alpha + if (actionScore > alpha || (depth == maxDepth - && val == alpha + && actionScore == alpha && RandomUtil.nextBoolean())) { // Adding random for equal value to get change sometimes - alpha = val; + alpha = actionScore; bestNode = newNode; - bestNode.setScore(val); + bestNode.setScore(actionScore); if (!newNode.getChildren().isEmpty()) { bestNode.setCombat(newNode.getChildren().get(0).getCombat()); } @@ -565,7 +598,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { .stream() .map(a -> a.toString() + listTargets(game, a.getTargets(), " (targeting %s)", "")) .collect(Collectors.joining("; ")); - logger.info("Sim Prio [" + depth + "] -- Saved best node yet <" + bestNode.getScore() + scoreInfo + "> " + abilitiesInfo); + logger.info("Sim Prio [" + depth + "] >> BEST action chain found <" + bestNode.getScore() + scoreInfo + "> " + abilitiesInfo); node.children.clear(); node.children.add(bestNode); node.setScore(bestNode.getScore()); @@ -573,22 +606,22 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } // no need to check other actions - if (val == GameStateEvaluator2.WIN_GAME_SCORE) { + if (actionScore == GameStateEvaluator2.WIN_GAME_SCORE) { logger.debug("Sim Prio -- win - break"); break; } } else { - if (val < beta) { - beta = val; + if (actionScore < beta) { + beta = actionScore; bestNode = newNode; - bestNode.setScore(val); + bestNode.setScore(actionScore); if (!newNode.getChildren().isEmpty()) { bestNode.setCombat(newNode.getChildren().get(0).getCombat()); } } // no need to check other actions - if (val == GameStateEvaluator2.LOSE_GAME_SCORE) { + if (actionScore == GameStateEvaluator2.LOSE_GAME_SCORE) { logger.debug("Sim Prio -- lose - break"); break; } @@ -623,6 +656,14 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } } + private String printDiffScore(int score) { + if (score >= 0) { + return "+" + score; + } else { + return "" + score; + } + } + /** * Various AI optimizations for actions. * From a87b28f348900ef554f6f5136c402614a82d1c77 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 26 Sep 2021 14:34:32 -0400 Subject: [PATCH 191/231] [MID] various text fixes --- Mage.Sets/src/mage/cards/b/Bladebrand.java | 2 +- .../mage/cards/f/FalkenrathPitFighter.java | 2 +- Mage.Sets/src/mage/cards/j/JackOLantern.java | 6 ++-- .../mage/cards/k/KatildaDawnhartPrime.java | 24 ++++++++++------ .../src/mage/cards/m/MightOfTheOldWays.java | 2 ++ .../src/mage/cards/o/OdricsOutrider.java | 4 ++- .../src/mage/cards/o/OldStickfingers.java | 2 +- .../src/mage/cards/r/RootcoilCreeper.java | 12 ++++++-- .../src/mage/cards/s/SarythTheVipersFang.java | 6 ++-- .../src/mage/cards/s/SlaughterSpecialist.java | 2 +- .../src/mage/cards/s/SlogurkTheOverslime.java | 13 ++++++--- Mage.Sets/src/mage/cards/s/Startle.java | 2 +- .../mage/cards/t/TeferiWhoSlowsTheSunset.java | 28 ++++++++++--------- .../src/mage/cards/t/TheMeathookMassacre.java | 2 +- .../src/mage/cards/t/ThermoAlchemist.java | 22 ++++++++------- .../src/mage/cards/w/WinterthornBlessing.java | 2 +- .../java/mage/verify/VerifyCardDataTest.java | 2 +- ...ThisOrAnotherCreatureTriggeredAbility.java | 13 +++++---- .../costs/common/DiscardHandCost.java | 2 +- ...ToHandChosenControlledPermanentEffect.java | 3 +- .../abilities/mana/AnyColorManaAbility.java | 14 ++++++---- .../main/java/mage/filter/StaticFilters.java | 9 +++++- .../permanent/token/ConsumingBlobToken.java | 2 +- 23 files changed, 108 insertions(+), 68 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/Bladebrand.java b/Mage.Sets/src/mage/cards/b/Bladebrand.java index 472d4cc8092..737bd290b20 100644 --- a/Mage.Sets/src/mage/cards/b/Bladebrand.java +++ b/Mage.Sets/src/mage/cards/b/Bladebrand.java @@ -26,7 +26,7 @@ public final class Bladebrand extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Draw a card. - this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1).concatBy("
")); } private Bladebrand(final Bladebrand card) { diff --git a/Mage.Sets/src/mage/cards/f/FalkenrathPitFighter.java b/Mage.Sets/src/mage/cards/f/FalkenrathPitFighter.java index 8309aefdfc5..48587739a13 100644 --- a/Mage.Sets/src/mage/cards/f/FalkenrathPitFighter.java +++ b/Mage.Sets/src/mage/cards/f/FalkenrathPitFighter.java @@ -24,7 +24,7 @@ import java.util.UUID; */ public final class FalkenrathPitFighter extends CardImpl { - private static final FilterControlledPermanent filter = new FilterControlledPermanent(SubType.VAMPIRE); + private static final FilterControlledPermanent filter = new FilterControlledPermanent(SubType.VAMPIRE, "Vampire"); public FalkenrathPitFighter(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}"); diff --git a/Mage.Sets/src/mage/cards/j/JackOLantern.java b/Mage.Sets/src/mage/cards/j/JackOLantern.java index 0ef4aae1f98..99d3651b684 100644 --- a/Mage.Sets/src/mage/cards/j/JackOLantern.java +++ b/Mage.Sets/src/mage/cards/j/JackOLantern.java @@ -1,15 +1,15 @@ package mage.cards.j; -import mage.Mana; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.ExileSourceFromGraveCost; import mage.abilities.costs.common.SacrificeSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.ExileTargetEffect; -import mage.abilities.mana.SimpleManaAbility; +import mage.abilities.mana.AnyColorManaAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -35,7 +35,7 @@ public final class JackOLantern extends CardImpl { this.addAbility(ability); // {1}, Exile Jack-o'-Lantern from your graveyard: Add one mana of any color. - ability = new SimpleManaAbility(Zone.GRAVEYARD, Mana.AnyMana(1), new GenericManaCost(1)); + ability = new AnyColorManaAbility(Zone.GRAVEYARD, new GenericManaCost(1), StaticValue.get(1), false); ability.addCost(new ExileSourceFromGraveCost()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/k/KatildaDawnhartPrime.java b/Mage.Sets/src/mage/cards/k/KatildaDawnhartPrime.java index 7f2294002c3..1a80e57098b 100644 --- a/Mage.Sets/src/mage/cards/k/KatildaDawnhartPrime.java +++ b/Mage.Sets/src/mage/cards/k/KatildaDawnhartPrime.java @@ -1,8 +1,5 @@ package mage.cards.k; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; import mage.MageInt; import mage.Mana; import mage.ObjectColor; @@ -16,11 +13,11 @@ import mage.abilities.effects.common.counter.AddCountersAllEffect; import mage.abilities.effects.mana.ManaEffect; import mage.abilities.keyword.ProtectionAbility; import mage.abilities.mana.ActivatedManaAbilityImpl; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; import mage.choices.Choice; import mage.choices.ChoiceImpl; import mage.constants.*; -import mage.cards.CardImpl; -import mage.cards.CardSetInfo; import mage.counters.CounterType; import mage.filter.FilterPermanent; import mage.filter.StaticFilters; @@ -29,14 +26,17 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + /** - * * @author weirddan455 */ public final class KatildaDawnhartPrime extends CardImpl { private static final FilterPermanent filter = new FilterPermanent(SubType.WEREWOLF, "Werewolves"); - private static final FilterControlledCreaturePermanent filter2 = new FilterControlledCreaturePermanent(SubType.HUMAN, "Human creatures you control"); + private static final FilterPermanent filter2 = new FilterControlledCreaturePermanent(SubType.HUMAN, "Human creatures"); public KatildaDawnhartPrime(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{W}"); @@ -51,10 +51,16 @@ public final class KatildaDawnhartPrime extends CardImpl { this.addAbility(new ProtectionAbility(filter)); // Human creatures you control have "{T}: Add one mana of any of this creature's colors." - this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect(new KatildaDawnhartPrimeManaAbility(), Duration.WhileOnBattlefield, filter2))); + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( + new KatildaDawnhartPrimeManaAbility(), Duration.WhileOnBattlefield, filter2 + ))); // {4}{G}{W}, {T}: Put a +1/+1 counter on each creature you control. - this.addAbility(new SimpleActivatedAbility(new AddCountersAllEffect(CounterType.P1P1.createInstance(), StaticFilters.FILTER_CONTROLLED_CREATURE), new ManaCostsImpl<>("{4}{G}{W}"))); + Ability ability = new SimpleActivatedAbility(new AddCountersAllEffect( + CounterType.P1P1.createInstance(), StaticFilters.FILTER_CONTROLLED_CREATURE + ), new ManaCostsImpl<>("{4}{G}{W}")); + ability.addCost(new TapSourceCost()); + this.addAbility(ability); } private KatildaDawnhartPrime(final KatildaDawnhartPrime card) { diff --git a/Mage.Sets/src/mage/cards/m/MightOfTheOldWays.java b/Mage.Sets/src/mage/cards/m/MightOfTheOldWays.java index e56f49a0142..a340f3c8182 100644 --- a/Mage.Sets/src/mage/cards/m/MightOfTheOldWays.java +++ b/Mage.Sets/src/mage/cards/m/MightOfTheOldWays.java @@ -9,6 +9,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; import mage.constants.CardType; +import mage.target.common.TargetCreaturePermanent; import java.util.UUID; @@ -22,6 +23,7 @@ public final class MightOfTheOldWays extends CardImpl { // Target creature gets +2/+2 until end of turn. this.getSpellAbility().addEffect(new BoostTargetEffect(2, 2)); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Coven — Then if you control three or more creatures with different powers, draw a card. this.getSpellAbility().addEffect(new ConditionalOneShotEffect( diff --git a/Mage.Sets/src/mage/cards/o/OdricsOutrider.java b/Mage.Sets/src/mage/cards/o/OdricsOutrider.java index 1a8a042d803..28c5a7ef0db 100644 --- a/Mage.Sets/src/mage/cards/o/OdricsOutrider.java +++ b/Mage.Sets/src/mage/cards/o/OdricsOutrider.java @@ -9,6 +9,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.counters.CounterType; +import mage.filter.StaticFilters; import mage.target.common.TargetControlledCreaturePermanent; import java.util.UUID; @@ -28,7 +29,8 @@ public final class OdricsOutrider extends CardImpl { // Whenever Odric's Outrider or another creature you control dies, put a +1/+1 counter on target creature you control. Ability ability = new DiesThisOrAnotherCreatureTriggeredAbility( - new AddCountersTargetEffect(CounterType.P1P1.createInstance()), false + new AddCountersTargetEffect(CounterType.P1P1.createInstance()), + false, StaticFilters.FILTER_CONTROLLED_CREATURE ); ability.addTarget(new TargetControlledCreaturePermanent()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/o/OldStickfingers.java b/Mage.Sets/src/mage/cards/o/OldStickfingers.java index 09c0cc178d3..64bf0dbb52a 100644 --- a/Mage.Sets/src/mage/cards/o/OldStickfingers.java +++ b/Mage.Sets/src/mage/cards/o/OldStickfingers.java @@ -33,7 +33,7 @@ public final class OldStickfingers extends CardImpl { this.addAbility(new CastSourceTriggeredAbility(new OldStickfingersEffect())); // Old Stickfingers' power and toughness are equal to the number of creature cards in your graveyard. - DynamicValue value = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURE); + DynamicValue value = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURES); this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetPowerToughnessSourceEffect(value, Duration.EndOfGame))); } diff --git a/Mage.Sets/src/mage/cards/r/RootcoilCreeper.java b/Mage.Sets/src/mage/cards/r/RootcoilCreeper.java index 5439d14ac18..248b8e5a938 100644 --- a/Mage.Sets/src/mage/cards/r/RootcoilCreeper.java +++ b/Mage.Sets/src/mage/cards/r/RootcoilCreeper.java @@ -33,7 +33,7 @@ import java.util.UUID; */ public final class RootcoilCreeper extends CardImpl { - private static final FilterCard filter = new FilterOwnedCard("card with flashback you own in exile"); + private static final FilterCard filter = new FilterOwnedCard("card with flashback you own from exile"); static { filter.add(new AbilityPredicate(FlashbackAbility.class)); @@ -51,10 +51,16 @@ public final class RootcoilCreeper extends CardImpl { this.addAbility(new AnyColorManaAbility()); // {T}: Add two mana of any one color. Spend this mana only to cast spells from your graveyard. - this.addAbility(new ConditionalAnyColorManaAbility(2, new RootcoilCreeperManaBuilder())); + this.addAbility(new ConditionalAnyColorManaAbility( + new TapSourceCost(), 2, new RootcoilCreeperManaBuilder(), true + )); // {G}{U}, {T}, Exile Rootcoil Creeper: Return target card with flashback you own in exile to your hand. - Ability ability = new SimpleActivatedAbility(new ReturnToHandTargetEffect(), new ManaCostsImpl<>("{G}{U}")); + Ability ability = new SimpleActivatedAbility( + new ReturnToHandTargetEffect() + .setText("return target card with flashback you own in exile to your hand"), + new ManaCostsImpl<>("{G}{U}") + ); ability.addCost(new TapSourceCost()); ability.addCost(new ExileSourceCost()); ability.addTarget(new TargetCardInExile(filter)); diff --git a/Mage.Sets/src/mage/cards/s/SarythTheVipersFang.java b/Mage.Sets/src/mage/cards/s/SarythTheVipersFang.java index be8a2d116e7..37ca2031495 100644 --- a/Mage.Sets/src/mage/cards/s/SarythTheVipersFang.java +++ b/Mage.Sets/src/mage/cards/s/SarythTheVipersFang.java @@ -18,6 +18,7 @@ import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterUntappedCreature; import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AnotherPredicate; import mage.filter.predicate.permanent.TappedPredicate; import mage.target.TargetPermanent; @@ -28,8 +29,8 @@ import java.util.UUID; */ public final class SarythTheVipersFang extends CardImpl { - private static final FilterPermanent filterTapped = new FilterControlledCreaturePermanent("tapped creatures you control"); - private static final FilterPermanent filterAbility = new FilterControlledPermanent("creature or land you control"); + private static final FilterPermanent filterTapped = new FilterControlledCreaturePermanent("tapped creatures"); + private static final FilterPermanent filterAbility = new FilterControlledPermanent("another target creature or land you control"); static { filterTapped.add(TappedPredicate.TAPPED); @@ -37,6 +38,7 @@ public final class SarythTheVipersFang extends CardImpl { CardType.CREATURE.getPredicate(), CardType.LAND.getPredicate() )); + filterAbility.add(AnotherPredicate.instance); } public SarythTheVipersFang(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Sets/src/mage/cards/s/SlaughterSpecialist.java b/Mage.Sets/src/mage/cards/s/SlaughterSpecialist.java index 2546bfbe793..6987cc67ef9 100644 --- a/Mage.Sets/src/mage/cards/s/SlaughterSpecialist.java +++ b/Mage.Sets/src/mage/cards/s/SlaughterSpecialist.java @@ -38,7 +38,7 @@ public final class SlaughterSpecialist extends CardImpl { // Whenever a creature an opponent controls dies, put a +1/+1 counter on Slaughter Specialist. this.addAbility(new DiesCreatureTriggeredAbility( new AddCountersSourceEffect(CounterType.P1P1.createInstance()), - false, StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE + false, StaticFilters.FILTER_OPPONENTS_PERMANENT_A_CREATURE )); } diff --git a/Mage.Sets/src/mage/cards/s/SlogurkTheOverslime.java b/Mage.Sets/src/mage/cards/s/SlogurkTheOverslime.java index 292337f839e..11f66bc3273 100644 --- a/Mage.Sets/src/mage/cards/s/SlogurkTheOverslime.java +++ b/Mage.Sets/src/mage/cards/s/SlogurkTheOverslime.java @@ -12,7 +12,10 @@ import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.TargetController; import mage.counters.CounterType; import mage.filter.StaticFilters; import mage.filter.common.FilterLandCard; @@ -27,7 +30,7 @@ public final class SlogurkTheOverslime extends CardImpl { public SlogurkTheOverslime(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}{U}"); - + this.addSuperType(SuperType.LEGENDARY); this.subtype.add(SubType.OOZE); this.power = new MageInt(3); @@ -43,8 +46,10 @@ public final class SlogurkTheOverslime extends CardImpl { )); // Remove three +1/+1 counters from Slogurk: Return it to its owner's hand. - this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new ReturnToHandSourceEffect(), - new RemoveCountersSourceCost(CounterType.P1P1.createInstance(3)))); + this.addAbility(new SimpleActivatedAbility( + new ReturnToHandSourceEffect().setText("return it to its owner's hand"), + new RemoveCountersSourceCost(CounterType.P1P1.createInstance(3)) + )); // When Slogurk leaves the battlefield, return up to three target land cards from your graveyard to your hand. Ability ability = new LeavesBattlefieldTriggeredAbility(new ReturnToHandTargetEffect() diff --git a/Mage.Sets/src/mage/cards/s/Startle.java b/Mage.Sets/src/mage/cards/s/Startle.java index 58d3d8fe40f..c9ba316a270 100644 --- a/Mage.Sets/src/mage/cards/s/Startle.java +++ b/Mage.Sets/src/mage/cards/s/Startle.java @@ -25,7 +25,7 @@ public final class Startle extends CardImpl { this.getSpellAbility().addEffect(new CreateTokenEffect(new ZombieDecayedToken())); // Draw a card. - this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1).concatBy("
")); } private Startle(final Startle card) { diff --git a/Mage.Sets/src/mage/cards/t/TeferiWhoSlowsTheSunset.java b/Mage.Sets/src/mage/cards/t/TeferiWhoSlowsTheSunset.java index bcbbc31e51c..995228baba4 100644 --- a/Mage.Sets/src/mage/cards/t/TeferiWhoSlowsTheSunset.java +++ b/Mage.Sets/src/mage/cards/t/TeferiWhoSlowsTheSunset.java @@ -5,6 +5,7 @@ import mage.abilities.LoyaltyAbility; import mage.abilities.common.PlaneswalkerEntersWithLoyaltyCountersAbility; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.GainLifeEffect; import mage.abilities.effects.common.GetEmblemEffect; import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; import mage.cards.CardImpl; @@ -13,11 +14,10 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.SuperType; -import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.command.emblems.TeferiWhoSlowsTheSunsetEmblem; import mage.game.permanent.Permanent; -import mage.players.Player; import mage.target.Target; import mage.target.common.TargetArtifactPermanent; import mage.target.common.TargetCreaturePermanent; @@ -39,13 +39,17 @@ public final class TeferiWhoSlowsTheSunset extends CardImpl { // +1: Choose up to one target artifact, up to one target creature, and up to one target land. Untap the chosen permanents you control. Tap the chosen permanents you don't control. You gain 2 life. Ability ability = new LoyaltyAbility(new TeferiWhoSlowsTheSunsetEffect(), 1); + ability.addEffect(new GainLifeEffect(2)); ability.addTarget(new TargetArtifactPermanent()); ability.addTarget(new TargetCreaturePermanent()); ability.addTarget(new TargetLandPermanent()); this.addAbility(ability); // −2: Look at the top three cards of your library. Put one of them into your hand and the rest on the bottom of your library in any order. - this.addAbility(new LoyaltyAbility(new LookLibraryAndPickControllerEffect(StaticValue.get(3), false, StaticValue.get(1), new FilterCard("card"), false, false), -2)); + this.addAbility(new LoyaltyAbility(new LookLibraryAndPickControllerEffect( + StaticValue.get(3), false, StaticValue.get(1), + StaticFilters.FILTER_CARD, false, false + ), -2)); // −7: You get an emblem with "Untap all permanents you control during each opponent's untap step" and "You draw a card during each opponent's draw step." this.addAbility(new LoyaltyAbility(new GetEmblemEffect(new TeferiWhoSlowsTheSunsetEmblem()), -7)); @@ -67,7 +71,7 @@ class TeferiWhoSlowsTheSunsetEffect extends OneShotEffect { super(Outcome.Benefit); staticText = "Choose up to one target artifact, up to one target creature, and up to one target land. " + "Untap the chosen permanents you control. " + - "Tap the chosen permanents you don't control. "; + "Tap the chosen permanents you don't control."; } private TeferiWhoSlowsTheSunsetEffect(final TeferiWhoSlowsTheSunsetEffect effect) { @@ -81,19 +85,17 @@ class TeferiWhoSlowsTheSunsetEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); for (Target target : source.getTargets()) { Permanent targetPermanent = game.getPermanent(target.getFirstTarget()); - if (targetPermanent != null) { - if (targetPermanent.getControllerId() == player.getId()) { - targetPermanent.untap(game); - } else { - targetPermanent.tap(source, game); - } + if (targetPermanent == null) { + continue; + } + if (targetPermanent.isControlledBy(source.getControllerId())) { + targetPermanent.untap(game); + } else { + targetPermanent.tap(source, game); } } - - player.gainLife(2, game, source); return true; } } diff --git a/Mage.Sets/src/mage/cards/t/TheMeathookMassacre.java b/Mage.Sets/src/mage/cards/t/TheMeathookMassacre.java index def254de989..0401c6a7366 100644 --- a/Mage.Sets/src/mage/cards/t/TheMeathookMassacre.java +++ b/Mage.Sets/src/mage/cards/t/TheMeathookMassacre.java @@ -43,7 +43,7 @@ public final class TheMeathookMassacre extends CardImpl { // Whenever a creature an opponent controls dies, you gain 1 life. this.addAbility(new DiesCreatureTriggeredAbility( new GainLifeEffect(1), false, - StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE + StaticFilters.FILTER_OPPONENTS_PERMANENT_A_CREATURE )); } diff --git a/Mage.Sets/src/mage/cards/t/ThermoAlchemist.java b/Mage.Sets/src/mage/cards/t/ThermoAlchemist.java index ecf22e35355..5eb60808170 100644 --- a/Mage.Sets/src/mage/cards/t/ThermoAlchemist.java +++ b/Mage.Sets/src/mage/cards/t/ThermoAlchemist.java @@ -1,9 +1,6 @@ - package mage.cards.t; -import java.util.UUID; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SpellCastControllerTriggeredAbility; import mage.abilities.costs.common.TapSourceCost; @@ -15,17 +12,17 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.TargetController; -import mage.constants.Zone; -import mage.filter.common.FilterInstantOrSorcerySpell; +import mage.filter.StaticFilters; + +import java.util.UUID; /** - * * @author LevelX2 */ public final class ThermoAlchemist extends CardImpl { public ThermoAlchemist(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{1}{R}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}"); this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.SHAMAN); this.power = new MageInt(0); @@ -33,11 +30,16 @@ public final class ThermoAlchemist extends CardImpl { // Defender this.addAbility(DefenderAbility.getInstance()); + // {T}: Thermo-Alchemist deals 1 damage to each opponent. - Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DamagePlayersEffect(1, TargetController.OPPONENT), new TapSourceCost()); - this.addAbility(ability); + this.addAbility(new SimpleActivatedAbility( + new DamagePlayersEffect(1, TargetController.OPPONENT), new TapSourceCost() + )); + // Whenever you cast an instant or sorcery spell, untap Thermo-Alchemist. - this.addAbility(new SpellCastControllerTriggeredAbility(new UntapSourceEffect(), new FilterInstantOrSorcerySpell(), false)); + this.addAbility(new SpellCastControllerTriggeredAbility( + new UntapSourceEffect(), StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false + )); } private ThermoAlchemist(final ThermoAlchemist card) { diff --git a/Mage.Sets/src/mage/cards/w/WinterthornBlessing.java b/Mage.Sets/src/mage/cards/w/WinterthornBlessing.java index 1f7fdb490c7..e1d11051f50 100644 --- a/Mage.Sets/src/mage/cards/w/WinterthornBlessing.java +++ b/Mage.Sets/src/mage/cards/w/WinterthornBlessing.java @@ -30,7 +30,7 @@ public final class WinterthornBlessing extends CardImpl { this.getSpellAbility().addTarget(new TargetPermanent(0, 1, StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL)); this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance()).setTargetPointer(new FirstTargetPointer())); this.getSpellAbility().addEffect(new TapTargetEffect().setTargetPointer(new SecondTargetPointer()).setText("tap up to one target creature you don't control")); - this.getSpellAbility().addEffect(new DontUntapInControllersNextUntapStepTargetEffect().setTargetPointer(new SecondTargetPointer()).setText("that creature doesn't untap during its controller's next untap step")); + this.getSpellAbility().addEffect(new DontUntapInControllersNextUntapStepTargetEffect().setTargetPointer(new SecondTargetPointer()).setText(", and that creature doesn't untap during its controller's next untap step")); // Flashback {1}{G}{U} this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{1}{G}{U}"))); diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index bb32451f2ea..b6c610ef3e1 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -58,7 +58,7 @@ public class VerifyCardDataTest { private static final Logger logger = Logger.getLogger(VerifyCardDataTest.class); - private static final String FULL_ABILITIES_CHECK_SET_CODE = "AFR"; // check all abilities and output cards with wrong abilities texts; + private static final String FULL_ABILITIES_CHECK_SET_CODE = "MID"; // check all abilities and output cards with wrong abilities texts; private static final boolean AUTO_FIX_SAMPLE_DECKS = false; // debug only: auto-fix sample decks by test_checkSampleDecks test run private static final boolean ONLY_TEXT = false; // use when checking text locally, suppresses unnecessary checks and output messages diff --git a/Mage/src/main/java/mage/abilities/common/DiesThisOrAnotherCreatureTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DiesThisOrAnotherCreatureTriggeredAbility.java index 1fc842ee8b7..bccded75fcf 100644 --- a/Mage/src/main/java/mage/abilities/common/DiesThisOrAnotherCreatureTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DiesThisOrAnotherCreatureTriggeredAbility.java @@ -4,7 +4,8 @@ import mage.MageObject; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.Zone; -import mage.filter.common.FilterCreaturePermanent; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; @@ -14,14 +15,14 @@ import mage.game.events.ZoneChangeEvent; */ public class DiesThisOrAnotherCreatureTriggeredAbility extends TriggeredAbilityImpl { - protected FilterCreaturePermanent filter; + protected FilterPermanent filter; private boolean applyFilterOnSource = false; public DiesThisOrAnotherCreatureTriggeredAbility(Effect effect, boolean optional) { - this(effect, optional, new FilterCreaturePermanent()); + this(effect, optional, StaticFilters.FILTER_PERMANENT_CREATURE); } - public DiesThisOrAnotherCreatureTriggeredAbility(Effect effect, boolean optional, FilterCreaturePermanent filter) { + public DiesThisOrAnotherCreatureTriggeredAbility(Effect effect, boolean optional, FilterPermanent filter) { super(Zone.ALL, effect, optional); // Needs "ALL" if the source itself should trigger or multiple (incl. source go to grave) this.filter = filter; } @@ -63,7 +64,7 @@ public class DiesThisOrAnotherCreatureTriggeredAbility extends TriggeredAbilityI } return false; } - + @Override public boolean isInUseableZone(Game game, MageObject source, GameEvent event) { return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, event, game); @@ -71,6 +72,6 @@ public class DiesThisOrAnotherCreatureTriggeredAbility extends TriggeredAbilityI @Override public String getTriggerPhrase() { - return "Whenever {this} or another " + filter.getMessage() + " dies, " ; + return "Whenever {this} or another " + filter.getMessage() + " dies, "; } } diff --git a/Mage/src/main/java/mage/abilities/costs/common/DiscardHandCost.java b/Mage/src/main/java/mage/abilities/costs/common/DiscardHandCost.java index ec4c6a7c9fe..74dbc145f3b 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/DiscardHandCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/DiscardHandCost.java @@ -44,6 +44,6 @@ public class DiscardHandCost extends CostImpl { @Override public String getText() { - return "Discard your hand"; + return "discard your hand"; } } \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/effects/common/ReturnToHandChosenControlledPermanentEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ReturnToHandChosenControlledPermanentEffect.java index 0af04bc69fe..1f7e6ae523e 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ReturnToHandChosenControlledPermanentEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ReturnToHandChosenControlledPermanentEffect.java @@ -38,8 +38,9 @@ public class ReturnToHandChosenControlledPermanentEffect extends ReturnToHandCho @Override protected String getText() { - StringBuilder sb = new StringBuilder("return "); + StringBuilder sb = new StringBuilder("return"); if (!filter.getMessage().startsWith("another")) { + sb.append(' '); if(filter.getMessage().startsWith("a")){ sb.append("an"); } diff --git a/Mage/src/main/java/mage/abilities/mana/AnyColorManaAbility.java b/Mage/src/main/java/mage/abilities/mana/AnyColorManaAbility.java index 1dc32fb3834..942fcded4b6 100644 --- a/Mage/src/main/java/mage/abilities/mana/AnyColorManaAbility.java +++ b/Mage/src/main/java/mage/abilities/mana/AnyColorManaAbility.java @@ -1,17 +1,18 @@ package mage.abilities.mana; -import java.util.ArrayList; -import java.util.List; import mage.Mana; import mage.abilities.costs.Cost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; -import mage.abilities.effects.mana.ManaEffect; import mage.abilities.effects.mana.AddManaOfAnyColorEffect; +import mage.abilities.effects.mana.ManaEffect; import mage.constants.Zone; import mage.game.Game; +import java.util.ArrayList; +import java.util.List; + public class AnyColorManaAbility extends ActivatedManaAbilityImpl { public AnyColorManaAbility() { @@ -28,14 +29,17 @@ public class AnyColorManaAbility extends ActivatedManaAbilityImpl { } /** - * * @param cost * @param netAmount dynamic value used during available mana calculation to * set the max possible amount the source can produce * @param setFlag */ public AnyColorManaAbility(Cost cost, DynamicValue netAmount, boolean setFlag) { - super(Zone.BATTLEFIELD, new AddManaOfAnyColorEffect(1, netAmount, setFlag), cost); + this(Zone.BATTLEFIELD, cost, netAmount, setFlag); + } + + public AnyColorManaAbility(Zone zone, Cost cost, DynamicValue netAmount, boolean setFlag) { + super(zone, new AddManaOfAnyColorEffect(1, netAmount, setFlag), cost); this.netMana.add(new Mana(0, 0, 0, 0, 0, 0, 1, 0)); } diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index 4b66c81727b..c0717cec1f2 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -389,6 +389,13 @@ public final class StaticFilters { FILTER_OPPONENTS_PERMANENT_CREATURE.setLockedFilter(true); } + public static final FilterCreaturePermanent FILTER_OPPONENTS_PERMANENT_A_CREATURE = new FilterCreaturePermanent("a creature an opponent controls"); + + static { + FILTER_OPPONENTS_PERMANENT_A_CREATURE.add(TargetController.OPPONENT.getControllerPredicate()); + FILTER_OPPONENTS_PERMANENT_A_CREATURE.setLockedFilter(true); + } + public static final FilterPermanent FILTER_OPPONENTS_PERMANENT_ARTIFACT = new FilterPermanent("artifact an opponent controls"); static { @@ -447,7 +454,7 @@ public final class StaticFilters { FILTER_CONTROLLED_ANOTHER_CREATURE.setLockedFilter(true); } - public static final FilterControlledCreaturePermanent FILTER_CONTROLLED_UNTAPPED_CREATURES = new FilterControlledCreaturePermanent("untapped creature you control"); + public static final FilterControlledCreaturePermanent FILTER_CONTROLLED_UNTAPPED_CREATURES = new FilterControlledCreaturePermanent("untapped creatures you control"); static { FILTER_CONTROLLED_UNTAPPED_CREATURES.add(TappedPredicate.UNTAPPED); diff --git a/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobToken.java b/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobToken.java index d3d113fc666..c64de225556 100644 --- a/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobToken.java @@ -16,7 +16,7 @@ import mage.game.Game; public final class ConsumingBlobToken extends TokenImpl { public ConsumingBlobToken() { - super("Ooze", "green Ooze creature token with \"This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1.\""); + super("Ooze", "green Ooze creature token with \"This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1\"."); setOriginalExpansionSetCode("MID"); cardType.add(CardType.CREATURE); subtype.add(SubType.OOZE); From 9bcd7e0dcdd5fde8637ec0ccd1d75d5687439243 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 27 Sep 2021 08:03:34 -0400 Subject: [PATCH 192/231] [MIC] Implemented Moorland Rescuer --- .../src/mage/cards/m/MoorlandRescuer.java | 135 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 136 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MoorlandRescuer.java diff --git a/Mage.Sets/src/mage/cards/m/MoorlandRescuer.java b/Mage.Sets/src/mage/cards/m/MoorlandRescuer.java new file mode 100644 index 00000000000..4b4d579ab2e --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MoorlandRescuer.java @@ -0,0 +1,135 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.MageObjectReferencePredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MoorlandRescuer extends CardImpl { + + public MoorlandRescuer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.KNIGHT); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // When Moorland Rescuer dies, return any number of other creature cards with total power X or less from your graveyard to the battlefield, where X is Moorland Rescuer's power. Exile Moorland Rescuer. + this.addAbility(new DiesSourceTriggeredAbility(new MoorlandRescuerEffect())); + } + + private MoorlandRescuer(final MoorlandRescuer card) { + super(card); + } + + @Override + public MoorlandRescuer copy() { + return new MoorlandRescuer(this); + } +} + +class MoorlandRescuerEffect extends OneShotEffect { + + MoorlandRescuerEffect() { + super(Outcome.Benefit); + staticText = "return any number of other creature cards with total power X or less " + + "from your graveyard to the battlefield, where X is {this}'s power. Exile {this}"; + } + + private MoorlandRescuerEffect(final MoorlandRescuerEffect effect) { + super(effect); + } + + @Override + public MoorlandRescuerEffect copy() { + return new MoorlandRescuerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = (Permanent) getValue("permanentLeftBattlefield"); + if (player == null || permanent == null) { + return false; + } + TargetCard target = new MoorlandRescuerTarget(permanent.getPower().getValue(), source, game); + player.choose(outcome, player.getGraveyard(), target, game); + player.moveCards(new CardsImpl(target.getTargets()), Zone.BATTLEFIELD, source, game); + Card sourceCard = (Card) source.getSourceObjectIfItStillExists(game); + if (sourceCard != null) { + player.moveCards(sourceCard, Zone.EXILED, source, game); + } + return true; + } +} + +class MoorlandRescuerTarget extends TargetCardInYourGraveyard { + + private final int xValue; + + MoorlandRescuerTarget(int xValue, Ability source, Game game) { + super(0, Integer.MAX_VALUE, makeFilter(xValue, source, game)); + this.xValue = xValue; + this.notTarget = true; + } + + private MoorlandRescuerTarget(final MoorlandRescuerTarget target) { + super(target); + this.xValue = target.xValue; + } + + @Override + public MoorlandRescuerTarget copy() { + return new MoorlandRescuerTarget(this); + } + + @Override + public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + if (!super.canTarget(controllerId, id, source, game)) { + return false; + } + Card card = game.getCard(id); + if (card == null) { + return false; + } + int powerSum = this + .getTargets() + .stream() + .map(game::getCard) + .map(Card::getPower) + .mapToInt(MageInt::getValue) + .sum(); + return card.getPower().getValue() + powerSum <= xValue; + } + + private static FilterCard makeFilter(int xValue, Ability source, Game game) { + FilterCard filter = new FilterCreatureCard( + "creature cards with total power " + xValue + " or less from your graveyard" + ); + filter.add(Predicates.not(new MageObjectReferencePredicate(source.getSourceObject(game), game))); + return filter; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index f5cbf3f332d..911fc715352 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -106,6 +106,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Lord of the Accursed", 124, Rarity.UNCOMMON, mage.cards.l.LordOfTheAccursed.class)); cards.add(new SetCardInfo("Midnight Reaper", 125, Rarity.RARE, mage.cards.m.MidnightReaper.class)); cards.add(new SetCardInfo("Mikaeus, the Lunarch", 89, Rarity.MYTHIC, mage.cards.m.MikaeusTheLunarch.class)); + cards.add(new SetCardInfo("Moorland Rescuer", 7, Rarity.RARE, mage.cards.m.MoorlandRescuer.class)); cards.add(new SetCardInfo("Mortuary Mire", 176, Rarity.COMMON, mage.cards.m.MortuaryMire.class)); cards.add(new SetCardInfo("Myriad Landscape", 177, Rarity.UNCOMMON, mage.cards.m.MyriadLandscape.class)); cards.add(new SetCardInfo("Odric, Master Tactician", 90, Rarity.RARE, mage.cards.o.OdricMasterTactician.class)); From 11d5fc7eae460f7f6918038d3aefb670f5f81ef7 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 27 Sep 2021 08:40:16 -0400 Subject: [PATCH 193/231] [MIC] Implemented Prowling Geistcatcher --- .../mage/cards/p/ProwlingGeistcatcher.java | 120 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + .../SacrificePermanentTriggeredAbility.java | 17 ++- 3 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/p/ProwlingGeistcatcher.java diff --git a/Mage.Sets/src/mage/cards/p/ProwlingGeistcatcher.java b/Mage.Sets/src/mage/cards/p/ProwlingGeistcatcher.java new file mode 100644 index 00000000000..18dd75e861b --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/ProwlingGeistcatcher.java @@ -0,0 +1,120 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.LeavesBattlefieldTriggeredAbility; +import mage.abilities.common.SacrificePermanentTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentToken; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ProwlingGeistcatcher extends CardImpl { + + public ProwlingGeistcatcher(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Whenever you sacrifice another creature, exile it. If that creature was a token, put a +1/+1 counter on Prowling Geistcatcher. + this.addAbility(new SacrificePermanentTriggeredAbility( + new ProwlingGeistcatcherExileEffect(), + StaticFilters.FILTER_CONTROLLED_ANOTHER_CREATURE, true + )); + + // When Prowling Geistcatcher leaves the battlefield, return each card exiled with it to the battlefield under your control. + this.addAbility(new LeavesBattlefieldTriggeredAbility(new ProwlingGeistcatcherReturnEffect(), false)); + } + + private ProwlingGeistcatcher(final ProwlingGeistcatcher card) { + super(card); + } + + @Override + public ProwlingGeistcatcher copy() { + return new ProwlingGeistcatcher(this); + } +} + +class ProwlingGeistcatcherExileEffect extends OneShotEffect { + + ProwlingGeistcatcherExileEffect() { + super(Outcome.Benefit); + staticText = "exile it. If that creature was a token, put a +1/+1 counter on {this}"; + } + + private ProwlingGeistcatcherExileEffect(final ProwlingGeistcatcherExileEffect effect) { + super(effect); + } + + @Override + public ProwlingGeistcatcherExileEffect copy() { + return new ProwlingGeistcatcherExileEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Card card = game.getCard(getTargetPointer().getFirst(game, source)); + if (player != null && card != null) { + player.moveCardsToExile( + card, source, game, true, + CardUtil.getExileZoneId(game, source), + CardUtil.getSourceLogName(game, source) + ); + } + Permanent exiled = (Permanent) getValue("sacrificedPermanent"); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (exiled instanceof PermanentToken && permanent != null) { + permanent.addCounters(CounterType.P1P1.createInstance(), source, game); + } + return true; + } +} + +class ProwlingGeistcatcherReturnEffect extends OneShotEffect { + + ProwlingGeistcatcherReturnEffect() { + super(Outcome.Benefit); + staticText = "return each card exiled with it to the battlefield under your control"; + } + + private ProwlingGeistcatcherReturnEffect(final ProwlingGeistcatcherReturnEffect effect) { + super(effect); + } + + @Override + public ProwlingGeistcatcherReturnEffect copy() { + return new ProwlingGeistcatcherReturnEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + return player.moveCards(game.getExile().getExileZone( + CardUtil.getExileZoneId(game, source) + ), Zone.BATTLEFIELD, source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 911fc715352..089e071c93b 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -114,6 +114,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Orzhov Advokist", 91, Rarity.UNCOMMON, mage.cards.o.OrzhovAdvokist.class)); cards.add(new SetCardInfo("Overseer of the Damned", 127, Rarity.RARE, mage.cards.o.OverseerOfTheDamned.class)); cards.add(new SetCardInfo("Path of Ancestry", 178, Rarity.COMMON, mage.cards.p.PathOfAncestry.class)); + cards.add(new SetCardInfo("Prowling Geistcatcher", 21, Rarity.RARE, mage.cards.p.ProwlingGeistcatcher.class)); cards.add(new SetCardInfo("Ravenous Rotbelly", 22, Rarity.RARE, mage.cards.r.RavenousRotbelly.class)); cards.add(new SetCardInfo("Return to Dust", 92, Rarity.UNCOMMON, mage.cards.r.ReturnToDust.class)); cards.add(new SetCardInfo("Riders of Gavony", 93, Rarity.RARE, mage.cards.r.RidersOfGavony.class)); diff --git a/Mage/src/main/java/mage/abilities/common/SacrificePermanentTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/SacrificePermanentTriggeredAbility.java index 3a3c5dc2855..8747f3c093d 100644 --- a/Mage/src/main/java/mage/abilities/common/SacrificePermanentTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/SacrificePermanentTriggeredAbility.java @@ -7,6 +7,7 @@ import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; import mage.target.targetpointer.FixedTarget; /** @@ -50,14 +51,16 @@ public class SacrificePermanentTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (isControlledBy(event.getPlayerId()) - && filter.match(game.getPermanentOrLKIBattlefield(event.getTargetId()), getSourceId(), getControllerId(), game)) { - if (setTargetPointer) { - this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game)); - } - return true; + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); + if (!isControlledBy(event.getPlayerId()) || permanent == null + || !filter.match(permanent, getSourceId(), getControllerId(), game)) { + return false; } - return false; + this.getEffects().setValue("sacrificedPermanent", permanent); + if (setTargetPointer) { + this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game)); + } + return true; } @Override From c36f976741fbf0a43b76c15f68d03ab5802e57fd Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Mon, 27 Sep 2021 08:45:18 -0500 Subject: [PATCH 194/231] - Fixed #8326 --- Mage.Sets/src/mage/cards/v/ValkiGodOfLies.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/cards/v/ValkiGodOfLies.java b/Mage.Sets/src/mage/cards/v/ValkiGodOfLies.java index 8d2d2538ac2..dee95d173be 100644 --- a/Mage.Sets/src/mage/cards/v/ValkiGodOfLies.java +++ b/Mage.Sets/src/mage/cards/v/ValkiGodOfLies.java @@ -283,14 +283,16 @@ class ExileTopCardEachPlayersLibrary extends OneShotEffect { && Tibalt != null) { for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) { Player player = game.getPlayer(playerId); - if (player != null) { + if (player != null + && player.getLibrary().hasCards()) { Card topCard = player.getLibrary().getFromTop(game); cardsToExile.add(topCard); } } // exile all cards at one time - controller.moveCardsToExile(cardsToExile, source, game, true, exileId, Tibalt.getName()); - return true; + if (!cardsToExile.isEmpty()) { + return controller.moveCardsToExile(cardsToExile, source, game, true, exileId, Tibalt.getName()); + } } return false; } From bf172f7c8f9107d2bd5f5b777e77439ba1c25046 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Mon, 27 Sep 2021 09:48:01 -0400 Subject: [PATCH 195/231] Fixed #8321 (#8323) --- Mage.Sets/src/mage/cards/s/StrengthOfTheTajuru.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/s/StrengthOfTheTajuru.java b/Mage.Sets/src/mage/cards/s/StrengthOfTheTajuru.java index 4fcdabf85ee..a9053ff2492 100644 --- a/Mage.Sets/src/mage/cards/s/StrengthOfTheTajuru.java +++ b/Mage.Sets/src/mage/cards/s/StrengthOfTheTajuru.java @@ -68,7 +68,7 @@ class StrengthOfTheTajuruAddCountersTargetEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { int affectedTargets = 0; - int amount = source.getManaCostsToPay().getX() + 1; + int amount = source.getManaCostsToPay().getX(); Counter counter = CounterType.P1P1.createInstance(amount); for (UUID uuid : targetPointer.getTargets(game, source)) { Permanent permanent = game.getPermanent(uuid); From 0e1abaec5dcf2952b2235c1d73ccf931814306e1 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Mon, 27 Sep 2021 23:29:27 +0400 Subject: [PATCH 196/231] Dauthi Voidwalker - fixed that it doesn't work with cards (related to a3ca9fc03a7d4fda05095aa8508b949d43b16eca); --- .../src/mage/cards/d/DauthiVoidwalker.java | 11 ++++-- .../single/mh2/DauthiVoidwalkerTest.java | 38 ++++++++++++++++++- Mage/src/main/java/mage/util/CardUtil.java | 10 ++++- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/Mage.Sets/src/mage/cards/d/DauthiVoidwalker.java b/Mage.Sets/src/mage/cards/d/DauthiVoidwalker.java index 47f3e6acff7..9ab5d97c7af 100644 --- a/Mage.Sets/src/mage/cards/d/DauthiVoidwalker.java +++ b/Mage.Sets/src/mage/cards/d/DauthiVoidwalker.java @@ -19,7 +19,6 @@ import mage.filter.FilterCard; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; -import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInExile; import mage.target.targetpointer.FixedTarget; @@ -82,11 +81,15 @@ class DauthiVoidwalkerReplacementEffect extends ReplacementEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { Player controller = game.getPlayer(source.getControllerId()); - Permanent permanent = ((ZoneChangeEvent) event).getTarget(); - if (controller == null || permanent == null) { + Card card = ((ZoneChangeEvent) event).getTarget(); + if (card == null) { + card = game.getCard(event.getTargetId()); + } + + if (controller == null || card == null) { return false; } - CardUtil.moveCardWithCounter(game, source, controller, permanent, Zone.EXILED, CounterType.VOID.createInstance()); + CardUtil.moveCardWithCounter(game, source, controller, card, Zone.EXILED, CounterType.VOID.createInstance()); return true; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/DauthiVoidwalkerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/DauthiVoidwalkerTest.java index cb3aa068f82..f126aa30453 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/DauthiVoidwalkerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/DauthiVoidwalkerTest.java @@ -11,7 +11,7 @@ import org.mage.test.serverside.base.CardTestPlayerBase; public class DauthiVoidwalkerTest extends CardTestPlayerBase { @Test - public void test_Play() { + public void test_FromBattlefield() { // If a card would be put into an opponent's graveyard from anywhere, instead exile it with a void counter on it. // {T}, Sacrifice Dauthi Voidwalker: Choose an exiled card an opponent owns with a void counter on it. You may play it this turn without paying its mana cost. addCard(Zone.BATTLEFIELD, playerA, "Dauthi Voidwalker", 1); @@ -39,4 +39,40 @@ public class DauthiVoidwalkerTest extends CardTestPlayerBase { execute(); assertAllCommandsUsed(); } + + @Test + public void test_FromStack() { + // If a card would be put into an opponent's graveyard from anywhere, instead exile it with a void counter on it. + // {T}, Sacrifice Dauthi Voidwalker: Choose an exiled card an opponent owns with a void counter on it. You may play it this turn without paying its mana cost. + addCard(Zone.BATTLEFIELD, playerA, "Dauthi Voidwalker", 1); + // + addCard(Zone.HAND, playerB, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + // + // Counter target spell + addCard(Zone.HAND, playerA, "Cancel"); // {1}{U}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + + // B try to cast and get counter + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cancel", "Lightning Bolt", "Lightning Bolt"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + // countered bolt must be exiled and got void counter + checkExileCount("after exile", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", 1); + + // can play it for free + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}, Sacrifice"); + setChoice(playerA, "Lightning Bolt"); + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 3); + } } diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index a5a294dc306..402ecd44191 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -20,7 +20,6 @@ import mage.abilities.hint.HintUtils; import mage.cards.*; import mage.constants.*; import mage.counters.Counter; -import mage.counters.CounterType; import mage.filter.Filter; import mage.filter.predicate.mageobject.NamePredicate; import mage.game.CardState; @@ -1381,7 +1380,7 @@ public final class CardUtil { * @param game * @param source * @param controller - * @param card can be card or permanent + * @param card can be card or permanent * @param toZone * @param counter */ @@ -1390,6 +1389,13 @@ public final class CardUtil { throw new IllegalArgumentException("Wrong code usage - method doesn't support moving to battlefield zone"); } + // workaround: + // in ZONE_CHANGE replace events you must set new zone by event's setToZone, + // BUT for counter effect you need to complete zone change event first (so moveCards calls here) + // TODO: must be fixed someday by: + // * or by new event ZONE_CHANGED to apply counter effect on it + // * or by counter effects applier in ZONE_CHANGE event (see copy or token as example) + // move to zone if (!controller.moveCards(card, toZone, source, game)) { return false; From 532f158ab4c9f16fd95c6f10e3683812ac41bb05 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Mon, 27 Sep 2021 15:37:16 -0500 Subject: [PATCH 197/231] [AFR] Fixed Monk Class level 3 ability (fixes #8318) --- Mage.Sets/src/mage/cards/m/MonkClass.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/m/MonkClass.java b/Mage.Sets/src/mage/cards/m/MonkClass.java index b603c85c54e..48e52376304 100644 --- a/Mage.Sets/src/mage/cards/m/MonkClass.java +++ b/Mage.Sets/src/mage/cards/m/MonkClass.java @@ -114,7 +114,7 @@ class MonkClassEffect extends OneShotEffect { } player.moveCards(card, Zone.EXILED, source, game); game.addEffect(new GainAbilityTargetEffect( - new SimpleStaticAbility(new MonkClassCastEffect()), + new SimpleStaticAbility(Zone.EXILED, new MonkClassCastEffect()), Duration.Custom, null, true ).setTargetPointer(new FixedTarget(card, game)), source); return true; From eec3c44f1e13021338c584974bd2367f3e839473 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Mon, 27 Sep 2021 16:01:28 -0500 Subject: [PATCH 198/231] Removed game.preventDamage method (fixes #8280) --- Mage/src/main/java/mage/game/Game.java | 18 ++---------------- Mage/src/main/java/mage/game/GameImpl.java | 5 ----- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index e6e9ea934b1..9cbdbf01d50 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -316,31 +316,17 @@ public interface Game extends MageItem, Serializable, Copyable { boolean replaceEvent(GameEvent event, Ability targetAbility); /** - * Creates and fires an damage prevention event + * Creates and fires a damage prevention event * * @param damageEvent damage event that will be replaced (instanceof * check will be done) * @param source ability that's the source of the prevention effect * @param game * @param amountToPrevent max preventable amount - * @return true prevention was successfull / false prevention was replaced + * @return true prevention was successful / false prevention was replaced */ PreventionEffectData preventDamage(GameEvent damageEvent, Ability source, Game game, int amountToPrevent); - /** - * Creates and fires an damage prevention event - * - * @param event damage event that will be replaced (instanceof - * check will be done) - * @param source ability that's the source of the prevention - * effect - * @param game - * @param preventAllDamage true if there is no limit to the damage that can - * be prevented - * @return true prevention was successfull / false prevention was replaced - */ - PreventionEffectData preventDamage(GameEvent event, Ability source, Game game, boolean preventAllDamage); - void start(UUID choosingPlayerId); void resume(); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index c07465a3335..71cb83352f3 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -3067,11 +3067,6 @@ public abstract class GameImpl implements Game { return state.replaceEvent(event, targetAbility, this); } - @Override - public PreventionEffectData preventDamage(GameEvent event, Ability source, Game game, boolean preventAllDamage) { - return preventDamage(event, source, game, Integer.MAX_VALUE); - } - @Override public PreventionEffectData preventDamage(GameEvent event, Ability source, Game game, int amountToPrevent) { PreventionEffectData result = new PreventionEffectData(amountToPrevent); From f0ae098b2e55253d75c48414c07f4a4236a0f0ce Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Mon, 27 Sep 2021 16:33:43 -0500 Subject: [PATCH 199/231] [AFR] Fixed Monk Class level 3 interaction with MDFCs and lands --- Mage.Sets/src/mage/cards/m/MonkClass.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/cards/m/MonkClass.java b/Mage.Sets/src/mage/cards/m/MonkClass.java index 48e52376304..f756912b88f 100644 --- a/Mage.Sets/src/mage/cards/m/MonkClass.java +++ b/Mage.Sets/src/mage/cards/m/MonkClass.java @@ -23,6 +23,7 @@ import mage.game.Game; import mage.players.Player; import mage.target.common.TargetNonlandPermanent; import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; import mage.watchers.common.SpellsCastWatcher; import java.util.UUID; @@ -134,12 +135,13 @@ class MonkClassCastEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (!sourceId.equals(source.getSourceId()) || !source.isControlledBy(affectedControllerId)) { + UUID mainCardId = CardUtil.getMainCardId(game, sourceId); + if (!mainCardId.equals(source.getSourceId()) || !source.isControlledBy(affectedControllerId)) { return false; } - Card card = game.getCard(source.getSourceId()); + Card card = game.getCard(sourceId); SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - return card != null && watcher != null + return card != null && watcher != null && !card.isLand(game) && watcher.getSpellsCastThisTurn(affectedControllerId).size() > 0; } From 7ca17020ef8683027581bbf4a7c3ab338f61208f Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Mon, 27 Sep 2021 16:35:10 -0500 Subject: [PATCH 200/231] - Fixed #8317 --- .../mage/abilities/common/ExploitCreatureTriggeredAbility.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mage/src/main/java/mage/abilities/common/ExploitCreatureTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/ExploitCreatureTriggeredAbility.java index 1280f6d25f8..25494195784 100644 --- a/Mage/src/main/java/mage/abilities/common/ExploitCreatureTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/ExploitCreatureTriggeredAbility.java @@ -25,6 +25,8 @@ public class ExploitCreatureTriggeredAbility extends TriggeredAbilityImpl { public ExploitCreatureTriggeredAbility(Effect effect, boolean optional, SetTargetPointer setTargetPointer) { super(Zone.BATTLEFIELD, effect, optional); this.setTargetPointer = setTargetPointer; + // For example: if the creature with the Exploit ability is sacrificed, the trigger ability must use the controller of the LKI, not the owner + setLeavesTheBattlefieldTrigger(true); // https://github.com/magefree/mage/issues/8317 } public ExploitCreatureTriggeredAbility(final ExploitCreatureTriggeredAbility ability) { From 9d804174947cf5960242111026be546c17435751 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Mon, 27 Sep 2021 23:02:53 -0400 Subject: [PATCH 201/231] Fix Skeletal Scrying effect --- Mage.Sets/src/mage/cards/s/SkeletalScrying.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/s/SkeletalScrying.java b/Mage.Sets/src/mage/cards/s/SkeletalScrying.java index 3c935af9537..fe3181c71b5 100644 --- a/Mage.Sets/src/mage/cards/s/SkeletalScrying.java +++ b/Mage.Sets/src/mage/cards/s/SkeletalScrying.java @@ -6,7 +6,7 @@ import mage.abilities.costs.CostAdjuster; import mage.abilities.costs.common.ExileFromGraveCost; import mage.abilities.dynamicvalue.common.ManacostVariableValue; import mage.abilities.effects.common.DrawCardSourceControllerEffect; -import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; import mage.abilities.effects.common.InfoEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -41,7 +41,7 @@ public final class SkeletalScrying extends CardImpl { this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect( ManacostVariableValue.REGULAR ).setText("you draw X cards")); - this.getSpellAbility().addEffect(new GainLifeEffect( + this.getSpellAbility().addEffect(new LoseLifeSourceControllerEffect( ManacostVariableValue.REGULAR ).concatBy("and")); } From bc86c04dcd298c49dd653876ca5a54072e8b1db6 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Tue, 28 Sep 2021 12:23:39 -0400 Subject: [PATCH 202/231] Fix Flameshot and Ignite Disorder displayed rules text (#8333) --- Mage.Sets/src/mage/cards/f/Flameshot.java | 2 +- Mage.Sets/src/mage/cards/i/IgniteDisorder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/f/Flameshot.java b/Mage.Sets/src/mage/cards/f/Flameshot.java index 15fde82d4ad..d82386829b1 100644 --- a/Mage.Sets/src/mage/cards/f/Flameshot.java +++ b/Mage.Sets/src/mage/cards/f/Flameshot.java @@ -32,7 +32,7 @@ public final class Flameshot extends CardImpl { this.addAbility(new AlternativeCostSourceAbility(new DiscardTargetCost(new TargetCardInHand(filter)))); // Flameshot deals 3 damage divided as you choose among one, two, or three target creatures. - this.getSpellAbility().addEffect(new DamageMultiEffect(3)); + this.getSpellAbility().addEffect(new DamageMultiEffect(3).setText("{this} deals 3 damage divided as you choose among one, two, or three target creatures")); this.getSpellAbility().addTarget(new TargetCreaturePermanentAmount(3)); } diff --git a/Mage.Sets/src/mage/cards/i/IgniteDisorder.java b/Mage.Sets/src/mage/cards/i/IgniteDisorder.java index de752843f36..7a15ec028a3 100644 --- a/Mage.Sets/src/mage/cards/i/IgniteDisorder.java +++ b/Mage.Sets/src/mage/cards/i/IgniteDisorder.java @@ -31,7 +31,7 @@ public final class IgniteDisorder extends CardImpl { // Ignite Disorder deals 3 damage divided as you choose among one, two, or three target white and/or blue creatures. - this.getSpellAbility().addEffect(new DamageMultiEffect(3)); + this.getSpellAbility().addEffect(new DamageMultiEffect(3).setText("{this} deals 3 damage divided as you choose among one, two, or three target white and/or blue creatures")); this.getSpellAbility().addTarget(new TargetCreaturePermanentAmount(3, filter)); } From 03e605de09952195e85404d2bb92578f0daf88e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Sep 2021 20:25:54 +0400 Subject: [PATCH 203/231] Bump junit-jupiter from 5.7.0 to 5.8.1 (#8328) Bumps [junit-jupiter](https://github.com/junit-team/junit5) from 5.7.0 to 5.8.1. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.0...r5.8.1) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ffa1aff2100..45a31a66910 100644 --- a/pom.xml +++ b/pom.xml @@ -180,7 +180,7 @@ org.junit.jupiter junit-jupiter - 5.7.0 + 5.8.1 org.assertj From 6f76c3371ec580a5b1cb8511c354320dd323dd80 Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Tue, 28 Sep 2021 16:49:43 -0500 Subject: [PATCH 204/231] - Fixed #8334. Refactored and simplified some aspects of the Foretell ability. --- .../src/mage/cards/e/EtherealValkyrie.java | 1 + .../abilities/keyword/ForetellAbility.java | 3 ++ .../main/java/mage/game/events/GameEvent.java | 1 + .../mage/watchers/common/ForetoldWatcher.java | 32 ++++++------------- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java b/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java index 9a9f83918b7..1da51c68698 100644 --- a/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java +++ b/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java @@ -177,6 +177,7 @@ class EtherealValkyrieEffect extends OneShotEffect { foretellAbility.activate(game, true); ContinuousEffect effect = foretellAbility.new ForetellAddCostEffect(new MageObjectReference(exileCard, game)); game.addEffect(effect, source); + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.FORETOLD, exileCard.getId(), null, null)); return true; } } diff --git a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java index 60929049109..6bcf3bf45e4 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java @@ -119,7 +119,10 @@ public class ForetellAbility extends SpecialAction { Player controller = game.getPlayer(source.getControllerId()); if (controller != null && card != null) { + + // get main card id UUID mainCardId = card.getMainCard().getId(); + // retrieve the exileId of the foretold card UUID exileId = CardUtil.getExileZoneId(mainCardId.toString() + "foretellAbility", game); diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 704d001fa14..bad2b187433 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -464,6 +464,7 @@ public class GameEvent implements Serializable { VENTURE, VENTURED, DUNGEON_COMPLETED, REMOVED_FROM_COMBAT, // targetId id of permanent removed from combat + FORETOLD, // targetId id of card foretold //custom events CUSTOM_EVENT } diff --git a/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java b/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java index 68aad21d073..f62a879d051 100644 --- a/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java @@ -6,11 +6,8 @@ import java.util.UUID; import mage.abilities.keyword.ForetellAbility; import mage.cards.Card; import mage.constants.WatcherScope; -import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.stack.Spell; -import mage.util.CardUtil; import mage.watchers.Watcher; /** @@ -21,7 +18,7 @@ public class ForetoldWatcher extends Watcher { // If foretell was activated or a card was Foretold by the controller this turn, this list stores it. Cleared at the end of the turn. private final Set foretellCardsThisTurn = new HashSet<>(); - private final Set foretoldCardsThisTurn = new HashSet<>(); + private final Set foretoldCards = new HashSet<>(); public ForetoldWatcher() { super(WatcherScope.GAME); @@ -35,41 +32,30 @@ public class ForetoldWatcher extends Watcher { && card.getAbilities(game).containsClass(ForetellAbility.class) && controllerId == event.getPlayerId()) { foretellCardsThisTurn.add(card.getId()); + foretoldCards.add(card.getId()); } } - if (event.getType() == GameEvent.EventType.SPELL_CAST - && event.getZone() == Zone.EXILED) { - Spell spell = (Spell) game.getObject(event.getTargetId()); - if (spell != null - && controllerId == event.getPlayerId()) { - UUID exileId = CardUtil.getExileZoneId(spell.getSourceId().toString() + "foretellAbility", game); - if (exileId != null) { - foretoldCardsThisTurn.add(spell.getSourceId()); - } + // Ethereal Valkyrie + if (event.getType() == GameEvent.EventType.FORETOLD) { + Card card = game.getCard(event.getTargetId()); + if (card != null) { + // Ethereal Valkyrie does not Foretell the card, it becomes Foretold, so don't add it to the Foretell list + foretoldCards.add(card.getId()); } } } - public boolean cardUsedForetell(UUID sourceId) { - return foretellCardsThisTurn.contains(sourceId); - } - public boolean cardWasForetold(UUID sourceId) { - return foretoldCardsThisTurn.contains(sourceId); + return foretoldCards.contains(sourceId); } public int countNumberForetellThisTurn() { return foretellCardsThisTurn.size(); } - public int countNumberForetoldThisTurn() { - return foretoldCardsThisTurn.size(); - } - @Override public void reset() { super.reset(); foretellCardsThisTurn.clear(); - foretoldCardsThisTurn.clear(); } } From f9beed6a8916324ef944a09ea6f6f25049d0f2a4 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 29 Sep 2021 16:01:36 +0400 Subject: [PATCH 205/231] Dev: clear pom files, fixed wrong test packages and scope, added zip tests; --- Mage.Client/pom.xml | 21 +---- Mage.Server.Plugins/Mage.Player.AI/pom.xml | 1 - .../Mage.Player.AIMCTS/pom.xml | 1 - .../Mage.Player.AIMinimax/pom.xml | 1 - Mage.Server/pom.xml | 6 -- Mage.Sets/pom.xml | 1 - Mage.Tests/pom.xml | 15 ++-- .../test/decks/exporter}/DeckFormatsTest.java | 3 +- .../exporter/MtgArenaDeckExporterTest.java | 4 +- .../test/utils/ZipFilesReadWriteTest.java | 76 ++++++++++++++++++ Mage.Tests/src/test/resources/images.zip | Bin 0 -> 1636 bytes Mage.Verify/pom.xml | 1 - Mage/pom.xml | 1 - .../ConditionalContinuousEffect.java | 7 +- pom.xml | 18 +++++ 15 files changed, 115 insertions(+), 41 deletions(-) rename {Mage/src/main/java/mage/cards/decks => Mage.Tests/src/test/java/org/mage/test/decks/exporter}/DeckFormatsTest.java (96%) rename {Mage/src/main/java/mage/cards => Mage.Tests/src/test/java/org/mage/test}/decks/exporter/MtgArenaDeckExporterTest.java (92%) create mode 100644 Mage.Tests/src/test/java/org/mage/test/utils/ZipFilesReadWriteTest.java create mode 100644 Mage.Tests/src/test/resources/images.zip diff --git a/Mage.Client/pom.xml b/Mage.Client/pom.xml index 0ff115a1879..57b9da979ed 100644 --- a/Mage.Client/pom.xml +++ b/Mage.Client/pom.xml @@ -43,6 +43,10 @@ org.slf4j slf4j-log4j12 + + net.java.truevfs + truevfs-profile-base + @@ -88,7 +92,6 @@ junit junit - jar test @@ -112,22 +115,6 @@ jsoup 1.14.2 - - truevfs-profile-base - net.java.truevfs - jar - 0.11.1 - - - truevfs-access-swing - net.java.truevfs - - - truecommons-key-swing - net.java.truecommons - - - diff --git a/Mage.Server.Plugins/Mage.Player.AI/pom.xml b/Mage.Server.Plugins/Mage.Player.AI/pom.xml index b6b558e6e81..48086d621f8 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AI/pom.xml @@ -18,7 +18,6 @@ log4j log4j - jar ${project.groupId} diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml index 8435f3ea3ed..ece840b665f 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml @@ -18,7 +18,6 @@ log4j log4j - jar ${project.groupId} diff --git a/Mage.Server.Plugins/Mage.Player.AIMinimax/pom.xml b/Mage.Server.Plugins/Mage.Player.AIMinimax/pom.xml index 3b773ba2ea1..5e43462cba3 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMinimax/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AIMinimax/pom.xml @@ -18,7 +18,6 @@ log4j log4j - jar ${project.groupId} diff --git a/Mage.Server/pom.xml b/Mage.Server/pom.xml index 79e1f17a73c..7af167954cf 100644 --- a/Mage.Server/pom.xml +++ b/Mage.Server/pom.xml @@ -233,37 +233,31 @@ org.apache.shiro shiro-core 1.8.0 - jar com.google.api-client google-api-client 1.31.1 - jar com.google.apis google-api-services-gmail v1-rev20210614-1.32.1 - jar com.google.oauth-client google-oauth-client-java6 1.31.0 - jar com.google.oauth-client google-oauth-client-jetty 1.31.2 - jar javax.mail mail 1.5.0-b01 - jar com.sun.jersey diff --git a/Mage.Sets/pom.xml b/Mage.Sets/pom.xml index 4dac5f304e8..ad724706d41 100644 --- a/Mage.Sets/pom.xml +++ b/Mage.Sets/pom.xml @@ -24,7 +24,6 @@ log4j log4j - jar diff --git a/Mage.Tests/pom.xml b/Mage.Tests/pom.xml index 5e39abef7d1..1f54378e9dd 100644 --- a/Mage.Tests/pom.xml +++ b/Mage.Tests/pom.xml @@ -34,11 +34,6 @@ mage-server ${mage-version} - - junit - junit - test - ${project.groupId} mage-game-twoplayerduel @@ -52,10 +47,14 @@ compile + + junit + junit + test + log4j log4j - jar ${project.groupId} @@ -73,6 +72,10 @@ jaxb-runtime 3.0.2 + + net.java.truevfs + truevfs-profile-base + diff --git a/Mage/src/main/java/mage/cards/decks/DeckFormatsTest.java b/Mage.Tests/src/test/java/org/mage/test/decks/exporter/DeckFormatsTest.java similarity index 96% rename from Mage/src/main/java/mage/cards/decks/DeckFormatsTest.java rename to Mage.Tests/src/test/java/org/mage/test/decks/exporter/DeckFormatsTest.java index 6dfe1d612f7..47c85e69c9c 100644 --- a/Mage/src/main/java/mage/cards/decks/DeckFormatsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/decks/exporter/DeckFormatsTest.java @@ -1,5 +1,6 @@ -package mage.cards.decks; +package org.mage.test.decks.exporter; +import mage.cards.decks.DeckFormats; import org.junit.Assert; import org.junit.Test; diff --git a/Mage/src/main/java/mage/cards/decks/exporter/MtgArenaDeckExporterTest.java b/Mage.Tests/src/test/java/org/mage/test/decks/exporter/MtgArenaDeckExporterTest.java similarity index 92% rename from Mage/src/main/java/mage/cards/decks/exporter/MtgArenaDeckExporterTest.java rename to Mage.Tests/src/test/java/org/mage/test/decks/exporter/MtgArenaDeckExporterTest.java index af7666a45d9..9a40ab631cf 100644 --- a/Mage/src/main/java/mage/cards/decks/exporter/MtgArenaDeckExporterTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/decks/exporter/MtgArenaDeckExporterTest.java @@ -1,7 +1,9 @@ -package mage.cards.decks.exporter; +package org.mage.test.decks.exporter; import mage.cards.decks.DeckCardInfo; import mage.cards.decks.DeckCardLists; +import mage.cards.decks.exporter.DeckExporter; +import mage.cards.decks.exporter.MtgArenaDeckExporter; import org.junit.Test; import java.io.ByteArrayOutputStream; diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/ZipFilesReadWriteTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/ZipFilesReadWriteTest.java new file mode 100644 index 00000000000..a88497f3607 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/utils/ZipFilesReadWriteTest.java @@ -0,0 +1,76 @@ +package org.mage.test.utils; + +import net.java.truevfs.access.TFile; +import net.java.truevfs.access.TFileReader; +import net.java.truevfs.access.TFileWriter; +import net.java.truevfs.access.TVFS; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Paths; +import java.util.Arrays; + +/** + * @author JayDi85 + */ +public class ZipFilesReadWriteTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void test_Read() { + // exists + TFile fileZip = new TFile(Paths.get("src", "test", "resources", "images.zip").toString()); + Assert.assertTrue(fileZip.exists()); + TFile fileZipDir = new TFile(Paths.get("src", "test", "resources", "images.zip", "SET").toString()); + Assert.assertTrue(fileZipDir.exists()); + TFile fileZipFile = new TFile(Paths.get("src", "test", "resources", "images.zip", "SET", "image1.png").toString()); + Assert.assertTrue(fileZipFile.exists()); + + // not exists + TFile fileNotZip = new TFile(Paths.get("src", "test", "resources", "images-FAIL.zip").toString()); + Assert.assertFalse(fileNotZip.exists()); + TFile fileNotZipDir = new TFile(Paths.get("src", "test", "resources", "images.zip", "SET-FAIL").toString()); + Assert.assertFalse(fileNotZipDir.exists()); + TFile fileNotZipFile = new TFile(Paths.get("src", "test", "resources", "images.zip", "SET", "image1-FAIL.png").toString()); + Assert.assertFalse(fileNotZipFile.exists()); + + // reading + Assert.assertEquals(3, fileZipDir.list().length); + Assert.assertTrue(Arrays.asList(fileZipDir.list()).contains("image1.png")); + Assert.assertTrue(Arrays.asList(fileZipDir.list()).contains("image2.png")); + Assert.assertTrue(Arrays.asList(fileZipDir.list()).contains("image3.png")); + } + + @Test + public void test_write() { + try { + String zipPath = tempFolder.newFolder().getAbsolutePath(); + TFile fileWriteZip = new TFile(Paths.get(zipPath, "temp-images.zip", "DIR", "test.txt").toString()); + Assert.assertFalse(fileWriteZip.exists()); + + Writer writer = new TFileWriter(fileWriteZip); + try { + writer.write("test text"); + writer.close(); + Assert.assertTrue(fileWriteZip.exists()); + + TFileReader reader = new TFileReader(fileWriteZip); + BufferedReader br = new BufferedReader(reader); + Assert.assertEquals(br.readLine(), "test text"); + reader.close(); + } finally { + TVFS.umount(); + } + } catch (IOException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + } +} diff --git a/Mage.Tests/src/test/resources/images.zip b/Mage.Tests/src/test/resources/images.zip new file mode 100644 index 0000000000000000000000000000000000000000..4453745230d28aad2abcd4fb9fed6bffd0a2b4d2 GIT binary patch literal 1636 zcmWIWW@h1H00Et4Ho;&9lwbkU!LA|t0YDWZKsEE3SN(hnU8dl?xRb^tLSR7GZP zVtT5fUO`?u;|@mmf1Lq-?z~)5AX9lgJzRo-G!F=KumQ=i(wOH!inG8YvKVN>5fEma zs?8_Oz`&U0>EaloaXxjDZI^VQK+9`e-iSs6=1Z()2f3LPTb{G*IvVG|_H_0uwtdaP zs{2|+Eha=x;XIu8CWBQde_7oBJ-#+h$9U&nbq_ZcEiiulwaR6ar{>+c=hL_3{`M-` zfBUW8-epV;XVZkw=1dIwwQAL80VmmHmWIh4GHIJ{PVTUg?|1KKa435CE}Frc;jzFW zh64--Bo17ar8 zB({9()Z(^|1zwtanJiibj=keEZtjRVYOA269$LH7<6Y=ihJ;A3mw*3>$GPyOZR2M! znCoZ1`}eiG_FonAH&6JJo7VsCUp50nY3%>NtMV%H9(5`|8&ed2PAmSGnHKM*d7i;* z$J%2lMv^ll7q9wWZ~r+dt-5@F*kjqOV?+UZxVff?cCd*e@Fkx{fZ5DJ|Dd0&AI;4%_VEa(wDpVeBThUZr|tcJX^2p zYVjSr_UPzpULUjYPu2BevqNr`A1aD>|F>6tm*Zv~U0d-zgFpUe|}Dsaq3@o<*m z?IkNGe($^g)u^;W_0;9UbBj$M-h4aFc4b`I|Lt>lEjBckpLAL_?XKI^=|xA@O0Bay z!=iO_qt(-O=WntLZpuBl{?w;EzKdomU4J&Wi&!U5wot*9WhC{y>=9L9q z%qeEpzVhzdFaD&}R}|-{%;LKKK|ZHDXlpKy2jhtlfv}aE|F4h8{2RaR!jt_j%k(xS z3G^jS&^GL5oXJ+i#L;|1;pn8BzZ@=`8k#mV@uh4nnI|aGd?R79q3HkiY8jCdh4YQJ z9k0?-_Q=b5Da^gF;PBEPmToZ)rf-j@8MP(pNF^_ja8_h^z`b@&dXAm*#(D1*(1(dg!* orbT4)Q<#y=2d7MI=0`k)m=B`^yjekoJOc|5mI2fE4Hgg&02y^-6aWAK literal 0 HcmV?d00001 diff --git a/Mage.Verify/pom.xml b/Mage.Verify/pom.xml index df584fd29b9..5f2e558c01d 100644 --- a/Mage.Verify/pom.xml +++ b/Mage.Verify/pom.xml @@ -48,7 +48,6 @@ log4j log4j - jar diff --git a/Mage/pom.xml b/Mage/pom.xml index 261fa2208f6..714e3446529 100644 --- a/Mage/pom.xml +++ b/Mage/pom.xml @@ -22,7 +22,6 @@ log4j log4j - jar diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalContinuousEffect.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalContinuousEffect.java index 357d41588e1..d4904a82f60 100644 --- a/Mage/src/main/java/mage/abilities/decorator/ConditionalContinuousEffect.java +++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalContinuousEffect.java @@ -9,7 +9,6 @@ import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffectImpl; import mage.constants.*; import mage.game.Game; -import org.junit.Assert; import java.util.*; @@ -48,13 +47,13 @@ public class ConditionalContinuousEffect extends ContinuousEffectImpl { // checks for compatibility EffectType needType = EffectType.CONTINUOUS; if (effect.getEffectType() != needType) { - Assert.fail("ConditionalContinuousEffect supports only " + needType.toString() + " but found " + effect.getEffectType().toString()); + throw new IllegalArgumentException("ConditionalContinuousEffect supports only " + needType + " but found " + effect.getEffectType().toString()); } if (otherwiseEffect != null && otherwiseEffect.getEffectType() != needType) { - Assert.fail("ConditionalContinuousEffect supports only " + needType.toString() + " but found " + effect.getEffectType().toString()); + throw new IllegalArgumentException("ConditionalContinuousEffect supports only " + needType.toString() + " but found " + effect.getEffectType().toString()); } if (otherwiseEffect != null && effect.getEffectType() != otherwiseEffect.getEffectType()) { - Assert.fail("ConditionalContinuousEffect must be same but found " + effect.getEffectType().toString() + " and " + otherwiseEffect.getEffectType().toString()); + throw new IllegalArgumentException("ConditionalContinuousEffect must be same but found " + effect.getEffectType().toString() + " and " + otherwiseEffect.getEffectType().toString()); } } diff --git a/pom.xml b/pom.xml index 45a31a66910..f498f2ca3b8 100644 --- a/pom.xml +++ b/pom.xml @@ -156,11 +156,13 @@ + junit junit 4.13.1 + test log4j @@ -192,6 +194,22 @@ commons-lang3 3.11 + + + net.java.truevfs + truevfs-profile-base + 0.11.1 + + + net.java.truevfs + truevfs-access-swing + + + net.java.truecommons + truecommons-key-swing + + + From edbb99f0ba475e1256647cf32d9d8ee9e8b4b135 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Wed, 29 Sep 2021 09:03:46 -0400 Subject: [PATCH 206/231] Implement booster collation for (original) Innistrad and Dark Ascension (#8339) --- Mage.Sets/src/mage/sets/DarkAscension.java | 58 ++++++++++++ Mage.Sets/src/mage/sets/Innistrad.java | 93 +++++++++++++++++++ .../main/java/mage/cards/ExpansionSet.java | 21 ++++- 3 files changed, 167 insertions(+), 5 deletions(-) diff --git a/Mage.Sets/src/mage/sets/DarkAscension.java b/Mage.Sets/src/mage/sets/DarkAscension.java index f05aa666531..930e9c466c5 100644 --- a/Mage.Sets/src/mage/sets/DarkAscension.java +++ b/Mage.Sets/src/mage/sets/DarkAscension.java @@ -2,9 +2,16 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + /** * * @author North @@ -201,4 +208,55 @@ public final class DarkAscension extends ExpansionSet { cards.add(new SetCardInfo("Young Wolf", 134, Rarity.COMMON, mage.cards.y.YoungWolf.class)); cards.add(new SetCardInfo("Zombie Apocalypse", 80, Rarity.RARE, mage.cards.z.ZombieApocalypse.class)); } + + @Override + public BoosterCollator createCollator() { + return new DarkAscensionCollator(); + } +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/dka.html +// Using USA collation for common/uncommon, rare collation inferred from other sets +class DarkAscensionCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "66", "47", "100", "110", "86", "51", "21", "75", "148", "2", "52", "119", "102", "54", "49", "4", "87", "109", "148", "91", "40", "76", "110", "22", "106", "65", "43", "132", "17", "157", "76", "87", "40", "21", "49", "118", "75", "86", "54", "3", "119", "51", "47", "113", "2", "91", "59", "102", "27", "118", "43", "17", "109", "4", "59", "100", "66", "52", "132", "22", "157", "113", "106", "27", "65", "3"); + private final CardRun commonB = new CardRun(true, "103", "126", "29", "8", "73", "88", "60", "111", "19", "15", "29", "107", "35", "126", "150", "90", "73", "129", "6", "15", "68", "46", "155", "105", "72", "14", "129", "77", "38", "6", "121", "88", "134", "31", "111", "72", "103", "35", "19", "121", "90", "14", "31", "68", "105", "8", "60", "150", "134", "155", "38", "107", "46", "77"); + private final CardRun uncommonA = new CardRun(true, "5", "128", "101", "153", "45", "145", "12", "67", "78", "117", "97", "44", "153", "5", "136", "57", "128", "135", "101", "154", "53", "7", "12", "57", "116", "135", "97", "44", "58", "92", "12", "136", "145", "117", "5", "78", "116", "92", "53", "58", "145", "10", "97", "7", "116", "45", "67", "154", "58", "10", "92", "44", "117", "153", "7", "78", "128", "45", "101", "67", "136", "53", "10", "135", "154", "57"); + private final CardRun uncommonB = new CardRun(true, "83", "48", "16", "144", "127", "104", "42", "130", "48", "16", "84", "79", "141", "32", "130", "26", "127", "74", "83", "9", "42", "61", "144", "143", "108", "84", "26", "141", "127", "9", "48", "104", "74", "143", "79", "144", "108", "26", "32", "83", "104", "61", "42", "143", "16", "141", "130", "84", "32", "74", "108", "9", "79", "61"); + private final CardRun rare = new CardRun(false, "11", "18", "20", "23", "24", "25", "30", "33", "34", "36", "37", "39", "41", "56", "62", "63", "64", "69", "80", "82", "85", "89", "93", "95", "96", "112", "114", "115", "120", "123", "124", "149", "152", "156", "158", "11", "18", "20", "23", "24", "25", "30", "33", "34", "36", "37", "39", "41", "56", "62", "63", "64", "69", "80", "82", "85", "89", "93", "95", "96", "112", "114", "115", "120", "123", "124", "149", "152", "156", "158", "1", "28", "70", "99", "131", "137", "138", "139", "142", "151"); + private final CardRun doubleFaced = new CardRun(false, "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "13", "55", "94", "125", "50", "81", "122", "146", "50", "81", "122", "146", "50", "81", "122", "146", "50", "81", "122", "146", "50", "81", "122", "146", "50", "81", "122", "146", "50", "81", "122", "146", "50", "81", "122", "146", "50", "81", "122", "146", "71", "98", "133", "71", "98", "133", "71", "98", "133", "140", "147", "140", "147"); + private final CardRun land = new CardRun(false, "ISD_250", "ISD_251", "ISD_252", "ISD_253", "ISD_254", "ISD_255", "ISD_256", "ISD_257", "ISD_258", "ISD_259", "ISD_260", "ISD_261", "ISD_262", "ISD_263", "ISD_264"); + + private final BoosterStructure AAAAABBBB = new BoosterStructure( + commonA, commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rare); + private final BoosterStructure D1 = new BoosterStructure(doubleFaced); + private final BoosterStructure L1 = new BoosterStructure(land); + + private final RarityConfiguration commonRuns = new RarityConfiguration(AAAAABBBB); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 1.65 A uncommons (33 / 20) + // 1.35 B uncommons (27 / 20) + // These numbers are the same for all sets with 40 uncommons in asymmetrical A/B print runs + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, + ABB, ABB, ABB, ABB, ABB, ABB, ABB + ); + private final RarityConfiguration rareRuns = new RarityConfiguration(R1); + private final RarityConfiguration dfcRuns = new RarityConfiguration(D1); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(rareRuns.getNext().makeRun()); + booster.addAll(dfcRuns.getNext().makeRun()); + booster.addAll(landRuns.getNext().makeRun()); + return booster; + } } diff --git a/Mage.Sets/src/mage/sets/Innistrad.java b/Mage.Sets/src/mage/sets/Innistrad.java index 1d6be5fd480..6f062c6cc04 100644 --- a/Mage.Sets/src/mage/sets/Innistrad.java +++ b/Mage.Sets/src/mage/sets/Innistrad.java @@ -1,9 +1,16 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + /** * @author BetaSteward_at_googlemail.com */ @@ -312,4 +319,90 @@ public final class Innistrad extends ExpansionSet { cards.add(new SetCardInfo("Wreath of Geists", 211, Rarity.UNCOMMON, mage.cards.w.WreathOfGeists.class)); } + @Override + public BoosterCollator createCollator() { + return new InnistradCollator(); + } +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/isd.html +// Using USA collation for common/uncommon, rare collation inferred from other sets +class InnistradCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "103", "48", "57", "128", "11", "30", "96", "202", "160", "86", "55", "12", "151", "237", "179", "59", "42", "101", "96", "39", "128", "210", "55", "142", "30", "95", "124", "202", "59", "4", "160", "50", "190", "142", "86", "103", "184", "4", "131", "31", "50", "219", "210", "95", "135", "237", "101", "11", "63", "12", "184", "131", "57", "219", "116", "42", "124", "48", "190", "135", "63", "151", "31", "39", "116", "179"); + private final CardRun commonB = new CardRun(true, "127", "37", "198", "143", "54", "69", "224", "108", "93", "34", "154", "169", "54", "144", "111", "24", "65", "92", "154", "234", "175", "169", "34", "69", "111", "143", "65", "14", "154", "175", "24", "108", "198", "224", "65", "34", "144", "69", "170", "92", "108", "37", "143", "14", "175", "56", "198", "127", "234", "93", "144", "37", "54", "170", "111", "56", "14", "93", "127", "169", "224", "92", "24", "56", "234", "170"); + private final CardRun commonC1 = new CardRun(true, "204", "73", "132", "106", "80", "146", "197", "29", "40", "123", "73", "166", "195", "230", "107", "18", "216", "52", "132", "183", "123", "138", "40", "106", "204", "230", "43", "5", "167", "197", "120", "146", "196", "33", "43", "216", "81", "126", "167", "52", "183", "18", "29", "120", "166", "107", "81", "196", "80", "138", "5", "246", "195", "126", "33"); + private final CardRun commonC2 = new CardRun(true, "41", "173", "74", "206", "102", "7", "75", "153", "79", "113", "173", "148", "201", "118", "75", "83", "156", "41", "200", "91", "113", "102", "79", "7", "156", "173", "1", "91", "74", "246", "118", "153", "7", "206", "83", "201", "1", "74", "148", "118", "156", "200", "113", "206", "41", "79", "91", "153", "201", "75", "102", "1", "200", "148", "83"); + private final CardRun uncommonA = new CardRun(true, "125", "76", "133", "20", "22", "221", "98", "171", "66", "139", "100", "49", "32", "235", "133", "191", "125", "58", "229", "35", "137", "72", "98", "203", "221", "20", "76", "217", "150", "122", "72", "187", "88", "15", "240", "229", "161", "53", "235", "97", "100", "15", "232", "171", "139", "53", "217", "97", "240", "35", "150", "191", "88", "58", "227", "22", "66", "187", "137", "122", "161", "32", "232", "227", "49", "203"); + private final CardRun uncommonB = new CardRun(true, "62", "223", "157", "85", "28", "218", "16", "192", "60", "225", "157", "180", "28", "45", "110", "222", "172", "163", "27", "60", "162", "188", "223", "110", "70", "19", "163", "218", "192", "85", "172", "109", "45", "19", "158", "225", "233", "119", "188", "26", "62", "27", "158", "222", "211", "119", "70", "233", "162", "26", "16", "109", "211", "180"); + private final CardRun rare = new CardRun(false, "2", "6", "9", "10", "13", "17", "21", "25", "36", "44", "46", "61", "67", "71", "78", "82", "84", "89", "94", "99", "104", "115", "117", "121", "130", "134", "136", "140", "141", "147", "164", "174", "177", "186", "189", "194", "199", "205", "212", "220", "228", "231", "236", "238", "239", "241", "242", "243", "244", "245", "247", "248", "249", "2", "6", "9", "10", "13", "17", "21", "25", "36", "44", "46", "61", "67", "71", "78", "82", "84", "89", "94", "99", "104", "115", "117", "121", "130", "134", "136", "140", "141", "147", "164", "174", "177", "186", "189", "194", "199", "205", "212", "220", "228", "231", "236", "238", "239", "241", "242", "243", "244", "245", "247", "248", "249", "3", "23", "68", "77", "87", "105", "112", "129", "155", "178", "207", "213", "214", "215", "226"); + private final CardRun doubleFaced = new CardRun(false, "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "38", "51", "165", "168", "185", "209", "8", "47", "114", "145", "159", "182", "208", "8", "47", "114", "145", "159", "182", "208", "8", "47", "114", "145", "159", "182", "208", "8", "47", "114", "145", "159", "182", "208", "8", "47", "114", "145", "159", "182", "208", "8", "47", "114", "145", "159", "182", "208", "64", "90", "149", "152", "176", "193", "64", "90", "149", "152", "176", "193", "181"); + private final CardRun land = new CardRun(false, "250", "251", "252", "253", "254", "255", "256", "257", "258", "259", "260", "261", "262", "263", "264"); + + private final BoosterStructure AABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2 + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rare); + private final BoosterStructure D1 = new BoosterStructure(doubleFaced); + private final BoosterStructure L1 = new BoosterStructure(land); + + // In order for equal numbers of each common to exist, the average booster must contain: + // 2.95 A commons (162 / 55) + // 1.96 B commons (108 / 55) + // 2.46 C1 commons (135 / 55) + // 1.64 C2 commons (90 / 55) + private final RarityConfiguration commonRuns = new RarityConfiguration( + AABBC1C1C1C1C1, AABBC1C1C1C1C1, AABBC1C1C1C1C1, AABBC1C1C1C1C1, AABBC1C1C1C1C1, + AABBC1C1C1C1C1, AABBC1C1C1C1C1, AABBC1C1C1C1C1, AABBC1C1C1C1C1, AABBC1C1C1C1C1, + AABBC1C1C1C1C1, AABBC1C1C1C1C1, AABBC1C1C1C1C1, AABBC1C1C1C1C1, + AAABC1C1C1C1C1, AAABC1C1C1C1C1, AAABC1C1C1C1C1, AAABC1C1C1C1C1, AAABC1C1C1C1C1, + AAABC1C1C1C1C1, AAABC1C1C1C1C1, AAABC1C1C1C1C1, AAABC1C1C1C1C1, AAABC1C1C1C1C1, + AAABC1C1C1C1C1, AAABC1C1C1C1C1, AAABC1C1C1C1C1, + AAABBC2C2C2C2, AAABBC2C2C2C2, AAABBC2C2C2C2, AAABBC2C2C2C2, AAABBC2C2C2C2, + AAABBC2C2C2C2, AAABBC2C2C2C2, AAABBC2C2C2C2, AAABBC2C2C2C2, AAABBC2C2C2C2, + AAABBC2C2C2C2, AAABBC2C2C2C2, AAABBC2C2C2C2, AAABBC2C2C2C2, AAABBC2C2C2C2, + AAABBC2C2C2C2, AAABBC2C2C2C2, + AAAABBBC2C2, AAAABBBC2C2, AAAABBBC2C2, AAAABBBC2C2, AAAABBBC2C2, AAAABBBC2C2, + AAAABBBC2C2, AAAABBBC2C2, AAAABBBC2C2, AAAABBBC2C2, AAAABBBC2C2 + ); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 1.65 A uncommons (33 / 20) + // 1.35 B uncommons (27 / 20) + // These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, + ABB, ABB, ABB, ABB, ABB, ABB, ABB + ); + private final RarityConfiguration rareRuns = new RarityConfiguration(R1); + private final RarityConfiguration dfcRuns = new RarityConfiguration(D1); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(rareRuns.getNext().makeRun()); + booster.addAll(dfcRuns.getNext().makeRun()); + booster.addAll(landRuns.getNext().makeRun()); + return booster; + } } diff --git a/Mage/src/main/java/mage/cards/ExpansionSet.java b/Mage/src/main/java/mage/cards/ExpansionSet.java index 68276cc5295..4afae0f6080 100644 --- a/Mage/src/main/java/mage/cards/ExpansionSet.java +++ b/Mage/src/main/java/mage/cards/ExpansionSet.java @@ -287,8 +287,10 @@ public abstract class ExpansionSet implements Serializable { } private List createBoosterUsingCollator(BoosterCollator collator) { - if (inBoosterMap.isEmpty()) { - generateBoosterMap(); + synchronized (inBoosterMap) { + if (inBoosterMap.isEmpty()) { + generateBoosterMap(); + } } return collator .makeBooster() @@ -304,6 +306,15 @@ public abstract class ExpansionSet implements Serializable { .findCards(new CardCriteria().setCodes(code)) .stream() .forEach(cardInfo -> inBoosterMap.put(cardInfo.getCardNumber(), cardInfo)); + // get basic lands from parent set if this set doesn't have them + if (!hasBasicLands && parentSet != null) { + String parentCode = parentSet.code; + CardRepository + .instance + .findCards(new CardCriteria().setCodes(parentCode).rarities(Rarity.LAND)) + .stream() + .forEach(cardInfo -> inBoosterMap.put(parentCode + "_" + cardInfo.getCardNumber(), cardInfo)); + } } protected boolean boosterIsValid(List booster) { @@ -621,9 +632,9 @@ public abstract class ExpansionSet implements Serializable { List savedCardsInfos = savedCards.get(rarity); if (savedCardsInfos == null) { CardCriteria criteria = new CardCriteria(); - if (rarity == Rarity.LAND) { - // get basic lands from parent set if current haven't it - criteria.setCodes(!hasBasicLands && parentSet != null ? parentSet.code : this.code); + if (rarity == Rarity.LAND && !hasBasicLands && parentSet != null) { + // get basic lands from parent set if this set doesn't have them + criteria.setCodes(parentSet.code); } else { criteria.setCodes(this.code); } From 5044a8c603f098630192e19a2f85333a27401040 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Wed, 29 Sep 2021 09:04:53 -0400 Subject: [PATCH 207/231] Implement booster collation for Shards of Alara, Zendikar and Scars of Mirrodin (#8316) --- Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java | 10 +- Mage.Sets/src/mage/sets/ScarsOfMirrodin.java | 128 +++++++++++++++++- Mage.Sets/src/mage/sets/ShardsOfAlara.java | 118 +++++++++++++++- Mage.Sets/src/mage/sets/Zendikar.java | 127 +++++++++++++++++ 4 files changed, 378 insertions(+), 5 deletions(-) diff --git a/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java b/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java index ff4fa3967ef..294115614ce 100644 --- a/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java +++ b/Mage.Sets/src/mage/sets/RiseOfTheEldrazi.java @@ -1,4 +1,3 @@ - package mage.sets; import mage.cards.ExpansionSet; @@ -328,6 +327,11 @@ class RiseOfTheEldraziCollator implements BoosterCollator { commonB, commonB, commonB, commonC2, commonC2, commonC2, commonC2 ); + private final BoosterStructure AAABBBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( commonA, commonA, commonA, commonA, commonB, commonB, commonB, @@ -366,9 +370,9 @@ class RiseOfTheEldraziCollator implements BoosterCollator { AAABC2C2C2C2C2C2, AAABBC2C2C2C2C2, + AAABBC2C2C2C2C2, AAABBBC2C2C2C2, - AAABBBC2C2C2C2, - AAABBBC2C2C2C2, + AAABBBBC2C2C2, AAAABBBC2C2C2, AAAABBBC2C2C2, AAAABBBC2C2C2, diff --git a/Mage.Sets/src/mage/sets/ScarsOfMirrodin.java b/Mage.Sets/src/mage/sets/ScarsOfMirrodin.java index 0742778d39d..f868cad144d 100644 --- a/Mage.Sets/src/mage/sets/ScarsOfMirrodin.java +++ b/Mage.Sets/src/mage/sets/ScarsOfMirrodin.java @@ -1,10 +1,16 @@ - package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + /** * * @author nantuko84 @@ -277,4 +283,124 @@ public final class ScarsOfMirrodin extends ExpansionSet { cards.add(new SetCardInfo("Wurmcoil Engine", 223, Rarity.MYTHIC, mage.cards.w.WurmcoilEngine.class)); } + @Override + public BoosterCollator createCollator() { + return new ScarsOfMirrodinCollator(); + } +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/som.html +// Using USA collation +class ScarsOfMirrodinCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "70", "8", "189", "38", "106", "166", "65", "23", "221", "112", "138", "75", "84", "168", "13", "29", "157", "68", "117", "156", "89", "210", "70", "44", "200", "10", "170", "63", "130", "4", "168", "38", "157", "88", "8", "204", "65", "29", "221", "106", "23", "200", "130", "166", "75", "36", "189", "89", "13", "204", "4", "138", "68", "44", "202", "84", "112", "210", "63", "10", "156", "36", "88", "202", "117", "170"); + private final CardRun commonB = new CardRun(true, "114", "58", "207", "109", "18", "146", "107", "37", "142", "114", "147", "41", "2", "192", "107", "52", "146", "125", "18", "209", "129", "178", "58", "91", "192", "55", "52", "207", "125", "2", "186", "91", "142", "129", "37", "209", "109", "103", "178", "41", "18", "207", "58", "186", "103", "129", "146", "41", "91", "178", "55", "37", "147", "125", "186", "114", "52", "209", "55", "103", "192", "109", "107", "147", "2", "142"); + private final CardRun commonC1 = new CardRun(true, "116", "54", "184", "97", "15", "160", "113", "51", "203", "54", "153", "82", "21", "155", "120", "45", "187", "60", "97", "153", "46", "15", "133", "45", "165", "227", "92", "184", "31", "7", "60", "116", "187", "227", "96", "219", "51", "21", "191", "120", "77", "165", "82", "219", "133", "92", "203", "46", "77", "155", "113", "7", "96", "31", "191"); + private final CardRun commonC2 = new CardRun(true, "197", "56", "27", "102", "136", "19", "140", "20", "76", "49", "131", "222", "158", "56", "136", "80", "20", "131", "140", "100", "158", "160", "80", "49", "20", "134", "222", "27", "218", "42", "100", "76", "197", "140", "102", "131", "42", "80", "218", "19", "197", "158", "134", "42", "56", "100", "19", "222", "136", "27", "49", "102", "134", "76", "218"); + private final CardRun uncommonA = new CardRun(true, "78", "108", "171", "90", "47", "185", "101", "132", "217", "3", "214", "71", "167", "127", "53", "159", "85", "161", "71", "216", "26", "149", "62", "164", "124", "47", "161", "61", "81", "3", "185", "26", "143", "62", "50", "213", "90", "216", "124", "214", "30", "167", "17", "139", "61", "1", "171", "85", "30", "217", "81", "108", "139", "1", "149", "78", "127", "159", "101", "53", "213", "17", "132", "164", "50", "143"); + private final CardRun uncommonB = new CardRun(true, "59", "151", "115", "9", "211", "40", "144", "59", "198", "83", "174", "128", "48", "199", "5", "9", "152", "148", "67", "99", "151", "34", "16", "211", "174", "87", "115", "198", "40", "74", "181", "215", "83", "111", "190", "34", "162", "16", "144", "67", "152", "111", "181", "87", "148", "5", "74", "199", "128", "190", "99", "215", "48", "162"); + private final CardRun rareA = new CardRun(true, "180", "95", "119", "201", "177", "66", "122", "14", "205", "226", "220", "43", "223", "95", "121", "69", "212", "22", "183", "229", "173", "28", "73", "194", "33", "98", "205", "25", "179", "224", "182", "122", "93", "201", "22", "180", "43", "64", "188", "229", "212", "119", "145", "28", "69", "194", "14", "135", "163", "93", "183", "224", "220", "25", "39", "188", "66", "182", "121", "98", "145", "73", "11", "226", "163", "33"); + private final CardRun rareB = new CardRun(true, "154", "104", "225", "193", "24", "172", "118", "105", "175", "72", "169", "228", "141", "208", "35", "150", "86", "126", "118", "12", "195", "176", "72", "32", "137", "105", "123", "150", "24", "196", "225", "141", "57", "154", "206", "6", "228", "104", "110", "137", "126", "175", "35", "169", "94", "195", "172", "57", "12", "86", "206", "79", "110", "196", "32"); + private final CardRun land = new CardRun(false, "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", "240", "241", "242", "243", "244", "245", "246", "247", "248", "249"); + + private final BoosterStructure AAABC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABC2C2C2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, + commonC2, commonC2, commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBC2C2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBBC2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2 + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA); + private final BoosterStructure R2 = new BoosterStructure(rareB); + private final BoosterStructure L1 = new BoosterStructure(land); + + // In order for equal numbers of each common to exist, the average booster must contain: + // 3.27 A commons (36 / 11) + // 2.18 B commons (24 / 11) + // 2.73 C1 commons (30 / 11, or 60 / 11 in each C1 booster) + // 1.82 C2 commons (20 / 11, or 40 / 11 in each C2 booster) + // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs + // and with 10 common slots per booster + private final RarityConfiguration commonRuns = new RarityConfiguration( + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + + AAABC2C2C2C2C2C2, + AAABBC2C2C2C2C2, + AAABBC2C2C2C2C2, + AAABBBC2C2C2C2, + AAABBBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBBC2C2 + ); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 1.65 A uncommons (33 / 20) + // 1.35 B uncommons (27 / 20) + // These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, + ABB, ABB, ABB, ABB, ABB, ABB, ABB + ); + private final RarityConfiguration rareRuns = new RarityConfiguration( + R1, R1, R1, R1, R1, R1, + R2, R2, R2, R2, R2 + ); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(rareRuns.getNext().makeRun()); + booster.addAll(landRuns.getNext().makeRun()); + return booster; + } } diff --git a/Mage.Sets/src/mage/sets/ShardsOfAlara.java b/Mage.Sets/src/mage/sets/ShardsOfAlara.java index 252bc48035e..17cd13b1de7 100644 --- a/Mage.Sets/src/mage/sets/ShardsOfAlara.java +++ b/Mage.Sets/src/mage/sets/ShardsOfAlara.java @@ -1,10 +1,16 @@ - package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + /** * * @author BetaSteward_at_googlemail.com @@ -278,4 +284,114 @@ public final class ShardsOfAlara extends ExpansionSet { cards.add(new SetCardInfo("Yoked Plowbeast", 31, Rarity.COMMON, mage.cards.y.YokedPlowbeast.class)); } + @Override + public BoosterCollator createCollator() { + return new ShardsOfAlaraCollator(); + } +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/ala.html +// Using USA collation +class ShardsOfAlaraCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "134", "116", "42", "18", "71", "195", "126", "118", "32", "11", "92", "128", "116", "37", "31", "65", "165", "126", "110", "46", "22", "74", "127", "115", "62", "30", "67", "165", "147", "104", "59", "18", "92", "144", "118", "42", "1", "73", "195", "128", "111", "59", "11", "65", "134", "104", "62", "22", "71", "176", "144", "115", "46", "30", "73", "127", "110", "32", "31", "74", "176", "147", "111", "37", "1", "67"); + private final CardRun commonB = new CardRun(true, "189", "130", "93", "28", "198", "47", "136", "203", "88", "12", "169", "102", "130", "26", "93", "153", "61", "86", "203", "47", "28", "139", "207", "130", "97", "88", "198", "26", "61", "203", "36", "24", "86", "189", "136", "93", "12", "153", "102", "87", "207", "36", "28", "88", "169", "139", "97", "24", "189", "61", "136", "153", "47", "26", "87", "198", "139", "102", "24", "169", "36", "86", "207", "12", "97", "87"); + private final CardRun commonC1 = new CardRun(true, "221", "13", "149", "90", "213", "108", "8", "133", "34", "156", "107", "4", "90", "221", "214", "66", "125", "57", "8", "121", "218", "54", "149", "15", "83", "227", "33", "186", "94", "141", "4", "66", "215", "34", "107", "227", "81", "213", "133", "13", "54", "108", "156", "141", "214", "83", "94", "15", "125", "33", "215", "121", "186", "81", "57"); + private final CardRun commonC2 = new CardRun(true, "223", "75", "162", "120", "132", "52", "216", "208", "225", "218", "173", "120", "152", "75", "52", "20", "162", "225", "77", "132", "212", "158", "105", "224", "162", "35", "77", "223", "216", "132", "208", "75", "158", "52", "224", "212", "152", "105", "208", "35", "216", "20", "223", "173", "152", "224", "77", "120", "35", "158", "20", "225", "212", "173", "105"); + private final CardRun uncommonA = new CardRun(true, "161", "129", "43", "184", "228", "171", "5", "226", "205", "114", "202", "76", "142", "155", "43", "197", "27", "222", "168", "106", "188", "85", "142", "180", "40", "209", "229", "5", "220", "168", "161", "112", "80", "184", "145", "155", "44", "27", "228", "157", "175", "106", "76", "205", "222", "129", "197", "40", "19", "209", "220", "157", "114", "175", "85", "145", "188", "229", "44", "180", "19", "171", "112", "226", "202", "80"); + private final CardRun uncommonB = new CardRun(true, "82", "124", "23", "181", "29", "137", "98", "68", "174", "39", "123", "99", "78", "167", "53", "117", "2", "181", "200", "39", "113", "124", "68", "177", "41", "201", "3", "99", "151", "58", "123", "117", "72", "200", "53", "2", "137", "82", "167", "113", "201", "29", "72", "190", "41", "78", "23", "151", "174", "98", "177", "3", "58", "190"); + private final CardRun rareA = new CardRun(true, "55", "100", "183", "89", "159", "148", "6", "219", "70", "192", "95", "48", "187", "49", "183", "140", "21", "217", "70", "204", "100", "164", "95", "49", "148", "187", "25", "211", "163", "89", "160", "150", "109", "17", "48", "199", "25", "138", "163", "64", "206", "91", "103", "219", "45", "199", "6", "140", "159", "55", "164", "64", "109", "217", "17", "160", "138", "103", "206", "45", "204", "211", "21", "91", "150", "192"); + private final CardRun rareB = new CardRun(true, "69", "196", "10", "84", "122", "194", "51", "16", "210", "131", "96", "56", "154", "146", "122", "7", "191", "14", "50", "170", "84", "135", "119", "193", "38", "79", "10", "182", "131", "51", "185", "63", "101", "143", "178", "14", "56", "119", "9", "135", "69", "172", "38", "96", "146", "179", "7", "50", "63", "166", "101", "16", "60", "79", "143"); + private final CardRun land = new CardRun(false, "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", "240", "241", "242", "243", "244", "245", "246", "247", "248", "249"); + + private final BoosterStructure AAABC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC2C2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBBC2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2 + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA); + private final BoosterStructure R2 = new BoosterStructure(rareB); + private final BoosterStructure L1 = new BoosterStructure(land); + + // In order for equal numbers of each common to exist, the average booster must contain: + // 3.27 A commons (36 / 11) + // 2.18 B commons (24 / 11) + // 2.73 C1 commons (30 / 11, or 60 / 11 in each C1 booster) + // 1.82 C2 commons (20 / 11, or 40 / 11 in each C2 booster) + // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs + // and with 10 common slots per booster + private final RarityConfiguration commonRuns = new RarityConfiguration( + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + + AAABBC2C2C2C2C2, + AAABBC2C2C2C2C2, + AAABBC2C2C2C2C2, + AAABBC2C2C2C2C2, + AAABBBC2C2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBBC2C2, + AAAABBBBC2C2 + ); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 1.65 A uncommons (33 / 20) + // 1.35 B uncommons (27 / 20) + // These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, + ABB, ABB, ABB, ABB, ABB, ABB, ABB + ); + private final RarityConfiguration rareRuns = new RarityConfiguration( + R1, R1, R1, R1, R1, R1, + R2, R2, R2, R2, R2 + ); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(rareRuns.getNext().makeRun()); + booster.addAll(landRuns.getNext().makeRun()); + return booster; + } } diff --git a/Mage.Sets/src/mage/sets/Zendikar.java b/Mage.Sets/src/mage/sets/Zendikar.java index 5656790fcc0..9521768efba 100644 --- a/Mage.Sets/src/mage/sets/Zendikar.java +++ b/Mage.Sets/src/mage/sets/Zendikar.java @@ -3,9 +3,16 @@ package mage.sets; import mage.ObjectColor; import mage.cards.CardGraphicInfo; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + /** * @author BetaSteward_at_googlemail.com */ @@ -298,4 +305,124 @@ public final class Zendikar extends ExpansionSet { cards.add(new SetCardInfo("Zendikar Farguide", 194, Rarity.COMMON, mage.cards.z.ZendikarFarguide.class)); } + @Override + public BoosterCollator createCollator() { + return new ZendikarCollator(); + } +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/zen.html +// Using USA collation +class ZendikarCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "87", "189", "58", "118", "7", "222", "163", "66", "14", "132", "195", "106", "192", "77", "118", "23", "222", "189", "48", "3", "145", "202", "98", "192", "60", "226", "216", "94", "169", "78", "7", "119", "195", "90", "181", "60", "132", "216", "87", "169", "77", "23", "226", "207", "94", "181", "78", "148", "14", "90", "193", "58", "31", "145", "207", "98", "163", "48", "148", "3", "106", "193", "66", "31", "119", "202"); + private final CardRun commonB = new CardRun(true, "113", "173", "141", "97", "225", "22", "165", "150", "49", "113", "206", "20", "173", "152", "97", "75", "18", "188", "151", "225", "85", "201", "36", "165", "152", "115", "49", "20", "188", "141", "76", "97", "206", "22", "174", "152", "85", "49", "36", "173", "151", "75", "113", "201", "188", "22", "150", "115", "225", "20", "174", "141", "85", "76", "206", "18", "165", "151", "115", "75", "36", "174", "150", "201", "76", "18"); + private final CardRun commonC1 = new CardRun(true, "155", "194", "28", "73", "167", "112", "35", "65", "138", "158", "91", "121", "44", "29", "166", "103", "37", "72", "112", "138", "167", "104", "136", "194", "35", "44", "84", "149", "158", "21", "183", "73", "117", "136", "179", "28", "67", "103", "29", "72", "204", "155", "183", "104", "149", "166", "37", "65", "91", "21", "67", "84", "121", "179", "117"); + private final CardRun commonC2 = new CardRun(true, "147", "70", "171", "93", "5", "128", "227", "27", "86", "43", "50", "129", "30", "171", "80", "27", "128", "70", "93", "147", "227", "5", "50", "26", "186", "125", "51", "86", "171", "27", "43", "129", "5", "147", "50", "227", "86", "26", "43", "80", "186", "128", "51", "30", "125", "70", "26", "93", "186", "129", "51", "80", "125", "204", "30"); + private final CardRun uncommonA = new CardRun(true, "32", "55", "114", "139", "185", "2", "52", "108", "127", "156", "197", "17", "40", "89", "124", "190", "38", "56", "116", "137", "160", "209", "17", "55", "108", "139", "156", "38", "46", "114", "127", "185", "209", "16", "71", "116", "133", "157", "32", "56", "89", "137", "180", "205", "16", "52", "95", "142", "160", "34", "46", "101", "133", "180", "197", "2", "40", "95", "124", "157", "34", "71", "101", "142", "190", "205"); + private final CardRun uncommonB = new CardRun(true, "130", "33", "102", "64", "214", "198", "153", "176", "59", "109", "215", "19", "102", "47", "177", "210", "146", "164", "198", "33", "74", "217", "105", "130", "208", "24", "47", "215", "144", "177", "88", "19", "214", "59", "153", "161", "4", "224", "109", "208", "144", "164", "64", "210", "88", "146", "24", "176", "217", "4", "74", "161", "105", "224"); + private final CardRun rareA = new CardRun(true, "191", "140", "45", "82", "123", "229", "187", "100", "41", "9", "218", "120", "172", "25", "92", "42", "213", "6", "187", "83", "122", "218", "53", "1", "100", "8", "45", "229", "159", "126", "96", "219", "200", "54", "9", "143", "172", "223", "170", "82", "122", "68", "6", "220", "41", "8", "143", "159", "99", "219", "1", "68", "123", "92", "213", "12", "191", "126", "83", "42", "220", "168", "54", "96", "25", "223"); + private final CardRun rareB = new CardRun(true, "175", "211", "135", "162", "134", "61", "10", "107", "69", "203", "175", "212", "39", "154", "196", "62", "79", "184", "11", "211", "57", "228", "81", "131", "162", "62", "13", "182", "79", "212", "135", "63", "11", "81", "196", "221", "178", "134", "203", "61", "110", "15", "199", "10", "182", "221", "184", "131", "69", "228", "110", "39", "63", "111", "15"); + private final CardRun land = new CardRun(false, "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", "240", "241", "242", "243", "244", "245", "246", "247", "248", "249"); + + private final BoosterStructure AAABC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABC2C2C2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, + commonC2, commonC2, commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBC2C2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAABBBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBBC2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2 + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rareA); + private final BoosterStructure R2 = new BoosterStructure(rareB); + private final BoosterStructure L1 = new BoosterStructure(land); + + // In order for equal numbers of each common to exist, the average booster must contain: + // 3.27 A commons (36 / 11) + // 2.18 B commons (24 / 11) + // 2.73 C1 commons (30 / 11, or 60 / 11 in each C1 booster) + // 1.82 C2 commons (20 / 11, or 40 / 11 in each C2 booster) + // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs + // and with 10 common slots per booster + private final RarityConfiguration commonRuns = new RarityConfiguration( + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + + AAABC2C2C2C2C2C2, + AAABBC2C2C2C2C2, + AAABBC2C2C2C2C2, + AAABBBC2C2C2C2, + AAABBBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBBC2C2 + ); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 1.65 A uncommons (33 / 20) + // 1.35 B uncommons (27 / 20) + // These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, + ABB, ABB, ABB, ABB, ABB, ABB, ABB + ); + private final RarityConfiguration rareRuns = new RarityConfiguration( + R1, R1, R1, R1, R1, R1, + R2, R2, R2, R2, R2 + ); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(rareRuns.getNext().makeRun()); + booster.addAll(landRuns.getNext().makeRun()); + return booster; + } } From 7d92ad45d96f6ea57ad1aa2f157b7b53d68feebf Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 29 Sep 2021 17:06:18 +0400 Subject: [PATCH 208/231] Dev: updated zip libs --- pom.xml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index f498f2ca3b8..505fdc766a6 100644 --- a/pom.xml +++ b/pom.xml @@ -198,17 +198,7 @@ net.java.truevfs truevfs-profile-base - 0.11.1 - - - net.java.truevfs - truevfs-access-swing - - - net.java.truecommons - truecommons-key-swing - - + 0.14.0 From 970125c5c4b07f438db9a9d66424d0dc785080a8 Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Wed, 29 Sep 2021 09:45:08 -0500 Subject: [PATCH 209/231] - small refactor Foretell ability --- .../main/java/mage/abilities/keyword/ForetellAbility.java | 2 ++ Mage/src/main/java/mage/game/events/GameEvent.java | 1 + .../main/java/mage/watchers/common/ForetoldWatcher.java | 7 +++---- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java index 6bcf3bf45e4..045cfd70856 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java @@ -24,6 +24,7 @@ import mage.util.CardUtil; import mage.watchers.common.ForetoldWatcher; import java.util.UUID; +import mage.game.events.GameEvent; /** * @author jeffwadsworth @@ -142,6 +143,7 @@ public class ForetellAbility extends SpecialAction { effect.apply(game, source); card.setFaceDown(true, game); game.addEffect(new ForetellAddCostEffect(new MageObjectReference(card, game)), source); + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.FORETELL, card.getId(), null, source.getControllerId())); return true; } return false; diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index bad2b187433..69c54154821 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -465,6 +465,7 @@ public class GameEvent implements Serializable { DUNGEON_COMPLETED, REMOVED_FROM_COMBAT, // targetId id of permanent removed from combat FORETOLD, // targetId id of card foretold + FORETELL, // targetId id of card foretell playerId id of the controller //custom events CUSTOM_EVENT } diff --git a/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java b/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java index f62a879d051..3213e8b76de 100644 --- a/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java @@ -3,11 +3,11 @@ package mage.watchers.common; import java.util.HashSet; import java.util.Set; import java.util.UUID; -import mage.abilities.keyword.ForetellAbility; import mage.cards.Card; import mage.constants.WatcherScope; import mage.game.Game; import mage.game.events.GameEvent; +import mage.util.CardUtil; import mage.watchers.Watcher; /** @@ -26,10 +26,9 @@ public class ForetoldWatcher extends Watcher { @Override public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.TAKEN_SPECIAL_ACTION) { - Card card = game.getCard(event.getSourceId()); + if (event.getType() == GameEvent.EventType.FORETELL) { + Card card = game.getCard(event.getTargetId()); if (card != null - && card.getAbilities(game).containsClass(ForetellAbility.class) && controllerId == event.getPlayerId()) { foretellCardsThisTurn.add(card.getId()); foretoldCards.add(card.getId()); From 30ec52a4f2965572d8cc4bab6273b9b6295b1f0d Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Thu, 30 Sep 2021 13:40:14 +0400 Subject: [PATCH 210/231] * GUI: improved connection dialog (removed ping popups in register/new dialog, fixed app freeze on register, etc); --- .../main/java/mage/remote/SessionImpl.java | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/Mage.Common/src/main/java/mage/remote/SessionImpl.java b/Mage.Common/src/main/java/mage/remote/SessionImpl.java index 4b730261dd6..3162aa68e60 100644 --- a/Mage.Common/src/main/java/mage/remote/SessionImpl.java +++ b/Mage.Common/src/main/java/mage/remote/SessionImpl.java @@ -122,11 +122,23 @@ public class SessionImpl implements Session { client.showMessage("Remote task error. " + message); } - private boolean doRemoteWorkAndHandleErrors(RemotingTask remoting) { + private boolean doRemoteWorkAndHandleErrors(boolean closeConnectionOnFinish, boolean mustWaitServerMessageOnFail, + RemotingTask remoting) { // execute remote task and wait result, can be canceled lastRemotingTask = remoting; try { - return remoting.doWork(); + boolean res = remoting.doWork(); + if (!res && mustWaitServerMessageOnFail) { + // server send detail error as separate message by existing connection, + // so you need wait some time before disconnect + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + logger.fatal("waiting of error message had failed", e); + Thread.currentThread().interrupt(); + } + } + return res; } catch (InterruptedException | CancellationException t) { // was canceled by user, nothing to show } catch (MalformedURLException ex) { @@ -180,13 +192,16 @@ public class SessionImpl implements Session { } } finally { lastRemotingTask = null; + if (closeConnectionOnFinish) { + disconnect(false); // it's ok on mutiple calls + } } return false; } @Override public synchronized boolean register(final Connection connection) { - return doRemoteConnection(connection) && doRemoteWorkAndHandleErrors(new RemotingTask() { + return doRemoteConnection(connection) && doRemoteWorkAndHandleErrors(true, true, new RemotingTask() { @Override public boolean work() throws Throwable { logger.info("Registration: username " + getUserName() + " for email " + getEmail()); @@ -199,7 +214,7 @@ public class SessionImpl implements Session { @Override public synchronized boolean emailAuthToken(final Connection connection) { - return doRemoteConnection(connection) && doRemoteWorkAndHandleErrors(new RemotingTask() { + return doRemoteConnection(connection) && doRemoteWorkAndHandleErrors(true, true, new RemotingTask() { @Override public boolean work() throws Throwable { logger.info("Auth request: requesting auth token for username " + getUserName() + " to email " + getEmail()); @@ -212,7 +227,7 @@ public class SessionImpl implements Session { @Override public synchronized boolean resetPassword(final Connection connection) { - return doRemoteConnection(connection) && doRemoteWorkAndHandleErrors(new RemotingTask() { + return doRemoteConnection(connection) && doRemoteWorkAndHandleErrors(true, true, new RemotingTask() { @Override public boolean work() throws Throwable { logger.info("Password reset: reseting password for username " + getUserName()); @@ -225,7 +240,7 @@ public class SessionImpl implements Session { @Override public synchronized boolean connect(final Connection connection) { - return doRemoteConnection(connection) && doRemoteWorkAndHandleErrors(new RemotingTask() { + return doRemoteConnection(connection) && doRemoteWorkAndHandleErrors(false, true, new RemotingTask() { @Override public boolean work() throws Throwable { setLastError(""); @@ -258,7 +273,6 @@ public class SessionImpl implements Session { } logger.info("Logging: FAIL"); - disconnect(false); return false; } }); @@ -437,7 +451,7 @@ public class SessionImpl implements Session { boolean result; try { - result = doRemoteWorkAndHandleErrors(lastRemotingTask); + result = doRemoteWorkAndHandleErrors(false, false, lastRemotingTask); } finally { lastRemotingTask = null; } @@ -529,6 +543,7 @@ public class SessionImpl implements Session { if (sessionState == SessionState.DISCONNECTING || sessionState == SessionState.CONNECTING) { sessionState = SessionState.DISCONNECTED; + serverState = null; logger.info("Disconnecting DONE"); if (askForReconnect) { client.showError("Network error. You have been disconnected from " + connection.getHost()); @@ -1654,7 +1669,10 @@ public class SessionImpl implements Session { @Override public boolean ping() { try { - if (isConnected() && sessionId != null) { + // ping must work after login only, all other actions are single call (example: register new user) + // sessionId fills on connection + // serverState fills on good login + if (isConnected() && sessionId != null && serverState != null) { long startTime = System.nanoTime(); if (!server.ping(sessionId, pingInfo)) { logger.error("Ping failed: " + this.getUserName() + " Session: " + sessionId + " to MAGE server at " + connection.getHost() + ':' + connection.getPort()); From b7c78d0758ef2c4f71bbbbe40e56f78e43fd078b Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Thu, 30 Sep 2021 08:10:46 -0400 Subject: [PATCH 211/231] Implement booster collation for DOM, WAR and MID (#8346) --- Mage.Sets/src/mage/sets/Dominaria.java | 110 ++++++++++++++++- .../src/mage/sets/InnistradMidnightHunt.java | 84 +++++++++++++ Mage.Sets/src/mage/sets/WarOfTheSpark.java | 116 ++++++++++++++++++ 3 files changed, 309 insertions(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/sets/Dominaria.java b/Mage.Sets/src/mage/sets/Dominaria.java index 98d533f1954..8d4a4d441c2 100644 --- a/Mage.Sets/src/mage/sets/Dominaria.java +++ b/Mage.Sets/src/mage/sets/Dominaria.java @@ -1,10 +1,16 @@ - package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + /** * * @author fireshoes @@ -310,4 +316,106 @@ public final class Dominaria extends ExpansionSet { cards.add(new SetCardInfo("Zahid, Djinn of the Lamp", 76, Rarity.RARE, mage.cards.z.ZahidDjinnOfTheLamp.class)); cards.add(new SetCardInfo("Zhalfirin Void", 249, Rarity.UNCOMMON, mage.cards.z.ZhalfirinVoid.class)); } + + @Override + public BoosterCollator createCollator() { + return new DominariaCollator(); + } +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/dom.html +// Using USA collation for common/uncommon, rare collation inferred from other sets +class DominariaCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "5", "67", "125", "15", "62", "120", "10", "43", "151", "24", "45", "144", "32", "48", "115", "17", "44", "136", "20", "50", "117", "34", "53", "127", "2", "60", "126", "22", "73", "138", "11", "67", "120", "15", "71", "124", "20", "62", "151", "10", "43", "125", "5", "44", "136", "24", "50", "144", "11", "48", "117", "32", "45", "115", "22", "60", "126", "17", "53", "127", "34", "71", "138", "2", "73", "124"); + private final CardRun commonB = new CardRun(true, "79", "153", "87", "164", "77", "163", "104", "171", "89", "169", "80", "162", "112", "155", "83", "167", "92", "169", "101", "170", "105", "176", "87", "164", "77", "153", "104", "158", "89", "171", "79", "162", "80", "163", "83", "170", "105", "167", "101", "155", "112", "176", "92", "158", "87", "170", "79", "153", "104", "164", "77", "169", "112", "162", "83", "167", "89", "171", "92", "163", "80", "155", "105", "176", "101", "158"); + private final CardRun commonC1 = new CardRun(true, "47", "78", "3", "230", "177", "40", "85", "227", "142", "9", "168", "232", "91", "72", "236", "140", "19", "106", "221", "178", "46", "141", "3", "78", "177", "225", "47", "118", "212", "85", "9", "154", "227", "72", "84", "142", "232", "19", "168", "106", "230", "40", "141", "236", "91", "226", "178", "221", "140", "46", "84", "212", "154", "118", "225"); + private final CardRun commonC2 = new CardRun(true, "135", "27", "209", "94", "37", "189", "226", "139", "52", "110", "229", "29", "7", "157", "41", "134", "156", "216", "94", "29", "135", "157", "52", "27", "209", "139", "63", "7", "189", "134", "229", "110", "37", "41", "156", "216", "135", "29", "94", "63", "189", "209", "139", "157", "27", "52", "229", "7", "134", "41", "37", "110", "216", "156", "63"); + private final CardRun uncommonA = new CardRun(true, "235", "119", "179", "145", "99", "38", "219", "175", "243", "33", "116", "210", "70", "160", "152", "81", "23", "235", "186", "242", "14", "130", "210", "54", "159", "121", "90", "38", "219", "180", "245", "23", "145", "179", "65", "186", "116", "82", "75", "121", "175", "99", "14", "119", "235", "70", "159", "130", "81", "54", "243", "160", "33", "82", "152", "210", "65", "180", "145", "90", "75", "242", "159", "99", "245", "116", "219", "70", "175", "119", "81", "65", "243", "186", "82", "38", "130", "235", "75", "160", "33", "121", "70", "152", "179", "90", "23", "145", "180", "54", "159", "116", "14", "65", "242", "179", "99", "38", "119", "210", "245", "180", "152", "23", "54", "243", "175", "90", "14", "130", "219", "242", "160", "121", "82", "75", "245", "186", "81", "33"); + private final CardRun uncommonB = new CardRun(true, "231", "103", "188", "31", "64", "222", "249", "161", "28", "185", "150", "213", "97", "8", "181", "49", "220", "246", "188", "228", "56", "123", "218", "93", "185", "30", "74", "231", "244", "181", "31", "64", "128", "222", "107", "161", "137", "51", "213", "249", "103", "28", "49", "150", "220", "97", "188", "123", "56", "228", "137", "246", "30", "64", "244", "218", "107", "161", "128", "74", "213", "249", "93", "31", "51", "150", "222", "103", "185", "123", "56", "231", "97", "8", "220", "74", "137", "228", "107", "181", "150", "49", "218", "244", "93", "28", "51", "128", "213", "188", "8", "137", "64", "222", "246", "97", "30", "56", "31", "231", "103", "28", "123", "74", "220", "249", "161", "107", "246", "51", "228", "185", "8", "128", "49", "218", "244", "93", "30", "181"); + private final CardRun uncommonLegend = new CardRun(true, "203", "109", "204", "66", "191", "4", "205", "111", "206", "12", "196", "165", "202", "69", "203", "113", "190", "109", "208", "25", "194", "148", "196", "12", "204", "66", "208", "111", "191", "165", "202", "4", "205", "113", "194", "148", "206", "25", "190", "69"); + // Shalai (35), Josu Vess (95), Darigaaz (193) and Muldrotha (199) are on the non-legend sheet; boosters containing one of them will also contain an uncommon legend + private final CardRun rare = new CardRun(false, "6", "13", "18", "35", "39", "42", "55", "57", "61", "68", "88", "95", "98", "102", "114", "122", "129", "131", "133", "143", "147", "166", "173", "182", "183", "184", "187", "200", "201", "211", "214", "215", "217", "223", "233", "238", "239", "240", "241", "247", "248", "6", "13", "18", "35", "39", "42", "55", "57", "61", "68", "88", "95", "98", "102", "114", "122", "129", "131", "133", "143", "147", "166", "173", "182", "183", "184", "187", "200", "201", "211", "214", "215", "217", "223", "233", "238", "239", "240", "241", "247", "248", "1", "21", "100", "132", "193", "199", "207", "224", "237"); + private final CardRun rareLegend = new CardRun(false, "16", "36", "58", "76", "96", "108", "146", "172", "192", "195", "198", "234", "16", "36", "58", "76", "96", "108", "146", "172", "192", "195", "198", "234", "26", "59", "86", "149", "174", "197"); + private final CardRun land = new CardRun(false, "250", "251", "252", "253", "254", "255", "256", "257", "258", "259", "260", "261", "262", "263", "264", "265", "266", "267", "268", "269"); + + private final BoosterStructure AABBC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAAABBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBBC2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2 + ); + private final BoosterStructure ABL = new BoosterStructure(uncommonA, uncommonB, uncommonLegend, rare); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB, rareLegend); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB, rareLegend); + private final BoosterStructure L1 = new BoosterStructure(land); + + // In order for equal numbers of each common to exist, the average booster must contain: + // 3.27 A commons (36 / 11) + // 2.18 B commons (24 / 11) + // 2.73 C1 commons (30 / 11, or 60 / 11 in each C1 booster) + // 1.82 C2 commons (20 / 11, or 40 / 11 in each C2 booster) + // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs + // and with 10 common slots per booster + private final RarityConfiguration commonRuns = new RarityConfiguration( + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBBC2C2 + ); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 1.125 A uncommons (9 / 8) + // 1.125 B uncommons (9 / 8) + // 0.75 uncommon legend (6 / 8) + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + ABL, ABL, ABL, ABL, ABL, ABL, + AAB, ABB + ); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(landRuns.getNext().makeRun()); + return booster; + } } diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 0361ced3dd4..3f8f6f286ae 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -1,9 +1,14 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -341,4 +346,83 @@ public final class InnistradMidnightHunt extends ExpansionSet { cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); // remove when mechanic is fully implemented } + /* + @Override + public BoosterCollator createCollator() { + return new InnistradMidnightHuntCollator(); + } + */ +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/mid.html +// Using USA collation for common/uncommon, rare collation inferred from other sets +class InnistradMidnightHuntCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "202", "23", "48", "125", "184", "153", "74", "116", "171", "259", "52", "87", "201", "135", "79", "98", "189", "253", "54", "114", "175", "35", "44", "121", "193", "136", "73", "86", "199", "19", "253", "67", "117", "186", "151", "82", "88", "170", "5", "50", "119", "192", "261", "58", "96", "191", "39", "56", "84", "195", "127", "78", "118", "188", "12", "43", "93", "184", "259", "79", "116", "171", "153", "74", "98", "202", "258", "52", "125", "189", "23", "135", "48", "87", "201", "35", "54", "121", "199", "254", "44", "114", "191", "19", "136", "73", "86", "193", "261", "82", "96", "175", "5", "254", "58", "117", "186", "151", "78", "119", "170", "39", "50", "88", "192", "258", "67", "118", "188", "127", "56", "84", "195", "12", "43", "93"); + private final CardRun commonB = new CardRun(true, "140", "198", "32", "145", "60", "22", "167", "106", "9", "128", "180", "287", "130", "66", "24", "161", "95", "40", "144", "185", "30", "148", "150", "21", "138", "72", "14", "156", "99", "36", "140", "60", "10", "132", "301", "8", "145", "106", "22", "167", "198", "32", "130", "66", "40", "128", "150", "9", "148", "95", "24", "161", "185", "14", "156", "72", "30", "132", "144", "36", "138", "99", "10", "140", "198", "32", "145", "60", "22", "128", "180", "21", "167", "106", "40", "130", "66", "24", "144", "95", "9", "148", "150", "8", "161", "185", "30", "138", "72", "14", "156", "99", "36", "132", "10", "21"); + private final CardRun commonDFC = new CardRun(true, "100", "55", "169", "293", "28", "120", "42", "305", "143", "27", "100", "55", "298", "163", "28", "120", "42", "203", "296", "27", "100", "55", "169", "143", "28", "291", "42", "203", "163", "27"); + private final CardRun uncommonA = new CardRun(true, "38", "53", "210", "97", "49", "26", "51", "115", "20", "70", "228", "113", "16", "75", "205", "90", "11", "83", "255", "123", "29", "65", "102", "31", "53", "262", "101", "38", "64", "210", "49", "20", "97", "51", "228", "90", "26", "75", "115", "16", "83", "205", "113", "11", "65", "102", "29", "70", "255", "31", "123", "64", "262", "97", "38", "53", "101", "20", "49", "210", "51", "115", "26", "75", "228", "113", "16", "83", "205", "90", "11", "65", "102", "31", "70", "255", "123", "29", "53", "262", "97", "38", "64", "210", "101", "49", "20", "51", "115", "26", "75", "228", "90", "16", "65", "205", "113", "11", "83", "102", "31", "70", "255", "123", "29", "64", "262", "101"); + private final CardRun uncommonB = new CardRun(true, "107", "229", "183", "237", "152", "238", "176", "220", "158", "216", "300", "222", "133", "244", "182", "226", "164", "251", "6", "212", "147", "247", "196", "239", "166", "249", "176", "243", "152", "214", "178", "154", "107", "221", "179", "238", "155", "229", "173", "237", "164", "216", "183", "220", "158", "239", "182", "222", "147", "226", "6", "244", "166", "212", "196", "251", "133", "247", "152", "237", "178", "243", "155", "249", "173", "214", "107", "229", "179", "238", "154", "221", "176", "220", "164", "308", "183", "226", "133", "244", "182", "222", "147", "212", "6", "247", "158", "251", "196", "239", "166", "243", "178", "249", "155", "221", "173", "154", "214"); + private final CardRun uncommonDFC1 = new CardRun(true, "190", "231", "139", "92", "302", "68", "160", "218", "299", "2", "165", "310", "190", "92", "292", "68", "187", "85", "160", "231", "174", "85", "165", "2", "303", "218", "139", "289", "187", "92", "295", "68", "174", "218", "297", "2"); + private final CardRun uncommonDFC2 = new CardRun(false, "3", "4", "13", "45", "47", "63", "105", "126", "141", "181", "256"); + private final CardRun rare = new CardRun(false, "1", "15", "18", "33", "37", "41", "46", "57", "62", "69", "76", "81", "89", "91", "103", "108", "111", "122", "131", "134", "137", "142", "146", "168", "172", "197", "200", "206", "207", "209", "213", "215", "219", "223", "224", "225", "227", "230", "232", "234", "235", "236", "241", "242", "248", "250", "252", "257", "260", "263", "265", "266", "267", "1", "15", "18", "33", "37", "41", "46", "57", "62", "69", "76", "81", "89", "91", "103", "108", "111", "122", "131", "134", "137", "142", "146", "168", "172", "197", "200", "206", "207", "209", "213", "215", "219", "223", "224", "225", "227", "230", "232", "234", "235", "236", "241", "242", "248", "250", "252", "257", "260", "263", "265", "266", "267", "25", "34", "59", "77", "110", "112", "124", "129", "149", "162", "177", "194", "208", "240", "245"); + private final CardRun rareDFC = new CardRun(false, "7", "61", "80", "94", "104", "157", "159", "204", "217", "233", "246", "7", "61", "80", "94", "104", "157", "159", "204", "217", "233", "246", "17", "71", "109", "211", "264"); + private final CardRun land = new CardRun(false, "268", "269", "270", "271", "272", "273", "274", "275", "276", "277"); + + private final BoosterStructure AAAAABBBBD = new BoosterStructure( + commonA, commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonDFC + ); + private final BoosterStructure AAAAAABBBD = new BoosterStructure( + commonA, commonA, commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonDFC + ); + private final BoosterStructure ABD1 = new BoosterStructure(uncommonA, uncommonB, uncommonDFC1, rare); + private final BoosterStructure BBD1 = new BoosterStructure(uncommonB, uncommonB, uncommonDFC1, rare); private final BoosterStructure ABD2 = new BoosterStructure(uncommonA, uncommonB, uncommonDFC2, rare); + private final BoosterStructure BBD2 = new BoosterStructure(uncommonB, uncommonB, uncommonDFC2, rare); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB, rareDFC); + private final BoosterStructure L1 = new BoosterStructure(land); + + // In order for equal numbers of each common to exist, the average booster must contain: + // 5.8 A commons (58 / 10) + // 3.2 B commons (32 / 10) + private final RarityConfiguration commonRuns = new RarityConfiguration( + AAAAABBBBD, AAAAABBBBD, + AAAAAABBBD, AAAAAABBBD, AAAAAABBBD, AAAAAABBBD, + AAAAAABBBD, AAAAAABBBD, AAAAAABBBD, AAAAAABBBD + ); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 0.98 A uncommons (81 / 83) + // 1.19 B uncommons (99 / 83) + // 0.43 DFC1 uncommon (36 / 83) + // 0.40 DFC2 uncommon (33 / 83) + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + ABD1, ABD1, ABD1, ABD1, ABD1, ABD1, + ABD1, ABD1, ABD1, ABD1, ABD1, ABD1, + ABD1, ABD1, ABD1, ABD1, ABD1, ABD1, + ABD1, ABD1, ABD1, ABD1, ABD1, ABD1, + ABD1, ABD1, ABD1, ABD1, ABD1, ABD1, + ABD1, ABD1, ABD1, ABD1, ABD1, BBD1, + + ABD2, ABD2, ABD2, ABD2, ABD2, ABD2, + ABD2, ABD2, ABD2, ABD2, ABD2, ABD2, + ABD2, ABD2, ABD2, ABD2, ABD2, ABD2, + ABD2, ABD2, ABD2, ABD2, ABD2, ABD2, + ABD2, ABD2, ABD2, ABD2, ABD2, ABD2, + ABD2, ABD2, BBD2, + + ABB, ABB, ABB, ABB, ABB, ABB, ABB, + ABB, ABB, ABB, ABB, ABB, ABB, ABB + ); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(landRuns.getNext().makeRun()); + return booster; + } } diff --git a/Mage.Sets/src/mage/sets/WarOfTheSpark.java b/Mage.Sets/src/mage/sets/WarOfTheSpark.java index 4b6f2b67a2c..e86c4dbfe29 100644 --- a/Mage.Sets/src/mage/sets/WarOfTheSpark.java +++ b/Mage.Sets/src/mage/sets/WarOfTheSpark.java @@ -1,9 +1,16 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + public final class WarOfTheSpark extends ExpansionSet { private static final WarOfTheSpark instance = new WarOfTheSpark(); @@ -336,4 +343,113 @@ public final class WarOfTheSpark extends ExpansionSet { cards.add(new SetCardInfo("Wardscale Crocodile", 183, Rarity.COMMON, mage.cards.w.WardscaleCrocodile.class)); cards.add(new SetCardInfo("Widespread Brutality", 226, Rarity.RARE, mage.cards.w.WidespreadBrutality.class)); } + + @Override + public BoosterCollator createCollator() { + return new WarOfTheSparkCollator(); + } +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/war.html +// Using USA collation for common/uncommon, rare collation inferred from other sets +class WarOfTheSparkCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "57", "129", "19", "69", "113", "39", "40", "134", "7", "58", "141", "14", "72", "130", "23", "46", "114", "5", "47", "118", "33", "63", "148", "19", "40", "132", "29", "70", "129", "25", "67", "128", "22", "57", "142", "39", "72", "113", "7", "47", "134", "14", "60", "141", "5", "67", "114", "21", "58", "118", "33", "69", "148", "25", "70", "132", "29", "63", "130", "22", "46", "142", "23", "60", "128", "21"); + private final CardRun commonB = new CardRun(true, "175", "103", "176", "108", "165", "95", "183", "88", "182", "106", "158", "104", "161", "110", "177", "76", "175", "81", "168", "108", "174", "95", "166", "94", "176", "107", "183", "106", "161", "103", "158", "104", "165", "110", "182", "88", "176", "108", "166", "76", "177", "81", "174", "95", "175", "94", "168", "107", "182", "106", "158", "103", "183", "88", "161", "104", "165", "110", "177", "81", "166", "107", "168", "76", "174", "94"); + private final CardRun commonC1 = new CardRun(true, "243", "154", "8", "42", "117", "101", "136", "74", "156", "240", "48", "11", "96", "123", "20", "9", "241", "73", "8", "112", "131", "173", "239", "120", "84", "38", "64", "11", "136", "109", "42", "144", "154", "243", "101", "74", "20", "117", "64", "156", "123", "96", "9", "48", "239", "120", "109", "38", "131", "241", "173", "84", "73", "144", "112"); + private final CardRun commonC2 = new CardRun(true, "145", "149", "93", "242", "162", "105", "149", "10", "75", "151", "240", "44", "139", "153", "242", "93", "179", "75", "105", "35", "162", "145", "71", "77", "246", "153", "36", "151", "139", "44", "149", "10", "179", "145", "35", "93", "246", "71", "36", "77", "162", "35", "44", "246", "151", "75", "10", "242", "105", "179", "139", "71", "77", "36", "153"); + private final CardRun uncommonA = new CardRun(true, "159", "205", "49", "188", "116", "189", "4", "204", "171", "121", "215", "6", "200", "87", "202", "212", "41", "147", "190", "31", "78", "205", "178", "192", "111", "188", "55", "26", "204", "122", "4", "189", "87", "215", "159", "116", "49", "200", "78", "147", "202", "171", "212", "6", "111", "121", "41", "178", "31", "192", "55", "122", "190", "26"); + private final CardRun uncommonB = new CardRun(true, "245", "237", "206", "124", "27", "198", "115", "3", "102", "195", "126", "59", "222", "157", "65", "247", "225", "90", "167", "193", "17", "185", "245", "98", "155", "199", "170", "43", "210", "237", "80", "157", "201", "52", "90", "238", "15", "206", "124", "198", "3", "27", "65", "222", "193", "167", "102", "195", "59", "199", "115", "43", "247", "225", "155", "80", "17", "52", "126", "170", "201", "98", "185", "238", "15", "210"); + private final CardRun uncommonPlaneswalker = new CardRun(false, "32", "37", "56", "61", "83", "100", "135", "146", "150", "164", "227", "228", "229", "230", "231", "232", "233", "234", "235", "236"); + private final CardRun rare = new CardRun(false, "18", "24", "28", "30", "34", "45", "50", "62", "66", "68", "79", "82", "85", "86", "89", "99", "125", "137", "138", "140", "152", "172", "181", "186", "187", "194", "196", "197", "203", "209", "214", "216", "218", "219", "223", "224", "226", "244", "248", "249", "18", "24", "28", "30", "34", "45", "50", "62", "66", "68", "79", "82", "85", "86", "89", "99", "125", "137", "138", "140", "152", "172", "181", "186", "187", "194", "196", "197", "203", "209", "214", "216", "218", "219", "223", "224", "226", "244", "248", "249", "12", "16", "51", "53", "91", "92", "127", "133", "160", "163", "208", "213"); + private final CardRun rarePlaneswalker = new CardRun(false, "1", "2", "54", "119", "143", "169", "180", "184", "191", "211", "217", "220", "221", "1", "2", "54", "119", "143", "169", "180", "184", "191", "211", "217", "220", "221", "13", "97", "207"); + private final CardRun land = new CardRun(false, "250", "251", "252", "253", "254", "255", "256", "257", "258", "259", "260", "261", "262", "263", "264"); + + private final BoosterStructure AABBC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAAABBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBBC2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, commonB, + commonC2, commonC2 + ); + private final BoosterStructure ABP = new BoosterStructure(uncommonA, uncommonB, uncommonPlaneswalker, rare); + private final BoosterStructure BBP = new BoosterStructure(uncommonB, uncommonB, uncommonPlaneswalker, rare); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB, rarePlaneswalker); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB, rarePlaneswalker); + private final BoosterStructure L1 = new BoosterStructure(land); + + // In order for equal numbers of each common to exist, the average booster must contain: + // 3.27 A commons (36 / 11) + // 2.18 B commons (24 / 11) + // 2.73 C1 commons (30 / 11, or 60 / 11 in each C1 booster) + // 1.82 C2 commons (20 / 11, or 40 / 11 in each C2 booster) + // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs + // and with 10 common slots per booster + private final RarityConfiguration commonRuns = new RarityConfiguration( + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBBC2C2 + ); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 1.01 A uncommons (81 / 80) + // 1.24 B uncommons (99 / 80) + // 0.75 uncommon planeswalker (60 / 80) + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, + ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, + ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, + ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, + ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, ABP, + ABP, ABP, ABP, ABP, BBP, BBP, BBP, BBP, BBP, BBP, + + AAB, AAB, AAB, AAB, AAB, AAB, AAB, ABB, ABB, ABB, + ABB, ABB, ABB, ABB, ABB, ABB, ABB, ABB, ABB, ABB + ); + private final RarityConfiguration landRuns = new RarityConfiguration(L1); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(landRuns.getNext().makeRun()); + return booster; + } } From f47aea4fbac8002a0961c6cca6af5c40f7b7c4cc Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 30 Sep 2021 08:32:27 -0400 Subject: [PATCH 212/231] [MIC] Implemented Kurbis, Harvest Celebrant --- .../mage/cards/k/KurbisHarvestCelebrant.java | 70 +++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 71 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KurbisHarvestCelebrant.java diff --git a/Mage.Sets/src/mage/cards/k/KurbisHarvestCelebrant.java b/Mage.Sets/src/mage/cards/k/KurbisHarvestCelebrant.java new file mode 100644 index 00000000000..e52cdc8e959 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KurbisHarvestCelebrant.java @@ -0,0 +1,70 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.RemoveCountersSourceCost; +import mage.abilities.dynamicvalue.common.ManaSpentToCastCount; +import mage.abilities.effects.common.PreventDamageToTargetEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +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.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KurbisHarvestCelebrant extends CardImpl { + + private static final FilterPermanent filter + = new FilterCreaturePermanent("another creature with a +1/+1 counter on it"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(CounterType.P1P1.getPredicate()); + } + + public KurbisHarvestCelebrant(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{X}{G}{G}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.TREEFOLK); + this.power = new MageInt(0); + this.toughness = new MageInt(0); + + // Kurbis, Harvest Celebrant enters the battlefield with a number of +1/+1 counters on it equal to the amount of mana spent to cast it. + this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect( + CounterType.P1P1.createInstance(), ManaSpentToCastCount.instance, true + ), "with a number of +1/+1 counters on it equal to the amount of mana spent to cast it")); + + // Remove a +1/+1 counter from Kurbis: Prevent all damage that would be dealt this turn to another target creature with a +1/+1 counter on it. + Ability ability = new SimpleActivatedAbility( + new PreventDamageToTargetEffect(Duration.EndOfTurn) + .setText("prevent all damage that would be dealt this turn " + + "to another target creature with a +1/+1 counter on it"), + new RemoveCountersSourceCost(CounterType.P1P1.createInstance()) + ); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private KurbisHarvestCelebrant(final KurbisHarvestCelebrant card) { + super(card); + } + + @Override + public KurbisHarvestCelebrant copy() { + return new KurbisHarvestCelebrant(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 089e071c93b..031cb9042b2 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -97,6 +97,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Kessig Cagebreakers", 142, Rarity.RARE, mage.cards.k.KessigCagebreakers.class)); cards.add(new SetCardInfo("Knight of the White Orchid", 88, Rarity.RARE, mage.cards.k.KnightOfTheWhiteOrchid.class)); cards.add(new SetCardInfo("Krosan Verge", 175, Rarity.UNCOMMON, mage.cards.k.KrosanVerge.class)); + cards.add(new SetCardInfo("Kurbis, Harvest Celebrant", 27, Rarity.RARE, mage.cards.k.KurbisHarvestCelebrant.class)); cards.add(new SetCardInfo("Kyler, Sigardian Emissary", 4, Rarity.MYTHIC, mage.cards.k.KylerSigardianEmissary.class)); cards.add(new SetCardInfo("Leinore, Autumn Sovereign", 1, Rarity.MYTHIC, mage.cards.l.LeinoreAutumnSovereign.class)); cards.add(new SetCardInfo("Lifecrafter's Bestiary", 160, Rarity.RARE, mage.cards.l.LifecraftersBestiary.class)); From 7b1e3fae7b6dab5c5338ae00e923173c1cb051f0 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 30 Sep 2021 08:52:24 -0400 Subject: [PATCH 213/231] [MIC] Implemented Wall of Mourning --- .../src/mage/cards/w/WallOfMourning.java | 125 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + Mage/src/main/java/mage/cards/CardsImpl.java | 3 +- 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/w/WallOfMourning.java diff --git a/Mage.Sets/src/mage/cards/w/WallOfMourning.java b/Mage.Sets/src/mage/cards/w/WallOfMourning.java new file mode 100644 index 00000000000..c97593d0a7c --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WallOfMourning.java @@ -0,0 +1,125 @@ +package mage.cards.w; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.CovenCondition; +import mage.abilities.dynamicvalue.common.OpponentsCount; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.hint.common.CovenHint; +import mage.abilities.keyword.DefenderAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.ExileZone; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WallOfMourning extends CardImpl { + + public WallOfMourning(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.WALL); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + + // Defender + this.addAbility(DefenderAbility.getInstance()); + + // When Wall of Mourning enters the battlefield, exile a card from the top of your library face down for each opponent you have. + this.addAbility(new EntersBattlefieldTriggeredAbility(new WallOfMourningExileEffect())); + + // Coven — At the beginning of your end step, if you control three or more creatures with different powers, put a card exiled with Wall of Mourning into its owner's hand. + this.addAbility(new BeginningOfEndStepTriggeredAbility( + Zone.BATTLEFIELD, new WallOfMourningReturnEffect(), + TargetController.YOU, CovenCondition.instance, false + ).addHint(CovenHint.instance).setAbilityWord(AbilityWord.COVEN)); + } + + private WallOfMourning(final WallOfMourning card) { + super(card); + } + + @Override + public WallOfMourning copy() { + return new WallOfMourning(this); + } +} + +class WallOfMourningExileEffect extends OneShotEffect { + + WallOfMourningExileEffect() { + super(Outcome.Benefit); + staticText = "exile a card from the top of your library face down for each opponent you have"; + } + + private WallOfMourningExileEffect(final WallOfMourningExileEffect effect) { + super(effect); + } + + @Override + public WallOfMourningExileEffect copy() { + return new WallOfMourningExileEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + int opponents = OpponentsCount.instance.calculate(game, source, this); + Set cards = player.getLibrary().getTopCards(game, opponents); + cards.removeIf(Objects::isNull); + player.moveCardsToExile( + cards, source, game, false, + CardUtil.getExileZoneId(game, source), + CardUtil.getSourceLogName(game, source) + ); + for (Card card : cards) { + card.setFaceDown(true, game); + } + return true; + } +} + +class WallOfMourningReturnEffect extends OneShotEffect { + + WallOfMourningReturnEffect() { + super(Outcome.Benefit); + staticText = "put a card exiled with {this} into its owner's hand"; + } + + private WallOfMourningReturnEffect(final WallOfMourningReturnEffect effect) { + super(effect); + } + + @Override + public WallOfMourningReturnEffect copy() { + return new WallOfMourningReturnEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + ExileZone exileZone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source)); + if (exileZone == null || exileZone.isEmpty()) { + return false; + } + return player.moveCards(exileZone.getRandom(game), Zone.HAND, source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 031cb9042b2..7c13f01eab9 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -159,6 +159,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Visions of Duplicity", 33, Rarity.RARE, mage.cards.v.VisionsOfDuplicity.class)); cards.add(new SetCardInfo("Visions of Glory", 32, Rarity.RARE, mage.cards.v.VisionsOfGlory.class)); cards.add(new SetCardInfo("Visions of Ruin", 36, Rarity.RARE, mage.cards.v.VisionsOfRuin.class)); + cards.add(new SetCardInfo("Wall of Mourning", 10, Rarity.RARE, mage.cards.w.WallOfMourning.class)); cards.add(new SetCardInfo("Wild Beastmaster", 146, Rarity.RARE, mage.cards.w.WildBeastmaster.class)); cards.add(new SetCardInfo("Wilhelt, the Rotcleaver", 2, Rarity.MYTHIC, mage.cards.w.WilheltTheRotcleaver.class)); cards.add(new SetCardInfo("Yavimaya Elder", 147, Rarity.COMMON, mage.cards.y.YavimayaElder.class)); diff --git a/Mage/src/main/java/mage/cards/CardsImpl.java b/Mage/src/main/java/mage/cards/CardsImpl.java index 4c7b2a725cb..b0f45e4ab9c 100644 --- a/Mage/src/main/java/mage/cards/CardsImpl.java +++ b/Mage/src/main/java/mage/cards/CardsImpl.java @@ -91,8 +91,7 @@ public class CardsImpl extends LinkedHashSet implements Cards, Serializabl if (this.isEmpty()) { return null; } - UUID[] cards = this.toArray(new UUID[this.size()]); - MageObject object = game.getObject(cards[RandomUtil.nextInt(cards.length)]); // neccessary if permanent tokens are in the collection + MageObject object = game.getObject(RandomUtil.randomFromCollection(this)); // neccessary if permanent tokens are in the collection if (object instanceof Card) { return (Card) object; } From 1b36053b17139629a8c8d81e1d8f953ca33026ed Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 30 Sep 2021 20:46:03 -0400 Subject: [PATCH 214/231] [AFC] added passing and failing tests for Belt of Giant Strength (#8347) --- .../single/afc/BeltOfGiantStrengthTest.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java new file mode 100644 index 00000000000..38262ef5d75 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java @@ -0,0 +1,60 @@ +package org.mage.test.cards.single.afc; + +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class BeltOfGiantStrengthTest extends CardTestPlayerBase { + + private static final String belt = "Belt of Giant Strength"; + private static final String gigantosauras = "Gigantosaurus"; + + @Test + public void testWithManaAvailable() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, belt); + addCard(Zone.BATTLEFIELD, playerA, gigantosauras); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", gigantosauras); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertIsAttachedTo(playerA, belt, gigantosauras); + Assert.assertTrue( + "All Forests should be untapped", + currentGame + .getBattlefield() + .getAllActivePermanents() + .stream() + .filter(permanent -> permanent.hasSubtype(SubType.FOREST, currentGame)) + .noneMatch(Permanent::isTapped) + ); + } + + @Ignore // currently failing, need to fix + @Test + public void testWithoutManaAvailable() { + addCard(Zone.BATTLEFIELD, playerA, belt); + addCard(Zone.BATTLEFIELD, playerA, gigantosauras); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", gigantosauras); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertIsAttachedTo(playerA, belt, gigantosauras); + } +} From fa29b8a2a276ef4a87ef4dee8272d6a86c69d8ff Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Fri, 1 Oct 2021 01:58:17 -0400 Subject: [PATCH 215/231] Implement collation for Modern Masters (2013) and Eternal Masters --- Mage.Sets/src/mage/sets/EternalMasters.java | 106 ++++++++++++++++- .../src/mage/sets/InnistradMidnightHunt.java | 3 +- Mage.Sets/src/mage/sets/ModernMasters.java | 109 ++++++++++++++++++ 3 files changed, 215 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/sets/EternalMasters.java b/Mage.Sets/src/mage/sets/EternalMasters.java index a6c0a96d5c1..1604fd2c7b3 100644 --- a/Mage.Sets/src/mage/sets/EternalMasters.java +++ b/Mage.Sets/src/mage/sets/EternalMasters.java @@ -1,10 +1,16 @@ - package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + /** * @author fireshoes */ @@ -277,4 +283,100 @@ public final class EternalMasters extends ExpansionSet { cards.add(new SetCardInfo("Young Pyromancer", 155, Rarity.UNCOMMON, mage.cards.y.YoungPyromancer.class)); cards.add(new SetCardInfo("Zealous Persecution", 213, Rarity.UNCOMMON, mage.cards.z.ZealousPersecution.class)); } -} \ No newline at end of file + + @Override + public BoosterCollator createCollator() { + return new EternalMastersCollator(); + } +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/ema.html +// Using USA collation for all rarities +// Foil rare sheet used for regular rares as regular rare sheet is not known +class EternalMastersCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "25", "85", "153", "4", "84", "117", "36", "104", "126", "1", "116", "129", "24", "107", "138", "27", "94", "136", "29", "111", "149", "8", "100", "139", "3", "80", "125", "14", "86", "152", "31", "103", "126", "4", "84", "153", "36", "85", "120", "25", "104", "117", "1", "111", "149", "27", "107", "129", "24", "94", "136", "8", "116", "139", "3", "80", "138", "29", "100", "152", "31", "103", "125", "14", "86", "120"); + private final CardRun commonB = new CardRun(true, "51", "162", "74", "189", "52", "176", "53", "188", "63", "184", "76", "179", "60", "161", "69", "183", "72", "156", "51", "164", "41", "194", "64", "184", "76", "176", "52", "189", "74", "188", "63", "162", "53", "161", "60", "156", "69", "183", "72", "194", "41", "179", "64", "164", "51", "189", "53", "184", "52", "188", "76", "176", "63", "161", "69", "162", "74", "156", "72", "183", "60", "194", "41", "179", "64", "164"); + private final CardRun commonC1 = new CardRun(true, "237", "165", "243", "144", "88", "236", "71", "6", "140", "115", "238", "109", "146", "20", "239", "47", "89", "128", "244", "23", "247", "237", "137", "75", "178", "110", "21", "144", "45", "243", "115", "163", "6", "140", "71", "88", "238", "128", "165", "239", "75", "109", "236", "137", "178", "20", "45", "89", "146", "21", "163", "47", "244", "110", "23"); + private final CardRun commonC2 = new CardRun(true, "185", "246", "122", "37", "97", "43", "245", "191", "102", "249", "59", "167", "175", "26", "130", "229", "247", "65", "18", "185", "245", "43", "102", "191", "246", "59", "37", "97", "122", "249", "175", "167", "65", "245", "18", "185", "229", "130", "102", "26", "43", "229", "59", "249", "97", "122", "37", "191", "246", "65", "167", "175", "18", "130", "26"); + private final CardRun uncommonA = new CardRun(true, "123", "205", "182", "32", "61", "172", "195", "192", "133", "79", "227", "11", "170", "201", "155", "180", "12", "151", "28", "166", "119", "54", "160", "5", "10", "226", "131", "190", "118", "90", "134", "200", "11", "79", "121", "182", "99", "135", "201", "32", "174", "224", "197", "227", "172", "48", "133", "205", "190", "119", "159", "28", "192", "61", "123", "195", "12", "170", "155", "226", "54", "166", "151", "180", "118", "192", "10", "121", "159", "48", "174", "135", "99", "200", "160", "32", "195", "224", "123", "172", "205", "79", "5", "133", "170", "131", "61", "134", "166", "201", "28", "155", "227", "180", "119", "182", "54", "90", "11", "197", "226", "151", "190", "12", "131", "99", "118", "5", "134", "159", "121", "48", "90", "135", "197", "174", "224", "10", "200", "160"); + private final CardRun uncommonB = new CardRun(true, "68", "101", "212", "15", "233", "66", "81", "30", "58", "141", "92", "213", "55", "105", "34", "83", "77", "113", "221", "217", "235", "142", "67", "242", "157", "13", "208", "44", "91", "230", "168", "73", "209", "40", "19", "58", "231", "78", "35", "218", "212", "81", "141", "95", "233", "68", "92", "30", "142", "101", "66", "15", "213", "34", "55", "105", "217", "83", "67", "235", "44", "113", "242", "221", "77", "95", "168", "78", "13", "68", "208", "230", "157", "91", "73", "141", "105", "19", "40", "209", "66", "231", "81", "30", "218", "101", "58", "83", "212", "77", "142", "92", "213", "15", "233", "113", "55", "34", "67", "221", "35", "217", "44", "91", "235", "168", "242", "13", "157", "208", "95", "218", "73", "19", "231", "78", "209", "40", "230", "35"); + private final CardRun rare = new CardRun(true, "114", "87", "186", "57", "106", "214", "56", "9", "93", "228", "22", "203", "62", "187", "148", "143", "215", "70", "108", "240", "232", "33", "206", "17", "193", "177", "145", "216", "82", "124", "223", "234", "38", "210", "16", "196", "207", "147", "220", "96", "143", "199", "241", "39", "211", "22", "198", "225", "150", "222", "7", "145", "9", "248", "42", "108", "33", "202", "219", "169", "186", "127", "147", "106", "132", "46", "56", "38", "228", "204", "171", "187", "114", "150", "154", "214", "50", "223", "39", "232", "158", "173", "193", "17", "169", "87", "215", "203", "93", "42", "234", "112", "181", "196", "96", "171", "124", "216", "206", "127", "46", "241", "98", "70", "198", "62", "173", "49", "220", "210", "16", "50", "248", "2", "82", "202", "132", "181", "7", "222", "211"); + private final CardRun foilCommon = new CardRun(true, "1", "80", "41", "156", "117", "236", "24", "249", "63", "178", "137", "3", "84", "43", "161", "120", "237", "25", "103", "64", "179", "138", "4", "85", "45", "162", "122", "238", "26", "104", "65", "183", "139", "6", "86", "47", "163", "125", "239", "27", "107", "69", "184", "140", "8", "88", "51", "164", "126", "243", "29", "109", "71", "185", "144", "14", "89", "52", "165", "128", "244", "229", "110", "72", "188", "146", "18", "94", "53", "167", "129", "245", "36", "111", "74", "189", "149", "20", "97", "59", "175", "130", "246", "37", "115", "75", "191", "152", "21", "100", "60", "176", "136", "247", "31", "116", "76", "194", "153", "23", "102"); + private final CardRun foilUncommon = new CardRun(true, "5", "78", "40", "195", "118", "218", "157", "30", "105", "135", "174", "10", "79", "44", "197", "119", "221", "159", "32", "212", "141", "180", "11", "81", "48", "200", "121", "224", "160", "34", "213", "142", "182", "12", "83", "54", "201", "123", "226", "166", "35", "217", "151", "190", "13", "90", "55", "205", "131", "227", "168", "113", "233", "155", "192", "15", "91", "58", "208", "133", "230", "170", "73", "235", "99", "67", "19", "92", "61", "209", "134", "231", "172", "77", "242", "101", "68", "28", "95", "66"); + + private final BoosterStructure AABBC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAAABBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rare); + private final BoosterStructure FC = new BoosterStructure(foilCommon); + private final BoosterStructure FU = new BoosterStructure(foilUncommon); + + // In order for equal numbers of each common to exist, the average booster must contain: + // 3.27 A commons (36 / 11) + // 2.18 B commons (24 / 11) + // 2.73 C1 commons (30 / 11, or 60 / 11 in each C1 booster) + // 1.82 C2 commons (20 / 11, or 40 / 11 in each C2 booster) + // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs + // and with 10 common slots per booster + private final RarityConfiguration commonRuns = new RarityConfiguration( + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2 + ); + private final RarityConfiguration uncommonRuns = new RarityConfiguration(AAB, ABB); + private final RarityConfiguration rareRuns = new RarityConfiguration(R1); + private final RarityConfiguration foilRuns = new RarityConfiguration( + FC, FC, FC, FC, FC, FC, FC, FC, FC, FC, + FU, FU, FU, + R1 + ); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(rareRuns.getNext().makeRun()); + booster.addAll(foilRuns.getNext().makeRun()); + return booster; + } +} diff --git a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java index 3f8f6f286ae..a4201ba0e33 100644 --- a/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java +++ b/Mage.Sets/src/mage/sets/InnistradMidnightHunt.java @@ -379,7 +379,8 @@ class InnistradMidnightHuntCollator implements BoosterCollator { commonDFC ); private final BoosterStructure ABD1 = new BoosterStructure(uncommonA, uncommonB, uncommonDFC1, rare); - private final BoosterStructure BBD1 = new BoosterStructure(uncommonB, uncommonB, uncommonDFC1, rare); private final BoosterStructure ABD2 = new BoosterStructure(uncommonA, uncommonB, uncommonDFC2, rare); + private final BoosterStructure BBD1 = new BoosterStructure(uncommonB, uncommonB, uncommonDFC1, rare); + private final BoosterStructure ABD2 = new BoosterStructure(uncommonA, uncommonB, uncommonDFC2, rare); private final BoosterStructure BBD2 = new BoosterStructure(uncommonB, uncommonB, uncommonDFC2, rare); private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB, rareDFC); private final BoosterStructure L1 = new BoosterStructure(land); diff --git a/Mage.Sets/src/mage/sets/ModernMasters.java b/Mage.Sets/src/mage/sets/ModernMasters.java index 9b87034f21c..ef1807cd2b9 100644 --- a/Mage.Sets/src/mage/sets/ModernMasters.java +++ b/Mage.Sets/src/mage/sets/ModernMasters.java @@ -1,9 +1,16 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.collation.BoosterCollator; +import mage.collation.BoosterStructure; +import mage.collation.CardRun; +import mage.collation.RarityConfiguration; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.ArrayList; +import java.util.List; + /** * @author LevelX2 */ @@ -256,4 +263,106 @@ public final class ModernMasters extends ExpansionSet { cards.add(new SetCardInfo("Yosei, the Morning Star", 35, Rarity.MYTHIC, mage.cards.y.YoseiTheMorningStar.class)); } + @Override + public BoosterCollator createCollator() { + return new ModernMastersCollator(); + } +} + +// Booster collation info from https://www.lethe.xyz/mtg/collation/mma.html +// Using USA collation for all rarities +// Foil rare sheet used for regular rares as regular rare sheet is not known +class ModernMastersCollator implements BoosterCollator { + private final CardRun commonA = new CardRun(true, "156", "6", "207", "69", "215", "81", "61", "148", "3", "200", "112", "157", "36", "79", "11", "21", "107", "41", "146", "86", "207", "2", "50", "119", "156", "101", "34", "139", "41", "145", "79", "3", "121", "98", "69", "157", "21", "112", "101", "146", "50", "86", "34", "119", "145", "136", "61", "103", "6", "215", "168", "81", "39", "200", "11", "107", "148", "98", "36", "121", "2", "168", "136", "39", "103", "139"); + private final CardRun commonB = new CardRun(true, "73", "51", "113", "140", "18", "94", "68", "212", "169", "87", "113", "42", "140", "7", "224", "126", "73", "19", "167", "60", "169", "116", "85", "224", "147", "126", "18", "60", "94", "137", "212", "12", "51", "116", "85", "18", "68", "140", "137", "73", "42", "167", "7", "87", "51", "126", "19", "224", "42", "94", "113", "169", "7", "212", "87", "147", "60", "12", "137", "167", "85", "19", "68", "116", "147", "12"); + private final CardRun commonC1 = new CardRun(true, "77", "9", "122", "44", "151", "127", "161", "23", "71", "53", "105", "214", "143", "8", "104", "199", "131", "45", "17", "80", "59", "161", "132", "71", "100", "33", "44", "155", "201", "127", "17", "77", "65", "105", "165", "53", "9", "80", "131", "201", "151", "45", "104", "23", "122", "165", "100", "65", "33", "143", "132", "8", "199", "59", "155"); + private final CardRun commonC2 = new CardRun(true, "202", "163", "40", "58", "114", "57", "196", "173", "109", "95", "209", "163", "58", "115", "196", "92", "142", "40", "114", "95", "163", "27", "214", "99", "109", "24", "142", "202", "115", "57", "28", "173", "58", "27", "99", "209", "114", "92", "28", "202", "109", "196", "27", "57", "95", "173", "24", "92", "115", "40", "28", "209", "142", "99", "24"); + private final CardRun uncommonA = new CardRun(true, "66", "191", "211", "227", "144", "90", "192", "29", "64", "133", "228", "91", "184", "159", "10", "67", "227", "190", "162", "211", "206", "133", "181", "225", "159", "110", "66", "22", "191", "184", "154", "226", "37", "72", "194", "129", "144", "213", "138", "192", "206", "64", "16", "225", "179", "90", "162", "110", "10", "190", "213", "37", "194", "91", "138", "29", "226", "181", "67", "16", "154", "72", "179", "228", "22", "129"); + private final CardRun uncommonB = new CardRun(true, "83", "229", "55", "124", "25", "222", "78", "195", "62", "171", "32", "93", "210", "124", "54", "15", "222", "149", "88", "118", "55", "30", "185", "152", "62", "134", "205", "25", "158", "83", "43", "135", "175", "15", "152", "88", "54", "134", "185", "78", "171", "229", "32", "118", "195", "93", "158", "210", "43", "175", "205", "135", "30", "149"); + private final CardRun rare = new CardRun(true, "26", "96", "117", "178", "218", "141", "150", "111", "219", "197", "89", "204", "47", "70", "178", "198", "84", "13", "108", "75", "123", "177", "172", "4", "164", "120", "74", "31", "46", "186", "153", "97", "26", "197", "187", "76", "170", "160", "221", "188", "5", "56", "102", "38", "1", "193", "203", "97", "4", "141", "180", "125", "84", "20", "49", "189", "52", "123", "76", "172", "5", "106", "56", "31", "174", "47", "208", "220", "177", "38", "223", "49", "108", "117", "102", "82", "176", "48", "128", "96", "187", "63", "223", "182", "111", "188", "153", "203", "204", "130", "216", "189", "14", "1", "52", "166", "46", "217", "174", "183", "198", "74", "63", "125", "219", "160", "180", "130", "176", "20", "193", "220", "164", "106", "208", "170", "14", "221", "82", "35", "186"); + private final CardRun foilCommon = new CardRun(true, "201", "79", "132", "8", "168", "41", "80", "12", "139", "103", "50", "215", "24", "148", "92", "142", "196", "59", "114", "212", "104", "146", "169", "28", "81", "40", "21", "207", "131", "101", "58", "19", "157", "73", "119", "140", "200", "57", "27", "199", "112", "151", "77", "224", "107", "33", "61", "94", "202", "121", "143", "87", "23", "60", "98", "214", "65", "173", "3", "105", "39", "209", "7", "36", "115", "167", "99", "69", "6", "116", "163", "95", "145", "9", "53", "136", "155", "113", "161", "68", "34", "100", "156", "45", "18", "122", "71", "51", "126", "165", "2", "85", "127", "147", "42", "17", "137", "44", "109", "86", "11"); + private final CardRun foilUncommon = new CardRun(true, "144", "206", "190", "124", "158", "67", "22", "83", "227", "25", "175", "210", "229", "78", "211", "37", "10", "162", "213", "134", "90", "29", "185", "171", "88", "64", "15", "228", "30", "159", "37", "191", "124", "195", "67", "91", "25", "78", "134", "162", "62", "149", "229", "22", "64", "138", "110", "175", "192", "90", "152", "91", "135", "154", "129", "15", "55", "30", "184", "54", "225", "133", "152", "228", "110", "83", "149", "93", "226", "154", "210", "181", "138", "54", "158", "184", "225", "72", "66", "227", "16", "205", "118", "62", "194", "179", "10", "211", "185", "222", "43", "206", "192", "144", "29", "195", "88", "205", "179", "191", "32", "129", "213", "226", "55", "222", "133", "190", "72", "135", "93", "32", "43", "171", "194", "181", "66", "118", "159", "16"); + + private final BoosterStructure AABBC1C1C1C1C1C1 = new BoosterStructure( + commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAABBC1C1C1C1C1 = new BoosterStructure( + commonA, commonA, commonA, + commonB, commonB, + commonC1, commonC1, commonC1, commonC1, commonC1 + ); + private final BoosterStructure AAAABBC2C2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, + commonC2, commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAAABBBC2C2C2 = new BoosterStructure( + commonA, commonA, commonA, commonA, + commonB, commonB, commonB, + commonC2, commonC2, commonC2 + ); + private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB); + private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB); + private final BoosterStructure R1 = new BoosterStructure(rare); + private final BoosterStructure FC = new BoosterStructure(foilCommon); + private final BoosterStructure FU = new BoosterStructure(foilUncommon); + + // In order for equal numbers of each common to exist, the average booster must contain: + // 3.27 A commons (36 / 11) + // 2.18 B commons (24 / 11) + // 2.73 C1 commons (30 / 11, or 60 / 11 in each C1 booster) + // 1.82 C2 commons (20 / 11, or 40 / 11 in each C2 booster) + // These numbers are the same for all sets with 101 commons in A/B/C1/C2 print runs + // and with 10 common slots per booster + private final RarityConfiguration commonRuns = new RarityConfiguration( + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AABBC1C1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + AAABBC1C1C1C1C1, + + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBC2C2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2, + AAAABBBC2C2C2 + ); + // In order for equal numbers of each uncommon to exist, the average booster must contain: + // 1.65 A uncommons (33 / 20) + // 1.35 B uncommons (27 / 20) + // These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs + private final RarityConfiguration uncommonRuns = new RarityConfiguration( + AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, + ABB, ABB, ABB, ABB, ABB, ABB, ABB + ); + private final RarityConfiguration rareRuns = new RarityConfiguration(R1); + private final RarityConfiguration foilRuns = new RarityConfiguration( + FC, FC, FC, FC, FC, FC, FC, FC, FC, FC, + FU, FU, FU, + R1 + ); + + @Override + public List makeBooster() { + List booster = new ArrayList<>(); + booster.addAll(commonRuns.getNext().makeRun()); + booster.addAll(uncommonRuns.getNext().makeRun()); + booster.addAll(rareRuns.getNext().makeRun()); + booster.addAll(foilRuns.getNext().makeRun()); + return booster; + } } From c899c332b83b3a58617089988ca22d1ae5734f25 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 1 Oct 2021 13:41:54 +0400 Subject: [PATCH 216/231] Dev: updated json lib, added tests; --- .../card/dl/sources/ScryfallImageSource.java | 4 +- Mage.Common/pom.xml | 16 +- .../org/mage/test/utils/JsonGsonTest.java | 37 +++++ .../src/test/resources/scryfall-card.json | 154 ++++++++++++++++++ 4 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java create mode 100644 Mage.Tests/src/test/resources/scryfall-card.json diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java index 96fcb74f8df..ad2ba5dae1a 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java @@ -173,9 +173,7 @@ public enum ScryfallImageSource implements CardImageSource { } // OK, found card data, parse it - JsonParser jp = new JsonParser(); - JsonElement root = jp.parse(new InputStreamReader(jsonStream)); - JsonObject jsonCard = root.getAsJsonObject(); + JsonObject jsonCard = JsonParser.parseReader(new InputStreamReader(jsonStream)).getAsJsonObject(); if (!jsonCard.has("card_faces")) { throw new MageException("Couldn't find card_faces in card's JSON data: " + jsonUrl); } diff --git a/Mage.Common/pom.xml b/Mage.Common/pom.xml index c6d83d8238f..aeb8fc37bb5 100644 --- a/Mage.Common/pom.xml +++ b/Mage.Common/pom.xml @@ -20,11 +20,18 @@ mage ${project.version} + com.googlecode.jspf jspf-core 0.9.1 + + + com.google.code.gson + gson + 2.8.8 + org.jboss.remoting @@ -41,21 +48,19 @@ jboss-serialization 4.2.2.GA + concurrent concurrent 1.3.4 + trove trove 1.0.2 - - com.google.code.gson - gson - 2.8.6 - + org.junit.jupiter junit-jupiter @@ -72,6 +77,7 @@ test + diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java new file mode 100644 index 00000000000..beb358fd842 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java @@ -0,0 +1,37 @@ +package org.mage.test.utils; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.Assert; +import org.junit.Test; + +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Paths; + +/** + * Gson (Google json) lib uses for scryfall and mtgjson data. + *

+ * Tests: + * - unknown data parser tests here (JsonParser.parseReader) + * - class parser tests in mtgjson verify tests (new Gson().fromJson) + */ +public class JsonGsonTest { + + @Test + public void test_ReadByStreamParser() { + String sampleFileName = Paths.get("src", "test", "resources", "scryfall-card.json").toString(); + try { + // low level parser for unknown data structor + JsonObject json = JsonParser.parseReader(new FileReader(sampleFileName)).getAsJsonObject(); + Assert.assertEquals("Unknown data", "card", json.get("object").getAsString()); + JsonArray jsonFaces = json.getAsJsonArray("card_faces"); + Assert.assertEquals("Card must have 2 faces", 2, jsonFaces.size()); + Assert.assertEquals("Unknown second side", "Infectious Curse", jsonFaces.get(1).getAsJsonObject().get("name").getAsString()); + } catch (IOException e) { + e.printStackTrace(); + Assert.fail("Can't load sample json file: " + sampleFileName); + } + } +} diff --git a/Mage.Tests/src/test/resources/scryfall-card.json b/Mage.Tests/src/test/resources/scryfall-card.json new file mode 100644 index 00000000000..80cde7e0cf2 --- /dev/null +++ b/Mage.Tests/src/test/resources/scryfall-card.json @@ -0,0 +1,154 @@ +{ + "object": "card", + "id": "8093f8b0-5d50-48ca-b616-e92535a47138", + "oracle_id": "80b07882-e144-4815-8b6f-04b3ab343d97", + "multiverse_ids": [ + 409843, + 409844 + ], + "mtgo_id": 60370, + "mtgo_foil_id": 60371, + "tcgplayer_id": 116486, + "cardmarket_id": 289120, + "name": "Accursed Witch // Infectious Curse", + "lang": "en", + "released_at": "2016-04-08", + "uri": "https://api.scryfall.com/cards/8093f8b0-5d50-48ca-b616-e92535a47138", + "scryfall_uri": "https://scryfall.com/card/soi/97/accursed-witch-infectious-curse?utm_source=api", + "layout": "transform", + "highres_image": true, + "image_status": "highres_scan", + "cmc": 4.0, + "type_line": "Creature — Human Shaman // Enchantment — Aura Curse", + "color_identity": [ + "B" + ], + "keywords": [ + "Enchant" + ], + "card_faces": [ + { + "object": "card_face", + "name": "Accursed Witch", + "mana_cost": "{3}{B}", + "type_line": "Creature — Human Shaman", + "oracle_text": "Spells your opponents cast that target Accursed Witch cost {1} less to cast.\nWhen Accursed Witch dies, return it to the battlefield transformed under your control attached to target opponent.", + "colors": [ + "B" + ], + "power": "4", + "toughness": "2", + "artist": "Wesley Burt", + "artist_id": "b98c9d94-8bdb-49af-871d-7bac92274535", + "illustration_id": "e648ea98-8935-4b00-b3d9-d4d1e98026d8", + "image_uris": { + "small": "https://c1.scryfall.com/file/scryfall-cards/small/front/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328", + "normal": "https://c1.scryfall.com/file/scryfall-cards/normal/front/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328", + "large": "https://c1.scryfall.com/file/scryfall-cards/large/front/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328", + "png": "https://c1.scryfall.com/file/scryfall-cards/png/front/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.png?1576384328", + "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/front/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328", + "border_crop": "https://c1.scryfall.com/file/scryfall-cards/border_crop/front/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328" + } + }, + { + "object": "card_face", + "name": "Infectious Curse", + "mana_cost": "", + "type_line": "Enchantment — Aura Curse", + "oracle_text": "Enchant player\nSpells you cast that target enchanted player cost {1} less to cast.\nAt the beginning of enchanted player's upkeep, that player loses 1 life and you gain 1 life.", + "colors": [ + "B" + ], + "color_indicator": [ + "B" + ], + "artist": "Wesley Burt", + "artist_id": "b98c9d94-8bdb-49af-871d-7bac92274535", + "illustration_id": "828863bf-ddb7-4719-bcfd-2a20c667829f", + "image_uris": { + "small": "https://c1.scryfall.com/file/scryfall-cards/small/back/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328", + "normal": "https://c1.scryfall.com/file/scryfall-cards/normal/back/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328", + "large": "https://c1.scryfall.com/file/scryfall-cards/large/back/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328", + "png": "https://c1.scryfall.com/file/scryfall-cards/png/back/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.png?1576384328", + "art_crop": "https://c1.scryfall.com/file/scryfall-cards/art_crop/back/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328", + "border_crop": "https://c1.scryfall.com/file/scryfall-cards/border_crop/back/8/0/8093f8b0-5d50-48ca-b616-e92535a47138.jpg?1576384328" + } + } + ], + "legalities": { + "standard": "not_legal", + "future": "not_legal", + "historic": "not_legal", + "gladiator": "not_legal", + "pioneer": "legal", + "modern": "legal", + "legacy": "legal", + "pauper": "not_legal", + "vintage": "legal", + "penny": "not_legal", + "commander": "legal", + "brawl": "not_legal", + "historicbrawl": "not_legal", + "paupercommander": "restricted", + "duel": "legal", + "oldschool": "not_legal", + "premodern": "not_legal" + }, + "games": [ + "paper", + "mtgo" + ], + "reserved": false, + "foil": true, + "nonfoil": true, + "finishes": [ + "nonfoil", + "foil" + ], + "oversized": false, + "promo": false, + "reprint": false, + "variation": false, + "set_id": "5e914d7e-c1e9-446c-a33d-d093c02b2743", + "set": "soi", + "set_name": "Shadows over Innistrad", + "set_type": "expansion", + "set_uri": "https://api.scryfall.com/sets/5e914d7e-c1e9-446c-a33d-d093c02b2743", + "set_search_uri": "https://api.scryfall.com/cards/search?order=set&q=e%3Asoi&unique=prints", + "scryfall_set_uri": "https://scryfall.com/sets/soi?utm_source=api", + "rulings_uri": "https://api.scryfall.com/cards/8093f8b0-5d50-48ca-b616-e92535a47138/rulings", + "prints_search_uri": "https://api.scryfall.com/cards/search?order=released&q=oracleid%3A80b07882-e144-4815-8b6f-04b3ab343d97&unique=prints", + "collector_number": "97", + "digital": false, + "rarity": "uncommon", + "card_back_id": "0aeebaf5-8c7d-4636-9e82-8c27447861f7", + "artist": "Wesley Burt", + "artist_ids": [ + "b98c9d94-8bdb-49af-871d-7bac92274535" + ], + "border_color": "black", + "frame": "2015", + "frame_effects": [ + "sunmoondfc" + ], + "full_art": false, + "textless": false, + "booster": true, + "story_spotlight": false, + "edhrec_rank": 6115, + "prices": { + "usd": "0.31", + "usd_foil": "0.77", + "usd_etched": null, + "eur": "0.16", + "eur_foil": null, + "tix": "0.03" + }, + "related_uris": { + "gatherer": "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=409843", + "tcgplayer_infinite_articles": "https://infinite.tcgplayer.com/search?contentMode=article&game=magic&partner=scryfall&q=Accursed+Witch+%2F%2F+Infectious+Curse&utm_campaign=affiliate&utm_medium=api&utm_source=scryfall", + "tcgplayer_infinite_decks": "https://infinite.tcgplayer.com/search?contentMode=deck&game=magic&partner=scryfall&q=Accursed+Witch+%2F%2F+Infectious+Curse&utm_campaign=affiliate&utm_medium=api&utm_source=scryfall", + "edhrec": "https://edhrec.com/route/?cc=Accursed+Witch", + "mtgtop8": "https://mtgtop8.com/search?MD_check=1&SB_check=1&cards=Accursed+Witch" + } +} \ No newline at end of file From 4e6de42e71d0a4cd6103ebd41084dba6d44d6981 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 09:43:33 +0000 Subject: [PATCH 217/231] Bump jaxb-impl from 2.3.3 to 3.0.2 Bumps jaxb-impl from 2.3.3 to 3.0.2. --- updated-dependencies: - dependency-name: com.sun.xml.bind:jaxb-impl dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- Mage.Server/pom.xml | 2 +- Mage.Tests/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mage.Server/pom.xml b/Mage.Server/pom.xml index 7af167954cf..74d8ac03c9e 100644 --- a/Mage.Server/pom.xml +++ b/Mage.Server/pom.xml @@ -42,7 +42,7 @@ com.sun.xml.bind jaxb-impl - 2.3.3 + 3.0.2 diff --git a/Mage.Tests/pom.xml b/Mage.Tests/pom.xml index 1f54378e9dd..f9489fd45aa 100644 --- a/Mage.Tests/pom.xml +++ b/Mage.Tests/pom.xml @@ -64,7 +64,7 @@ com.sun.xml.bind jaxb-impl - 2.3.3 + 3.0.2 From fa70af6131e6eed275997f0bdd6f3dd0146a9b8a Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 1 Oct 2021 14:10:39 +0400 Subject: [PATCH 218/231] Dev: removed test data from resources to data folder; --- .../src/test/{resources => data}/config_error.xml | 0 .../java/mage/server/util/ConfigFactoryTest.java | 2 +- Mage.Tests/src/test/{resources => data}/images.zip | Bin .../src/test/{resources => data}/scryfall-card.json | 0 .../test/java/org/mage/test/utils/JsonGsonTest.java | 4 ++-- .../org/mage/test/utils/ZipFilesReadWriteTest.java | 12 ++++++------ .../importer/samples => data/importer}/testdeck.cod | 0 .../importer/samples => data/importer}/testdeck.dec | 0 .../samples => data/importer}/testdeck.draft | 0 .../samples => data/importer}/testdeck.json | 0 .../samples => data/importer}/testdeck.mtga | 0 .../samples => data/importer}/testdeck.mwDeck | 0 .../importer/samples => data/importer}/testdeck.o8d | 0 .../cards/decks/importer/CodDeckImportTest.java | 7 ++++++- .../cards/decks/importer/DecDeckImportTest.java | 7 ++++++- .../cards/decks/importer/DraftLogImporterTest.java | 7 ++++++- .../mage/cards/decks/importer/MtgaImporterTest.java | 7 ++++++- .../cards/decks/importer/MtgjsonDeckImportTest.java | 7 ++++++- .../cards/decks/importer/MwsDeckImportTest.java | 7 ++++++- .../cards/decks/importer/O8dDeckImportTest.java | 7 ++++++- 20 files changed, 51 insertions(+), 16 deletions(-) rename Mage.Server/src/test/{resources => data}/config_error.xml (100%) rename Mage.Tests/src/test/{resources => data}/images.zip (100%) rename Mage.Tests/src/test/{resources => data}/scryfall-card.json (100%) rename Mage/src/test/{java/mage/cards/decks/importer/samples => data/importer}/testdeck.cod (100%) rename Mage/src/test/{java/mage/cards/decks/importer/samples => data/importer}/testdeck.dec (100%) rename Mage/src/test/{java/mage/cards/decks/importer/samples => data/importer}/testdeck.draft (100%) rename Mage/src/test/{java/mage/cards/decks/importer/samples => data/importer}/testdeck.json (100%) rename Mage/src/test/{java/mage/cards/decks/importer/samples => data/importer}/testdeck.mtga (100%) rename Mage/src/test/{java/mage/cards/decks/importer/samples => data/importer}/testdeck.mwDeck (100%) rename Mage/src/test/{java/mage/cards/decks/importer/samples => data/importer}/testdeck.o8d (100%) diff --git a/Mage.Server/src/test/resources/config_error.xml b/Mage.Server/src/test/data/config_error.xml similarity index 100% rename from Mage.Server/src/test/resources/config_error.xml rename to Mage.Server/src/test/data/config_error.xml diff --git a/Mage.Server/src/test/java/mage/server/util/ConfigFactoryTest.java b/Mage.Server/src/test/java/mage/server/util/ConfigFactoryTest.java index 3563287fad1..c487037479b 100644 --- a/Mage.Server/src/test/java/mage/server/util/ConfigFactoryTest.java +++ b/Mage.Server/src/test/java/mage/server/util/ConfigFactoryTest.java @@ -24,7 +24,7 @@ public class ConfigFactoryTest { @DisplayName("should fail if config is malformed") void failOnMalformed() { assertThatExceptionOfType(ConfigurationException.class) - .isThrownBy(() -> ConfigFactory.loadFromFile(Paths.get("src", "test", "resources", "config_error.xml").toString())); + .isThrownBy(() -> ConfigFactory.loadFromFile(Paths.get("src", "test", "data", "config_error.xml").toString())); } @Test diff --git a/Mage.Tests/src/test/resources/images.zip b/Mage.Tests/src/test/data/images.zip similarity index 100% rename from Mage.Tests/src/test/resources/images.zip rename to Mage.Tests/src/test/data/images.zip diff --git a/Mage.Tests/src/test/resources/scryfall-card.json b/Mage.Tests/src/test/data/scryfall-card.json similarity index 100% rename from Mage.Tests/src/test/resources/scryfall-card.json rename to Mage.Tests/src/test/data/scryfall-card.json diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java index beb358fd842..9ce4cb2f417 100644 --- a/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java @@ -21,9 +21,9 @@ public class JsonGsonTest { @Test public void test_ReadByStreamParser() { - String sampleFileName = Paths.get("src", "test", "resources", "scryfall-card.json").toString(); + String sampleFileName = Paths.get("src", "test", "data", "scryfall-card.json").toString(); try { - // low level parser for unknown data structor + // low level parser for unknown data structure JsonObject json = JsonParser.parseReader(new FileReader(sampleFileName)).getAsJsonObject(); Assert.assertEquals("Unknown data", "card", json.get("object").getAsString()); JsonArray jsonFaces = json.getAsJsonArray("card_faces"); diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/ZipFilesReadWriteTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/ZipFilesReadWriteTest.java index a88497f3607..f90aa7e5d9b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/utils/ZipFilesReadWriteTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/utils/ZipFilesReadWriteTest.java @@ -26,19 +26,19 @@ public class ZipFilesReadWriteTest { @Test public void test_Read() { // exists - TFile fileZip = new TFile(Paths.get("src", "test", "resources", "images.zip").toString()); + TFile fileZip = new TFile(Paths.get("src", "test", "data", "images.zip").toString()); Assert.assertTrue(fileZip.exists()); - TFile fileZipDir = new TFile(Paths.get("src", "test", "resources", "images.zip", "SET").toString()); + TFile fileZipDir = new TFile(Paths.get("src", "test", "data", "images.zip", "SET").toString()); Assert.assertTrue(fileZipDir.exists()); - TFile fileZipFile = new TFile(Paths.get("src", "test", "resources", "images.zip", "SET", "image1.png").toString()); + TFile fileZipFile = new TFile(Paths.get("src", "test", "data", "images.zip", "SET", "image1.png").toString()); Assert.assertTrue(fileZipFile.exists()); // not exists - TFile fileNotZip = new TFile(Paths.get("src", "test", "resources", "images-FAIL.zip").toString()); + TFile fileNotZip = new TFile(Paths.get("src", "test", "data", "images-FAIL.zip").toString()); Assert.assertFalse(fileNotZip.exists()); - TFile fileNotZipDir = new TFile(Paths.get("src", "test", "resources", "images.zip", "SET-FAIL").toString()); + TFile fileNotZipDir = new TFile(Paths.get("src", "test", "data", "images.zip", "SET-FAIL").toString()); Assert.assertFalse(fileNotZipDir.exists()); - TFile fileNotZipFile = new TFile(Paths.get("src", "test", "resources", "images.zip", "SET", "image1-FAIL.png").toString()); + TFile fileNotZipFile = new TFile(Paths.get("src", "test", "data", "images.zip", "SET", "image1-FAIL.png").toString()); Assert.assertFalse(fileNotZipFile.exists()); // reading diff --git a/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.cod b/Mage/src/test/data/importer/testdeck.cod similarity index 100% rename from Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.cod rename to Mage/src/test/data/importer/testdeck.cod diff --git a/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.dec b/Mage/src/test/data/importer/testdeck.dec similarity index 100% rename from Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.dec rename to Mage/src/test/data/importer/testdeck.dec diff --git a/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.draft b/Mage/src/test/data/importer/testdeck.draft similarity index 100% rename from Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.draft rename to Mage/src/test/data/importer/testdeck.draft diff --git a/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.json b/Mage/src/test/data/importer/testdeck.json similarity index 100% rename from Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.json rename to Mage/src/test/data/importer/testdeck.json diff --git a/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.mtga b/Mage/src/test/data/importer/testdeck.mtga similarity index 100% rename from Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.mtga rename to Mage/src/test/data/importer/testdeck.mtga diff --git a/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.mwDeck b/Mage/src/test/data/importer/testdeck.mwDeck similarity index 100% rename from Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.mwDeck rename to Mage/src/test/data/importer/testdeck.mwDeck diff --git a/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.o8d b/Mage/src/test/data/importer/testdeck.o8d similarity index 100% rename from Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.o8d rename to Mage/src/test/data/importer/testdeck.o8d diff --git a/Mage/src/test/java/mage/cards/decks/importer/CodDeckImportTest.java b/Mage/src/test/java/mage/cards/decks/importer/CodDeckImportTest.java index c6ccb497143..c087b837ce8 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/CodDeckImportTest.java +++ b/Mage/src/test/java/mage/cards/decks/importer/CodDeckImportTest.java @@ -3,6 +3,8 @@ package mage.cards.decks.importer; import mage.cards.decks.DeckCardLists; import org.junit.Test; +import java.nio.file.Paths; + import static org.junit.Assert.assertEquals; public class CodDeckImportTest { @@ -23,7 +25,10 @@ public class CodDeckImportTest { }; StringBuilder errors = new StringBuilder(); DeckCardLists deck = importer.importDeck( - "src/test/java/mage/cards/decks/importer/samples/testdeck.cod", errors, false); + Paths.get("src", "test", "data", "importer", "testdeck.cod").toString(), + errors, + false + ); assertEquals("Deck Name", deck.getName()); TestDeckChecker.checker() diff --git a/Mage/src/test/java/mage/cards/decks/importer/DecDeckImportTest.java b/Mage/src/test/java/mage/cards/decks/importer/DecDeckImportTest.java index 1c7078d6ef0..8dd592af013 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/DecDeckImportTest.java +++ b/Mage/src/test/java/mage/cards/decks/importer/DecDeckImportTest.java @@ -3,6 +3,8 @@ package mage.cards.decks.importer; import mage.cards.decks.DeckCardLists; import org.junit.Test; +import java.nio.file.Paths; + import static org.junit.Assert.assertEquals; public class DecDeckImportTest { @@ -19,7 +21,10 @@ public class DecDeckImportTest { } }; DeckCardLists deck = importer.importDeck( - "src/test/java/mage/cards/decks/importer/samples/testdeck.dec", errors, false); + Paths.get("src", "test", "data", "importer", "testdeck.dec").toString(), + errors, + false + ); TestDeckChecker.checker() .addMain("Masticore", 4) diff --git a/Mage/src/test/java/mage/cards/decks/importer/DraftLogImporterTest.java b/Mage/src/test/java/mage/cards/decks/importer/DraftLogImporterTest.java index 80a2ea9309e..38a044e8150 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/DraftLogImporterTest.java +++ b/Mage/src/test/java/mage/cards/decks/importer/DraftLogImporterTest.java @@ -3,6 +3,8 @@ package mage.cards.decks.importer; import mage.cards.decks.DeckCardLists; import org.junit.Test; +import java.nio.file.Paths; + import static org.junit.Assert.assertEquals; public class DraftLogImporterTest { @@ -19,7 +21,10 @@ public class DraftLogImporterTest { } }; DeckCardLists deck = importer.importDeck( - "src/test/java/mage/cards/decks/importer/samples/testdeck.draft", errors, false); + Paths.get("src", "test", "data", "importer", "testdeck.draft").toString(), + errors, + false + ); TestDeckChecker.checker() .addMain("Raging Ravine", 1) diff --git a/Mage/src/test/java/mage/cards/decks/importer/MtgaImporterTest.java b/Mage/src/test/java/mage/cards/decks/importer/MtgaImporterTest.java index f4d186ac5c3..54632b4fd63 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/MtgaImporterTest.java +++ b/Mage/src/test/java/mage/cards/decks/importer/MtgaImporterTest.java @@ -3,6 +3,8 @@ package mage.cards.decks.importer; import mage.cards.decks.DeckCardLists; import org.junit.Test; +import java.nio.file.Paths; + import static org.junit.Assert.assertEquals; public class MtgaImporterTest { @@ -19,7 +21,10 @@ public class MtgaImporterTest { }; StringBuilder errors = new StringBuilder(); DeckCardLists deck = importer.importDeck( - "src/test/java/mage/cards/decks/importer/samples/testdeck.mtga", errors, false); + Paths.get("src", "test", "data", "importer", "testdeck.mtga").toString(), + errors, + false + ); TestDeckChecker.checker() .addMain("Niv-Mizzet Reborn", 1) diff --git a/Mage/src/test/java/mage/cards/decks/importer/MtgjsonDeckImportTest.java b/Mage/src/test/java/mage/cards/decks/importer/MtgjsonDeckImportTest.java index 84bb4cb1ade..00d28cae702 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/MtgjsonDeckImportTest.java +++ b/Mage/src/test/java/mage/cards/decks/importer/MtgjsonDeckImportTest.java @@ -3,6 +3,8 @@ package mage.cards.decks.importer; import mage.cards.decks.DeckCardLists; import org.junit.Test; +import java.nio.file.Paths; + import static org.junit.Assert.assertEquals; public class MtgjsonDeckImportTest { @@ -21,7 +23,10 @@ public class MtgjsonDeckImportTest { // offline deck from https://mtgjson.com/api/v5/decks/ArcaneTempo_GRN.json DeckCardLists deck = importer.importDeck( - "src/test/java/mage/cards/decks/importer/samples/testdeck.json", errors, false); + Paths.get("src", "test", "data", "importer", "testdeck.json").toString(), + errors, + false + ); assertEquals("Arcane Tempo", deck.getName()); TestDeckChecker.checker() .addMain("Goblin Electromancer", 4) diff --git a/Mage/src/test/java/mage/cards/decks/importer/MwsDeckImportTest.java b/Mage/src/test/java/mage/cards/decks/importer/MwsDeckImportTest.java index ef2b1a96bae..f449e1f7105 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/MwsDeckImportTest.java +++ b/Mage/src/test/java/mage/cards/decks/importer/MwsDeckImportTest.java @@ -3,6 +3,8 @@ package mage.cards.decks.importer; import mage.cards.decks.DeckCardLists; import org.junit.Test; +import java.nio.file.Paths; + import static org.junit.Assert.assertEquals; public class MwsDeckImportTest { @@ -19,7 +21,10 @@ public class MwsDeckImportTest { }; StringBuilder errors = new StringBuilder(); DeckCardLists deck = importer.importDeck( - "src/test/java/mage/cards/decks/importer/samples/testdeck.mwDeck", errors, false); + Paths.get("src", "test", "data", "importer", "testdeck.mwDeck").toString(), + errors, + false + ); TestDeckChecker.checker() .addMain("Mutavault", 4) diff --git a/Mage/src/test/java/mage/cards/decks/importer/O8dDeckImportTest.java b/Mage/src/test/java/mage/cards/decks/importer/O8dDeckImportTest.java index be230c36bac..c5a51c30d69 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/O8dDeckImportTest.java +++ b/Mage/src/test/java/mage/cards/decks/importer/O8dDeckImportTest.java @@ -3,6 +3,8 @@ package mage.cards.decks.importer; import mage.cards.decks.DeckCardLists; import org.junit.Test; +import java.nio.file.Paths; + import static org.junit.Assert.assertEquals; public class O8dDeckImportTest { @@ -19,7 +21,10 @@ public class O8dDeckImportTest { }; StringBuilder errors = new StringBuilder(); DeckCardLists deck = importer.importDeck( - "src/test/java/mage/cards/decks/importer/samples/testdeck.o8d", errors, false); + Paths.get("src", "test", "data", "importer", "testdeck.o8d").toString(), + errors, + false + ); TestDeckChecker.checker() .addMain("Forest", 1) From 1c6eb10bd1f0da15b8252bacca47d0187d64e928 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 1 Oct 2021 17:57:54 +0400 Subject: [PATCH 219/231] Dev: migrated to single json lib (removed json-simple from deck import code), clean up guava lib usage in cards; --- .../card/dl/sources/ScryfallImageSource.java | 16 +++--- Mage.Common/pom.xml | 11 ++-- .../mage/cards/a/AkromaVisionOfIxidor.java | 7 +-- .../src/mage/cards/c/ConfrontThePast.java | 4 +- .../mage/cards/e/EcologicalAppreciation.java | 7 ++- .../src/mage/cards/k/KitesailSkirmisher.java | 4 +- .../src/mage/cards/t/TayamLuminousEnigma.java | 5 +- .../org/mage/test/utils/JsonGsonTest.java | 14 ++++-- Mage/pom.xml | 9 ++-- .../cards/decks/importer/CodDeckImporter.java | 4 +- .../cards/decks/importer/DeckImporter.java | 6 +-- .../decks/importer/JsonDeckImporter.java | 29 +++++------ .../decks/importer/MtgjsonDeckImporter.java | 37 ++++++++------ .../cards/decks/importer/O8dDeckImporter.java | 4 +- .../decks/importer/PlainTextDeckImporter.java | 10 ++-- Mage/src/main/java/mage/util/JsonUtil.java | 50 +++++++++++++++++++ pom.xml | 6 +++ 17 files changed, 147 insertions(+), 76 deletions(-) create mode 100644 Mage/src/main/java/mage/util/JsonUtil.java diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java index ad2ba5dae1a..31688a61c3f 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java @@ -1,11 +1,11 @@ package org.mage.plugins.card.dl.sources; import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import mage.MageException; import mage.client.util.CardLanguage; +import mage.util.JsonUtil; import org.apache.log4j.Logger; import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; @@ -174,14 +174,18 @@ public enum ScryfallImageSource implements CardImageSource { // OK, found card data, parse it JsonObject jsonCard = JsonParser.parseReader(new InputStreamReader(jsonStream)).getAsJsonObject(); - if (!jsonCard.has("card_faces")) { + JsonArray jsonFaces = JsonUtil.getAsArray(jsonCard, "card_faces"); + if (jsonFaces == null) { throw new MageException("Couldn't find card_faces in card's JSON data: " + jsonUrl); } - JsonArray jsonCardFaces = jsonCard.getAsJsonArray("card_faces"); - JsonObject jsonCardFace = jsonCardFaces.get(card.isSecondSide() ? 1 : 0).getAsJsonObject(); - JsonObject jsonImageUris = jsonCardFace.getAsJsonObject("image_uris"); - return jsonImageUris.get("large").getAsString(); + JsonObject jsonFace = jsonFaces.get(card.isSecondSide() ? 1 : 0).getAsJsonObject(); + JsonObject jsonImages = JsonUtil.getAsObject(jsonFace, "image_uris"); + if (jsonImages == null) { + throw new MageException("Couldn't find image_uris in card's JSON data: " + jsonUrl); + } + + return JsonUtil.getAsString(jsonImages, "large"); } @Override diff --git a/Mage.Common/pom.xml b/Mage.Common/pom.xml index aeb8fc37bb5..4173847a931 100644 --- a/Mage.Common/pom.xml +++ b/Mage.Common/pom.xml @@ -26,12 +26,6 @@ jspf-core 0.9.1 - - - com.google.code.gson - gson - 2.8.8 - org.jboss.remoting @@ -61,6 +55,11 @@ 1.0.2 + + log4j + log4j + + org.junit.jupiter junit-jupiter diff --git a/Mage.Sets/src/mage/cards/a/AkromaVisionOfIxidor.java b/Mage.Sets/src/mage/cards/a/AkromaVisionOfIxidor.java index 5e5ad8c2cd2..c859b98ff03 100644 --- a/Mage.Sets/src/mage/cards/a/AkromaVisionOfIxidor.java +++ b/Mage.Sets/src/mage/cards/a/AkromaVisionOfIxidor.java @@ -1,6 +1,5 @@ package mage.cards.a; -import com.google.common.collect.Sets; import mage.MageInt; import mage.abilities.Abilities; import mage.abilities.Ability; @@ -17,6 +16,8 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.targetpointer.FixedTarget; +import java.util.Arrays; +import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -66,7 +67,7 @@ public final class AkromaVisionOfIxidor extends CardImpl { class AkromaVisionOfIxidorEffect extends OneShotEffect { - private static final Set> classes = Sets.newHashSet( + private static final Set> classes = new HashSet<>(Arrays.asList( FlyingAbility.class, FirstStrikeAbility.class, DoubleStrikeAbility.class, @@ -81,7 +82,7 @@ class AkromaVisionOfIxidorEffect extends OneShotEffect { TrampleAbility.class, VigilanceAbility.class, PartnerAbility.class - ); + )); AkromaVisionOfIxidorEffect() { super(Outcome.Benefit); diff --git a/Mage.Sets/src/mage/cards/c/ConfrontThePast.java b/Mage.Sets/src/mage/cards/c/ConfrontThePast.java index 19b1507a8f4..8cafe8f05d2 100644 --- a/Mage.Sets/src/mage/cards/c/ConfrontThePast.java +++ b/Mage.Sets/src/mage/cards/c/ConfrontThePast.java @@ -1,6 +1,5 @@ package mage.cards.c; -import com.google.common.collect.Iterables; import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.effects.OneShotEffect; @@ -65,7 +64,8 @@ enum ConfrontThePastAdjuster implements TargetAdjuster { @Override public void adjustTargets(Ability ability, Game game) { - if (Iterables.getOnlyElement(ability.getEffects()) instanceof ReturnFromGraveyardToBattlefieldTargetEffect) { + if (ability.getEffects().size() == 1 + && ability.getEffects().get(0) instanceof ReturnFromGraveyardToBattlefieldTargetEffect) { int xValue = ability.getManaCostsToPay().getX(); ability.getTargets().clear(); FilterPermanentCard filter = new FilterPermanentCard("planeswalker card with mana value X or less"); diff --git a/Mage.Sets/src/mage/cards/e/EcologicalAppreciation.java b/Mage.Sets/src/mage/cards/e/EcologicalAppreciation.java index 51900dabbac..422f92a1d28 100644 --- a/Mage.Sets/src/mage/cards/e/EcologicalAppreciation.java +++ b/Mage.Sets/src/mage/cards/e/EcologicalAppreciation.java @@ -1,6 +1,5 @@ package mage.cards.e; -import com.google.common.collect.Sets; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; @@ -22,6 +21,7 @@ import mage.target.common.TargetCardInYourGraveyard; import mage.target.common.TargetOpponent; import mage.util.CardUtil; +import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -109,7 +109,10 @@ class EcologicalAppreciationEffect extends OneShotEffect { Set disallowedCards = this.getTargets().stream() .map(game::getCard) .collect(Collectors.toSet()); - return isValidTarget(card, Sets.union(disallowedCards, cards.getCards(game))); + Set checkList = new HashSet<>(); + checkList.addAll(disallowedCards); + checkList.addAll(cards.getCards(game)); + return isValidTarget(card, checkList); } }; targetCardsInGY.setNotTarget(true); diff --git a/Mage.Sets/src/mage/cards/k/KitesailSkirmisher.java b/Mage.Sets/src/mage/cards/k/KitesailSkirmisher.java index 87ce26c7c37..8c0a5231491 100644 --- a/Mage.Sets/src/mage/cards/k/KitesailSkirmisher.java +++ b/Mage.Sets/src/mage/cards/k/KitesailSkirmisher.java @@ -1,6 +1,5 @@ package mage.cards.k; -import com.google.common.base.Objects; import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; @@ -22,6 +21,7 @@ import mage.filter.predicate.mageobject.AnotherPredicate; import mage.game.Game; import mage.target.TargetPermanent; +import java.util.Objects; import java.util.UUID; /** @@ -74,7 +74,7 @@ enum KitesailSkirmisherPredicate implements ObjectSourcePlayerPredicate input, Game game) { - return Objects.equal( + return Objects.equals( game.getCombat().getDefenderId(input.getSourceId()), game.getCombat().getDefenderId(input.getObject().getId()) ); diff --git a/Mage.Sets/src/mage/cards/t/TayamLuminousEnigma.java b/Mage.Sets/src/mage/cards/t/TayamLuminousEnigma.java index 720399c2928..fde6ef9fbed 100644 --- a/Mage.Sets/src/mage/cards/t/TayamLuminousEnigma.java +++ b/Mage.Sets/src/mage/cards/t/TayamLuminousEnigma.java @@ -35,8 +35,6 @@ import java.util.HashSet; import java.util.Set; import java.util.UUID; -import static com.google.common.collect.Iterables.getOnlyElement; - /** * @author htrajan */ @@ -97,8 +95,7 @@ class TayamLuminousEnigmaCost extends RemoveCounterCost { Player controller = game.getPlayer(controllerId); for (int i = 0; i < countersToRemove; i++) { if (target.choose(Outcome.UnboostCreature, controllerId, source.getSourceId(), game)) { - UUID targetId = getOnlyElement(target.getTargets()); - Permanent permanent = game.getPermanent(targetId); + Permanent permanent = game.getPermanent(target.getFirstTarget()); if (permanent != null) { if (!permanent.getCounters(game).isEmpty()) { String counterName = null; diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java index 9ce4cb2f417..ec546ada70f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/utils/JsonGsonTest.java @@ -3,6 +3,7 @@ package org.mage.test.utils; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import mage.util.JsonUtil; import org.junit.Assert; import org.junit.Test; @@ -25,10 +26,17 @@ public class JsonGsonTest { try { // low level parser for unknown data structure JsonObject json = JsonParser.parseReader(new FileReader(sampleFileName)).getAsJsonObject(); - Assert.assertEquals("Unknown data", "card", json.get("object").getAsString()); - JsonArray jsonFaces = json.getAsJsonArray("card_faces"); + + // data types + Assert.assertEquals("string", "card", JsonUtil.getAsString(json, "object")); + JsonArray jsonFaces = JsonUtil.getAsArray(json, "card_faces"); + Assert.assertEquals("int", 60370, JsonUtil.getAsInt(json, "mtgo_id")); + Assert.assertTrue("boolean", JsonUtil.getAsBoolean(json, "highres_image")); + Assert.assertEquals("double", 4.0, JsonUtil.getAsDouble(json, "cmc"), 0.0); + Assert.assertNotNull("array", jsonFaces); + Assert.assertEquals("Card must have 2 faces", 2, jsonFaces.size()); - Assert.assertEquals("Unknown second side", "Infectious Curse", jsonFaces.get(1).getAsJsonObject().get("name").getAsString()); + Assert.assertEquals("Unknown second side", "Infectious Curse", JsonUtil.getAsString(jsonFaces.get(1).getAsJsonObject(), "name")); } catch (IOException e) { e.printStackTrace(); Assert.fail("Can't load sample json file: " + sampleFileName); diff --git a/Mage/pom.xml b/Mage/pom.xml index 714e3446529..7eb30b9a715 100644 --- a/Mage/pom.xml +++ b/Mage/pom.xml @@ -14,11 +14,6 @@ Mage Framework - - com.googlecode.json-simple - json-simple - 1.1.1 - log4j log4j @@ -34,6 +29,10 @@ com.google.guava guava + + com.google.code.gson + gson + com.j256.ormlite ormlite-jdbc diff --git a/Mage/src/main/java/mage/cards/decks/importer/CodDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/CodDeckImporter.java index e518e5ea79b..383f917ef2f 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/CodDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/CodDeckImporter.java @@ -22,9 +22,9 @@ public class CodDeckImporter extends XmlDeckImporter { * @return */ @Override - public DeckCardLists importDeck(String filename, StringBuilder errorMessages, boolean saveAutoFixedFile) { + public DeckCardLists importDeck(String fileName, StringBuilder errorMessages, boolean saveAutoFixedFile) { try { - Document doc = getXmlDocument(filename); + Document doc = getXmlDocument(fileName); DeckCardLists decklist = new DeckCardLists(); List mainCards = getNodes(doc, "/cockatrice_deck/zone[@name='main']/card"); diff --git a/Mage/src/main/java/mage/cards/decks/importer/DeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/DeckImporter.java index 26785c1909b..57bc486956d 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/DeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/DeckImporter.java @@ -86,10 +86,10 @@ public abstract class DeckImporter { } } - public abstract DeckCardLists importDeck(String file, StringBuilder errorMessages, boolean saveAutoFixedFile); + public abstract DeckCardLists importDeck(String fileName, StringBuilder errorMessages, boolean saveAutoFixedFile); - public DeckCardLists importDeck(String file, boolean saveAutoFixedFile) { - return importDeck(file, new StringBuilder(), saveAutoFixedFile); + public DeckCardLists importDeck(String fileName, boolean saveAutoFixedFile) { + return importDeck(fileName, new StringBuilder(), saveAutoFixedFile); } public CardLookup getCardLookup() { diff --git a/Mage/src/main/java/mage/cards/decks/importer/JsonDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/JsonDeckImporter.java index 9e5a7e52cc3..1502f291333 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/JsonDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/JsonDeckImporter.java @@ -1,9 +1,7 @@ package mage.cards.decks.importer; +import com.google.gson.*; import mage.cards.decks.DeckCardLists; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; import java.io.File; import java.io.FileReader; @@ -16,26 +14,25 @@ public abstract class JsonDeckImporter extends DeckImporter { protected StringBuilder sbMessage = new StringBuilder(); /** - * @param file file to import + * @param fileName file to import * @param errorMessages you can setup output messages to showup to user * @param saveAutoFixedFile do not supported for that format * @return decks list */ - public DeckCardLists importDeck(String file, StringBuilder errorMessages, boolean saveAutoFixedFile) { - File f = new File(file); + public DeckCardLists importDeck(String fileName, StringBuilder errorMessages, boolean saveAutoFixedFile) { + File f = new File(fileName); DeckCardLists deckList = new DeckCardLists(); if (!f.exists()) { - logger.warn("Deckfile " + file + " not found."); + logger.warn("Deckfile " + fileName + " not found."); return deckList; } sbMessage.setLength(0); try { try (FileReader reader = new FileReader(f)) { - try { // Json parsing - JSONParser parser = new JSONParser(); - JSONObject rootObj = (JSONObject) parser.parse(reader); - readJson(rootObj, deckList); + try { + JsonObject json = JsonParser.parseReader(reader).getAsJsonObject(); + readJson(json, deckList); if (sbMessage.length() > 0) { if (errorMessages != null) { @@ -46,8 +43,8 @@ public abstract class JsonDeckImporter extends DeckImporter { logger.fatal(sbMessage); } } - } catch (ParseException ex) { - logger.fatal(null, ex); + } catch (JsonParseException ex) { + logger.fatal("Can't parse json-deck: " + fileName, ex); } } catch (Exception ex) { logger.fatal(null, ex); @@ -59,9 +56,9 @@ public abstract class JsonDeckImporter extends DeckImporter { } @Override - public DeckCardLists importDeck(String file, boolean saveAutoFixedFile) { - return importDeck(file, null, saveAutoFixedFile); + public DeckCardLists importDeck(String fileName, boolean saveAutoFixedFile) { + return importDeck(fileName, null, saveAutoFixedFile); } - protected abstract void readJson(JSONObject line, DeckCardLists decklist); + protected abstract void readJson(JsonObject json, DeckCardLists decklist); } diff --git a/Mage/src/main/java/mage/cards/decks/importer/MtgjsonDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/MtgjsonDeckImporter.java index de0e800518d..5a0cc5371ea 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/MtgjsonDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/MtgjsonDeckImporter.java @@ -1,10 +1,11 @@ package mage.cards.decks.importer; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import mage.cards.decks.DeckCardInfo; import mage.cards.decks.DeckCardLists; import mage.cards.repository.CardInfo; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; +import mage.util.JsonUtil; import java.util.List; import java.util.Optional; @@ -16,39 +17,45 @@ import java.util.Optional; public class MtgjsonDeckImporter extends JsonDeckImporter { @Override - protected void readJson(JSONObject rootObj, DeckCardLists deckList) { - JSONObject data = (JSONObject) rootObj.get("data"); + protected void readJson(JsonObject json, DeckCardLists deckList) { + JsonObject data = JsonUtil.getAsObject(json, "data"); if (data == null) { sbMessage.append("Could not find data in json").append("'\n"); return; } // info - String deckSet = (String) data.get("code"); - String name = (String) data.get("name"); - if (name != null) { + String deckSet = JsonUtil.getAsString(data, "code"); + String name = JsonUtil.getAsString(data, "name"); + if (!name.isEmpty()) { deckList.setName(name); } + // mainboard - JSONArray mainBoard = (JSONArray) data.get("mainBoard"); + JsonArray mainBoard = JsonUtil.getAsArray(data, "mainBoard"); List mainDeckList = deckList.getCards(); addBoardToList(mainBoard, mainDeckList, deckSet); + // sideboard - JSONArray sideBoard = (JSONArray) data.get("sideBoard"); + JsonArray sideBoard = JsonUtil.getAsArray(data, "sideBoard"); List sideDeckList = deckList.getSideboard(); addBoardToList(sideBoard, sideDeckList, deckSet); } - private void addBoardToList(JSONArray board, List list, String deckSet) { + private void addBoardToList(JsonArray board, List list, String deckSet) { + if (board == null || board.isEmpty()) { + return; + } + board.forEach(arrayCard -> { - JSONObject card = (JSONObject) arrayCard; - String name = (String) card.get("name"); - String setCode = (String) card.get("setCode"); - if (setCode == null || setCode.isEmpty()) { + JsonObject card = (JsonObject) arrayCard; + String name = JsonUtil.getAsString(card, "name"); + String setCode = JsonUtil.getAsString(card, "setCode"); + if (setCode.isEmpty()) { setCode = deckSet; } - int num = ((Number) card.get("count")).intValue(); + int num = JsonUtil.getAsInt(card, "count"); Optional cardLookup = getCardLookup().lookupCardInfo(name, setCode); if (!cardLookup.isPresent()) { sbMessage.append("Could not find card: '").append(name).append("'\n"); diff --git a/Mage/src/main/java/mage/cards/decks/importer/O8dDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/O8dDeckImporter.java index 3426f57c6f8..1a98f0454f6 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/O8dDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/O8dDeckImporter.java @@ -22,9 +22,9 @@ public class O8dDeckImporter extends XmlDeckImporter { * @return */ @Override - public DeckCardLists importDeck(String filename, StringBuilder errorMessages, boolean saveAutoFixedFile) { + public DeckCardLists importDeck(String fileName, StringBuilder errorMessages, boolean saveAutoFixedFile) { try { - Document doc = getXmlDocument(filename); + Document doc = getXmlDocument(fileName); DeckCardLists decklist = new DeckCardLists(); List mainCards = getNodes(doc, "/deck/section[@name='Main']/card"); diff --git a/Mage/src/main/java/mage/cards/decks/importer/PlainTextDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/PlainTextDeckImporter.java index c6fd9a996fc..86c7965185c 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/PlainTextDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/PlainTextDeckImporter.java @@ -26,13 +26,13 @@ public abstract class PlainTextDeckImporter extends DeckImporter { * @param saveAutoFixedFile save fixed deck file (if any fixes applied) * @return decks list */ - public DeckCardLists importDeck(String file, StringBuilder errorMessages, boolean saveAutoFixedFile) { - File f = new File(file); + public DeckCardLists importDeck(String fileName, StringBuilder errorMessages, boolean saveAutoFixedFile) { + File f = new File(fileName); List originalFile = new ArrayList<>(); List fixedFile = new ArrayList<>(); DeckCardLists deckList = new DeckCardLists(); if (!f.exists()) { - logger.warn("Deckfile " + file + " not found."); + logger.warn("Deckfile " + fileName + " not found."); return deckList; } lineCount = 0; @@ -94,8 +94,8 @@ public abstract class PlainTextDeckImporter extends DeckImporter { @Override - public DeckCardLists importDeck(String file, boolean saveAutoFixedFile) { - return importDeck(file, null, saveAutoFixedFile); + public DeckCardLists importDeck(String fileName, boolean saveAutoFixedFile) { + return importDeck(fileName, null, saveAutoFixedFile); } /** diff --git a/Mage/src/main/java/mage/util/JsonUtil.java b/Mage/src/main/java/mage/util/JsonUtil.java new file mode 100644 index 00000000000..6455d139a86 --- /dev/null +++ b/Mage/src/main/java/mage/util/JsonUtil.java @@ -0,0 +1,50 @@ +package mage.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +/** + * @author JayDi85 + */ +public class JsonUtil { + + public static JsonObject getAsObject(JsonObject json, String field) { + return json.has(field) ? json.get(field).getAsJsonObject() : null; + } + + public static JsonArray getAsArray(JsonObject json, String field) { + return json.has(field) ? json.get(field).getAsJsonArray() : null; + } + + public static String getAsString(JsonObject json, String field) { + return getAsString(json, field, ""); + } + + public static String getAsString(JsonObject json, String field, String nullValue) { + return json.has(field) ? json.get(field).getAsString() : nullValue; + } + + public static int getAsInt(JsonObject json, String field) { + return getAsInt(json, field, 0); + } + + public static int getAsInt(JsonObject json, String field, int nullValue) { + return json.has(field) ? json.get(field).getAsInt() : nullValue; + } + + public static double getAsDouble(JsonObject json, String field) { + return getAsDouble(json, field, 0.0); + } + + public static double getAsDouble(JsonObject json, String field, double nullValue) { + return json.has(field) ? json.get(field).getAsDouble() : nullValue; + } + + public static boolean getAsBoolean(JsonObject json, String field) { + return getAsBoolean(json, field, false); + } + + public static boolean getAsBoolean(JsonObject json, String field, boolean nullValue) { + return json.has(field) ? json.get(field).getAsBoolean() : nullValue; + } +} diff --git a/pom.xml b/pom.xml index 505fdc766a6..7d1aab37be8 100644 --- a/pom.xml +++ b/pom.xml @@ -174,6 +174,12 @@ slf4j-log4j12 1.8.0-beta2 + + + com.google.code.gson + gson + 2.8.8 + com.google.guava guava From ec87af8d9aefa4b10b65da7c26c88fe34e2975f9 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Fri, 1 Oct 2021 10:54:18 -0500 Subject: [PATCH 220/231] [MIC] Implemented Ghouls' Night Out (#8345) * [MIC] Implemented Ghouls' Night Out * [MIC] Ghouls' Night Out - Made requested changes --- .../src/mage/cards/g/GhoulsNightOut.java | 153 ++++++++++++++++++ .../src/mage/sets/MidnightHuntCommander.java | 1 + 2 files changed, 154 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/g/GhoulsNightOut.java diff --git a/Mage.Sets/src/mage/cards/g/GhoulsNightOut.java b/Mage.Sets/src/mage/cards/g/GhoulsNightOut.java new file mode 100644 index 00000000000..86f1ba5d8ef --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GhoulsNightOut.java @@ -0,0 +1,153 @@ +package mage.cards.g; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.DecayedAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.targetpointer.FixedTargets; + +/** + * + * @author weirddan455 + */ +public final class GhoulsNightOut extends CardImpl { + + public GhoulsNightOut(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}{B}"); + + // For each player, choose a creature card in that player's graveyard. Put those cards onto the battlefield under your control. They're black Zombies in addition to their other colors and types and they gain decayed. + this.getSpellAbility().addEffect(new GhoulsNightOutEffect()); + } + + private GhoulsNightOut(final GhoulsNightOut card) { + super(card); + } + + @Override + public GhoulsNightOut copy() { + return new GhoulsNightOut(this); + } +} + +class GhoulsNightOutEffect extends OneShotEffect { + + public GhoulsNightOutEffect() { + super(Outcome.PutCreatureInPlay); + staticText = "For each player, choose a creature card in that player's graveyard. Put those cards onto the battlefield under your control. They're black Zombies in addition to their other colors and types and they gain decayed"; + } + + private GhoulsNightOutEffect(final GhoulsNightOutEffect effect) { + super(effect); + } + + @Override + public GhoulsNightOutEffect copy() { + return new GhoulsNightOutEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + UUID controllerId = source.getControllerId(); + Player controller = game.getPlayer(controllerId); + if (controller != null) { + Set cardsToBattlefield = new HashSet<>(); + for (UUID playerId : game.getState().getPlayersInRange(controllerId, game)) { + Player player = game.getPlayer(playerId); + if (player != null) { + boolean creatureInGraveyard = false; + for (UUID cardId : player.getGraveyard()) { + Card card = game.getCard(cardId); + if (card != null && card.isCreature(game)) { + creatureInGraveyard = true; + break; + } + } + if (creatureInGraveyard) { + FilterCreatureCard filter = new FilterCreatureCard("creature card in " + player.getName() + "'s graveyard"); + TargetCard target = new TargetCard(Zone.GRAVEYARD, filter); + target.setNotTarget(true); + controller.chooseTarget(controllerId.equals(playerId) ? Outcome.Benefit : Outcome.Detriment, player.getGraveyard(), target, source, game); + Card card = game.getCard(target.getFirstTarget()); + if (card != null) { + cardsToBattlefield.add(card); + } + } + } + } + if (!cardsToBattlefield.isEmpty()) { + controller.moveCards(cardsToBattlefield, Zone.BATTLEFIELD, source, game); + cardsToBattlefield.removeIf(card -> game.getState().getZone(card.getId()) != Zone.BATTLEFIELD); + if (!cardsToBattlefield.isEmpty()) { + game.addEffect(new GhoulsNightOutTypeChangingEffect( + ).setTargetPointer(new FixedTargets(cardsToBattlefield, game)), source); + game.addEffect(new GainAbilityTargetEffect( + new DecayedAbility(), Duration.Custom + ).setTargetPointer(new FixedTargets(cardsToBattlefield, game)), source); + return true; + } + } + } + return false; + } +} + +class GhoulsNightOutTypeChangingEffect extends ContinuousEffectImpl { + + public GhoulsNightOutTypeChangingEffect() { + super(Duration.Custom, Outcome.Neutral); + } + + private GhoulsNightOutTypeChangingEffect(final GhoulsNightOutTypeChangingEffect effect) { + super(effect); + } + + @Override + public GhoulsNightOutTypeChangingEffect copy() { + return new GhoulsNightOutTypeChangingEffect(this); + } + + @Override + public boolean apply(Layer layer, SubLayer subLayer, Ability source, Game game) { + boolean isActive = false; + for (UUID permId : targetPointer.getTargets(game, source)) { + Permanent permanent = game.getPermanent(permId); + if (permanent != null) { + switch (layer) { + case ColorChangingEffects_5: + permanent.getColor(game).setBlack(true); + isActive = true; + break; + case TypeChangingEffects_4: + permanent.addSubType(game, SubType.ZOMBIE); + isActive = true; + break; + } + } + } + return isActive; + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean hasLayer(Layer layer) { + return layer == Layer.ColorChangingEffects_5 || layer == Layer.TypeChangingEffects_4; + } +} diff --git a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java index 7c13f01eab9..94c82fcad2f 100644 --- a/Mage.Sets/src/mage/sets/MidnightHuntCommander.java +++ b/Mage.Sets/src/mage/sets/MidnightHuntCommander.java @@ -79,6 +79,7 @@ public final class MidnightHuntCommander extends ExpansionSet { cards.add(new SetCardInfo("Fleshbag Marauder", 118, Rarity.UNCOMMON, mage.cards.f.FleshbagMarauder.class)); cards.add(new SetCardInfo("Forgotten Creation", 100, Rarity.RARE, mage.cards.f.ForgottenCreation.class)); cards.add(new SetCardInfo("Fortified Village", 174, Rarity.RARE, mage.cards.f.FortifiedVillage.class)); + cards.add(new SetCardInfo("Ghouls' Night Out", 19, Rarity.RARE, mage.cards.g.GhoulsNightOut.class)); cards.add(new SetCardInfo("Gisa and Geralf", 150, Rarity.MYTHIC, mage.cards.g.GisaAndGeralf.class)); cards.add(new SetCardInfo("Gleaming Overseer", 151, Rarity.UNCOMMON, mage.cards.g.GleamingOverseer.class)); cards.add(new SetCardInfo("Go for the Throat", 119, Rarity.UNCOMMON, mage.cards.g.GoForTheThroat.class)); From 301539d75b49e56e9b6bb16f35ead4feac8537c0 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 1 Oct 2021 21:52:09 +0400 Subject: [PATCH 221/231] Server improves: * Server: improved messages on register/reset dialogs; * Tests: added database compatible tests on new code or libs (auth db); --- .../main/java/mage/remote/SessionImpl.java | 2 +- .../mage/server/AuthorizedUserRepository.java | 17 +++-- .../main/java/mage/server/MageServerImpl.java | 26 ++++--- .../src/main/java/mage/server/Main.java | 10 ++- .../src/main/java/mage/server/Session.java | 45 ++++++++---- .../src/main/java/mage/server/User.java | 2 +- .../src/test/data/users-db-sample.h2.mv.db | Bin 0 -> 36864 bytes .../serverside/DatabaseCompatibleTest.java | 66 ++++++++++++++++++ 8 files changed, 138 insertions(+), 30 deletions(-) create mode 100644 Mage.Tests/src/test/data/users-db-sample.h2.mv.db create mode 100644 Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseCompatibleTest.java diff --git a/Mage.Common/src/main/java/mage/remote/SessionImpl.java b/Mage.Common/src/main/java/mage/remote/SessionImpl.java index 3162aa68e60..c8b3f7f1a32 100644 --- a/Mage.Common/src/main/java/mage/remote/SessionImpl.java +++ b/Mage.Common/src/main/java/mage/remote/SessionImpl.java @@ -232,7 +232,7 @@ public class SessionImpl implements Session { public boolean work() throws Throwable { logger.info("Password reset: reseting password for username " + getUserName()); boolean result = server.resetPassword(sessionId, connection.getEmail(), connection.getAuthToken(), connection.getPassword()); - logger.info("Password reset: " + (result ? "DONE, check your email for new password" : "FAIL")); + logger.info("Password reset: " + (result ? "DONE, now you can login with new password" : "FAIL")); return result; } }); diff --git a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java index f1915e72f11..2f817d7db03 100644 --- a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java +++ b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java @@ -22,9 +22,7 @@ import java.io.File; import java.sql.SQLException; import java.util.List; -public enum AuthorizedUserRepository { - - instance; +public class AuthorizedUserRepository { private static final String JDBC_URL = "jdbc:h2:file:./db/authorized_user.h2;AUTO_SERVER=TRUE"; private static final String VERSION_ENTITY_NAME = "authorized_user"; @@ -32,15 +30,20 @@ public enum AuthorizedUserRepository { private static final long DB_VERSION = 2; private static final RandomNumberGenerator rng = new SecureRandomNumberGenerator(); + private static final AuthorizedUserRepository instance; + static { + instance = new AuthorizedUserRepository(JDBC_URL); + } + private Dao dao; - AuthorizedUserRepository() { + public AuthorizedUserRepository(String connectionString) { File file = new File("db"); if (!file.exists()) { file.mkdirs(); } try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(connectionString); TableUtils.createTableIfNotExists(connectionSource, AuthorizedUser.class); dao = DaoManager.createDao(connectionSource, AuthorizedUser.class); } catch (SQLException ex) { @@ -48,6 +51,10 @@ public enum AuthorizedUserRepository { } } + public static AuthorizedUserRepository getInstance() { + return instance; + } + public void add(final String userName, final String password, final String email) { try { Hash hash = new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, rng.nextBytes(), 1024); diff --git a/Mage.Server/src/main/java/mage/server/MageServerImpl.java b/Mage.Server/src/main/java/mage/server/MageServerImpl.java index 0992ab6cc0d..6fa1a21a887 100644 --- a/Mage.Server/src/main/java/mage/server/MageServerImpl.java +++ b/Mage.Server/src/main/java/mage/server/MageServerImpl.java @@ -83,15 +83,17 @@ public class MageServerImpl implements MageServer { @Override public boolean emailAuthToken(String sessionId, String email) throws MageException { if (!managerFactory.configSettings().isAuthenticationActivated()) { - sendErrorMessageToClient(sessionId, "Registration is disabled by the server config"); + sendErrorMessageToClient(sessionId, Session.REGISTRATION_DISABLED_MESSAGE); return false; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByEmail(email); if (authorizedUser == null) { sendErrorMessageToClient(sessionId, "No user was found with the email address " + email); logger.info("Auth token is requested for " + email + " but there's no such user in DB"); return false; } + String authToken = generateAuthToken(); activeAuthTokens.put(email, authToken); String subject = "XMage Password Reset Auth Token"; @@ -113,23 +115,31 @@ public class MageServerImpl implements MageServer { @Override public boolean resetPassword(String sessionId, String email, String authToken, String password) throws MageException { if (!managerFactory.configSettings().isAuthenticationActivated()) { - sendErrorMessageToClient(sessionId, "Registration is disabled by the server config"); + sendErrorMessageToClient(sessionId, Session.REGISTRATION_DISABLED_MESSAGE); return false; } + + // multi-step reset: + // - send auth token + // - check auth token to confirm reset + String storedAuthToken = activeAuthTokens.get(email); if (storedAuthToken == null || !storedAuthToken.equals(authToken)) { sendErrorMessageToClient(sessionId, "Invalid auth token"); logger.info("Invalid auth token " + authToken + " is sent for " + email); return false; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByEmail(email); if (authorizedUser == null) { - sendErrorMessageToClient(sessionId, "The user is no longer in the DB"); + sendErrorMessageToClient(sessionId, "User with that email doesn't exists"); logger.info("Auth token is valid, but the user with email address " + email + " is no longer in the DB"); return false; } - AuthorizedUserRepository.instance.remove(authorizedUser.getName()); - AuthorizedUserRepository.instance.add(authorizedUser.getName(), password, email); + + // recreate user with new password + AuthorizedUserRepository.getInstance().remove(authorizedUser.getName()); + AuthorizedUserRepository.getInstance().add(authorizedUser.getName(), password, email); activeAuthTokens.remove(email); return true; } @@ -1042,7 +1052,7 @@ public class MageServerImpl implements MageServer { @Override public void setActivation(final String sessionId, final String userName, boolean active) throws MageException { execute("setActivation", sessionId, () -> { - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName); + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); Optional u = managerFactory.userManager().getUserByName(userName); if (u.isPresent()) { User user = u.get(); diff --git a/Mage.Server/src/main/java/mage/server/Main.java b/Mage.Server/src/main/java/mage/server/Main.java index e78807b1623..2c967fb7d01 100644 --- a/Mage.Server/src/main/java/mage/server/Main.java +++ b/Mage.Server/src/main/java/mage/server/Main.java @@ -66,7 +66,15 @@ public final class Main { public static final PluginClassLoader classLoader = new PluginClassLoader(); private static TransporterServer server; + + // special test mode: + // - fast game buttons; + // - cheat commands; + // - no deck validation; + // - simplified registration and login (no password check); + // - debug main menu for GUI and rendering testing; private static boolean testMode; + private static boolean fastDbMode; /** @@ -98,7 +106,7 @@ public final class Main { if (config.isAuthenticationActivated()) { logger.info("Check authorized user DB version ..."); - if (!AuthorizedUserRepository.instance.checkAlterAndMigrateAuthorizedUser()) { + if (!AuthorizedUserRepository.getInstance().checkAlterAndMigrateAuthorizedUser()) { logger.fatal("Failed to start server."); return; } diff --git a/Mage.Server/src/main/java/mage/server/Session.java b/Mage.Server/src/main/java/mage/server/Session.java index 237a27ec62b..4d1dde28adb 100644 --- a/Mage.Server/src/main/java/mage/server/Session.java +++ b/Mage.Server/src/main/java/mage/server/Session.java @@ -35,6 +35,8 @@ public class Session { private static final Pattern alphabetsPattern = Pattern.compile("[a-zA-Z]"); private static final Pattern digitsPattern = Pattern.compile("[0-9]"); + public static final String REGISTRATION_DISABLED_MESSAGE = "Registration has been disabled on the server. You can use any name and empty password to login."; + private final ManagerFactory managerFactory; private final String sessionId; private UUID userId; @@ -60,30 +62,37 @@ public class Session { public String registerUser(String userName, String password, String email) throws MageException { if (!managerFactory.configSettings().isAuthenticationActivated()) { - String returnMessage = "Registration is disabled by the server config"; + String returnMessage = REGISTRATION_DISABLED_MESSAGE; sendErrorMessageToClient(returnMessage); return returnMessage; } - synchronized (AuthorizedUserRepository.instance) { + synchronized (AuthorizedUserRepository.getInstance()) { + // name String returnMessage = validateUserName(userName); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); return returnMessage; } + // auto-generated password RandomString randomString = new RandomString(10); password = randomString.nextString(); returnMessage = validatePassword(password, userName); if (returnMessage != null) { - sendErrorMessageToClient(returnMessage); + logger.warn("pas: " + password); + sendErrorMessageToClient("Auto-generated password fail, try again: " + returnMessage); return returnMessage; } + + // email returnMessage = validateEmail(email); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); return returnMessage; } - AuthorizedUserRepository.instance.add(userName, password, email); + + // create + AuthorizedUserRepository.getInstance().add(userName, password, email); String text = "You are successfully registered as " + userName + '.'; text += " Your initial, generated password is: " + password; @@ -95,15 +104,15 @@ public class Session { success = managerFactory.mailgunClient().sendMessage(email, subject, text); } if (success) { - String ok = "Sent a registration confirmation / initial password email to " + email + " for " + userName; + String ok = "Email with initial password sent to " + email + " for a user " + userName; logger.info(ok); sendInfoMessageToClient(ok); } else if (Main.isTestMode()) { - String ok = "Server is in test mode. Your account is registered with a password of " + password + " for " + userName; + String ok = "Email sending failed. Server is in test mode. Your account registered with a password " + password + " for a user " + userName; logger.info(ok); sendInfoMessageToClient(ok); } else { - String err = "Failed sending a registration confirmation / initial password email to " + email + " for " + userName; + String err = "Email sending failed. Try use another email address or service. Or reset password by email " + email + " for a user " + userName; logger.error(err); sendErrorMessageToClient(err); return err; @@ -113,9 +122,13 @@ public class Session { } private String validateUserName(String userName) { + // return error message or null on good name + if (userName.equals("Admin")) { + // virtual user for admin console return "User name Admin already in use"; } + ConfigSettings config = managerFactory.configSettings(); if (userName.length() < config.getMinUserNameLength()) { return "User name may not be shorter than " + config.getMinUserNameLength() + " characters"; @@ -123,15 +136,19 @@ public class Session { if (userName.length() > config.getMaxUserNameLength()) { return "User name may not be longer than " + config.getMaxUserNameLength() + " characters"; } + Pattern invalidUserNamePattern = Pattern.compile(managerFactory.configSettings().getInvalidUserNamePattern(), Pattern.CASE_INSENSITIVE); Matcher m = invalidUserNamePattern.matcher(userName); if (m.find()) { return "User name '" + userName + "' includes not allowed characters: use a-z, A-Z and 0-9"; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName); + + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); if (authorizedUser != null) { return "User name '" + userName + "' already in use"; } + + // all fine return null; } @@ -159,7 +176,7 @@ public class Session { if (email == null || email.isEmpty()) { return "Email address cannot be blank"; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByEmail(email); if (authorizedUser != null) { return "Email address '" + email + "' is associated with another user"; } @@ -182,8 +199,8 @@ public class Session { this.isAdmin = false; AuthorizedUser authorizedUser = null; if (managerFactory.configSettings().isAuthenticationActivated()) { - authorizedUser = AuthorizedUserRepository.instance.getByName(userName); - String errorMsg = "Wrong username or password. In case you haven't, please register your account first."; + authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); + String errorMsg = "Wrong username or password. You must register your account first."; if (authorizedUser == null) { return errorMsg; } @@ -193,16 +210,16 @@ public class Session { } if (!authorizedUser.active) { - return "Your profile is deactivated, you can't sign on."; + return "Your profile has been deactivated by admin."; } if (authorizedUser.lockedUntil != null) { if (authorizedUser.lockedUntil.compareTo(Calendar.getInstance().getTime()) > 0) { - return "Your profile is deactivated until " + SystemUtil.dateFormat.format(authorizedUser.lockedUntil); + return "Your profile has need deactivated by admin until " + SystemUtil.dateFormat.format(authorizedUser.lockedUntil); } else { + // unlock on timeout end managerFactory.userManager().createUser(userName, host, authorizedUser).ifPresent(user -> user.setLockedUntil(null) ); - } } } diff --git a/Mage.Server/src/main/java/mage/server/User.java b/Mage.Server/src/main/java/mage/server/User.java index bf7f025da4a..41e0378c96b 100644 --- a/Mage.Server/src/main/java/mage/server/User.java +++ b/Mage.Server/src/main/java/mage/server/User.java @@ -817,7 +817,7 @@ public class User { authorizedUser.chatLockedUntil = this.chatLockedUntil; authorizedUser.lockedUntil = this.lockedUntil; authorizedUser.active = this.active; - AuthorizedUserRepository.instance.update(authorizedUser); + AuthorizedUserRepository.getInstance().update(authorizedUser); } } } diff --git a/Mage.Tests/src/test/data/users-db-sample.h2.mv.db b/Mage.Tests/src/test/data/users-db-sample.h2.mv.db new file mode 100644 index 0000000000000000000000000000000000000000..91b77c4d569154529666f239d077686377467959 GIT binary patch literal 36864 zcmeHPO>Eo96((tE?bvY(6-h4L1C#xUy;g1hlM;|3vMpPRW!aJ_#|{EyDT;FJ*pe&D ziIbo}fTCzGy%gx7Ko33kShRcCLk|JkOOc|7qF8M2Jr|3fi$x!2h9W7;if!3m$2*3> z8Ir?!oR355eeY*7Nhx00YS>Sb3NA|d(5$xQwCu-DGg;FUj$Ex)hZ*`u5ugZA1SkR&0g3=cfFeK< zpa@U|C;}7#iok_HAVc~8g?JA1CMW_F0g3=cfFeK{xwo*(@SN3NVn&1BNEyqTKLrTOLZbS^tHnJSx^rDFC$diIgq@z}6C;CyN^ z&99`2GnrIzOj6ZxBrIRf<>Ki4a;j9izf_zJwZW&*eFQJXhuzys<>o<>nOW>|nR`BK zri&>vyOb~S*}R#aPZ#mE>BUqw*X5ejjG0|Y^JX@`2JLa%+|tZKdbXT516QcRI6eu( zF?%-1rAp?^Qa+!Co|p1%kMoK3HKP{NfZvzUT=d4!HX zn&Y99LvVH>hCV;yKN@Yqqk~7<>#@M&oaO!gy-xijAghdSX$*Dzwi|<~_bc}Efk8&o zqeIav(c$Pw^eW82k70;Q#~Py6V~ps*!60wLAoosXkeWA2-1Xarsl_5=Frs-fqUBS&6l>+Ie+ec+3T`a*1gg+)}cp1&L_RHjcPVnnUTJh7_d3a}r z#~FtGZ$yIcWfVTzlOozmXe*uLjO3!5uBQW8j^wMrGD&StIBg*jco8+V!GSyN-@GcAUD|wCcN--GXScU3_WF zs%$xvL^$3NrQj(U@9fp9ja*~>J@LEMOp zB6tNBp2QGg7|0HWf$SIvJF-KN^!7x2&KnwAVQto66RgE*tir0S$jWS$)me!ZhS3kM zAgPK7KZf*=B0v$K2v7tl0u+IF8G%mz?ZwlJemuRQRE4Tq)A}RT-LD?T@>RnBTP8R-)d_VF(W{ep`W1SR9 zDU%W_$fu?_BlX$AiYe9&!`gFr@7>U)u}ZDu!oWoW7ZxrmxUg~IYQT$grx;#2?1HEL#DP$rnZi!w(Xj>goIR8@XVGpT@m7Y z^}St4f(cJ^c?XLoxid+Xyg%MRrD{;P77(t%8Dv-U-YX*9&Afp>TWm|O2#V8wuatL? z*1YdhJVJROs|aNit&p@Kg>{a|dPWE`l4?6%U9Z}bS2uK9^Xdu9KW-_3y5qL-n3N5+ zMEXY&pa@U|C;}7#iU37`B0v#1R|GJB@cFmL|5cy=SAZ*2oipYCRtI%*{r{N%tCbFt zBx8~!8HlHVHZEh9qytUyd5U#Zrr*7t%m2rj&lnaW0Jv8SBV0ozdn@$b+BU52o4)Zn_%e%oI$KuCzL!vtWB7c+oCtyG*uNfkCTT-t`kmf5b1h{beOmS zeawL=dGw7lUa!o!s? zMGXws(lmdqjl^X%0)QWTJ^F8Wk;CtI(2#avea+bFbqxF1;eOr`JlVj}Yl|af9w3tk zsUwpIsUwpIsUwpIsUwqzS9eVwWCH7gts#qB@3+eP2|E85>9Wr2_r6Q#^%>9W$O-7qEdI7*cm0MM6D5^EkQ z(p4a3ZBkU}>%MRXz$=siz`ceHfPVo0hw{xYy$paq(+mI?VVVJO{uuzjrx^f~p)mWm zN6^y@fNTy@I?gNu;M2(O1_u~L)tK0D7UEQz;%IzX+OrZ<#kIy-b6UTz3YqQA)XL0@ z;I?nx-qU%opV@_dLtPQe`_%`VbE!KIg{J9jF5aI|Hbr5pl$c1R%xChm zg97CP0%4G2gUrwX#G+wrAqWjsB#1;q71@g<$qin;;H=GfqGMvl@hH~8DDr*&@&L%XLGo5zd~J;Y}I*9+}DU8qfQp|2*)2_EYvN&}mVQKh|kM;}ag6dz+%VBxnzE ztz4<`RCcEJ%lB&c8i}d2TFbb}PV2Y(Q%{}NYnS7`>RYGv`Whb? zM~W!>nqmG1@g`8Rzs_Gfpok8r498sSR74e`aC&hkIMw?66wmX`xFEqLhowCn)Hd;A zBF9X6Ai*VWtcM?Mp)`62wm`m;A8ZkfEjei>Qyt6Gh>29`i0%lA3u@NJVF?~Ma0(JU zNaB=QRh&fIZmL~U-K#bj`PS=RfgJvk({eh}Y?foGTeLAnfFeKJu0D_c3>ZBzdwGkh9592C z^LWj{^D~D%!N04W>)LwjavU_;^s(CQH)#wb#wkC4FzgyUIL;HfeLbZ_B||-9TbO6} z3uRep#0%=HO5vf>`2R^!dh1dv@KUeS_C}` z?fujmKL2-W!2gXi&HwLAUH19^Nn@7!=KtXT|C+|Mo@Goc`$bRxzZZv^;~wqJ|NHI!(9{3#waal|_wE1p`Whd&jfu})@c-Wc)^nF(zw8!>Dh>`E3`9MY_z(hw zms_U<=;TWvdaNKE4L}Ky$7v4_13hUB*SRmGaY Date: Fri, 1 Oct 2021 21:55:25 +0400 Subject: [PATCH 222/231] Merge fix --- Mage.Server/src/main/java/mage/server/Session.java | 1 - 1 file changed, 1 deletion(-) diff --git a/Mage.Server/src/main/java/mage/server/Session.java b/Mage.Server/src/main/java/mage/server/Session.java index 4d1dde28adb..90bc5bc8879 100644 --- a/Mage.Server/src/main/java/mage/server/Session.java +++ b/Mage.Server/src/main/java/mage/server/Session.java @@ -79,7 +79,6 @@ public class Session { password = randomString.nextString(); returnMessage = validatePassword(password, userName); if (returnMessage != null) { - logger.warn("pas: " + password); sendErrorMessageToClient("Auto-generated password fail, try again: " + returnMessage); return returnMessage; } From c45f8c91a377c37c04b189546aa62538403f07b3 Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Fri, 1 Oct 2021 15:56:40 -0500 Subject: [PATCH 223/231] - Fixed #8329 --- Mage.Sets/src/mage/cards/g/GiantGrowth.java | 7 +- .../mage/cards/y/YasharnImplacableEarth.java | 159 ++++++++++++------ .../costs/common/SacrificeXTargetCost.java | 5 + 3 files changed, 121 insertions(+), 50 deletions(-) diff --git a/Mage.Sets/src/mage/cards/g/GiantGrowth.java b/Mage.Sets/src/mage/cards/g/GiantGrowth.java index 7dcc98f4450..c4318a46a76 100644 --- a/Mage.Sets/src/mage/cards/g/GiantGrowth.java +++ b/Mage.Sets/src/mage/cards/g/GiantGrowth.java @@ -3,11 +3,13 @@ package mage.cards.g; import java.util.UUID; +import mage.abilities.effects.Effect; import mage.abilities.effects.common.continuous.BoostTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.Outcome; import mage.target.common.TargetCreaturePermanent; /** @@ -20,7 +22,10 @@ public final class GiantGrowth extends CardImpl { super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{G}"); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); - this.getSpellAbility().addEffect(new BoostTargetEffect(3, 3, Duration.EndOfTurn)); + Effect effect = new BoostTargetEffect(3, 3, Duration.EndOfTurn); + effect.setOutcome(Outcome.Benefit); + this.getSpellAbility().addEffect(effect); + } private GiantGrowth(final GiantGrowth card) { diff --git a/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java b/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java index 2cd5ae54e3c..14e22f0f5db 100644 --- a/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java +++ b/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java @@ -1,26 +1,34 @@ package mage.cards.y; +import java.util.Optional; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.SpellAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.common.SacrificeTargetCost; import mage.abilities.dynamicvalue.common.SubTypeAssignment; -import mage.abilities.effects.ContinuousEffectImpl; -import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; import mage.cards.*; import mage.constants.*; import mage.filter.FilterCard; -import mage.filter.StaticFilters; import mage.filter.predicate.Predicates; import mage.game.Game; -import mage.players.Player; import mage.target.common.TargetCardInLibrary; import java.util.UUID; +import mage.MageObject; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.costs.common.PayVariableLifeCost; +import mage.abilities.costs.common.SacrificeAllCost; +import mage.abilities.costs.common.SacrificeAttachedCost; +import mage.abilities.costs.common.SacrificeAttachmentCost; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.common.SacrificeXTargetCost; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.filter.Filter; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; /** * @author TheElk801 @@ -39,13 +47,12 @@ public final class YasharnImplacableEarth extends CardImpl { // When Yasharn, Implacable Earth enters the battlefield, search your library for a basic Forest card and a basic Plains card, reveal those cards, put them into your hand, then shuffle your library. this.addAbility(new EntersBattlefieldTriggeredAbility( new SearchLibraryPutInHandEffect(new YasharnImplacableEarthTarget(), true) - .setText("search your library for a basic Forest card and a basic Plains card, " + - "reveal those cards, put them into your hand, then shuffle") + .setText("search your library for a basic Forest card and a basic Plains card, " + + "reveal those cards, put them into your hand, then shuffle") )); // Players can't pay life or sacrifice nonland permanents to cast spells or activate abilities. Ability ability = new SimpleStaticAbility(new YasharnImplacableEarthEffect()); - ability.addEffect(new YasharnImplacableEarthSacrificeFilterEffect()); this.addAbility(ability); } @@ -105,14 +112,14 @@ class YasharnImplacableEarthTarget extends TargetCardInLibrary { } } -class YasharnImplacableEarthEffect extends ContinuousEffectImpl { +class YasharnImplacableEarthEffect extends ContinuousRuleModifyingEffectImpl { - YasharnImplacableEarthEffect() { - super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Detriment); + public YasharnImplacableEarthEffect() { + super(Duration.WhileOnBattlefield, Outcome.Neutral); staticText = "Players can't pay life or sacrifice nonland permanents to cast spells or activate abilities"; } - private YasharnImplacableEarthEffect(final YasharnImplacableEarthEffect effect) { + public YasharnImplacableEarthEffect(final YasharnImplacableEarthEffect effect) { super(effect); } @@ -123,48 +130,102 @@ class YasharnImplacableEarthEffect extends ContinuousEffectImpl { @Override public boolean apply(Game game, Ability source) { - for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { - Player player = game.getPlayer(playerId); - player.setCanPayLifeCost(false); - player.setCanPaySacrificeCostFilter(StaticFilters.FILTER_PERMANENTS_NON_LAND); - } return true; } -} - -class YasharnImplacableEarthSacrificeFilterEffect extends CostModificationEffectImpl { - - YasharnImplacableEarthSacrificeFilterEffect() { - super(Duration.WhileOnBattlefield, Outcome.Detriment, CostModificationType.SET_COST); - } - - private YasharnImplacableEarthSacrificeFilterEffect(YasharnImplacableEarthSacrificeFilterEffect effect) { - super(effect); - } @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { - for (Cost cost : abilityToModify.getCosts()) { - if (cost instanceof SacrificeTargetCost) { - ((SacrificeTargetCost) cost) - .getTargets() - .get(0) - .getFilter() - .add(CardType.LAND.getPredicate()); + public String getInfoMessage(Ability source, GameEvent event, Game game) { + MageObject mageObject = game.getObject(source.getSourceId()); + if (mageObject != null) { + return "Players can't pay life or sacrifice nonland permanents to cast spells or activate abilities. (" + mageObject.getIdName() + ")."; + } + return null; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + boolean canTargetLand = true; + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId()); + if (event.getType() == GameEvent.EventType.ACTIVATE_ABILITY + || event.getType() == GameEvent.EventType.CAST_SPELL) { + if (event.getType() == GameEvent.EventType.ACTIVATE_ABILITY) { + if (permanent == null) { + return false; + } + } + Optional ability = game.getAbility(event.getTargetId(), event.getSourceId()); + for (Cost cost : ability.get().getCosts()) { + if (cost instanceof PayLifeCost + || cost instanceof PayVariableLifeCost) { + return true; // can't pay with life + } + if (cost instanceof SacrificeSourceCost + && !permanent.isLand()) { + return true; + } + if (cost instanceof SacrificeTargetCost) { + SacrificeTargetCost sacrificeCost = (SacrificeTargetCost) cost; + Filter filter = sacrificeCost.getTargets().get(0).getFilter(); + for (Object predicate : filter.getPredicates()) { + if (predicate instanceof CardType.CardTypePredicate) { + if (!predicate.toString().equals("CardType(Land)")) { + canTargetLand = false; + } + } + } + return !canTargetLand; // must be nonland target + } + if (cost instanceof SacrificeAllCost) { + SacrificeAllCost sacrificeAllCost = (SacrificeAllCost) cost; + Filter filter = sacrificeAllCost.getTargets().get(0).getFilter(); + for (Object predicate : filter.getPredicates()) { + if (predicate instanceof CardType.CardTypePredicate) { + if (!predicate.toString().equals("CardType(Land)")) { + canTargetLand = false; + } + } + } + return !canTargetLand; // must be nonland target + } + if (cost instanceof SacrificeAttachedCost) { + SacrificeAttachedCost sacrificeAllCost = (SacrificeAttachedCost) cost; + Filter filter = sacrificeAllCost.getTargets().get(0).getFilter(); + for (Object predicate : filter.getPredicates()) { + if (predicate instanceof CardType.CardTypePredicate) { + if (!predicate.toString().equals("CardType(Land)")) { + canTargetLand = false; + } + } + } + return !canTargetLand; // must be nonland target + } + if (cost instanceof SacrificeAttachmentCost) { + SacrificeAttachmentCost sacrificeAllCost = (SacrificeAttachmentCost) cost; + Filter filter = sacrificeAllCost.getTargets().get(0).getFilter(); + for (Object predicate : filter.getPredicates()) { + if (predicate instanceof CardType.CardTypePredicate) { + if (!predicate.toString().equals("CardType(Land)")) { + canTargetLand = false; + } + } + } + return !canTargetLand; // must be nonland target + } + + if (cost instanceof SacrificeXTargetCost) { + SacrificeXTargetCost sacrificeCost = (SacrificeXTargetCost) cost; + Filter filter = sacrificeCost.getFilter(); + for (Object predicate : filter.getPredicates()) { + if (predicate instanceof CardType.CardTypePredicate) { + if (!predicate.toString().equals("CardType(Land)")) { + canTargetLand = false; + } + } + } + return !canTargetLand; // must be nonland target + } } } - return true; - } - - @Override - public boolean applies(Ability abilityToModify, Ability source, Game game) { - return (abilityToModify.getAbilityType() == AbilityType.ACTIVATED - || abilityToModify instanceof SpellAbility) - && game.getState().getPlayersInRange(source.getControllerId(), game).contains(abilityToModify.getControllerId()); - } - - @Override - public YasharnImplacableEarthSacrificeFilterEffect copy() { - return new YasharnImplacableEarthSacrificeFilterEffect(this); + return false; } } diff --git a/Mage/src/main/java/mage/abilities/costs/common/SacrificeXTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/SacrificeXTargetCost.java index a2b043a3365..27d6a1b3f46 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/SacrificeXTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/SacrificeXTargetCost.java @@ -4,6 +4,7 @@ import mage.abilities.Ability; import mage.abilities.costs.Cost; import mage.abilities.costs.VariableCostImpl; import mage.abilities.costs.VariableCostType; +import mage.filter.Filter; import mage.filter.common.FilterControlledPermanent; import mage.game.Game; import mage.target.common.TargetControlledPermanent; @@ -46,5 +47,9 @@ public class SacrificeXTargetCost extends VariableCostImpl { TargetControlledPermanent target = new TargetControlledPermanent(xValue, xValue, filter, true); return new SacrificeTargetCost(target); } + + public Filter getFilter() { + return filter; + } } From 16d20e73d2ec024782f6b06c0df5123ce80633fc Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Fri, 1 Oct 2021 17:03:54 -0500 Subject: [PATCH 224/231] - Fixed #8344. Works when blinked as well. --- .../mage/cards/g/GisaGloriousResurrector.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Mage.Sets/src/mage/cards/g/GisaGloriousResurrector.java b/Mage.Sets/src/mage/cards/g/GisaGloriousResurrector.java index cd7b87e4a31..d21bd6a7c0e 100644 --- a/Mage.Sets/src/mage/cards/g/GisaGloriousResurrector.java +++ b/Mage.Sets/src/mage/cards/g/GisaGloriousResurrector.java @@ -84,9 +84,12 @@ class GisaGloriousResurrectorExileEffect extends ReplacementEffectImpl { if (zEvent.getTarget() instanceof PermanentToken) { return player.moveCards(zEvent.getTarget(), Zone.EXILED, source, game); } + game.getState().setValue("GisaGloriousResurrectorExile" + + source.getSourceId().toString() + + game.getState().getZoneChangeCounter(source.getSourceId()), source); return player.moveCardsToExile( zEvent.getTarget(), source, game, false, - CardUtil.getExileZoneId(game, source), null + CardUtil.getExileZoneId(game, source), "Gisa, Glorious Resurrector" ); } @@ -109,8 +112,8 @@ class GisaGloriousResurrectorReturnEffect extends OneShotEffect { GisaGloriousResurrectorReturnEffect() { super(Outcome.Benefit); - staticText = "put all creature cards exiled with {this} " + - "onto the battlefield under your control. They gain decayed"; + staticText = "put all creature cards exiled with {this} " + + "onto the battlefield under your control. They gain decayed"; } private GisaGloriousResurrectorReturnEffect(final GisaGloriousResurrectorReturnEffect effect) { @@ -125,8 +128,16 @@ class GisaGloriousResurrectorReturnEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); - ExileZone exileZone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source)); - if (player == null || exileZone == null || exileZone.isEmpty()) { + Ability exiledWithSource = (Ability) game.getState().getValue("GisaGloriousResurrectorExile" + + source.getSourceId().toString() + + game.getState().getZoneChangeCounter(source.getSourceId())); + if (exiledWithSource == null) { + return false; + } + ExileZone exileZone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, exiledWithSource)); + if (player == null + || exileZone == null + || exileZone.isEmpty()) { return false; } Cards cards = new CardsImpl(exileZone.getCards(StaticFilters.FILTER_CARD_CREATURE, game)); From 08407eef25bd02c08098c3bb9751d8e0a9752746 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 2 Oct 2021 11:09:09 +0400 Subject: [PATCH 225/231] Dev: migrated to single logger lib (replaced slf4j usage by log4j), clean up and documented pom files, updated some libs; --- Mage.Client/pom.xml | 122 ++++++++++-------- .../java/mage/client/util/audio/LinePool.java | 31 ++--- Mage.Common/pom.xml | 2 - Mage.Server.Console/pom.xml | 1 - Mage.Server/pom.xml | 11 +- .../main/java/mage/server/MageServerImpl.java | 4 +- Mage.Sets/pom.xml | 1 - Mage.Tests/pom.xml | 1 - Mage.Verify/pom.xml | 1 - pom.xml | 61 +++++---- 10 files changed, 125 insertions(+), 110 deletions(-) diff --git a/Mage.Client/pom.xml b/Mage.Client/pom.xml index 57b9da979ed..18d54f76a7a 100644 --- a/Mage.Client/pom.xml +++ b/Mage.Client/pom.xml @@ -14,7 +14,6 @@ Mage Client - org.mage mage @@ -31,89 +30,105 @@ ${project.version} - com.googlecode.jspf - jspf-core - 0.9.1 + ${project.groupId} + mage-counter-plugin + 0.1 + runtime + + + + com.google.guava + guava log4j log4j - - org.slf4j - slf4j-log4j12 - net.java.truevfs truevfs-profile-base + + junit + junit + + + org.unbescape + unbescape + - + net.sf.trove4j trove4j 3.0.3 + + com.googlecode.jspf + jspf-core + 0.9.1 + + + + + com.mortennobel java-image-scaling 0.8.6 - com.google.guava - guava - - + + org.swinglabs swingx 1.6.1 - org.jetlang - jetlang - 0.2.23 - - - com.amazonaws - aws-java-sdk-s3 - 1.11.827 - - - com.jgoodies - forms - 1.2.1 - - - com.intellij - forms_rt - 7.0.3 - - - junit - junit - test - - - ${project.groupId} - mage-counter-plugin - 0.1 - runtime - - - org.jdesktop - beansbinding - 1.2.1 - - + org.swinglabs swing-layout 1.0.3 + + org.jetlang + jetlang + 0.2.23 + + + + + com.amazonaws + aws-java-sdk-s3 + 1.12.78 + + + + + com.jgoodies + forms + 1.2.1 + + + + com.intellij + forms_rt + 7.0.3 + + + + + org.jdesktop + beansbinding + 1.2.1 + + + org.jsoup jsoup - 1.14.2 + 1.14.3 @@ -135,6 +150,7 @@ + net.java.balloontip balloontip 1.2.4.1 @@ -153,15 +169,11 @@ + org.ocpsoft.prettytime prettytime 4.0.6.Final - - org.unbescape - unbescape - 1.1.6.RELEASE - diff --git a/Mage.Client/src/main/java/mage/client/util/audio/LinePool.java b/Mage.Client/src/main/java/mage/client/util/audio/LinePool.java index 633b677a350..cab0b04e7da 100644 --- a/Mage.Client/src/main/java/mage/client/util/audio/LinePool.java +++ b/Mage.Client/src/main/java/mage/client/util/audio/LinePool.java @@ -16,14 +16,13 @@ import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.Mixer; import javax.sound.sampled.SourceDataLine; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.log4j.Logger; import mage.utils.ThreadUtils; public class LinePool { - private final Logger log = LoggerFactory.getLogger(getClass()); + private final org.apache.log4j.Logger logger = Logger.getLogger(LinePool.class); private static final int LINE_CLEANUP_INTERVAL = 30000; private final Queue freeLines = new ArrayDeque<>(); @@ -55,7 +54,7 @@ public class LinePool { SourceDataLine line = (SourceDataLine) mixer.getLine(lineInfo); freeLines.add(line); } catch (LineUnavailableException e) { - log.warn("Failed to get line from mixer", e); + logger.warn("Failed to get line from mixer", e); } } new Timer("Line cleanup", true).scheduleAtFixedRate(new TimerTask() { @@ -65,7 +64,7 @@ public class LinePool { for (SourceDataLine sourceDataLine : freeLines) { if (sourceDataLine.isOpen()) { sourceDataLine.close(); - log.debug("Closed line {}", sourceDataLine); + logger.debug("Closed line " + sourceDataLine); } } } @@ -96,13 +95,13 @@ public class LinePool { public void playSound(final MageClip mageClip) { final SourceDataLine line; synchronized (LinePool.this) { - log.debug("Playing {}", mageClip.getFilename()); + logger.debug("Playing: " + mageClip.getFilename()); logLineStats(); line = borrowLine(); if (line == null) { // no lines available, queue sound to play it when a line is available queue.add(mageClip); - log.debug("Sound {} queued.", mageClip.getFilename()); + logger.debug("Sound queued: " + mageClip.getFilename()); return; } logLineStats(); @@ -113,19 +112,19 @@ public class LinePool { if (!line.isOpen()) { line.open(); line.addLineListener(event -> { - log.debug("Event: {}", event); + logger.debug("Event: " + event); if (event.getType() != Type.STOP) { return; } synchronized (LinePool.this) { - log.debug("Before stop on line {}", line); + logger.debug("Before stop on line " + line); logLineStats(); returnLine(line); - log.debug("After stop on line {}", line); + logger.debug("After stop on line " + line); logLineStats(); MageClip queuedSound = queue.poll(); if (queuedSound != null) { - log.debug("Playing queued sound {}", queuedSound); + logger.debug("Playing queued sound " + queuedSound); playSound(queuedSound); } } @@ -133,19 +132,21 @@ public class LinePool { } line.start(); } catch (LineUnavailableException e) { - log.warn("Failed to open line", e); + logger.warn("Failed to open line", e); } } byte[] buffer = mageClip.getBuffer(); - log.debug("Before write to line {}", line); + logger.debug("Before write to line " + line); line.write(buffer, 0, buffer.length); line.drain(); line.stop(); - log.debug("Line completed: {}", line); + logger.debug("Line completed: " + line); }); } private void logLineStats() { - log.debug("Free lines: {} Active: {} Busy: {}", freeLines.size(), activeLines.size(), busyLines.size()); + logger.debug(String.format("Free lines: %d; Active: %d; Busy: %d", + freeLines.size(), activeLines.size(), busyLines.size() + )); } } diff --git a/Mage.Common/pom.xml b/Mage.Common/pom.xml index 4173847a931..9f5ccb0e013 100644 --- a/Mage.Common/pom.xml +++ b/Mage.Common/pom.xml @@ -63,12 +63,10 @@ org.junit.jupiter junit-jupiter - test org.assertj assertj-core - test org.apache.commons diff --git a/Mage.Server.Console/pom.xml b/Mage.Server.Console/pom.xml index dfe03c7fe4b..044320253bf 100644 --- a/Mage.Server.Console/pom.xml +++ b/Mage.Server.Console/pom.xml @@ -27,7 +27,6 @@ junit junit - test diff --git a/Mage.Server/pom.xml b/Mage.Server/pom.xml index 74d8ac03c9e..7b4df3812d7 100644 --- a/Mage.Server/pom.xml +++ b/Mage.Server/pom.xml @@ -37,7 +37,6 @@ junit junit - test com.sun.xml.bind @@ -54,10 +53,6 @@ log4j log4j - - org.slf4j - slf4j-log4j12 - ${project.groupId} mage-player-ai @@ -284,12 +279,14 @@ org.junit.jupiter junit-jupiter - test org.assertj assertj-core - test + + + org.unbescape + unbescape diff --git a/Mage.Server/src/main/java/mage/server/MageServerImpl.java b/Mage.Server/src/main/java/mage/server/MageServerImpl.java index 6fa1a21a887..19608585bed 100644 --- a/Mage.Server/src/main/java/mage/server/MageServerImpl.java +++ b/Mage.Server/src/main/java/mage/server/MageServerImpl.java @@ -34,8 +34,8 @@ import mage.server.util.SystemUtil; import mage.utils.*; import mage.view.*; import mage.view.ChatMessage.MessageColor; -import org.apache.commons.lang3.StringEscapeUtils; import org.apache.log4j.Logger; +import org.unbescape.html.HtmlEscape; import javax.management.timer.Timer; import java.security.SecureRandom; @@ -489,7 +489,7 @@ public class MageServerImpl implements MageServer { public void sendChatMessage(final UUID chatId, final String userName, final String message) throws MageException { try { callExecutor.execute( - () -> managerFactory.chatManager().broadcast(chatId, userName, StringEscapeUtils.escapeHtml4(message), MessageColor.BLUE, true, null, ChatMessage.MessageType.TALK, null) + () -> managerFactory.chatManager().broadcast(chatId, userName, HtmlEscape.escapeHtml4(message), MessageColor.BLUE, true, null, ChatMessage.MessageType.TALK, null) ); } catch (Exception ex) { handleException(ex); diff --git a/Mage.Sets/pom.xml b/Mage.Sets/pom.xml index ad724706d41..8982ad941e6 100644 --- a/Mage.Sets/pom.xml +++ b/Mage.Sets/pom.xml @@ -29,7 +29,6 @@ junit junit - test diff --git a/Mage.Tests/pom.xml b/Mage.Tests/pom.xml index f9489fd45aa..3c9caf101ec 100644 --- a/Mage.Tests/pom.xml +++ b/Mage.Tests/pom.xml @@ -50,7 +50,6 @@ junit junit - test log4j diff --git a/Mage.Verify/pom.xml b/Mage.Verify/pom.xml index 5f2e558c01d..e27a1031c20 100644 --- a/Mage.Verify/pom.xml +++ b/Mage.Verify/pom.xml @@ -37,7 +37,6 @@ junit junit - test com.fasterxml.jackson.core diff --git a/pom.xml b/pom.xml index 7d1aab37be8..e3828019024 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.5.1 + 3.8.1 1.8 1.8 @@ -157,23 +157,14 @@ + - junit - junit - 4.13.1 - test - - + log4j log4j 1.2.17 - - org.slf4j - slf4j-log4j12 - 1.8.0-beta2 - com.google.code.gson @@ -181,24 +172,16 @@ 2.8.8 + com.google.guava guava 30.1.1-jre - org.junit.jupiter - junit-jupiter - 5.8.1 - - - org.assertj - assertj-core - 3.18.0 - - - org.apache.commons - commons-lang3 - 3.11 + + org.unbescape + unbescape + 1.1.6.RELEASE @@ -206,6 +189,34 @@ truevfs-profile-base 0.14.0 + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + + junit + junit + 4.13.1 + test + + + + org.junit.jupiter + junit-jupiter + 5.8.1 + test + + + + org.assertj + assertj-core + 3.19.0 + test + From b5033a3b7a588b753ccde6ecfad5b26d29cf0e8c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 2 Oct 2021 10:26:39 -0400 Subject: [PATCH 226/231] updated various ban lists --- .../src/mage/deck/AusHighlander.java | 10 ++++++---- .../src/mage/deck/CanadianHighlander.java | 9 +++++---- .../src/mage/deck/DuelCommander.java | 5 ++++- .../src/mage/deck/Oathbreaker.java | 2 -- .../src/mage/deck/TinyLeaders.java | 6 ++++-- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/AusHighlander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/AusHighlander.java index f7d76283ca4..99355992206 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/AusHighlander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/AusHighlander.java @@ -22,7 +22,7 @@ public class AusHighlander extends Constructed { pointMap.put("Black Lotus", 4); pointMap.put("Time Vault", 4); pointMap.put("Demonic Tutor", 3); - pointMap.put("Imperial Seal", 3); + pointMap.put("Mana Crypt", 3); pointMap.put("Mox Emerald", 3); pointMap.put("Mox Jet", 3); pointMap.put("Mox Pearl", 3); @@ -30,11 +30,12 @@ public class AusHighlander extends Constructed { pointMap.put("Mox Sapphire", 3); pointMap.put("Sol Ring", 3); pointMap.put("Thassa's Oracle", 3); + pointMap.put("Underworld Breach", 3); pointMap.put("Vampiric Tutor", 3); pointMap.put("Channel", 2); pointMap.put("Dig Through Time", 2); pointMap.put("Flash", 2); - pointMap.put("Mana Crypt", 2); + pointMap.put("Imperial Seal", 2); pointMap.put("Mind Twist", 2); pointMap.put("Mystical Tutor", 2); pointMap.put("Oko, Thief of Crowns", 2); @@ -46,11 +47,12 @@ public class AusHighlander extends Constructed { pointMap.put("Balance", 1); pointMap.put("Birthing Pod", 1); pointMap.put("Crop Rotation", 1); - pointMap.put("Dark Petition", 1); + pointMap.put("Deathrite Shaman", 1); pointMap.put("Doomsday", 1); pointMap.put("Enlightened Tutor", 1); pointMap.put("Fastbond", 1); pointMap.put("Force of Will", 1); + pointMap.put("Gifts Ungiven", 1); pointMap.put("Green Sun's Zenith", 1); pointMap.put("Hermit Druid", 1); pointMap.put("Intuition", 1); @@ -77,7 +79,7 @@ public class AusHighlander extends Constructed { pointMap.put("Timetwister", 1); pointMap.put("Tolarian Academy", 1); pointMap.put("Umezawa's Jitte", 1); - pointMap.put("Underworld Breach", 1); + pointMap.put("Uro, Titan of Nature's Wrath", 1); pointMap.put("Wasteland", 1); pointMap.put("Wishclaw Talisman", 1); pointMap.put("Wrenn and Six", 1); diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/CanadianHighlander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/CanadianHighlander.java index ca967a3244e..d5feec40142 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/CanadianHighlander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/CanadianHighlander.java @@ -40,15 +40,16 @@ public class CanadianHighlander extends Constructed { pointMap.put("Mox Ruby", 3); pointMap.put("Mox Sapphire", 3); pointMap.put("Mystical Tutor", 2); - pointMap.put("Natural Order", 4); + pointMap.put("Natural Order", 3); pointMap.put("Price of Progress", 1); - pointMap.put("Protean Hulk", 3); + pointMap.put("Protean Hulk", 2); pointMap.put("Sol Ring", 4); pointMap.put("Spellseeker", 2); pointMap.put("Strip Mine", 3); pointMap.put("Summoner's Pact", 1); pointMap.put("Survival of the Fittest", 2); pointMap.put("Tainted Pact", 1); + pointMap.put("Thassa's Oracle", 2); pointMap.put("Time Vault", 7); pointMap.put("Time Walk", 7); pointMap.put("Tinker", 3); @@ -56,8 +57,8 @@ public class CanadianHighlander extends Constructed { pointMap.put("Transmute Artifact", 1); pointMap.put("Treasure Cruise", 1); pointMap.put("True-Name Nemesis", 1); - pointMap.put("Umezawa's Jitte", 2); - pointMap.put("Underworld Breach", 1); + pointMap.put("Umezawa's Jitte", 1); + pointMap.put("Underworld Breach", 2); pointMap.put("Vampiric Tutor", 2); pointMap.put("Wishclaw Talisman", 1); pointMap.put("Yawgmoth's Will", 2); diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/DuelCommander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/DuelCommander.java index 786f19ce042..629758796b8 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/DuelCommander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/DuelCommander.java @@ -36,8 +36,9 @@ public class DuelCommander extends Commander { banned.add("Karakas"); banned.add("Library of Alexandria"); banned.add("Lion's Eye Diamond"); + banned.add("Lutri, The Spellchaser"); banned.add("Loyal Retainers"); - banned.add("Lutri, the Spellchaser"); + banned.add("Maddening Hex"); banned.add("Mana Crypt"); banned.add("Mana Drain"); banned.add("Mana Vault"); @@ -77,6 +78,7 @@ public class DuelCommander extends Commander { bannedCommander.add("Akiri, Line-Slinger"); bannedCommander.add("Arahbo, Roar of the World"); bannedCommander.add("Ardenn, Intrepid Archaeologist"); + bannedCommander.add("Asmoranomardicadaistinaculdacar"); bannedCommander.add("Baral, Chief of Compliance"); bannedCommander.add("Breya, Etherium Shaper"); bannedCommander.add("Bruse Tarl, Boorish Herder"); @@ -106,6 +108,7 @@ public class DuelCommander extends Commander { bannedCommander.add("Tymna, the Weaver"); bannedCommander.add("Urza, Lord High Artificer"); bannedCommander.add("Vial Smasher the Fierce"); + bannedCommander.add("Winota, Joiner of Forces"); bannedCommander.add("Yuriko, the Tiger's Shadow"); bannedCommander.add("Zurgo Bellstriker"); } diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Oathbreaker.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Oathbreaker.java index fa01ecfdd58..9a86f632844 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Oathbreaker.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Oathbreaker.java @@ -25,7 +25,6 @@ public class Oathbreaker extends Vintage { setName("Oathbreaker"); // banned = vintage + oathbreaker's list: https://oathbreakermtg.org/banned-list/ - // last updated 4/24/20 - Dark Ritual banned banned.add("Ad Nauseam"); banned.add("Ancestral Recall"); banned.add("Balance"); @@ -77,7 +76,6 @@ public class Oathbreaker extends Vintage { banned.add("Tooth and Nail"); banned.add("Trade Secrets"); banned.add("Upheaval"); - banned.add("Worldfire"); banned.add("Yawgmoth's Bargain"); } diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/TinyLeaders.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/TinyLeaders.java index 4e2c3907fcb..92f45d511ba 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/TinyLeaders.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/TinyLeaders.java @@ -28,12 +28,12 @@ public class TinyLeaders extends Constructed { } } //Banned list from tinyleaders.blodspot.ca/p/ban-list.html - //Ban list updated as of 11/08/14 banned.add("Ancestral Recall"); banned.add("Balance"); banned.add("Black Lotus"); banned.add("Black Vise"); banned.add("Channel"); + banned.add("Codie, Vociferous Codex"); banned.add("Counterbalance"); banned.add("Demonic Tutor"); banned.add("Earthcraft"); @@ -57,14 +57,16 @@ public class TinyLeaders extends Constructed { banned.add("Mox Pearl"); banned.add("Mox Ruby"); banned.add("Mox Sapphire"); - banned.add("Najeela, the Blade Blossom"); + banned.add("Najeela, the Blade-Blossom"); banned.add("Necropotence"); banned.add("Shahrazad"); + banned.add("Sisay, Weatherlight Captain"); banned.add("Skullclamp"); banned.add("Sol Ring"); banned.add("Strip Mine"); banned.add("Survival of the Fittest"); banned.add("Sword of Body and Mind"); + banned.add("Thassa's Oracle"); banned.add("The Tabernacle at Pendrell Vale"); banned.add("Time Vault"); banned.add("Time Walk"); From 7dd0ba5ce106cea319487f484c416cc0272585b9 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Sat, 2 Oct 2021 13:07:09 -0400 Subject: [PATCH 227/231] Fixed #8173 and #8357 (#8358) --- .../src/mage/sets/OathOfTheGatewatch.java | 22 ++++++++++++++----- .../src/mage/sets/TimeSpiralRemastered.java | 2 +- .../java/mage/verify/VerifyCardDataTest.java | 13 +++++++---- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Mage.Sets/src/mage/sets/OathOfTheGatewatch.java b/Mage.Sets/src/mage/sets/OathOfTheGatewatch.java index fe5f9f44728..025e70c4c5b 100644 --- a/Mage.Sets/src/mage/sets/OathOfTheGatewatch.java +++ b/Mage.Sets/src/mage/sets/OathOfTheGatewatch.java @@ -28,7 +28,7 @@ public final class OathOfTheGatewatch extends ExpansionSet { this.blockName = "Battle for Zendikar"; this.parentSet = BattleForZendikar.getInstance(); this.hasBoosters = true; - this.hasBasicLands = true; + this.hasBasicLands = false; this.numBoosterLands = 1; this.numBoosterCommon = 10; this.numBoosterUncommon = 3; @@ -213,10 +213,10 @@ public final class OathOfTheGatewatch extends ExpansionSet { cards.add(new SetCardInfo("Wandering Fumarole", 182, Rarity.RARE, mage.cards.w.WanderingFumarole.class)); cards.add(new SetCardInfo("Warden of Geometries", 11, Rarity.COMMON, mage.cards.w.WardenOfGeometries.class)); cards.add(new SetCardInfo("Warping Wail", 12, Rarity.UNCOMMON, mage.cards.w.WarpingWail.class)); - cards.add(new SetCardInfo("Wastes", "183a", Rarity.LAND, mage.cards.w.Wastes.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Wastes", "184a", Rarity.LAND, mage.cards.w.Wastes.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Wastes", 183, Rarity.LAND, mage.cards.w.Wastes.class, FULL_ART_BFZ_VARIOUS)); - cards.add(new SetCardInfo("Wastes", 184, Rarity.LAND, mage.cards.w.Wastes.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Wastes", "183a", Rarity.COMMON, mage.cards.w.Wastes.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Wastes", "184a", Rarity.COMMON, mage.cards.w.Wastes.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Wastes", 183, Rarity.COMMON, mage.cards.w.Wastes.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Wastes", 184, Rarity.COMMON, mage.cards.w.Wastes.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Weapons Trainer", 160, Rarity.UNCOMMON, mage.cards.w.WeaponsTrainer.class)); cards.add(new SetCardInfo("Witness the End", 82, Rarity.COMMON, mage.cards.w.WitnessTheEnd.class)); cards.add(new SetCardInfo("World Breaker", 126, Rarity.MYTHIC, mage.cards.w.WorldBreaker.class)); @@ -225,6 +225,16 @@ public final class OathOfTheGatewatch extends ExpansionSet { cards.add(new SetCardInfo("Zulaport Chainmage", 93, Rarity.COMMON, mage.cards.z.ZulaportChainmage.class)); } + @Override + public List getCardsByRarity(Rarity rarity) { + List cards = super.getCardsByRarity(rarity); + if (rarity == Rarity.COMMON) { + // only the full-art versions of Wastes are found in boosters + cards.removeIf(cardInfo -> cardInfo.getCardNumber().contains("a")); + } + return cards; + } + @Override public List getSpecialLand() { if (savedSpecialLand.isEmpty()) { @@ -237,4 +247,4 @@ public final class OathOfTheGatewatch extends ExpansionSet { return new ArrayList<>(savedSpecialLand); } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java b/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java index dce4984880a..431c3b07311 100644 --- a/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java +++ b/Mage.Sets/src/mage/sets/TimeSpiralRemastered.java @@ -30,7 +30,7 @@ public class TimeSpiralRemastered extends ExpansionSet { private TimeSpiralRemastered() { super("Time Spiral Remastered", "TSR", ExpansionSet.buildDate(2021, 3, 19), SetType.SUPPLEMENTAL); this.hasBoosters = true; - this.hasBasicLands = true; + this.hasBasicLands = false; this.maxCardNumberInBooster = 410; this.numBoosterCommon = 10; this.numBoosterUncommon = 3; diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index b6c610ef3e1..ed37b8723f1 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -1756,23 +1756,28 @@ public class VerifyCardDataTest { || checkName.equals("Forest") || checkName.equals("Swamp") || checkName.equals("Plains") - || checkName.equals("Mountain") - || checkName.equals("Wastes"); + || checkName.equals("Mountain"); } private void checkBasicLands(Card card, MtgJsonCard ref) { // basic lands must have Rarity.LAND and SuperType.BASIC // other cards can't have that stats - if (isBasicLandName(card.getName())) { + String name = card.getName(); + if (isBasicLandName(name)) { // lands - if (card.getRarity() != Rarity.LAND && card.getRarity() != Rarity.SPECIAL) { + if (card.getRarity() != Rarity.LAND) { fail(card, "rarity", "basic land must be Rarity.LAND"); } if (!card.getSuperType().contains(SuperType.BASIC)) { fail(card, "supertype", "basic land must be SuperType.BASIC"); } + } else if (name.equals("Wastes")) { + // Wastes are SuperType.BASIC but not necessarily Rarity.LAND + if (!card.getSuperType().contains(SuperType.BASIC)) { + fail(card, "supertype", "Wastes must be SuperType.BASIC"); + } } else { // non lands if (card.getRarity() == Rarity.LAND) { From dc593b41b5073630ba4682a9ea34547e3fe020d4 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sun, 3 Oct 2021 11:42:03 +0400 Subject: [PATCH 228/231] Github: added code coverage badge by sonarcloud --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index ccb9539531a..c744b8fe5ac 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # XMage — Magic, Another Game Engine -[![Build Status](https://travis-ci.org/magefree/mage.svg?branch=master)](https://travis-ci.org/magefree/mage) [![latest release](https://img.shields.io/github/v/release/magefree/mage)](https://github.com/magefree/mage/releases/) [![commints since latest release](https://img.shields.io/github/commits-since/magefree/mage/latest)](https://github.com/magefree/mage/commits/) [![Join the chat at https://gitter.im/magefree/mage](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/magefree/mage?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://travis-ci.org/magefree/mage.svg?branch=master)](https://travis-ci.org/magefree/mage) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=JayDi85_mage&metric=coverage)](https://sonarcloud.io/dashboard?id=JayDi85_mage) [![latest release](https://img.shields.io/github/v/release/magefree/mage)](https://github.com/magefree/mage/releases/) [![commints since latest release](https://img.shields.io/github/commits-since/magefree/mage/latest)](https://github.com/magefree/mage/commits/) [![Join the chat at https://gitter.im/magefree/mage](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/magefree/mage?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) XMage allows you to play Magic against one or more online players or computer opponents. It includes full rules enforcement for over **20 000** unique cards and more than 50.000 reprints from different editions. From 20fda878cdd1946e0e3bb5e66b0c92896e09adf4 Mon Sep 17 00:00:00 2001 From: "Alex W. Jackson" Date: Sun, 3 Oct 2021 06:11:25 -0400 Subject: [PATCH 229/231] Fixed #8361 --- Mage.Sets/src/mage/sets/Legions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/sets/Legions.java b/Mage.Sets/src/mage/sets/Legions.java index fec891e7e1b..1b616701b7a 100644 --- a/Mage.Sets/src/mage/sets/Legions.java +++ b/Mage.Sets/src/mage/sets/Legions.java @@ -27,7 +27,7 @@ public final class Legions extends ExpansionSet { this.numBoosterCommon = 11; this.numBoosterUncommon = 3; this.numBoosterRare = 1; - this.ratioBoosterMythic = 8; + this.ratioBoosterMythic = 0; cards.add(new SetCardInfo("Akroma, Angel of Wrath", 1, Rarity.RARE, mage.cards.a.AkromaAngelOfWrath.class)); cards.add(new SetCardInfo("Akroma's Devoted", 2, Rarity.UNCOMMON, mage.cards.a.AkromasDevoted.class)); cards.add(new SetCardInfo("Aphetto Exterminator", 59, Rarity.UNCOMMON, mage.cards.a.AphettoExterminator.class)); From 58ab020065ecb43b3102e7b2ca76cf679c9711c6 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sun, 3 Oct 2021 17:45:01 +0400 Subject: [PATCH 230/231] Dev: pom clean up, improved parent/child structure; --- Mage.Client/pom.xml | 8 -- Mage.Common/pom.xml | 13 --- Mage.Plugins/Mage.Counter.Plugin/pom.xml | 5 -- Mage.Server.Console/pom.xml | 4 - Mage.Server.Plugins/Mage.Player.AI/pom.xml | 4 - .../Mage.Player.AIMCTS/pom.xml | 4 - .../Mage.Player.AIMinimax/pom.xml | 4 - Mage.Server.Plugins/Mage.Player.Human/pom.xml | 5 -- Mage.Server/pom.xml | 67 ++++++--------- Mage.Sets/pom.xml | 9 -- Mage.Tests/pom.xml | 8 -- Mage.Verify/pom.xml | 9 -- Mage/pom.xml | 20 ----- pom.xml | 85 ++++++++++++------- 14 files changed, 77 insertions(+), 168 deletions(-) diff --git a/Mage.Client/pom.xml b/Mage.Client/pom.xml index 18d54f76a7a..3dbe81fc391 100644 --- a/Mage.Client/pom.xml +++ b/Mage.Client/pom.xml @@ -40,18 +40,10 @@ com.google.guava guava - - log4j - log4j - net.java.truevfs truevfs-profile-base - - junit - junit - org.unbescape unbescape diff --git a/Mage.Common/pom.xml b/Mage.Common/pom.xml index 9f5ccb0e013..14fa512b144 100644 --- a/Mage.Common/pom.xml +++ b/Mage.Common/pom.xml @@ -55,19 +55,6 @@ 1.0.2 - - log4j - log4j - - - - org.junit.jupiter - junit-jupiter - - - org.assertj - assertj-core - org.apache.commons commons-lang3 diff --git a/Mage.Plugins/Mage.Counter.Plugin/pom.xml b/Mage.Plugins/Mage.Counter.Plugin/pom.xml index 483cdf4e6a0..a68381e5a00 100644 --- a/Mage.Plugins/Mage.Counter.Plugin/pom.xml +++ b/Mage.Plugins/Mage.Counter.Plugin/pom.xml @@ -22,11 +22,6 @@ mage-common ${mage-version} - - log4j - log4j - provided - diff --git a/Mage.Server.Console/pom.xml b/Mage.Server.Console/pom.xml index 044320253bf..4628afb0de0 100644 --- a/Mage.Server.Console/pom.xml +++ b/Mage.Server.Console/pom.xml @@ -24,10 +24,6 @@ swingx 1.6.1 - - junit - junit - diff --git a/Mage.Server.Plugins/Mage.Player.AI/pom.xml b/Mage.Server.Plugins/Mage.Player.AI/pom.xml index 48086d621f8..db3e907f0ce 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AI/pom.xml @@ -15,10 +15,6 @@ Mage Player AI - - log4j - log4j - ${project.groupId} mage diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml index ece840b665f..10e62a3e731 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml @@ -15,10 +15,6 @@ Mage Player AI MCTS - - log4j - log4j - ${project.groupId} mage diff --git a/Mage.Server.Plugins/Mage.Player.AIMinimax/pom.xml b/Mage.Server.Plugins/Mage.Player.AIMinimax/pom.xml index 5e43462cba3..8b878dfacc3 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMinimax/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AIMinimax/pom.xml @@ -15,10 +15,6 @@ Mage Player AI Minimax - - log4j - log4j - ${project.groupId} mage diff --git a/Mage.Server.Plugins/Mage.Player.Human/pom.xml b/Mage.Server.Plugins/Mage.Player.Human/pom.xml index c4d102c8920..6aed2472706 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.Human/pom.xml @@ -25,11 +25,6 @@ mage-common ${project.version} - - log4j - log4j - provided - diff --git a/Mage.Server/pom.xml b/Mage.Server/pom.xml index 7b4df3812d7..f951213ed28 100644 --- a/Mage.Server/pom.xml +++ b/Mage.Server/pom.xml @@ -19,11 +19,6 @@ mage ${mage-version} - - org.apache.commons - commons-lang3 - 3.11 - ${project.groupId} mage-common @@ -34,25 +29,6 @@ mage-sets ${mage-version} - - junit - junit - - - com.sun.xml.bind - jaxb-impl - 3.0.2 - - - - org.glassfish.jaxb - jaxb-runtime - 3.0.2 - - - log4j - log4j - ${project.groupId} mage-player-ai @@ -83,12 +59,6 @@ ${project.version} runtime - - org.apache.commons - commons-compress - [1.19,) - - ${project.groupId} mage-game-commanderfreeforall @@ -179,7 +149,6 @@ ${project.version} runtime - ${project.groupId} mage-game-freeformcommanderfreeforall @@ -198,7 +167,6 @@ ${project.version} runtime - ${project.groupId} mage-game-oathbreakerduel @@ -211,7 +179,6 @@ ${project.version} runtime - ${project.groupId} mage-game-momirduel @@ -224,6 +191,28 @@ ${project.version} runtime + + + org.apache.commons + commons-lang3 + 3.11 + + + com.sun.xml.bind + jaxb-impl + 3.0.2 + + + + org.glassfish.jaxb + jaxb-runtime + 3.0.2 + + + org.apache.commons + commons-compress + [1.19,) + org.apache.shiro shiro-core @@ -270,19 +259,11 @@ 1.19.4 + org.xerial sqlite-jdbc 3.32.3.2 - - - - - org.junit.jupiter - junit-jupiter - - - org.assertj - assertj-core + runtime org.unbescape diff --git a/Mage.Sets/pom.xml b/Mage.Sets/pom.xml index 8982ad941e6..b14b92ca739 100644 --- a/Mage.Sets/pom.xml +++ b/Mage.Sets/pom.xml @@ -21,15 +21,6 @@ mage ${mage-version} - - log4j - log4j - - - - junit - junit - diff --git a/Mage.Tests/pom.xml b/Mage.Tests/pom.xml index 3c9caf101ec..3c73adbaf39 100644 --- a/Mage.Tests/pom.xml +++ b/Mage.Tests/pom.xml @@ -47,14 +47,6 @@ compile - - junit - junit - - - log4j - log4j - ${project.groupId} mage-game-freeforall diff --git a/Mage.Verify/pom.xml b/Mage.Verify/pom.xml index e27a1031c20..9084b90ba03 100644 --- a/Mage.Verify/pom.xml +++ b/Mage.Verify/pom.xml @@ -34,21 +34,12 @@ mage-client ${mage-version} - - junit - junit - com.fasterxml.jackson.core jackson-databind [2.9.9.1,) - - log4j - log4j - - org.reflections reflections diff --git a/Mage/pom.xml b/Mage/pom.xml index 5aa6179f4de..c704f8d6c88 100644 --- a/Mage/pom.xml +++ b/Mage/pom.xml @@ -14,17 +14,6 @@ Mage Framework - - log4j - log4j - - - - com.h2database - h2 - 1.4.197 - runtime - com.google.guava guava @@ -33,15 +22,6 @@ com.google.code.gson gson - - com.j256.ormlite - ormlite-jdbc - 5.6 - - - junit - junit - com.google.protobuf protobuf-java diff --git a/pom.xml b/pom.xml index e3828019024..0ec273bff11 100644 --- a/pom.xml +++ b/pom.xml @@ -1,15 +1,14 @@ - 4.0.0 - org.mage mage-root 1.4.50 pom Mage Root Mage Root POM + @@ -101,6 +100,7 @@ + Mage Mage.Common @@ -155,16 +155,59 @@ ${project.basedir}/../${aggregate.report.dir} + + + + + + + org.slf4j + slf4j-log4j12 + 1.7.12 + + + + + com.j256.ormlite + ormlite-jdbc + 5.6 + + + + + com.h2database + h2 + 1.4.197 + runtime + + + + + junit + junit + 4.13.1 + test + + + + org.junit.jupiter + junit-jupiter + 5.8.1 + test + + + + org.assertj + assertj-core + 3.19.0 + test + + + - - + + - - - log4j - log4j - 1.2.17 - com.google.code.gson @@ -195,28 +238,6 @@ commons-lang3 3.12.0 - - - - junit - junit - 4.13.1 - test - - - - org.junit.jupiter - junit-jupiter - 5.8.1 - test - - - - org.assertj - assertj-core - 3.19.0 - test - From abdccf97daabd08d17e79ae02222ca1caf1b8248 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sun, 3 Oct 2021 18:40:24 +0400 Subject: [PATCH 231/231] Dev: updated some libs to latest version; --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 0ec273bff11..cd0c012556c 100644 --- a/pom.xml +++ b/pom.xml @@ -163,7 +163,7 @@ org.slf4j slf4j-log4j12 - 1.7.12 + 1.7.32 @@ -185,7 +185,7 @@ junit junit - 4.13.1 + 4.13.2 test @@ -199,7 +199,7 @@ org.assertj assertj-core - 3.19.0 + 3.21.0 test