Compare commits

..

607 Commits

Author SHA1 Message Date
Hans Mackowiak
f14454e992 fix Tests for now 2025-06-01 23:21:49 +02:00
Hans Mackowiak
e4799a4f73 ImageKeys: unify PaperCard imageKeys 2025-06-01 23:09:27 +02:00
Seth Milliken
7fa89cbc2e fix: do not steal keyboard focus when "Visually Alert on Receipt of Priority" is enabled
When the "Visually Alert on Receipt of Priority" feature is enabled,
the `showTab()` call at the beginning of `forge.gui.framework.SDisplayUtil.remind()`
steals focus from the primary button whenever anything goes on the
stack.

This change, previously suggested by pfps as probably not necessary, is
indeed necessary after all to prevent that focus stealing by restoring
focus to the previous owner only if the owner was an `FButton`.

fixes https://github.com/Card-Forge/forge/issues/7660
2025-06-01 14:17:09 -04:00
Greg Sonnier
567e13c92b fixed locke_treasure_hunter.txt unblockable condition (#7752) 2025-06-01 08:30:27 +00:00
Hans Mackowiak
6f5a933de3 CardState: Refactor LandAbility/Aura/PermSpell (#7680)
* CardState: Refactor LandAbility/Aura/PermSpell

* Update CardState.java

Fix Room

* unify manaAbilities and nonManaAbilities and fix Room cards

* CardView: fix Left&Right Split not update abilities

* AbilityFactory: make Fuse Ability Secondary

* CardFactory: remove extra logic for LeftSplit and RightSplit

* AbilityFactory: set OriginalAbility for Fuse Parts

* SpellPermanent: fix Desc for CardState

* PermanentCreatureEffect: use getBasePowerString/getBaseToughnessString

* LandAbility: fix Modal Lands
2025-06-01 09:53:40 +02:00
kvn1338
e40567c9c8 Add TextBoxExchangeEffect Ability and 'Deadpool, Trading Card' (#7637)
* capure Textboxes to avoid LKI copy

* Fix copying keyworded traits twice

* Support keepTextChanges across all traits

Co-authored-by: kvn <kevni@secure.mailbox.org>
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-06-01 07:19:50 +00:00
tool4ever
928ac875b5 Fix only turning fully unlocked rooms face down (#7742)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-05-31 13:28:52 +02:00
Paul Hammerton
f4831bc51b Merge pull request #7740 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, SLD
2025-05-31 11:19:28 +01:00
Paul Hammerton
d3e0e79325 Edition updates: FCA, FIC, FIN, SLD 2025-05-31 11:05:48 +01:00
Renato Filipe Vidal Santos
ac3015eff1 Add files via upload (#7730) 2025-05-31 07:52:06 +00:00
tool4ever
554e73e352 Fix Estinien Varlineau (#7738) 2025-05-31 06:42:04 +00:00
Hans Mackowiak
fe86bb1be3 line endings 2025-05-31 00:01:54 +02:00
Fulgur14
f98eca3925 The FINAL Fantasy cards (#7736) 2025-05-30 18:01:51 +00:00
Fulgur14
8506fd2bab 4 FIN cards (#7727) 2025-05-30 18:01:34 +00:00
Hans Mackowiak
c2ffa227e2 Saga: do State-Based and Turn-Based Action only with Chapters (#7737) 2025-05-30 17:16:15 +00:00
Hans Mackowiak
f33f780b25 line endings 2025-05-30 18:58:30 +02:00
Renato Filipe Vidal Santos
855e4dcabd FIN: Memories Returning (#7731) 2025-05-30 10:23:13 +02:00
Fulgur14
40c9a06b21 Couple fixes (#7732)
* Update breaching_hippocamp.txt

* Update skanos_dragon_vassal.txt
2025-05-30 08:52:06 +03:00
Chris H
7c4cf9425f Update INSTALLATION.txt 2025-05-29 10:11:13 -04:00
Fulgur14
d2a6329e03 Leaked Rydia and Exdeath (FIN) (#7725)
* Leaked Rydia and Exdeath (FIN)

* Update exdeath_void_warlock_neo_exdeath_dimensions_end.txt

* Update rydia_summoner_of_mist.txt
2025-05-29 13:33:12 +03:00
Renato Filipe Vidal Santos
6e2717e371 FIN: 11 cards (#7724) 2025-05-29 09:22:54 +00:00
Chris H
2152b7ca7d Fix line endings 2025-05-28 23:25:57 -04:00
Fulgur14
08387f12cb Summon: Brynhildr (FIN) (#7722) 2025-05-28 19:48:22 +00:00
tool4ever
01800d3c49 Update summon_valefor.txt 2025-05-28 19:03:00 +02:00
Fulgur14
2b3735662e 7 FIN cards (#7721) 2025-05-28 18:59:07 +02:00
Renato Filipe Vidal Santos
5ab2b44343 FIN: Golbez, Crystal Collector 2025-05-28 18:48:42 +02:00
Renato Filipe Vidal Santos
0bdb9d7d27 FIN: 7 cards (#7717)
* Add files via upload

* Update thiefs_knife.txt

* Update swallowed_by_leviathan.txt

* Update aether_spike.txt

* Update swallowed_by_leviathan.txt
2025-05-28 15:45:16 +02:00
Renato Filipe Vidal Santos
1f4cebf186 FIN: The Masamune (#7719) 2025-05-28 14:04:50 +02:00
rwalters
f651a7d73d (Adventure Mode) Fix quest references and step requirements (#7697)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Fixing several text based references that looked created in bulk with similar problems. Kiora and Teferi don't require going back to the town that issued the quest, and the others reference the wrong location when arriving there for where the reputation goes
2025-05-28 11:45:24 +03:00
Fulgur14
453bae6849 Summon: G.F. Cerberus and X-ATM092 (FIN) (#7715) 2025-05-28 09:47:38 +02:00
Fulgur14
292cca0727 10 FIN cards, batch #3 (#7713) 2025-05-28 05:43:27 +00:00
Fulgur14
24f568101f 10 FIN cards, batch #2 (#7712) 2025-05-28 05:41:34 +00:00
tool4ever
ad73651382 Prishe's Wanderings: support CantSearch case (#7711) 2025-05-28 05:40:38 +00:00
Chris H
476f3dfe9c Fix line endings 2025-05-27 16:07:02 -04:00
Fulgur14
5977129096 10 FIN cards (#7710) 2025-05-27 19:58:55 +00:00
Greg Sonnier
44e243d428 fixed hraesvelgr_of_the_first_brood.txt mana cost (#7708) 2025-05-27 18:21:55 +00:00
Fulgur14
491e275b8a Rufus and Seifer (#7707) 2025-05-27 16:54:42 +00:00
Renato Filipe Vidal Santos
7bf6ba0779 FIN: Minwu (#7706) 2025-05-27 16:30:54 +00:00
Fulgur14
ea33d589ae Diamond Weapon (FIN) (#7702) 2025-05-27 16:30:02 +00:00
Renato Filipe Vidal Santos
1314c98a5a Add files via upload (#7701) 2025-05-27 17:29:23 +03:00
Fulgur14
66c1c485f2 Magic Damper (FIN) (#7704) 2025-05-27 14:28:41 +00:00
Hans Mackowiak
fe1d37bed6 Update Ikoria Lair of Behemoths.txt
Closes #7700
2025-05-27 13:34:38 +02:00
Fulgur14
64d54dc7c7 Gysahl Greens (FIN) (#7699) 2025-05-27 08:24:08 +00:00
Renato Filipe Vidal Santos
050cebf944 FIN: 6 cards (#7696) 2025-05-27 08:22:13 +02:00
Renato Filipe Vidal Santos
42e5d9437b Adventure cleanup: 2025-05-26 (#7695)
* Add files via upload

* Update quests.json

* Update quests.json

* Update shops.json

* Update skep_outer.tmx
2025-05-27 07:20:08 +03:00
Fulgur14
67aaf9902e Cactuar and Light of Judgment (FIN) (#7692) 2025-05-26 20:15:52 +00:00
Fulgur14
3d0436aeca Restoration Magic (FIN) (#7690) 2025-05-26 22:32:28 +03:00
Simisays
827d60610d Update fort_white_4_farm.tmx (#7694) 2025-05-26 22:29:53 +03:00
Renato Filipe Vidal Santos
69a1d76778 FIN: 2 cards 2025-05-26 18:15:15 +00:00
tool4ever
7bd5dbbe28 Fix text change of keywords missing traits (#7689) 2025-05-26 15:41:16 +00:00
Fulgur14
39613a8413 3 FIN commons (#7688) 2025-05-26 14:42:26 +00:00
Renato Filipe Vidal Santos
225ff335e8 FIN: Midgar, City of Mako (#7685) 2025-05-26 14:10:03 +00:00
Paul Hammerton
23a3de5446 Merge pull request #7684 from paulsnoops/edition-updates
Edition updates: FCA, FIN
2025-05-26 11:32:01 +01:00
Paul Hammerton
75fd7bbfda Edition updates: FCA, FIN 2025-05-26 11:27:50 +01:00
tool4ever
ed6a14e180 Fix LKI missing changed text (#7683) 2025-05-26 08:35:57 +00:00
Fulgur14
42b6d2c17d Dion, Bahamut's Dominant (FIN) (#7682) 2025-05-26 08:23:59 +00:00
Hans Mackowiak
9a93f0a16c TokenDb: fix Endure Token Images causing crash on Token Viewer (#7679) 2025-05-25 19:27:13 +02:00
Renato Filipe Vidal Santos
1c0d3031d3 MB2: 2 cards (#7678) 2025-05-25 17:05:22 +00:00
Hans Mackowiak
724697391c Update Aetherdrift Commander.txt 2025-05-25 18:03:42 +02:00
Renato Filipe Vidal Santos
7820c3f519 Adventure cleanup: 2025-05-25 (#7674)
* Update quests.json

* Update shops.json

* Update bluewizard_hard_bounce.dck

* Update kavu_domain.dck

* Update sandghoul.dck

* Update aerie_0.tmx

* Update barbariancamp_kobold_mine.tmx

* Update evilgrove_5_swamp.tmx

* Update grolnok_f1.tmx

* Update grove_1_bears.tmx

* Update black_castle.tmx

* Update blue_castle.tmx

* Update green_castle.tmx

* Update red_castle.tmx

* Update white_castle.tmx

* Update graveyard.tmx

* Update steppe.tmx

* Update town.tmx

* Update bog.tmx

* Update graveyard.tmx

* Update town.tmx

* Update maze_2.tmx

* Update slime_hive.tmx

* Update wastetown..tmx

* Update skep_outer.tmx

* Update tibalt_f1.tmx

* Update tibalt_f2.tmx

* Update tibalt_f3.tmx

* Update main.tsx

* Update enemies.json

* Update skep_outer.tmx

* Update items.json

* Update quests.json

* Update shops.json

* Update town_names_black.txt

* Update aerie_0.tmx

* Update sorins_amulet.txt

* Update sorins_boss_effect.txt
2025-05-25 16:35:28 +03:00
Renato Filipe Vidal Santos
fe80b3850e Update carrionette.txt (#7650) 2025-05-25 16:35:17 +03:00
rwalters
16a2ae6741 (Adventure Mode): Fix abandoned town (#7665)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Adventure Mode: Fix Quest_APortalToNowhere, in which the file path got moved but the point of interest json was not updated

* Manually altering points_of_interest.json instead of using the adventure editor tool

* Actual change was removed during synchronization with remote head, re-applying actual fix
2025-05-25 08:47:23 -04:00
tool4ever
ce19c4fb9d Update capricious_sliver.txt
Closes #7672
2025-05-24 20:21:00 +00:00
tool4ever
4911a7f951 Update professor_hojo.txt 2025-05-24 17:40:36 +00:00
Renato Filipe Vidal Santos
c6f994d47a Quick cleanup: 2025-05-24 2025-05-24 16:20:19 +00:00
Hans Mackowiak
f0a9077791 lf 2025-05-24 16:38:52 +02:00
Fulgur14
b6941a9b38 Gogo, Master of Mimicry (FIN) (#7655) 2025-05-24 13:16:13 +00:00
Fulgur14
7023eb10d3 10 FIN cards (Vaan and his band) (#7661)
* 4 FIN cards (Vaan and his band)

* Add files via upload

* Add files via upload

* Add files via upload
2025-05-24 16:12:46 +03:00
tool4ever
007e132559 UnlessCost: fix checking wrong payer (#7663)
* UnlessCost: fix checking wrong payer

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-05-24 12:10:48 +02:00
tool4ever
9b9fdb2e5e Edgar, King of Figaro and support (#7645) 2025-05-24 07:27:48 +00:00
Renato Filipe Vidal Santos
f1f4310608 FIN: Black Waltz No. 3 2025-05-24 06:36:44 +00:00
Renato Filipe Vidal Santos
8634f96e4b FIN: 3 cards (#7646) 2025-05-23 15:20:26 +00:00
Fulgur14
6eed4c31a2 Raubahn, Bull of Ala Mhigo (FIN) (#7659) 2025-05-23 14:22:31 +00:00
Hans Mackowiak
c7b9934072 ImageFetcher: add downloadUrl for HiddenCard (#7651) 2025-05-23 13:51:00 +02:00
Paul Hammerton
cc50d72d63 Merge pull request #7654 from paulsnoops/edition-updates
Edition updates: FCA, FIN, SLD
2025-05-23 10:10:34 +01:00
Paul Hammerton
fbceab3252 fix-the-gold-saucer 2025-05-23 10:06:11 +01:00
Paul Hammerton
780491ae4b Edition updates: FCA, FIN, SLD 2025-05-23 09:56:26 +01:00
tool4ever
994471e9b6 Update verazol_the_split_current.txt 2025-05-23 10:50:35 +02:00
Chris H
e1bf639e90 Fix broken json in tmx file 2025-05-22 21:43:40 -04:00
Chris H
4386cead3e Fix end lines 2025-05-22 21:43:19 -04:00
Fulgur14
9b0d0f7924 8 FIN Cards (#7648) 2025-05-22 20:53:45 +00:00
Fulgur14
b9be1bc647 Omega, Heartless Evolution (FIN) (#7647) 2025-05-22 17:16:10 +02:00
tool4ever
513e4c6c8d Update hraesvelgr_of_the_first_brood.txt 2025-05-22 16:37:28 +02:00
tool4ever
0f5b67a504 Update lyse_hext.txt 2025-05-22 16:36:52 +02:00
Fulgur14
72f455067f Tellah, Great Sage (FIN) (#7644) 2025-05-22 16:36:13 +02:00
Hans Mackowiak
6dc508b631 ~fix Token ImageKeys without Set (#7643) 2025-05-22 09:29:33 +03:00
Hans Mackowiak
deecf128c3 Fix Henzie again with SvarFallback (#7604)
* Fix Henzie again with SvarFallback

* henzie rename Svar
2025-05-22 07:20:22 +02:00
Renato Filipe Vidal Santos
5f497669e5 Quick cleanup: 2025-05-21 (#7642) 2025-05-21 20:16:00 +00:00
Fulgur14
90952d95c9 Summon: G.F. Ifrit (FIN) (#7641) 2025-05-21 18:49:24 +00:00
Renato Filipe Vidal Santos
5a5ef52492 FIN: Clash of the Eikons 2025-05-21 18:27:22 +00:00
Fulgur14
3b4df7211c Jenova, Snow, and Ether (FIN) (#7638) 2025-05-21 18:25:28 +00:00
Renato Filipe Vidal Santos
45329a6df0 FIN: 5 more cards (#7627) 2025-05-21 18:25:11 +00:00
Fulgur14
22607adf57 Qiqirn Merchant and the 9 remaining Town duals. (#7639) 2025-05-21 18:06:34 +00:00
Agetian
bd9c72fee9 - Update Libgdx to 1.13.5. (#7636) 2025-05-21 18:12:33 +03:00
Renato Filipe Vidal Santos
35641265dd Update remorseless_punishment.txt (#7635) 2025-05-21 17:43:05 +03:00
Renato Filipe Vidal Santos
fe37a8358b Fixing Firion (#7633) 2025-05-21 14:23:56 +00:00
Fulgur14
bb7ae64ef8 Slash of Light (FIN) (#7632) 2025-05-21 09:46:28 +00:00
Fulgur14
4d9db78cc3 Aettir and Priwen (FIN) (#7631) 2025-05-21 06:18:15 +00:00
Agetian
ce46c684b5 - Fix AI logic for Sorin, Vengeful Broodlord. (#7630) 2025-05-21 09:03:54 +03:00
Fulgur14
29d4e716f5 The remaining 4 Crystals and some other FIN cards (#7628) 2025-05-21 05:02:21 +00:00
Chris H
c6bfdd5b78 Landscape Sketchbook expansion (#7618)
* Expand landscape sketchbooks

* Remove the sketchbooks for sale that are earned via quests
2025-05-21 06:57:58 +03:00
Hans Mackowiak
559daf9fce update ImageKey for Other 2025-05-20 22:58:22 -04:00
Hans Mackowiak
9caa024fa5 transformed token image 2025-05-20 22:58:22 -04:00
Hans Mackowiak
3605b4e34e uppercase token set and fallback token 2025-05-20 22:58:22 -04:00
Hans Mackowiak
56832ff987 ~ update set token image location 2025-05-20 22:58:22 -04:00
Hans Mackowiak
338bb09747 PaperToken: cleanup imageKey without collectorNumber 2025-05-20 22:58:22 -04:00
Hans Mackowiak
b57a5e9ad1 PaperToken: add CollectorNumber and update Downloader 2025-05-20 22:58:22 -04:00
Simisays
67f8f4760e Update skep_outer.tmx 2025-05-20 22:51:20 -04:00
Ryan Walters
485427c682 Fix poi references and an exception generated by failing to address non-swapped poi references, focusing on quest 18, A Focused Mind 2025-05-20 09:05:21 -04:00
Renato Filipe Vidal Santos
8852732e6b FIC: 4 cards (#7624) 2025-05-20 12:27:28 +00:00
Renato Filipe Vidal Santos
6264761117 Quick cleanup: 2025-05-20 (#7622) 2025-05-20 11:07:29 +00:00
Paul Hammerton
a3b1c98abf Merge pull request #7620 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, PF25, PPRO, PSS5, PW25, SCH, SLD
2025-05-20 11:10:31 +01:00
Paul Hammerton
c566a4bed8 oops 2025-05-20 10:59:42 +01:00
Paul Hammerton
a16e2a9c37 don't add FSPL because JP only and same art 2025-05-20 10:53:43 +01:00
Paul Hammerton
f80bb13ed7 Edition updates: FCA, FIC, FIN, FSPL, PF25, PPRO, PSS5, PW25, SCH, SLD 2025-05-20 10:48:58 +01:00
Fulgur14
1c350b1766 From Father to Son (FIN) (#7619) 2025-05-20 09:24:36 +00:00
Fulgur14
0b5ce9c8fc Add 4 cards [FIN]
* Add files via upload

* Update TypeLists.txt

* Create g_1_1_frog.txt

* Update quina_qu_gourmet.txt

I realized that I forgot to set the amount of tokens.
2025-05-20 09:52:01 +03:00
Fulgur14
74c7f4b164 Coral Sword (FIN) (#7614) 2025-05-20 09:47:02 +03:00
Fulgur14
6128f1d720 Queen Brahne and Lindblum (FIN) (#7615)
* Add files via upload

* Add files via upload
2025-05-20 09:46:48 +03:00
Fulgur14
9e2dcbb630 Haste Magic (FIN) (#7616) 2025-05-20 09:46:12 +03:00
Hans Mackowiak
ec9fc88734 ~ fix IterableUtil.and and Iterable.or usage (#7607) 2025-05-20 05:47:26 +02:00
Renato Filipe Vidal Santos
59c404f6c4 Cleanup: Blazing Crescendo (#7617) 2025-05-19 19:40:00 +00:00
Renato Filipe Vidal Santos
fd727a909b Update white_auracite.txt (#7613) 2025-05-19 16:59:50 +02:00
Chris H
ce6ad65e12 Add some combat tests for the AI to help improve poor combat areas (#7600)
* Add some basic testing for AI Combat decisions

* Add some basic testing for AI Combat decisions
2025-05-19 12:30:53 +03:00
tool4ever
6b65f8972c Update lupine_prototype.txt 2025-05-19 09:05:03 +02:00
Renato Filipe Vidal Santos
2e7fe8a81b FIN: Ice Flan 2025-05-19 07:35:42 +02:00
tool4ever
6f9db790a6 Fix updating room in exile (#7608)
* Fix wrong zone breaking X
2025-05-18 18:04:48 +00:00
Simisays
645aff52cb Fix Sanguine Soothsayer (#7606) 2025-05-18 11:38:51 +00:00
Hans Mackowiak
4d4afbdf03 lf 2025-05-18 13:01:33 +02:00
Renato Filipe Vidal Santos
a8c1f5c969 Quick cleanup: 2025-05-17 (#7597) 2025-05-18 09:33:29 +00:00
Fulgur14
0f4c94d6f8 PuPu UFO (#7603) 2025-05-18 09:33:06 +00:00
Renato Filipe Vidal Santos
53479c60b4 Fixing Living Lectern, Embereth Veteran (#7599)
* Update living_lectern.txt

* Update embereth_veteran.txt
2025-05-18 09:43:22 +02:00
Chris H
0c7b8c5b04 Collecting Landscape Sketchbooks adds that set to the basic land dialog (#7602)
* Collecting Landscape Sketchbooks adds that set to the basic land dialog

* Optimize the edition mapping
2025-05-18 07:33:11 +03:00
tool4ever
7860957d8f Update astrologians_planisphere.txt 2025-05-17 16:04:24 +00:00
Renato Filipe Vidal Santos
07bc31f4c1 FIN: 2 more cards (#7592) 2025-05-17 12:37:01 +02:00
Paul Hammerton
99390f5967 Merge pull request #7595 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, RFIN, SLD
2025-05-17 11:29:12 +01:00
Paul Hammerton
ee8ca02128 Edition updates: FCA, FIC, FIN, RFIN, SLD 2025-05-17 11:12:04 +01:00
Hans Mackowiak
1bc7efba65 Update Cards with WithMayLook (#7593) 2025-05-17 10:39:20 +02:00
Hans Mackowiak
efc2357905 lf 2025-05-17 09:16:55 +02:00
Fulgur14
d5d01011f4 Update fandaniel_telophoroi_ascian.txt (#7590) 2025-05-17 06:21:34 +00:00
Fulgur14
c1a19ea4ae Buster Sword (FIN) (#7589) 2025-05-17 06:11:40 +00:00
Fulgur14
ad3fb1137a The Falcon, Airship Restored (FIC) (#7586) 2025-05-17 06:10:53 +00:00
Renato Filipe Vidal Santos
c5c6b36f4f FIC: last 2 cards + FIN: 2 cards (#7576)
* Add files via upload

* Add files via upload

* Add files via upload
2025-05-17 07:41:23 +03:00
Fulgur14
f2f92212bc How to suplex a train (#7578)
There's something strange with Phantom Train's ability where its P/T display doesn't update properly after getting the counter -- maybe because the counter is gained before animating?
2025-05-17 07:41:11 +03:00
Fulgur14
3226be58be Serah, Sazh, and Anima (FIN) (#7585)
* Serah, Sazh, and Anima (FIN)

* Update serah_farron_crystallized_serah.txt
2025-05-17 07:41:03 +03:00
Renato Filipe Vidal Santos
46cfcff0ca Update dancers_chakrams.txt 2025-05-16 23:01:48 -04:00
Fulgur14
b161c9612b Triple Triad (FIN) (#7572) 2025-05-16 18:43:59 +00:00
Renato Filipe Vidal Santos
91dee5c379 FIN: another 2 cards 2025-05-16 18:43:47 +00:00
Fulgur14
349129f88f Quistis Trepe (FIN) (#7581) 2025-05-16 15:50:27 +00:00
Hans Mackowiak
57ea25bbbd Update white_auracite.txt
Closes #7579
2025-05-16 15:43:11 +02:00
Hans Mackowiak
d1725af64d Update urianger_augurelt.txt
Close #7575
2025-05-16 13:19:14 +02:00
Fulgur14
72626ef214 Choco, Seeker of Paradise and Paladin's Arms (FIN) (#7571) 2025-05-16 06:25:02 +00:00
Chris H
e00d5ee30b Fix line endings 2025-05-15 22:19:14 -04:00
tool4ever
d6c42c3c8c Update hildibrand_manderville.txt
Closes #7574
2025-05-15 21:06:53 +00:00
Greg Sonnier
950a1f2a44 fixed missing cleanup in graha_tia_scion_reborn.txt (#7573) 2025-05-15 20:35:17 +00:00
Renato Filipe Vidal Santos
c895b0eeab FIC: yet another 5 cards (#7570) 2025-05-15 18:50:48 +00:00
Fulgur14
82c8fb20e8 Jecht and 2 other FIN cards (#7562) 2025-05-15 18:18:20 +02:00
Fulgur14
cf54f3e04e Relm's Sketching (FIN) (#7569) 2025-05-15 17:27:05 +02:00
Fulgur14
f4958a4b49 9 FIC/FIN cards... plus a token (#7561) 2025-05-15 15:51:13 +02:00
Renato Filipe Vidal Santos
760f9412ff FIC: another 5 cards 2025-05-15 15:15:57 +02:00
Fulgur14
71f2d41eb0 Sin, Unending Cataclysm (FIC) (#7563) 2025-05-15 15:15:38 +02:00
Fulgur14
51d7933ef3 10 FIC cards, batch 3 (#7547) 2025-05-15 15:13:45 +02:00
Fulgur14
f5938b47e1 10 FIN/FIC cards, batch 8 2025-05-15 15:13:35 +02:00
tool4ever
807d078799 Support for Lifestream's Blessing (#7565) 2025-05-15 15:11:20 +02:00
Renato Filipe Vidal Santos
bedb97183b FIC: 7 more cards (#7557) 2025-05-15 15:11:04 +02:00
Fulgur14
08919e3375 Summon: Yojimbo and Capital City (#7568) 2025-05-15 14:34:19 +02:00
Fulgur14
a117d65f51 10 FIN/FIC cards, batch 9 (#7560) 2025-05-15 14:19:26 +02:00
Fulgur14
b61e3015f3 8 FIN/FIC cards (#7556) 2025-05-15 13:09:49 +02:00
Fulgur14
83a512a075 10 FIN/FIC cards, batch 5 (#7550) 2025-05-15 13:08:39 +02:00
Fulgur14
c1630a2e47 Rejoin the Fight (FIC) 2025-05-15 13:08:11 +02:00
Fulgur14
1d0b50356f Auron's Inspiration (FIN) (#7566) 2025-05-15 12:34:13 +02:00
Hans Mackowiak
43d82ce1ce lf 2025-05-15 07:08:09 +02:00
tool4EvEr
c4a8765d6c 2 fixes 2025-05-14 21:27:00 -04:00
Chris H
15d49bc1b1 - Sort inventory by item type 2025-05-14 20:36:46 -04:00
Chris H
d84712b65d Lock smith prices when you click the Smith button 2025-05-14 20:36:28 -04:00
Fulgur14
eac94f7249 10 FIN/FIC cards, batch 6 (#7553) 2025-05-14 17:18:28 +02:00
rpg2014
d134c3dfcd Cleaned up the High DPI monochrome image
Cleaned up some pixels from the high dpi monochrome android icon
2025-05-14 09:09:15 -04:00
rpg2014
0a03327299 Adding monochrome icon to the android app
Added monochrome icon pngs for each mipmap size and added them to the xml configs for the icons.  The monochrome icons were made by editing the respective icon in paint.net.
2025-05-14 09:09:15 -04:00
Fulgur14
0a673aeadc 10 FIC/FIN cards, batch 4 (#7548) 2025-05-14 10:54:45 +02:00
Paul Hammerton
40f7a9a22a Edition updates: FCA, FIC, FIN, SLD 2025-05-14 09:22:30 +01:00
Paul Hammerton
e2ccf8960a Merge pull request #1 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, SLD
2025-05-14 09:18:05 +01:00
Paul Hammerton
66acc6b920 oops lands 2025-05-14 09:15:41 +01:00
Paul Hammerton
83abcf7c44 Update Final Fantasy.txt 2025-05-14 09:13:00 +01:00
Paul Hammerton
ef9d807b6e Update Final Fantasy Commander.txt 2025-05-14 09:04:54 +01:00
Paul Hammerton
83f334888e Update Final Fantasy Through the Ages.txt 2025-05-14 08:58:38 +01:00
Paul Hammerton
8359cfda35 Edition updates: FIC, FIN, SLD 2025-05-14 08:53:21 +01:00
Fulgur14
93b2d7c795 19 FIN/FIC cards + tokens/counters (#7525) 2025-05-14 07:30:38 +00:00
rwalters
ea74cc8f7f Fix enemy name (#7551)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Fix a misspelling in the name of a Merfolk Warrior when being previewed before a battle
2025-05-14 06:46:00 +03:00
Fulgur14
4e16a5ea60 Update dancers_chakrams.txt (#7549) 2025-05-13 23:23:48 +02:00
Renato Filipe Vidal Santos
00325fb511 MB2: Value Town 2025-05-13 20:52:52 +02:00
Renato Filipe Vidal Santos
fe042fa6d9 FIC: 5 cards (#7541) 2025-05-13 20:24:15 +02:00
Fulgur14
6830f15859 10 FIC/FIN cards, batch 2 (#7543) 2025-05-13 20:23:43 +02:00
Renato Filipe Vidal Santos
fb3a8ce003 FIC: another 5 cards (#7545) 2025-05-13 20:21:53 +02:00
tool4ever
edc6edc44b Support for Lord Jyscal Guado (#7546) 2025-05-13 20:21:12 +02:00
tool4ever
40b3edd426 Update charismatic_conqueror.txt 2025-05-13 18:58:06 +02:00
Fulgur14
f50aea1576 10 FIC/FIN cards, batch 1 (#7542) 2025-05-13 17:53:35 +02:00
Renato Filipe Vidal Santos
4ab67b1d3b FIN: 10 more cards (#7528) 2025-05-13 17:39:48 +02:00
Chris H
a4e4525769 Remember card not spell for tibalts trickery (#7539) 2025-05-13 17:08:40 +02:00
Hans Mackowiak
1d664145e0 Update Dominaria United.txt
Note from Scyfall:

Dominaria United series of 26 tokens
Dominaria United comes with two sets of tokens: a series of 26 bearing either DMU or DMC set codes, and a separate series of 12 tokens with exclusively the DMC set code. We don't know why they did things this way. We've opted to file the entire series of 26 here regardless of set code to avoid archival conflicts.
2025-05-13 10:11:26 +02:00
Fulgur14
32355499aa Matoya, Archon Elder and Valkyrie Aerial Unit (#7535) 2025-05-13 07:18:31 +02:00
Fulgur14
a475855c4c Update cunning_azurescale_divining_dive.txt (#7540) 2025-05-13 06:52:27 +02:00
Fulgur14
598d3a4958 Update esper_origins_summon_esper_maduin.txt 2025-05-12 23:36:54 -04:00
Fulgur14
35a4053212 Update sevinnes_reclamation.txt 2025-05-12 23:36:54 -04:00
Fulgur14
9d770a9bca Esper Origins and Ishgard, the Holy See (FIN) 2025-05-12 23:36:54 -04:00
Paul Hammerton
de15ab3a71 Alchemy Rebalancing for May 13, 2025 2025-05-12 23:33:07 -04:00
rwalters
35bc80767f Fix exit to world map edge cases (Adventure) (#7522)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Attempt to band-aid two problems relating to showing a dialog to return to the main map, one in which some patrolling enemies ignore the menu restriction and another in which such enemies touching a player who is transitioning out of a dungeon causing a soft lock upon returning to the map
2025-05-12 23:28:00 -04:00
Simisays
ef28a92dff update 2025-05-12 23:15:13 -04:00
Chris H
99f3ccb8d6 Fix endlines 2025-05-12 18:03:04 -04:00
Simisays
16148ec992 10 cards (#7485) 2025-05-12 21:41:22 +03:00
rwalters
93f7987312 Fix on the hunt hole (Adventure) (#7516)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Fix an edge case where the On The Hunt quest can end up failing to ever spawn an enemy to hunt

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-05-12 13:26:49 +03:00
Paul Hammerton
c7816daaf6 Remove Explorer from archived formats and net decks (#7524) 2025-05-12 13:14:17 +03:00
Simisays
bc458ecde4 FIN 10 cards (#7526) 2025-05-12 12:13:22 +02:00
Fulgur14
e4da62c254 3 FIN/FIC cards (#7529)
* 3 FIN cards

* Update starting_town.txt
2025-05-12 09:50:07 +02:00
Fulgur14
53588dfc24 Terra, Magical Adept (#7531)
* Terra, Magical Adept
2025-05-12 09:48:44 +02:00
Renato Filipe Vidal Santos
61c11b38a0 FIN: 4 Job select cards and support (#7520)
* Update CardFactoryUtil.java

* Update Card.java

* Update Keyword.java

* Update white_mages_staff.txt

* Update dragoons_lance.txt

* Update black_mages_rod.txt

* Update summoners_grimoire.txt

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-05-12 09:47:18 +02:00
tool4ever
fdcc33198b Update rhonas_the_indomitable.txt
Closes #7530
2025-05-12 08:13:13 +02:00
Hans Mackowiak
dd7a0e99e2 ~ lf 2025-05-12 07:00:53 +02:00
Hans Mackowiak
cb0e594a6e CardEdition: add collector number for other (#7504)
* CardEdition: add collector number for other

* EditionEntry record

* Add getOtherImageKey

* Update StaticData.java

* use getOtherImageKey in getFacedownImageKey

* Update CardEdition.java

Remove findOther in favor of getOtherSet

* Update CardEdition.java

return findOther, but with Aggregates.random

* ~ move more helper images to ImageKeys
2025-05-12 06:59:20 +02:00
tool4ever
059881a7b5 Some support for Desert Cenote (#7505)
---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-05-11 18:19:37 +00:00
Jetz
ca59ac925c Fix weird behavior in image view with certain filters 2025-05-11 10:00:31 -04:00
Fulgur14
0b5f14cd9c 5 FIN cards (#7518) 2025-05-11 10:27:38 +00:00
Paul Hammerton
1d987c25e9 Merge pull request #7523 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, PF25, PLG25, SLD
2025-05-11 10:43:43 +01:00
Paul Hammerton
5cb68bc2f0 Edition updates: FCA, FIC, FIN, PF25, PLG25, SLD 2025-05-11 10:36:31 +01:00
Hans Mackowiak
1715efced7 FIN: Tiered (#7517)
* Tiered: first example

* use ModeCost for both Spree and Tiered
2025-05-11 09:38:23 +02:00
Chris H
aeb39b99bb If the AI cloned your deck, don't use your deck as a reward base if there aren't more than 5 choices. 2025-05-10 09:05:54 -04:00
Renato Filipe Vidal Santos
778066a622 FIN: Y'shtola Rhul (#7514) 2025-05-10 11:04:03 +00:00
Jetz
75a056abe6 Add a parameter for battle protection RE that can be used to override it for custom types. 2025-05-09 23:19:42 -04:00
Jetz72
3915f316e2 Unused Import 2025-05-09 23:19:42 -04:00
Jetz
b52646ee7e Delete some logic that doesn't currently work 2025-05-09 23:19:42 -04:00
Jetz
cb1d48a566 Support for non-siege battles 2025-05-09 23:19:42 -04:00
Ryan Walters
733d56246e Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner) 2025-05-09 23:18:45 -04:00
distillate personality
7902ad71d7 Update forge-gui-mobile/src/forge/adventure/data/AdventureQuestStage.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-09 21:54:11 -04:00
birdbath
2666de0adb Fix for 'Fetch' objectives that allows the user to 'Use' an item in their inventory at the PoI to complete the objective instead. 2025-05-09 21:54:11 -04:00
Renato Filipe Vidal Santos
c2613a03b0 FIC: Yuna, G'raha and new token (#7512) 2025-05-09 08:26:30 +02:00
Chris H
b077cca3ac Fix line endings 2025-05-08 22:09:22 -04:00
Fulgur14
43fc281e65 Hildibrand Manderville (FIC) 2025-05-08 20:25:13 +00:00
Fulgur14
7b1f5d0a34 Zenos yae Galvus // Shinryu, Transcendent Rival (FIN) (#7503) 2025-05-08 20:05:13 +00:00
Paul Hammerton
330fe0a0a8 Merge pull request #7502 from paulsnoops/edition-updates
Edition updates: FIC, PURL, SLD
2025-05-07 18:08:15 +01:00
Paul Hammerton
30a4c24c35 Edition updates: FIC, PURL, SLD 2025-05-07 18:01:06 +01:00
Renato Filipe Vidal Santos
01a6a38285 YTDM: 4 cards (#7473) 2025-05-07 05:17:38 +00:00
Renato Filipe Vidal Santos
fe5aa37791 Update big_play.txt (#7500) 2025-05-07 06:43:39 +02:00
Simisays
42f1fba7c4 Adventure 3 small fixes (#7499)
* Update grolnok_f1.tmx

* update
2025-05-06 23:42:03 +03:00
autumnmyst
0c6f1ff58f UNF: Added the 4 eternal-format-legal dice modification/reroll cards (#7489) 2025-05-06 17:13:52 +00:00
Hans Mackowiak
8678b5ec5b Update Duskmourn House of Horror.txt
Moved to "other" section
2025-05-06 10:35:59 +02:00
Hans Mackowiak
71256972c4 Update Commander 2013.txt
remove token list
2025-05-05 22:32:37 +02:00
matthias8422
a76f23fc25 feature/Refactored-server-url-parsing-logic (#7496)
* HostPort now properly returns -1 when no port was specified

* refactored the URL parsing to now attempt a DNS lookup in cases where URI parsing fails
This now allows local computer names and localhost etc
2025-05-05 17:10:37 +00:00
Hans Mackowiak
3c7993d640 Update Secret Lair Drop Series.txt
Update token collector number and artist
2025-05-05 10:43:03 +02:00
Hans Mackowiak
71d984a5ff Update Archenemy.txt
reorder card list
2025-05-05 10:41:35 +02:00
Hans Mackowiak
be171c011a Volrath's Curse and Lost in Thought with Modes and IgnoreEffectCost (#7492)
* Refactor Damping Engine
2025-05-04 16:55:38 +00:00
tool4ever
becdd180f4 Some cleanup (#7493)
* Some cleanup

* Update StaticAbilityCantAttackBlock.java

* Some cleanup

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-05-04 18:11:00 +02:00
tool4ever
41efdb095a Update StaticAbilityCantAttackBlock.java 2025-05-04 12:25:24 +00:00
tool4ever
86453a6fa7 Update StaticAbilityCantAttackBlock.java 2025-05-04 12:24:22 +00:00
Hans Mackowiak
8448a6e20e Update cards with Static CantAttack or CantBlock (#7435)
* Update cards with Static CantAttack or CantBlock

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-05-04 13:14:58 +02:00
Hans Mackowiak
194662c9c7 StaticAbilityMode: add Enum and allow MultiMode (#7491) 2025-05-04 10:56:27 +02:00
Fulgur14
d3a33a0092 Update tifa_martial_artist.txt (#7488) 2025-05-03 21:12:06 +00:00
tool4ever
2bab13247c Some fixes (#7484) 2025-05-03 19:31:47 +02:00
Hans Mackowiak
da30b886c9 fix Card from Unknown Set calling getCardEdition 2025-05-03 16:47:39 +02:00
Hans Mackowiak
da38641ff8 Add CantBlock Mode (#7452) 2025-05-03 15:21:08 +02:00
Chris H
fcbc83c3ff Scryfall randomly changed the collector numbers 2025-05-02 16:03:25 -04:00
Hans Mackowiak
4ae93980b6 ~ remove last Unknown Artist 2025-05-02 09:42:04 -04:00
Hans Mackowiak
10b96138c3 ~ update other editions and remove wrong tokens 2025-05-02 09:42:04 -04:00
Hans Mackowiak
8706edbb01 CardEdition: have Token look for other Sets too 2025-05-02 09:42:04 -04:00
Renato Filipe Vidal Santos
4f5c074fe6 YTDM: 3 cards (#7448)
* Add files via upload

* Update loch_larent.txt
2025-04-30 19:55:06 +03:00
Hans Mackowiak
50ca71fc87 Update Return to Ravnica.txt 2025-04-30 13:24:44 +02:00
Hans Mackowiak
84134341e3 Update Bloomburrow Commander.txt
Update offspring token info
2025-04-30 09:58:39 +02:00
Hans Mackowiak
988518284c Update Bloomburrow.txt
Update offspring token info
2025-04-30 09:52:24 +02:00
Hans Mackowiak
1476621df1 CopyPermanentEffect: simplify getting PaperCard from DefinedName 2025-04-30 07:03:26 +02:00
Hans Mackowiak
cb8e13e933 Update Murders at Karlov Manor Commander.txt 2025-04-29 09:39:37 -04:00
Chris H
ea4d878a2d Update forge-gui/res/editions/Murders at Karlov Manor Commander.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
eeafc6400c Update Commander Masters.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
875b4557b0 Update Commander Anthology Volume II.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
bec25c68f2 Update Commander Anthology.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
f98aae35a5 Update Commander Legends Battle for Baldur's Gate.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
b1acb9f0a3 Update Commander Legends.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
89049db6a0 Update Modern Horizons 2.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
77378855e3 Update Tarkir Dragonstorm Commander.txt 2025-04-29 09:39:37 -04:00
Paul Hammerton
709582086f Edition token section fixes 2025-04-29 09:39:37 -04:00
Paul Hammerton
7eda9e204e Merge pull request #7468 from paulsnoops/ytdm-formats
Format updates: YTDM
2025-04-29 11:29:43 +01:00
Paul Hammerton
cf94d96d95 Merge pull request #7467 from paulsnoops/edition-updates
Edition updates: FIC, SLD, UNK, YTDM
2025-04-29 11:29:31 +01:00
Paul Hammerton
a78d361b2e FIC 2025-04-29 11:23:53 +01:00
Paul Hammerton
d01619c09c UNK 2025-04-29 11:19:05 +01:00
Paul Hammerton
fd76098765 Format updates: YTDM 2025-04-29 11:13:51 +01:00
Paul Hammerton
9fb08f1695 Edition updates: SLD, YTDM 2025-04-29 11:09:12 +01:00
Hans Mackowiak
4eef86daf1 Card: add FacedownImageKey for better logic and set codes (#7459)
* Card: add FacedownImageKey for better logic and set codes
2025-04-29 07:32:51 +02:00
matthias8422
d74d2da755 Added a new Couldnt connect to server error message
Re wrote the URL parsing logic again, but due to increased complexity put it into a utility class
Fixed desktop version opening up a lobby even when it did not connect to a server
Wired up the new error message to mobile, and both error messages to Desktop
2025-04-28 17:56:08 -04:00
Simisays
f69fd12ebd Update quests.json
xira fix as well
2025-04-28 16:07:29 -04:00
Simisays
d41cdca1bd update 2025-04-28 16:07:29 -04:00
Chris H
acfc413465 Fix AI targeting illegally for each player destroy targeting (#7463) 2025-04-28 07:22:59 +03:00
Agetian
7a2ef6c9fd Update LDA data for Tarkir: Dragonstorm, add puzzle PS_TDM3 (Tarkir: Dragonstorm 03) (#7461)
* - Update LDA for Tarkir: Dragonstorm

* - Add puzzle PS_TDM3.
2025-04-28 07:16:06 +03:00
Chris H
b214b6f8bf Update font-list.txt 2025-04-26 23:46:01 -04:00
Chris H
4d75f7b225 Update fonts location 2025-04-26 23:46:01 -04:00
Chris H
b7db4b4cbc Fix if collector number is null 2025-04-26 22:40:43 -04:00
Chris H
efaca50d97 Fix accidentally removed other blocks 2025-04-26 22:40:43 -04:00
Chris H
0157113103 M-Z token fixes 2025-04-26 22:40:43 -04:00
Chris H
bf70d8e6f6 A-M token updates 2025-04-26 22:40:43 -04:00
Hans Mackowiak
30ac5ae381 Update Duskmourn House of Horror.txt
add artist
2025-04-26 22:40:43 -04:00
Chris H
dd94169c9a Download token images when token collector numbers are defined 2025-04-26 22:40:43 -04:00
Dave
4c24e7248d MTGO Cube 2025 04 (#7449)
* MTGO Vintage Cube 2025-04

* MTGO Vintage Cube 2025-04.draft

* MTGO Vintage Cube 2025-04.dck

* Update MTGO Vintage Cube 2025-04.dck

Use more standard versions of cards
2025-04-26 09:21:18 +03:00
Renato Filipe Vidal Santos
40eb8de90c Fixing Ravenous Harpy, Reaper of Flight Moonsilver (#7447) 2025-04-23 19:07:00 +00:00
Jetz
8f60973d95 Put back blank collector number suffix. 2025-04-23 12:48:03 -04:00
Jetz
bc9df8872f Added support for amount in give card command.
Added 'give print' command.
2025-04-23 12:48:03 -04:00
Renato Filipe Vidal Santos
1071089879 Cleanup: "During your turn" Oracle update, living metal (#7446) 2025-04-23 16:39:30 +02:00
Renato Filipe Vidal Santos
28ae7fa729 Cleanup: NumAtt$ & NumDef$, new cards 2025-04-23 11:01:27 +02:00
Hans Mackowiak
e9c5bc46ae CardEdition: add TokenInSet 2025-04-22 19:51:52 -04:00
Paul Hammerton
9308dac257 Merge pull request #7442 from paulsnoops/cmdr-bnr-22-apr-25
Commander Banned and Restricted Announcement for April 22, 2025
2025-04-22 20:34:36 +01:00
Paul Hammerton
f5fa87410b Commander Banned and Restricted Announcement for April 22, 2025 2025-04-22 20:29:07 +01:00
Renato Filipe Vidal Santos
c3167a9928 Cleanup: Optional wheel update 2025-04-22 10:41:29 +02:00
Pedro Durán
c533bee4b3 Update card translations (#7126) 2025-04-22 10:20:14 +02:00
matthias8422
01a3b2723f Fixed bug where android boolean settings were not properly displaying their checked state (#7440) 2025-04-21 17:53:53 +00:00
Hans Mackowiak
8640a2aaad ~ lf 2025-04-21 16:45:07 +02:00
Paul Hammerton
9f5e5f3068 Update Planeswalker Championship Promos.txt 2025-04-21 10:10:26 -04:00
Paul Hammerton
3db209f128 Update Media and Collaboration Promos.txt 2025-04-21 10:10:26 -04:00
Chris H
351a94281c Remove language code if we can't find it the first request 2025-04-21 10:10:26 -04:00
Chris H
f190e7e678 Add the ability to use hyphens in Collector numbers 2025-04-21 10:10:26 -04:00
Fulgur14
e01d3e0a6d Tifa, Martial Artist 2025-04-21 09:59:38 +00:00
Chris H
cb3fcb259f Revert "Add the ability to use hyphens in Collector numbers"
This reverts commit 0940e7433b.
2025-04-20 17:12:23 -04:00
Chris H
0940e7433b Add the ability to use hyphens in Collector numbers 2025-04-20 17:10:10 -04:00
tool4ever
8e48ff3dfa Update zenith_festival.txt 2025-04-20 17:57:07 +00:00
matthias8422
07b10f736b Feature/network improvement android (#7418)
* initial network improvements for mobile

* Fixed settings menu message referencing proper localization string
2025-04-20 17:34:24 +00:00
Drecon84
d0e4e5bd17 [Adventure] Quest order fix 2025-04-20 12:32:10 -04:00
Hans Mackowiak
fd082ffabb Fix implements IPref 2025-04-20 14:28:47 +02:00
Drecon84
520dcb88d6 [Adventure] Fixing fallback dialog in flower cave
This piece of code didn't work and produced a fallback dialog. Fixed the formatting to make it work as it was supposed to.
2025-04-19 22:44:37 -04:00
Jetz
a4138ebd4d Fix cards reverting to first art in set when game is restarted 2025-04-19 13:15:29 -04:00
tool4ever
1118f5b135 Fix UntilHostLeavesPlay corner case (#7430) 2025-04-19 14:35:07 +00:00
Renato Filipe Vidal Santos
e78f921e4d Update nantuko_slicer.txt (#7421) 2025-04-19 09:34:21 +00:00
tool4ever
a6ec235052 Fix Escalate (#7428) 2025-04-19 08:09:41 +00:00
Jetz72
613cc549f7 Update DraftEffect.java
Delete extra if statement
2025-04-18 19:21:37 -04:00
Jetz72
69c7083592 Update forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java
Co-authored-by: Chris H <zenchristo@gmail.com>
2025-04-18 19:21:37 -04:00
Jetz
7f64e25691 Disable TokenCard flag on drafted cards by default. 2025-04-18 19:21:37 -04:00
Justin Babcock
2cb107380d Fix Knockout Maneuver wrong card type (#7425) 2025-04-18 20:13:36 +00:00
Drecon84
2a4eaea90a [Adventure] Fix for beating castles before getting there in main quest (#7391)
* First draft of the castle main quest bug workaround

Adds a possibility to start the game without the main quest and changes all castles to only let you into the gate if you either don't have a main quest active or are actively on the quest to defeat the castles.

* Rewrote main quest stuff

Wanted to decouple the main quest from the start of the game a little bit to allow the player to start without a main quest.

* Fully tested solution for main quest bug

Castles should now behave as expected.
Need to fix one minor bug still.

* Fixing the starting portal and wizard

This seems to have completely fixed the issues I have found with not starting with a main quest. Hopefully I have found all of the things that might break.
2025-04-18 12:54:09 -04:00
Paul Hammerton
8fdbaf3611 Merge pull request #7423 from paulsnoops/fix-sld
Edition updates: SLD
2025-04-18 09:19:06 +01:00
Paul Hammerton
55edec665f Edition updates: SLD 2025-04-18 09:15:41 +01:00
Paul Hammerton
f11c6a7038 Merge pull request #7422 from paulsnoops/edition-updates
Edition updates: PF25, SLD
2025-04-18 09:09:11 +01:00
Paul Hammerton
36150be7f3 Edition updates: PF25, SLD 2025-04-18 09:04:07 +01:00
Hans Mackowiak
b861994d2f add missing Enchant Keywords 2025-04-18 08:52:28 +02:00
matthias8422
0242189012 Fixed mobile server host UPnP dialog from accidently blocking ther GUI thread indefinatly (#7417) 2025-04-18 06:43:31 +00:00
tool4ever
69ddfdc18c Update envoy_of_the_ancestors.txt
Closes #7419
2025-04-18 06:39:56 +00:00
Hans Mackowiak
bef56d1caa add missing Enchant Keywords 2025-04-18 08:38:36 +02:00
tool4ever
c7f43e245d Fix missing LTB trigger (#7415) 2025-04-18 06:36:18 +00:00
Hans Mackowiak
939b7a22e0 Update fog_on_the_barrow_downs.txt 2025-04-18 06:12:52 +02:00
Jetz
a79a3503f5 Make PaperCardFlags serializable 2025-04-17 20:14:37 -04:00
NicolasCunha
9109207495 fix: concede shortcut not working due to event dispatch thread 2025-04-17 12:48:26 -04:00
Hans Mackowiak
dbac513027 Update tourachs_gate.txt 2025-04-17 16:50:31 +02:00
Hans Mackowiak
3152ac93f6 Update caribou_range.txt 2025-04-17 16:50:16 +02:00
Hans Mackowiak
fc56076f81 Update mystic_might.txt 2025-04-17 16:49:49 +02:00
Hans Mackowiak
ecc763faf6 Update hot_springs.txt 2025-04-17 16:48:50 +02:00
Hans Mackowiak
0130367873 Update bestial_bloodline.txt 2025-04-17 16:44:12 +02:00
Hans Mackowiak
c36b9b7610 Update ice_cage.txt 2025-04-17 16:18:30 +02:00
Renato Filipe Vidal Santos
774ab38335 Update nantuko_slicer.txt (#7413) 2025-04-17 15:34:10 +02:00
tool4ever
251ce2f83b Fix Torment of Hailfire in multiplayer (#7412)
Co-authored-by: TRT <>
2025-04-17 11:55:41 +03:00
Hans Mackowiak
2af47fe6df fix 'Land you control' auras 2025-04-17 06:43:04 +02:00
Hans Mackowiak
332c982695 Update crackling_emergence.txt
Fix Enchant
2025-04-17 05:58:03 +02:00
Hans Mackowiak
eb970665a6 Update harmonious_emergence.txt
Fix Enchant
2025-04-17 05:57:08 +02:00
Jetz72
4714319204 Store cards by collectorNumber instead of artIndex; PaperCard flag support (#7240)
* Refactor - Unknown set code to constant

* Refactor - Support for multiple initial selections for getChoices

* Covert noSell and marked color identities into serializable flags

* Fix cards in deck not being converted to newer noSell format

* unused imports

* Fix NPE

* Cleanup card filter

* Remove 14-year-old check that shouldn't be possible anymore

* CRLF -> LF

---------

Co-authored-by: Jetz <Jetz722@gmail.com>
2025-04-16 18:59:31 -04:00
tool4ever
c85214b9e3 Fix always targeting Graveyard with Aura (#7408) 2025-04-16 21:03:22 +00:00
tool4ever
d5854c9d1c Fix Artifact Possession (#7409) 2025-04-16 21:03:09 +00:00
Renato Filipe Vidal Santos
a3a2a5dd1b Update Reanimate, Kotis (#7403) 2025-04-16 11:22:23 +02:00
Chris H
8f155c9cca Verify we can still target before we add the target to our list (#7404) 2025-04-16 07:15:10 +02:00
matthias8422
366ed643a7 Feature/network improvements (#7365)
* Initial commit of network improvements
Seperated server properties into their own file, this will eventually help facilitate a headless server.
Fixed the localhost ip mapping to give the correct IP Address instead of failing and defaulting to "localhost"
Fixed UPnP as well as added some additional options and choices regarding UPnP
Added localization strings to all language files. (Translators will need to translate these, but the current English string is there for easy reference so they dont have to search the en-US file)

* Initial commit of network improvements
Seperated server properties into their own file, this will eventually help facilitate a headless server.
Fixed the localhost ip mapping to give the correct IP Address instead of failing and defaulting to "localhost"
Fixed UPnP as well as added some additional options and choices regarding UPnP
Added localization strings to all language files. (Translators will need to translate these, but the current English string is there for easy reference so they dont have to search the en-US file)

* Fixed properties file reference

* Refactored server address parsing logic to use the Java URI class for improved readability and robustness.
Extracted reusable code into separate functions to enhance modularity and maintainability.
General code cleanup to improve structure and readability.

* Fixed a potential issue if a protocol was already specified in the connection url.

* Removed logger implementation as changing loggers is out of scope for this PR
Reverted to JUPnP as its implementation is fixed in #7367
Made some of the new localization strings generic as they can be used elsewhere
Added a server.preferences.example file
removed the server port from the old location (forge.progile.properties.example)
Added a server port back into ForgeConstants as it doesnt make sense for the prefered hosting port of the user to override the default Forge connection port.

* resolve conflicts between this branch and master

* Implemented a parent class for all preference Enums so they can be passed into a function regardless of type using IPref, necessary since I separated server settings into its own FNetPref file
Added server preferences section to the preferences Desktop GUI
Added a port preference setting and a UPnP preference setting to the aforementioned server preferences section
Added a localizedComboBox and localizedComboBoxListener so that localized strings can be used in combobox dropdowns in the server preferences section.

TODO: (In scope)
The new server preferences section needs to be added to Android and IOS perhaps?

TODO: (out of scope)
GamePlayerUtil has a bunch on non localized english strings that should be converted to localized

* Fixed unused import

* Resolved merge conflicts
Added server settings to the reset to defaults function
2025-04-15 20:32:32 -04:00
tool4ever
e7becacd57 Fix StoreVoteNum (#7402)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-04-15 18:47:24 +02:00
Hans Mackowiak
2cf9293ab3 AttachAI: remove getFirstAttachSpell (#7400)
* AttachAI: remove getFirstAttachSpell

* Update GameAction.java

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-04-15 11:55:38 +03:00
Hans Mackowiak
88300de6e5 Update aligned_heart.txt
remove keywords
2025-04-15 07:02:53 +02:00
tool4ever
a4cb0924f3 Fix World rule (#7397) 2025-04-14 14:59:51 +00:00
Hans Mackowiak
e0001f8348 Update TokenAi, remove getFirstAttachSpell (#7398)
Removes getFirstAttachSpell
2025-04-14 16:47:53 +02:00
tool4ever
e380590c4c Fix Old-Growth Troll (#7396)
* Fix Old-Growth Troll

* Update GameAction.java

Add AI SVars to EmptySA

* Fix logic

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-04-14 15:24:09 +02:00
Hans Mackowiak
7410a2844f Update dance_of_the_dead.txt
Remove NewAttach
2025-04-14 14:45:54 +02:00
tool4ever
b4af62eda7 CantUntap Third Step (#7393) 2025-04-13 12:49:07 +00:00
tool4ever
9c14ba49a3 Fix Licids being able to reattach without ending the effect first (#7394) 2025-04-13 12:43:22 +00:00
Hans Mackowiak
25900ee10c Aura Spells have internal Attach Spell for multiple Enchant Keywords (#6996)
* Aura Spells have internal Attach Spell for multiple Enchant Keywords

* AttachAI: add logic for Reanimate Aura with AnimateAI
2025-04-13 13:36:31 +02:00
tool4ever
443ba2c1e0 Upgrade Jetty (#7390) 2025-04-12 21:47:04 +00:00
Hans Mackowiak
6ade60cd59 removed getDirectSVars (#7389) 2025-04-12 21:15:54 +02:00
tool4ever
436697e0b3 Update stadium_headliner.txt
Closes #7388
2025-04-12 14:29:52 +00:00
Hans Mackowiak
e7d8664386 Svar fallback changes (#7385)
* getSVarFallback use stream filter to find one with name
2025-04-12 10:44:19 +02:00
tool4ever
e2e3c658a0 Relocate android specific dependency (#7387) 2025-04-12 05:47:07 +00:00
tool4ever
2a4ea4cb5d Some fixes (#7386) 2025-04-11 20:40:35 +00:00
tool4ever
93215e6ce4 Fix Distended Mindbender (#7381) 2025-04-11 16:02:47 +02:00
tool4ever
f4a0de6392 JUPnP android fix (#7379) 2025-04-11 08:55:40 +02:00
Renato Filipe Vidal Santos
579033abb3 Update whirlwing_stormbrood_dynamic_soar.txt (#7377) 2025-04-10 19:32:01 +00:00
Renato Filipe Vidal Santos
8328866645 Cleanup: April 2025, pass 3 2025-04-10 16:44:28 +02:00
Hans Mackowiak
890ce2505c Unearth: add gain Haste as StaticLayer (#7369)
* Unearth: add gain Haste as StaticLayer
2025-04-10 16:40:37 +02:00
tool4ever
7e6697d100 Fix checking with LKI during combat (#7372) 2025-04-10 15:43:22 +02:00
Agetian
84d793b786 Add 4 Possibility Storm puzzles (#7371)
* - Add TDM/TDC achievements by Marek14.

* - Add 4 Possibility Storm puzzles.
2025-04-10 11:55:42 +03:00
tool4ever
386550da39 Fix JUPnP initialization (#7367)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-04-10 08:55:47 +03:00
tool4ever
6d3d11398d Update rite_of_renewal.txt 2025-04-09 15:46:27 +00:00
Greg Sonnier
5db9c189ba Fixed Renew cost in alchemists_assistant.txt (#7363) 2025-04-08 20:59:04 +00:00
Hans Mackowiak
8d3e3cb253 AttachAi: make KeepTapped logic generic (#7361) 2025-04-08 20:54:09 +00:00
Agetian
2042aaa033 - Add TDM/TDC achievements by Marek14. (#7362) 2025-04-08 10:09:24 +03:00
tool4ever
6fc5d218c9 Update savior_of_the_sleeping.txt 2025-04-07 17:27:34 +00:00
Hans Mackowiak
6332f5cbfc Update champion_of_dusan.txt
Fix Renew Cost
2025-04-07 17:22:37 +02:00
Chris H
44340998fd Update host_of_the_hereafter.txt 2025-04-07 09:30:00 -04:00
Chris H
5274c976ef Restore flatten plugin 2025-04-06 12:13:08 -04:00
GitHub Actions
280d2fed6d [maven-release-plugin] prepare for next development iteration 2025-04-06 12:13:08 -04:00
GitHub Actions
9ecea646d2 [maven-release-plugin] prepare release forge-2.0.03 2025-04-06 12:13:08 -04:00
Chris H
c4e5101a42 Update maven-publish for releases without deploying to FTP 2025-04-06 12:13:08 -04:00
Chris H
48a555817d Temporarily remove flatten plugin for release 2025-04-06 12:13:08 -04:00
Renato Filipe Vidal Santos
2cc2ae421a Cleanup: April 2025, pass 2 (#7350)
* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Update furious_forebear.txt
2025-04-06 17:05:44 +03:00
Paul Hammerton
9109b26484 TDM: Humbling Elder (#7355) 2025-04-06 07:45:55 +00:00
Hans Mackowiak
98e5eb9652 Adventure and Omen as ReplacementEffect in CardState (#7347) 2025-04-05 17:58:20 +00:00
tool4ever
4e93f95dff Update furious_forebear.txt 2025-04-05 17:48:51 +00:00
Renato Filipe Vidal Santos
44e1332fb4 Cleanup: April 2025, pass 1 2025-04-05 17:02:21 +00:00
Hans Mackowiak
e2972acad0 Put ForColor for Call of the Spirit Dragons (#7348)
* Support for Call the Spirit Dragons

* ~ better ChoiceTitle

* ~ add AI flags
2025-04-05 17:14:43 +02:00
Renato Filipe Vidal Santos
76086ffd35 Cleanup: Updating to Count$Valid, pass 4 2025-04-05 06:47:13 +00:00
tool4ever
3c3c47616b Update UnattachEffect.java
Closes #7345
2025-04-05 08:37:53 +02:00
tool4ever
de5a660a6a Update thought_lash.txt
Fix exiling own cards when stolen
2025-04-04 21:04:54 +00:00
Drecon84
d348a72ae4 Excluding more enemy types
These are also not suited for the tutorial. Excluding these as well.
2025-04-04 10:41:17 -04:00
Fulgur14
4055512698 Temur Roar deck 2025-04-04 10:37:56 -04:00
Renato Filipe Vidal Santos
b4b2346fb8 Cleanup: Updating to Count$Valid, pass 3 (#7342) 2025-04-04 11:20:44 +02:00
Paul Hammerton
caa5443874 Update tdm.rnk 2025-04-04 09:13:43 +01:00
Renato Filipe Vidal Santos
c7fb0bd3c3 Cleanup: Count$InYour[Zone], part 2 (#7325) 2025-04-04 09:58:01 +02:00
Renato Filipe Vidal Santos
1f5f62b21a Cleanup: Count$InYour[Zone], part 1 (#7324) 2025-04-04 09:57:21 +02:00
Chris H
04937b6447 Migrate upcoming card scripts 2025-04-03 23:36:00 -04:00
Chris H
94ea88b171 Migrate upcoming card scripts 2025-04-03 23:33:19 -04:00
Chris H
2188582e16 Add initial booster+set info for TDM 2025-04-03 23:19:46 -04:00
Drecon84
fbc73fa22b Small fix to clean up the code
Combining ifs that do not need to be split up.
2025-04-03 16:16:39 -04:00
Drecon84
31790c3dc9 Remove Quest items on NG+
This removes quest items from a NG+ run in adventure mode. It might not remove all when a file has multiple NG+'s already, but multiple resets will remove all of the items.
I decided to remove the quest item tag from the teleport runes, since they are not added by a quest but can be bought in the store instead. Keeping them seems a good move for NG+.
2025-04-03 16:16:39 -04:00
Hans Mackowiak
cdf3038ab6 Harmonize first try (#7321)
* Harmonize first try

* Use OptionalCost

* Keyworded don't need type

* TDM/TDC: 11 harmonize cards (#7323)

* Trickery extrinsic fix

* Split fix

* Annoying checks keep failing

* Fix logic

* Clean up

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
Co-authored-by: Renato Filipe Vidal Santos <45150760+dracontes@users.noreply.github.com>
Co-authored-by: TRT <>
2025-04-03 19:37:16 +02:00
tool4EvEr
ff1781b734 Fix creating token with Endure 0 2025-04-03 11:22:11 +02:00
Hans Mackowiak
49d7351eac ~ lf 2025-04-03 06:11:26 +02:00
Paul Hammerton
3b372eead7 Merge pull request #7330 from paulsnoops/edition-updates
Edition updates: FIN, SLD
2025-04-02 18:05:22 +01:00
Paul Hammerton
c815f7ed0c Edition updates: FIN, SLD 2025-04-02 17:59:44 +01:00
Fulgur14
04ac8d8de9 Gladiolus Amicitia (FIN) (#7329) 2025-04-02 15:42:57 +02:00
Renato Filipe Vidal Santos
6b961ed3e1 Update monstrous_rage.txt (#7326) 2025-04-02 08:34:22 +02:00
Renato Filipe Vidal Santos
17a2a23981 Cleanup: Count$TypeInYour[Zone].[Type] (#7312) 2025-04-01 11:01:45 +00:00
Renato Filipe Vidal Santos
74e83f2a94 Cleanup: Targeted effects that grant flashback (#7319) 2025-04-01 11:00:39 +00:00
tool4ever
8e25dd0e25 Clean up (#7322) 2025-04-01 08:46:15 +00:00
Drecon84
7f9260f54c WASD Adventure take 3
Now actually putting the keybinds back in. Forgot to unrevert previously.
2025-03-31 21:24:31 -04:00
Paul Hammerton
505d4b4f9a Merge pull request #7316 from HeitorBittenc/Alchemy-boosters-draft
Alchemy sets boosters added
2025-03-31 17:30:35 +01:00
Paul Hammerton
baca196066 Merge pull request #7317 from Fulgur14/Fulgur14-patch-452548
Mardu Surge deck
2025-03-31 17:30:27 +01:00
Paul Hammerton
b08cf3bf5b Merge pull request #7318 from paulsnoops/b-and-r-2025-03-31
Banned and Restricted Announcement for March 31, 2025
2025-03-31 17:29:49 +01:00
Paul Hammerton
b5ee3eb43c Banned and Restricted Announcement for March 31, 2025 2025-03-31 17:24:53 +01:00
Fulgur14
081e1a2960 Mardu Surge deck 2025-03-31 17:45:15 +02:00
Hans Mackowiak
d0310e257b Mobilize X: enable the keyword to pass the SVars to the static (#7310) 2025-03-31 09:32:38 +00:00
Renato Filipe Vidal Santos
a3733f1fa8 TDM: Avenger of the Fallen 2025-03-31 08:20:34 +00:00
HeitorBittenc
b4bd7947f9 Alchemy sets boosters added 2025-03-30 23:05:31 -03:00
Renato Filipe Vidal Santos
b56da433eb Update hunters_bow.txt 2025-03-30 16:20:07 +02:00
Hans Mackowiak
64b5906f08 lf 2025-03-30 13:09:12 +02:00
tool4EvEr
f7d94fabc9 Fix Damping Engine crashing 2025-03-30 11:27:39 +02:00
HeitorBittenc
87e0810603 Adventure Mode: Fix Waste town generic Equipment shop not appearing 2025-03-29 22:24:04 -04:00
Fulgur14
93f2fa8f43 Update Abzan Armor [TDC] [2025].dck 2025-03-29 20:47:00 -04:00
Fulgur14
c86bf402fd Update Jeskai Striker [TDC] [2025].dck 2025-03-29 20:47:00 -04:00
Drecon84
9926004cf1 Fix for main quest
This fixes the bug where the objective to free the wizard from the red castle doesn't complete.
2025-03-29 20:25:45 -04:00
Renato Filipe Vidal Santos
d0c24f49a9 Update captain_america_first_avenger.txt 2025-03-29 14:56:01 -04:00
Renato Filipe Vidal Santos
661f3b8e7a Update captain_america_first_avenger.txt 2025-03-29 14:56:01 -04:00
Chris H
14249150a0 Fix trailing comma in json 2025-03-29 14:55:08 -04:00
churrufli
93dccdeace Net Decks Archive Updates (#7283) 2025-03-29 19:23:58 +03:00
Renato Filipe Vidal Santos
706ef4ac6c TDM: 14 Omen-related cards (#7300) 2025-03-29 16:13:35 +00:00
Paul Hammerton
bd3994a217 Merge pull request #7302 from paulsnoops/edition-updates
Edition updates: PF24, PMEI, PSPL, PW25, SCH, SLP
2025-03-29 16:07:10 +00:00
HeitorBittenc
e722c4b63c Removed token image fetch attempt from old server (#7287) 2025-03-29 19:01:29 +03:00
Paul Hammerton
a062e0040d Edition updates: PF24, PMEI, PSPL, PW25, SCH, SLP 2025-03-29 15:57:12 +00:00
Hans Mackowiak
7fdd645026 Omen: first attempt (#7297)
* Omen: first attempt

* Support rendering

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-03-29 16:54:20 +01:00
Paul Hammerton
3670891ec9 Merge pull request #7301 from paulsnoops/tdm-tdc-formats
Add TDM & TDC to formats
2025-03-29 14:55:58 +00:00
Paul Hammerton
6f3dd8deba Add TDM & TDC to formats 2025-03-29 14:49:32 +00:00
Chris H
63ac4a3ee4 Fix line endings 2025-03-29 09:58:37 -04:00
Hans Mackowiak
4616ee715e Update pom.xml
fix CDATA for Add-Opens
2025-03-29 09:07:54 -04:00
Paul Hammerton
5ef6bf1c15 Merge pull request #7299 from paulsnoops/fix-pp-name
Fix Poised Practitioner name
2025-03-29 12:31:28 +00:00
Paul Hammerton
862b4e19b6 Fix Poised Practitioner name 2025-03-29 12:25:05 +00:00
Paul Hammerton
378524dc39 Merge pull request #7298 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-29 12:19:24 +00:00
Paul Hammerton
6d5f45a311 Edition updates: TDC, TDM 2025-03-29 12:16:13 +00:00
Fulgur14
0e00a52eb4 Update rainveil_rejuvenator.txt (#7294) 2025-03-29 08:14:43 +00:00
tool4ever
4b69d16c6d Update conduit_of_worlds.txt 2025-03-28 22:15:50 +00:00
Renato Filipe Vidal Santos
92e17a66f2 TDM: 6 cards (#7291) 2025-03-28 20:01:40 +00:00
Fulgur14
b601431591 TDM final spoiler batch - part 3 (#7292) 2025-03-28 18:49:19 +00:00
Fulgur14
76db40189e TDM final spoilers part 1 (#7289) 2025-03-28 18:46:00 +00:00
Renato Filipe Vidal Santos
8ddf8225c0 TDM: Mardu Siegebreaker 2025-03-28 18:43:56 +00:00
Fulgur14
359dd8d641 TDM final spoiler batch, Part 2 (#7290) 2025-03-28 18:41:12 +00:00
Fulgur14
878da9b06f Warden of the Grove (TDM) (#7281) 2025-03-27 19:24:57 +00:00
Fulgur14
d843004ad6 Hundred-Battle Veteran (TDM) (#7280)
The only problem I've found is that it's not shown in the "flashback" panel for some reason.
2025-03-27 18:57:53 +00:00
Hans Mackowiak
7f6024f81f Endure effect (#7254)
* add EndureEffect

* ~ fix style

* Add files via upload

* Update dusyut_earthcarver.txt

* add better Endure Message

* Update krumar_initiate.txt

* Fix message

* - Add basic EndureAi

* - Fix imports

* Update EndureAi.java

Apply static check for the token

* Update EndureAi.java

fix import

* Add files via upload

---------

Co-authored-by: Renato Filipe Vidal Santos <45150760+dracontes@users.noreply.github.com>
Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-03-27 19:21:18 +01:00
Paul Hammerton
5a37b49fcd Merge pull request #7279 from paulsnoops/edition-updates
Edition updates: SLD, TDM
2025-03-27 18:05:14 +00:00
Paul Hammerton
1c8cdac5be Edition updates: SLD, TDM 2025-03-27 18:00:21 +00:00
Fulgur14
20bd27d487 Lotuslight Dancers and Defibrillating Current (TDM) (#7277) 2025-03-27 15:23:34 +01:00
HeitorBittenc
fc761220d2 Removed 2 additional unnecessary requests for tokens images 2025-03-27 10:19:14 -04:00
HeitorBittenc
144681012c Removed print used for tests 2025-03-27 10:19:14 -04:00
HeitorBittenc
8fe3bd3c79 Removed unnecessary request for image fetching 2025-03-27 10:19:14 -04:00
tool4ever
da65308cf2 Some fixes (#7276)
* Fix unlocked Room token
2025-03-27 14:48:01 +01:00
Fulgur14
011457e949 2 TDM commons (#7271) 2025-03-27 15:29:02 +03:00
Renato Filipe Vidal Santos
c296025837 TDM: 10 cards (#7274) 2025-03-27 12:47:17 +01:00
Chris H
ac4c501629 Fix line endings 2025-03-26 19:05:22 -04:00
Heitor Bittencourt
fe062a9312 Refactor Entry to List + add comments 2025-03-26 16:05:22 -04:00
Hans Mackowiak
65d4505b67 Update FDeckViewer.java
Use Set instead of List
2025-03-26 16:05:22 -04:00
Heitor Bittencourt
073d7e537c indentation 2025-03-26 16:05:22 -04:00
Heitor Bittencourt
77dc367c95 Fix ConcurrentModificationException by iterating over a separate list 2025-03-26 16:05:22 -04:00
Heitor Bittencourt
7553d164f4 fix: copy to clipboard now collates copies of a card together. 2025-03-26 16:05:22 -04:00
Heitor Bittencourt
ee01e3d29f fix: copy to clipboard now collates copies of a card together. 2025-03-26 16:05:22 -04:00
Paul Hammerton
58f8c39197 Merge pull request #7267 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-26 18:57:52 +00:00
Paul Hammerton
7304fa862a Edition updates: TDC, TDM 2025-03-26 18:53:10 +00:00
Fulgur14
1ef8b9ca47 Another 10 TDM cards, including Exhales (#7266) 2025-03-26 18:48:36 +00:00
tool4ever
5191d2f9c4 Safer canUntap check (#7265) 2025-03-26 17:27:18 +00:00
Fulgur14
ff74e36fe5 Kheru Goldkeeper (#7264) 2025-03-26 11:12:49 +00:00
Renato Filipe Vidal Santos
0121619e93 TDM/TDC: 10 cards (#7263) 2025-03-26 09:11:47 +00:00
Drecon84
aeb279a6f8 WASD Movement Take 2 (#7251)
* Enter now still brings up the menu, controller might need it to type. WASD typing still works.
2025-03-26 07:27:16 +00:00
Fulgur14
20815552b9 3 TDC + 1 TDM cards (#7262) 2025-03-25 18:53:59 +00:00
tool4ever
ac67a36ccf Replace first withContext call with more stable AI prediction (#7261) 2025-03-25 18:08:36 +00:00
Fulgur14
fcd8b8fd35 Thunder of Unity (TDM) (#7257) 2025-03-25 17:02:03 +00:00
Renato Filipe Vidal Santos
fbe4ad5c44 TDM: 2 cards 2025-03-25 17:10:23 +01:00
Renato Filipe Vidal Santos
ee17483fff AddPower & AddToughness: Removing redundancy 2025-03-25 15:45:32 +01:00
Renato Filipe Vidal Santos
a451f1a234 Cleanup: NumAtt$ & NumDef$, part 8 (#6885) 2025-03-25 15:01:24 +01:00
Renato Filipe Vidal Santos
52c4c01a7d Cleanup: NumAtt$ & NumDef$, part 7 (#6884) 2025-03-25 14:59:00 +01:00
Renato Filipe Vidal Santos
b1bb0d669f Cleanup: NumAtt$ & NumDef$, part 6 (#6883) 2025-03-25 14:58:47 +01:00
Renato Filipe Vidal Santos
eb1f9783aa Cleanup: NumAtt$ & NumDef$, part 5 (#6882) 2025-03-25 14:58:36 +01:00
Renato Filipe Vidal Santos
0724d224fa Cleanup: NumAtt$ & NumDef$, part 4 (#6881) 2025-03-25 14:58:25 +01:00
Renato Filipe Vidal Santos
e7775cdfa9 Cleanup: NumAtt$ & NumDef$, part 3 (#6880) 2025-03-25 14:58:13 +01:00
Renato Filipe Vidal Santos
f8836f0c40 Cleanup: NumAtt$ & NumDef$, part 2 (#6879) 2025-03-25 14:57:47 +01:00
Renato Filipe Vidal Santos
4c342cfc6a Cleanup: NumAtt$ & NumDef$, part 1 2025-03-25 14:57:35 +01:00
Fulgur14
df05ab34fb 4 TDM cards (#7235) 2025-03-25 14:56:23 +01:00
Paul Hammerton
5da0e75252 Merge pull request #7255 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-25 10:07:21 +00:00
Paul Hammerton
92ec5d8f64 Edition updates: TDC, TDM 2025-03-25 09:55:59 +00:00
Renato Filipe Vidal Santos
6515fed9d2 TDM/TDC: 4 cards (#7252) 2025-03-25 07:10:37 +00:00
Chris H
5a7cd40614 Update README.md 2025-03-24 22:26:33 -04:00
Fulgur14
65b01e0822 10 TDM/TDC cards (#7249) 2025-03-24 21:19:13 +00:00
Renato Filipe Vidal Santos
643f893d43 TDM: 4 cards (#7248) 2025-03-24 20:42:02 +00:00
Paul Hammerton
e0c6b43214 Merge pull request #7247 from paulsnoops/edition-updates
Edition updates: TDC, TDM, SLD
2025-03-24 18:10:59 +00:00
Paul Hammerton
0f5d71f933 Edition updates: TDC, TDM, SLD 2025-03-24 17:54:03 +00:00
Paul Hammerton
e6a8b5ed74 Edition updates: TDC, TDM, SLD 2025-03-24 17:50:32 +00:00
LEGIONLAPTOP\dougc
24c11e47c4 Updating lang files with new strings 2025-03-24 12:39:50 -04:00
LEGIONLAPTOP\dougc
cb5f805767 Added CommanderGauntlet GameType, added CustomCommanderGauntlet to mobile 2025-03-24 12:39:50 -04:00
LEGIONLAPTOP\dougc
3788e01f38 Quick Commander and Build Commander Gauntlet both working on desktop 2025-03-24 12:39:50 -04:00
LEGIONLAPTOP\dougc
a9df4ea424 Renamed Commander Gauntlet to more accurate QUICK Commander Gauntlet 2025-03-24 12:39:50 -04:00
Chris H
25ba06d530 Revert "WASD movement for Adventure mode" (#7241)
* Revert "Improved WASD script"

This reverts commit fc901f1ebb.

* Revert "Fixed WASD movement"

This reverts commit c365f5a3d1.

* Revert "Update KeyBinding.java"

This reverts commit 49697c863c.

* Revert "Adventure Keybinds"

This reverts commit 4431c40de6.
2025-03-24 09:14:23 -04:00
tool4ever
9b81644f11 Fix Danny Pink triggering once for each type (#7245) 2025-03-24 13:09:57 +01:00
tool4ever
4b27536ed3 Update eshki_temurs_roar.txt
Closes #7238
2025-03-24 10:56:51 +01:00
Renato Filipe Vidal Santos
148da24456 TDM: 2 cards (#7244) 2025-03-24 09:19:51 +01:00
Renato Filipe Vidal Santos
d1be43fd83 TDM: Rot-Curse Rakshasa (#7242) 2025-03-24 07:06:02 +01:00
Chris H
f2df505237 Don't activate connive if Amount == 0 2025-03-23 13:14:58 -04:00
Chris H
bd37e26fab Make the AI more likely to sacrifice/chump block with Reef Worm + Spawn 2025-03-23 13:10:53 -04:00
Chris H
7954473476 FIx Line endings 2025-03-23 12:46:58 -04:00
Renato Filipe Vidal Santos
13287cefbd TDM: 5 Cards (#7234) 2025-03-23 13:48:29 +01:00
Fulgur14
9afbc91de1 1 TDC + 11 TDM cards (#7230) 2025-03-23 11:56:20 +01:00
Renato Filipe Vidal Santos
dfe5bd9ec9 TDM/TDC: 8 cards (#7223) 2025-03-23 10:11:28 +01:00
Renato Filipe Vidal Santos
e2411e34bd Update shiko_and_narset_unified.txt (#7236) 2025-03-23 10:07:41 +01:00
Chris H
f2998bdf9a FIx Line endings 2025-03-22 18:36:42 -04:00
Heitor Bittencourt
c52f886e89 Adventure: Disable Not For Sale Overlay Setting 2025-03-22 18:21:58 -04:00
Heitor Bittencourt
f972aa44ba Adventure: display shop name items on boosters removed 2025-03-22 18:21:09 -04:00
Heitor Bittencourt
ccafe0557f Adventure: Replaced Booster Shop Image 2025-03-22 18:21:09 -04:00
Paul Hammerton
f9f9b1a1f9 Merge pull request #7229 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-22 10:21:34 +00:00
Fulgur14
e867aacbf5 9 TDC/TDM cards (#7225) 2025-03-22 11:12:21 +01:00
Paul Hammerton
a09e9e4fd6 Edition updates: TDC, TDM 2025-03-22 10:10:35 +00:00
Renato Filipe Vidal Santos
fc320e6524 Fixing Will of the Mardu (#7226) 2025-03-22 08:08:48 +01:00
Chris H
2b6a1c9f3d Refilter targetable list while looping the target selections 2025-03-21 19:11:01 -04:00
Chris H
3e9cd2c226 Fix some adventure issues 2025-03-21 19:11:01 -04:00
Paul Hammerton
3f722abba2 Merge pull request #7224 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-21 18:01:50 +00:00
Paul Hammerton
06508f70a3 Edition updates: TDC, TDM 2025-03-21 17:42:54 +00:00
Fulgur14
8580108d1e 10 TDM cards (Sidisi and Sibsigs) (#7190) 2025-03-21 10:11:10 +01:00
Hans Mackowiak
300f34377c Update aligned_heart.txt
fix trigger
2025-03-21 09:35:41 +01:00
Renato Filipe Vidal Santos
c4828f510f TDC: 4 cards (#7221) 2025-03-21 09:06:50 +01:00
Fulgur14
04c400553a Another 10 TDM/TDC cards (#7220) 2025-03-21 08:36:50 +01:00
tool4ever
d2508333bc Fix NPE: Jacob Frye + Escape Detection (#7219)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-03-21 08:31:09 +03:00
Drecon84
fc901f1ebb Improved WASD script
Now the script works with arrays, much cleaner. I have not been able to test the controller support, it should work but I don't have the means to test it.
2025-03-20 20:35:54 -04:00
Drecon84
c365f5a3d1 Fixed WASD movement
Adventure moves with WASD now
2025-03-20 20:35:54 -04:00
Drecon84
49697c863c Update KeyBinding.java 2025-03-20 20:35:54 -04:00
Drecon84
4431c40de6 Adventure Keybinds
Can now move in adventure mode with WASD keys
2025-03-20 20:35:54 -04:00
Renato Filipe Vidal Santos
a0f6efb959 TDM: 6 cards 2025-03-20 21:02:06 +00:00
tool4ever
44fea0ae75 Fix AI running into timeout keeping thread running (#7215) 2025-03-20 18:17:41 +00:00
Paul Hammerton
95c970e23f Merge pull request #7217 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-20 17:43:54 +00:00
Paul Hammerton
8596151fa1 Edition updates: TDC, TDM 2025-03-20 17:32:44 +00:00
Paul Hammerton
2eac43734c Merge pull request #7216 from paulsnoops/master
LF
2025-03-20 17:29:09 +00:00
Paul Hammerton
dff91eb2aa LF 2025-03-20 17:25:12 +00:00
Renato Filipe Vidal Santos
aa122700a9 TDC: 4 cards (#7208) 2025-03-20 17:52:47 +01:00
Fulgur14
80f267df59 Dalkovan Packbeasts (TDM) (#7214) 2025-03-20 13:45:55 +01:00
tool4ever
235618c3bb Fix Valiant tracking incorrectly with controller change (#7200)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-03-20 15:26:53 +03:00
Fulgur14
b7e55e785e Perennation (TDM) (#7213) 2025-03-20 13:24:25 +03:00
Fulgur14
050c986d08 A few more TDM/TDC cards (#7207) 2025-03-20 10:34:16 +01:00
Fulgur14
2b83541ebc 9 TDM + 1 TDC card (Eshki, Temur's Roar and friends) (#7198) 2025-03-20 08:37:17 +01:00
Fulgur14
d40894ef6a Tempest Hawk + 3 TDC cards (#7202) 2025-03-20 08:36:49 +01:00
Hans Mackowiak
b756bda988 to LF 2025-03-20 00:25:45 +01:00
Heitor Bittencourt
0e64a88005 fix: removed unused imports 2025-03-19 19:11:08 -04:00
Heitor Bittencourt
0d952a54bd feat/adventure: 4 new booster packs,spawn rate changed, removed colorless packs 2025-03-19 19:11:08 -04:00
Hans Mackowiak
d3ff7f3b61 Update parseAbilityCost (#7178)
cleanup and simplify
2025-03-19 22:48:57 +01:00
Renato Filipe Vidal Santos
0f9e7eca89 TDC: 2 cards 2025-03-19 19:18:24 +00:00
Paul Hammerton
207b786fcd oops 2025-03-19 14:01:26 -04:00
Paul Hammerton
0f6fa87da0 Alchemy Rebalancing for March 4, 2025 2025-03-19 14:01:26 -04:00
Paul Hammerton
995c1167dc Merge pull request #7201 from paulsnoops/edition-updates
Edition updates: SPG, TDC, TDM
2025-03-19 17:37:21 +00:00
Paul Hammerton
1ab1d9c002 Edition updates: SPG, TDC, TDM 2025-03-19 17:32:09 +00:00
Renato Filipe Vidal Santos
4b1a6a2f87 TDM: 2 cards (#7199) 2025-03-19 17:08:48 +00:00
Renato Filipe Vidal Santos
dacecd9006 TDM: 4 cards (#7197) 2025-03-19 16:16:19 +00:00
Fulgur14
dd5d75613e 10 TDM cards (All-Out Assault of them) (#7195) 2025-03-19 16:15:57 +00:00
Renato Filipe Vidal Santos
d59a316d8c TDM: 8 cards (#7189) 2025-03-19 16:44:06 +01:00
Fulgur14
9c4f855f71 10 TDM cards (#7196) 2025-03-19 16:41:01 +01:00
Fulgur14
90131b4a70 10 TDM cards (Taigam and pawns) (#7187) 2025-03-19 08:23:13 +01:00
Fulgur14
fb6725f2d7 Update zurgo_thunders_decree.txt (#7188) 2025-03-18 19:37:16 +00:00
tool4ever
475c57af55 Try fix AI not resetting context (#7186) 2025-03-18 19:06:49 +00:00
Renato Filipe Vidal Santos
6ae119a415 TDM: Zurgo, Thunder's Decree and mobilize support (#7185) 2025-03-18 18:51:43 +00:00
Renato Filipe Vidal Santos
8b5fe276e7 TDM: 3 cards (#7184) 2025-03-18 09:56:26 +01:00
Hans Mackowiak
5e4d5c262d ~ lf 2025-03-17 21:15:38 +01:00
Fulgur14
9b82f1ef1f Update united_battlefront.txt (#7183) 2025-03-17 19:56:22 +00:00
Paul Hammerton
321d2d7e33 Merge pull request #7182 from paulsnoops/edition-updates
Edition updates: FIN, SLD, TDM
2025-03-17 17:47:38 +00:00
Paul Hammerton
5b7cca95e1 Edition updates: FIN, SLD, TDM 2025-03-17 17:40:59 +00:00
Hans Mackowiak
2e0d53c6fe CantUntap Second Step (#7172)
* refactor more CantUntap Statics

* ~ no new hidden keywords

* ~ more CantBeActivated

* Update Card.java

* Refactor scripts

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-03-17 17:48:39 +01:00
Fulgur14
9d4f6d2cbb United Battlefront (#7181) 2025-03-17 16:37:43 +00:00
tool4ever
9546b434e4 Update waltz_of_rage.txt 2025-03-17 17:01:40 +01:00
tool4ever
f230522657 Update contempt.txt (#7177) 2025-03-17 14:23:25 +01:00
tool4ever
a57a1f566a Refactor Telekinesis (#7174)
* Improve AI check
2025-03-17 12:06:03 +00:00
Renato Filipe Vidal Santos
8f8d6e6e30 FIN: Zell Dincht 2025-03-17 08:45:09 +00:00
tool4ever
3b8694483c Refactor Blinding Beam (#7171)
* Refactor Blinding Beam

* Refactor AI checking for Replacement while inactive

* Update stasis.txt

* Update sands_of_time.txt

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-03-16 21:14:28 +01:00
Hans Mackowiak
4328a12967 Cant untap first step (#7162)
* CantHappen for UntapStep
2025-03-16 11:47:27 +01:00
tool4ever
fdc85b85c3 Cleanup "Cleanup" calls (#7169) 2025-03-16 10:04:10 +00:00
Chris H
bb2eed23b7 Fix Griselbrand overdrawing in one turn 2025-03-15 19:20:44 -04:00
Renato Filipe Vidal Santos
72e33146de YDFT: 14 cards (#7122) 2025-03-15 22:01:58 +00:00
HeitorBittenc
e371617938 fix: removed unused images, booster pack shops now respect the restricted cards and editions. (#7161) 2025-03-15 13:14:04 +00:00
tool4ever
b363db2bbd Fix North Star (#7163) 2025-03-15 13:03:51 +00:00
HeitorBittenc
37a5958750 Added 7 Card booster packs shops in Adventure Mode (#7141)
* Added 7 Card booster packs shops in Adventure Mode

* Update CHANGES.txt

* Fix: trailing space, extra comma and wrong color on shop fixed.

* Removed unused imports

---------

Co-authored-by: Heitor Bittencourt <heitorbite@outlook.com>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-03-14 20:03:13 +03:00
Chris H
9091cfe3b0 Fix double mastesr 2022 basic lands 2025-03-14 11:36:57 -04:00
Chris H
e2614187ac Update brotherhood_vertibird.txt 2025-03-14 10:32:09 -04:00
Ayora29
5c69bf0470 Fix : Puzzle (Possibility Storm - Foundations #01). Opponent library is empty. (#7149) 2025-03-13 11:11:33 +01:00
tool4ever
383dc85166 Some cleanup (#7147) 2025-03-13 10:13:44 +01:00
Hans Mackowiak
7e47208888 Masako the humorless as Static 2025-03-13 08:37:47 +01:00
4852 changed files with 67492 additions and 37576 deletions

View File

@@ -2,10 +2,21 @@ name: Publish Desktop Forge
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
debug_enabled:
type: boolean
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
release_android:
type: boolean
description: 'Also try to release android build'
required: false
default: false
jobs: jobs:
build: build:
if: github.repository_owner == 'Card-Forge'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -32,10 +43,94 @@ jobs:
run: | run: |
git config user.email "actions@github.com" git config user.email "actions@github.com"
git config user.name "GitHub Actions" git config user.name "GitHub Actions"
- name: Build/Install/Publish to GitHub Packages Apache Maven
- name: Install old maven (3.8.1)
run: |
curl -o apache-maven-3.8.1-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz
tar xf apache-maven-3.8.1-bin.tar.gz
export PATH=$PWD/apache-maven-3.8.1/bin:$PATH
export MAVEN_HOME=$PWD/apache-maven-3.8.1
mvn --version
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
- name: Setup android requirements
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
run: |
JAVA_HOME=${JAVA_HOME_17_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;35.0.0" "platform-tools" "platforms;android-35"
cd forge-gui-android
echo "${{ secrets.FORGE_KEYSTORE }}" > forge.keystore.asc
gpg -d --passphrase "${{ secrets.FORGE_KEYSTORE_PASSPHRASE }}" --batch forge.keystore.asc > forge.keystore
cd -
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
curl -L -o android-maven-plugin-4.6.2.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.jar
curl -L -o android-maven-plugin-4.6.2.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.pom
cd -
mvn install -Dmaven.test.skip=true
mvn dependency:tree
- name: Build/Install/Publish Desktop to GitHub Packages Apache Maven
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_android }}
run: | run: |
export DISPLAY=":1" export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 & Xvfb :1 -screen 0 800x600x8 &
mvn -U -B clean -P windows-linux install release:clean release:prepare release:perform -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# build only desktop and only try to move desktop files
mvn -U -B clean -P windows-linux install -e -T 1C release:clean release:prepare release:perform -DskipTests
mkdir izpack
# move bz2 and jar from work dir to izpack dir
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
# move desktop build.txt and version.txt to izpack
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
cd izpack
ls
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
- name: Build/Install/Publish Desktop+Android to GitHub Packages Apache Maven
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# build both desktop and android
mvn -U -B clean -P windows-linux,android-release-build install -e -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=35.0.0
mkdir izpack
# move bz2 and jar from work dir to izpack dir
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
# move desktop build.txt and version.txt to izpack
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
# move android apk and assets.zip
mv /home/runner/work/forge/forge/forge-gui-android/target/*-signed-aligned.apk izpack/
mv /home/runner/work/forge/forge/forge-gui-android/target/assets.zip izpack/
cd izpack
ls
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Upload snapshot to GitHub Prerelease
uses: ncipollo/release-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: Release ${{ env.GIT_TAG }}
tag: ${{ env.GIT_TAG }}
artifacts: izpack/*
allowUpdates: true
removeArtifacts: true
makeLatest: true
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Release Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

View File

@@ -26,13 +26,13 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
### 📥 Desktop Installation ### 📥 Desktop Installation
1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest). 1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest).
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/). 2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots).
- **Tip:** Extract to a new folder to prevent version conflicts. - **Tip:** Extract to a new folder to prevent version conflicts.
3. **User Data Management:** Previous players data is preserved during upgrades. 3. **User Data Management:** Previous players data is preserved during upgrades.
4. **Java Requirement:** Ensure you have **Java 17 or later** installed. 4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
### 📱 Android Installation ### 📱 Android Installation
- Download the **APK** from the [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/). On the first launch, Forge will automatically download all necessary assets. - Download the **APK** from the [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots). On the first launch, Forge will automatically download all necessary assets.
--- ---

View File

@@ -37,6 +37,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates; import forge.game.spellability.SpellAbilityPredicates;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked; import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -1587,7 +1588,7 @@ public class AiAttackController {
// but there are no creatures it can target, no need to exert with it // but there are no creatures it can target, no need to exert with it
boolean missTarget = false; boolean missTarget = false;
for (StaticAbility st : c.getStaticAbilities()) { for (StaticAbility st : c.getStaticAbilities()) {
if (!"OptionalAttackCost".equals(st.getParam("Mode"))) { if (!st.checkMode(StaticAbilityMode.OptionalAttackCost)) {
continue; continue;
} }
SpellAbility sa = st.getPayingTrigSA(); SpellAbility sa = st.getPayingTrigSA();

View File

@@ -54,6 +54,7 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.*; import forge.game.spellability.*;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityDisableTriggers; import forge.game.staticability.StaticAbilityDisableTriggers;
import forge.game.staticability.StaticAbilityMode;
import forge.game.staticability.StaticAbilityMustTarget; import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
@@ -68,8 +69,10 @@ import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@@ -1127,7 +1130,7 @@ public class AiController {
// Memory Crystal-like effects need special handling // Memory Crystal-like effects need special handling
for (Card c : game.getCardsIn(ZoneType.Battlefield)) { for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
for (StaticAbility s : c.getStaticAbilities()) { for (StaticAbility s : c.getStaticAbilities()) {
if ("ReduceCost".equals(s.getParam("Mode")) if (s.checkMode(StaticAbilityMode.ReduceCost)
&& "Spell.Buyback".equals(s.getParam("ValidSpell"))) { && "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s); neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
} }
@@ -1707,7 +1710,8 @@ public class AiController {
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex); Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
} }
CompletableFuture<SpellAbility> future = CompletableFuture.supplyAsync(() -> { final ExecutorService executor = Executors.newSingleThreadExecutor();
Future<SpellAbility> future = executor.submit(() -> {
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast... //avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0); boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) { for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
@@ -1787,11 +1791,9 @@ public class AiController {
// instead of computing all available concurrently just add a simple timeout depending on the user prefs // instead of computing all available concurrently just add a simple timeout depending on the user prefs
try { try {
if (game.AI_CAN_USE_TIMEOUT) return future.get(game.getAITimeout(), TimeUnit.SECONDS);
return future.completeOnTimeout(null, game.getAITimeout(), TimeUnit.SECONDS).get();
else
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) { } catch (InterruptedException | ExecutionException | TimeoutException e) {
future.cancel(true);
return null; return null;
} }
} }
@@ -2367,7 +2369,7 @@ public class AiController {
// TODO move to more common place // TODO move to more common place
public static <T extends TriggerReplacementBase> List<T> filterList(List<T> input, Function<SpellAbility, Object> pred, Object value) { public static <T extends TriggerReplacementBase> List<T> filterList(List<T> input, Function<SpellAbility, Object> pred, Object value) {
return filterList(input, trb -> pred.apply(trb.ensureAbility()) == value); return filterList(input, trb -> trb.ensureAbility() != null && pred.apply(trb.ensureAbility()) == value);
} }
public static List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) { public static List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {

View File

@@ -48,6 +48,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility; import forge.game.trigger.WrappedAbility;
@@ -1458,15 +1459,14 @@ public class ComputerUtil {
// check for Continuous abilities that grant Haste // check for Continuous abilities that grant Haste
for (final Card c : all) { for (final Card c : all) {
for (StaticAbility stAb : c.getStaticAbilities()) { for (StaticAbility stAb : c.getStaticAbilities()) {
Map<String, String> params = stAb.getMapParams(); if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword") && stAb.getParam("AddKeyword").contains("Haste")) {
&& params.get("AddKeyword").contains("Haste")) {
if (c.isEquipment() && c.getEquipping() == null) { if (c.isEquipment() && c.getEquipping() == null) {
return true; return true;
} }
final String affected = params.get("Affected"); final String affected = stAb.getParam("Affected");
if (affected.contains("Creature.YouCtrl") if (affected.contains("Creature.YouCtrl")
|| affected.contains("Other+YouCtrl")) { || affected.contains("Other+YouCtrl")) {
return true; return true;
@@ -1519,11 +1519,10 @@ public class ComputerUtil {
for (final Card c : opp) { for (final Card c : opp) {
for (StaticAbility stAb : c.getStaticAbilities()) { for (StaticAbility stAb : c.getStaticAbilities()) {
Map<String, String> params = stAb.getMapParams(); if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword") && stAb.getParam("AddKeyword").contains("Haste")) {
&& params.get("AddKeyword").contains("Haste")) {
final ArrayList<String> affected = Lists.newArrayList(params.get("Affected").split(",")); final ArrayList<String> affected = Lists.newArrayList(stAb.getParam("Affected").split(","));
if (affected.contains("Creature")) { if (affected.contains("Creature")) {
return true; return true;
} }
@@ -2429,7 +2428,7 @@ public class ComputerUtil {
// Are we picking a type to reduce costs for that type? // Are we picking a type to reduce costs for that type?
boolean reducingCost = false; boolean reducingCost = false;
for (StaticAbility s : sa.getHostCard().getStaticAbilities()) { for (StaticAbility s : sa.getHostCard().getStaticAbilities()) {
if ("ReduceCost".equals(s.getParam("Mode")) && "Card.ChosenType".equals(s.getParam("ValidCard"))) { if (s.checkMode(StaticAbilityMode.ReduceCost) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
reducingCost = true; reducingCost = true;
break; break;
} }
@@ -2891,7 +2890,7 @@ public class ComputerUtil {
// Iceberg does use Ice as Storage // Iceberg does use Ice as Storage
|| (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName())) || (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName()))
// some lands does use Depletion as Storage Counter // some lands does use Depletion as Storage Counter
|| (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step.")) || (type.is(CounterEnumType.DEPLETION) && c.getReplacementEffects().anyMatch(r -> r.getMode().equals(ReplacementType.Untap) && r.getLayer().equals(ReplacementLayer.CantHappen)))
// treat Time Counters on suspended Cards as Bad, // treat Time Counters on suspended Cards as Bad,
// and also on Chronozoa // and also on Chronozoa
|| (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName()))) || (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName())))

View File

@@ -48,6 +48,7 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer; import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.zone.MagicStack; import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -691,6 +692,8 @@ public class ComputerUtilCard {
public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) { public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) {
AiBlockController aiBlk = new AiBlockController(ai, checkingOther); AiBlockController aiBlk = new AiBlockController(ai, checkingOther);
Combat combat = new Combat(ai); Combat combat = new Combat(ai);
// avoid removing original attacker
attacker.setCombatLKI(null);
combat.addAttacker(attacker, ai); combat.addAttacker(attacker, ai);
final List<Card> attackers = Lists.newArrayList(attacker); final List<Card> attackers = Lists.newArrayList(attacker);
aiBlk.assignBlockersGivenAttackers(combat, attackers); aiBlk.assignBlockersGivenAttackers(combat, attackers);
@@ -1211,8 +1214,7 @@ public class ComputerUtilCard {
// if this thing is both owned and controlled by an opponent and it has a continuous ability, // if this thing is both owned and controlled by an opponent and it has a continuous ability,
// assume it either benefits the player or disrupts the opponent // assume it either benefits the player or disrupts the opponent
for (final StaticAbility stAb : c.getStaticAbilities()) { for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams(); if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.isIntrinsic()) {
if (params.get("Mode").equals("Continuous") && stAb.isIntrinsic()) {
priority = true; priority = true;
break; break;
} }
@@ -1243,17 +1245,16 @@ public class ComputerUtilCard {
} }
} else { } else {
for (final StaticAbility stAb : c.getStaticAbilities()) { for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams();
//continuous buffs //continuous buffs
if (params.get("Mode").equals("Continuous") && "Creature.YouCtrl".equals(params.get("Affected"))) { if (stAb.checkMode(StaticAbilityMode.Continuous) && "Creature.YouCtrl".equals(stAb.getParam("Affected"))) {
int bonusPT = 0; int bonusPT = 0;
if (params.containsKey("AddPower")) { if (stAb.hasParam("AddPower")) {
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb); bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
} }
if (params.containsKey("AddToughness")) { if (stAb.hasParam("AddToughness")) {
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb); bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
} }
String kws = params.get("AddKeyword"); String kws = stAb.getParam("AddKeyword");
if (kws != null) { if (kws != null) {
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
} }
@@ -1735,11 +1736,6 @@ public class ComputerUtilCard {
pumped.setPTBoost(c.getPTBoostTable()); pumped.setPTBoost(c.getPTBoostTable());
pumped.addPTBoost(power + berserkPower, toughness, timestamp, 0); pumped.addPTBoost(power + berserkPower, toughness, timestamp, 0);
pumped.setSwitchPTTable(c.getSwitchPTTable());
if (sa.hasParam("SwitchPT")) {
pumped.addSwitchPT(timestamp, 0);
}
if (!kws.isEmpty()) { if (!kws.isEmpty()) {
pumped.addChangedCardKeywords(kws, null, false, timestamp, null, false); pumped.addChangedCardKeywords(kws, null, false, timestamp, null, false);
} }
@@ -1789,7 +1785,7 @@ public class ComputerUtilCard {
// remove old boost that might be copied // remove old boost that might be copied
for (final StaticAbility stAb : c.getStaticAbilities()) { for (final StaticAbility stAb : c.getStaticAbilities()) {
vCard.removePTBoost(c.getLayerTimestamp(), stAb.getId()); vCard.removePTBoost(c.getLayerTimestamp(), stAb.getId());
if (!stAb.checkMode("Continuous")) { if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue; continue;
} }
if (!stAb.hasParam("Affected")) { if (!stAb.hasParam("Affected")) {
@@ -1867,7 +1863,7 @@ public class ComputerUtilCard {
if (!c.isCreature()) { if (!c.isCreature()) {
return false; return false;
} }
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.hasKeyword("CARDNAME doesn't untap during your untap step.") && c.isTapped()) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) { if (c.hasKeyword("CARDNAME can't attack or block.") || (c.isTapped() && !c.canUntap(ai, true)) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
return true; return true;
} }
return false; return false;

View File

@@ -31,7 +31,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayment; import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.Untap; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer; import forge.game.replacement.ReplacementLayer;
@@ -39,6 +39,7 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked; import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMode;
import forge.game.staticability.StaticAbilityMustAttack; import forge.game.staticability.StaticAbilityMustAttack;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
@@ -101,7 +102,7 @@ public class ComputerUtilCombat {
return false; return false;
} }
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), "BeginCombat")) { if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), PhaseType.COMBAT_BEGIN)) {
return false; return false;
} }
@@ -118,7 +119,7 @@ public class ComputerUtilCombat {
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0) // || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
// || attacker.hasSVar("EndOfTurnLeavePlay")); // || attacker.hasSVar("EndOfTurnLeavePlay"));
// The creature won't untap next turn // The creature won't untap next turn
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && Untap.canUntap(attacker)); return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && attacker.canUntap(attacker.getController(), true));
} }
/** /**
@@ -900,7 +901,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command)); final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) { for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) { for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!stAb.checkMode("Continuous")) { if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue; continue;
} }
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) { if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
@@ -1196,7 +1197,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command)); final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) { for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) { for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!stAb.checkMode("Continuous")) { if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue; continue;
} }
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) { if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
@@ -1387,7 +1388,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield); final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
for (final Card card : cardList) { for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) { for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!"Continuous".equals(stAb.getParam("Mode"))) { if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue; continue;
} }
if (!stAb.hasParam("Affected")) { if (!stAb.hasParam("Affected")) {
@@ -1734,6 +1735,7 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false) final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); + predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
// AI should be less worried about Deathtouch
if (blocker.hasDoubleStrike()) { if (blocker.hasDoubleStrike()) {
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) { if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
return true; return true;
@@ -1963,6 +1965,7 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false) final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); + predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
// AI should be less worried about deathtouch
if (attacker.hasDoubleStrike()) { if (attacker.hasDoubleStrike()) {
if (attackerDamage >= defenderLife) { if (attackerDamage >= defenderLife) {
return true; return true;

View File

@@ -608,7 +608,7 @@ public class ComputerUtilCost {
} }
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect) return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(cost, sa, effect); && CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
} }
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) { public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {

View File

@@ -158,7 +158,7 @@ public class ComputerUtilMana {
} }
// Mana abilities on the same card // Mana abilities on the same card
String shardMana = shard.toString().replaceAll("\\{", "").replaceAll("\\}", ""); String shardMana = shard.toShortString();
boolean payWithAb1 = ability1.getManaPart().mana(ability1).contains(shardMana); boolean payWithAb1 = ability1.getManaPart().mana(ability1).contains(shardMana);
boolean payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana); boolean payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana);
@@ -642,24 +642,28 @@ public class ComputerUtilMana {
List<SpellAbility> paymentList = Lists.newArrayList(); List<SpellAbility> paymentList = Lists.newArrayList();
final ManaPool manapool = ai.getManaPool(); final ManaPool manapool = ai.getManaPool();
// Apply the color/type conversion matrix if necessary // Apply color/type conversion matrix if necessary (already done via autopay)
manapool.restoreColorReplacements(); if (ai.getControllingPlayer() == null) {
CardPlayOption mayPlay = sa.getMayPlayOption(); manapool.restoreColorReplacements();
if (!effect) { CardPlayOption mayPlay = sa.getMayPlayOption();
if (sa.isSpell() && mayPlay != null) { if (!effect) {
mayPlay.applyManaConvert(manapool); if (sa.isSpell() && mayPlay != null) {
} else if (sa.isActivatedAbility() && sa.getGrantorStatic() != null && sa.getGrantorStatic().hasParam("ManaConversion")) { mayPlay.applyManaConvert(manapool);
AbilityUtils.applyManaColorConversion(manapool, sa.getGrantorStatic().getParam("ManaConversion")); } else if (sa.isActivatedAbility() && sa.getGrantorStatic() != null && sa.getGrantorStatic().hasParam("ManaConversion")) {
AbilityUtils.applyManaColorConversion(manapool, sa.getGrantorStatic().getParam("ManaConversion"));
}
} }
if (sa.hasParam("ManaConversion")) {
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
}
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
} }
if (sa.hasParam("ManaConversion")) {
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
}
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
// not worth checking if it makes sense to not spend floating first
if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) { if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) {
CostPayment.handleOfferings(sa, test, cost.isPaid()); CostPayment.handleOfferings(sa, test, cost.isPaid());
return true; // paid all from floating mana // paid all from floating mana
return true;
} }
boolean purePhyrexian = cost.containsOnlyPhyrexianMana(); boolean purePhyrexian = cost.containsOnlyPhyrexianMana();

View File

@@ -160,12 +160,6 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(20, "protection"); value += addValue(20, "protection");
} }
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
// paired creatures are more valuable because they grant a bonus to the other creature // paired creatures are more valuable because they grant a bonus to the other creature
if (c.isPaired()) { if (c.isPaired()) {
value += addValue(14, "paired"); value += addValue(14, "paired");
@@ -213,11 +207,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(1, "untapped"); value += addValue(1, "untapped");
} }
if (!c.getManaAbilities().isEmpty()) { if (!c.canUntap(c.getController(), true)) {
value += addValue(10, "manadork");
}
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
if (c.isTapped()) { if (c.isTapped()) {
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
} else { } else {
@@ -226,6 +216,17 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} else { } else {
value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned"); value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned");
} }
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
// use scaling because the creature is only available halfway // use scaling because the creature is only available halfway
if (c.hasKeyword(Keyword.PHASING)) { if (c.hasKeyword(Keyword.PHASING)) {
value -= subValue(Math.max(20, value / 2), "phasing"); value -= subValue(Math.max(20, value / 2), "phasing");

View File

@@ -13,6 +13,7 @@ import forge.card.mana.ManaAtom;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.ApiType;
import forge.game.ability.effects.DetachedCardEffect; import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.*; import forge.game.card.*;
import forge.game.card.token.TokenInfo; import forge.game.card.token.TokenInfo;
@@ -1305,10 +1306,10 @@ public abstract class GameState {
} else if (info.startsWith("FaceDown")) { } else if (info.startsWith("FaceDown")) {
c.turnFaceDown(true); c.turnFaceDown(true);
if (info.endsWith("Manifested")) { if (info.endsWith("Manifested")) {
c.setManifested(true); c.setManifested(new SpellAbility.EmptySa(ApiType.Manifest, c));
} }
if (info.endsWith("Cloaked")) { if (info.endsWith("Cloaked")) {
c.setCloaked(true); c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
} }
} else if (info.startsWith("Transformed")) { } else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true); c.setState(CardStateName.Transformed, true);
@@ -1408,7 +1409,7 @@ public abstract class GameState {
} else if (info.equals("Foretold")) { } else if (info.equals("Foretold")) {
c.setForetold(true); c.setForetold(true);
c.turnFaceDown(true); c.turnFaceDown(true);
c.addMayLookTemp(c.getOwner()); c.addMayLookFaceDownExile(c.getOwner());
} else if (info.equals("ForetoldThisTurn")) { } else if (info.equals("ForetoldThisTurn")) {
c.setTurnInZone(turn); c.setTurnInZone(turn);
} else if (info.equals("IsToken")) { } else if (info.equals("IsToken")) {

View File

@@ -15,6 +15,7 @@ import forge.game.*;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect; import forge.game.ability.effects.CharmEffect;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.*; import forge.game.card.*;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
@@ -745,6 +746,30 @@ public class PlayerControllerAi extends PlayerController {
return Aggregates.random(rolls); return Aggregates.random(rolls);
} }
@Override
public List<Integer> chooseDiceToReroll(List<Integer> rolls) {
//TODO create AI logic for this
return new ArrayList<>();
}
@Override
public Integer chooseRollToModify(List<Integer> rolls) {
//TODO create AI logic for this
return Aggregates.random(rolls);
}
@Override
public RollDiceEffect.DieRollResult chooseRollToSwap(List<RollDiceEffect.DieRollResult> rolls) {
//TODO create AI logic for this
return Aggregates.random(rolls);
}
@Override
public String chooseRollSwapValue(List<String> swapChoices, Integer currentResult, int power, int toughness) {
//TODO create AI logic for this
return Aggregates.random(swapChoices);
}
@Override @Override
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) { public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
return !ComputerUtil.wantMulligan(player, cardsToReturn); return !ComputerUtil.wantMulligan(player, cardsToReturn);
@@ -1207,6 +1232,11 @@ public class PlayerControllerAi extends PlayerController {
return false; return false;
} }
public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView<Player> allPayers) {
// TODO logic for AI to pay rerolls and modification costs
return false;
}
@Override @Override
public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) { public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) {
for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) { for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) {

View File

@@ -1469,6 +1469,7 @@ public class SpecialCardAi {
if (best != null) { if (best != null) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(best); sa.getTargets().add(best);
sa.setXManaCostPaid(best.getCMC());
return true; return true;
} }

View File

@@ -258,7 +258,7 @@ public abstract class SpellAbilityAi {
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) { protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery()) return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed()) || (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery()) || (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai)); || (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
} }

View File

@@ -88,6 +88,7 @@ public enum SpellApiToAi {
.put(ApiType.EachDamage, DamageEachAi.class) .put(ApiType.EachDamage, DamageEachAi.class)
.put(ApiType.Effect, EffectAi.class) .put(ApiType.Effect, EffectAi.class)
.put(ApiType.Encode, EncodeAi.class) .put(ApiType.Encode, EncodeAi.class)
.put(ApiType.Endure, EndureAi.class)
.put(ApiType.EndCombatPhase, EndTurnAi.class) .put(ApiType.EndCombatPhase, EndTurnAi.class)
.put(ApiType.EndTurn, EndTurnAi.class) .put(ApiType.EndTurn, EndTurnAi.class)
.put(ApiType.ExchangeLife, LifeExchangeAi.class) .put(ApiType.ExchangeLife, LifeExchangeAi.class)

View File

@@ -24,6 +24,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityContinuous; import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer; import forge.game.staticability.StaticAbilityLayer;
import forge.game.staticability.StaticAbilityMode;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.FileSection; import forge.util.FileSection;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
@@ -562,7 +563,7 @@ public class AnimateAi extends SpellAbilityAi {
CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0); CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0);
if (traits != null) { if (traits != null) {
for (StaticAbility stAb : traits.getStaticAbilities()) { for (StaticAbility stAb : traits.getStaticAbilities()) {
if ("Continuous".equals(stAb.getParam("Mode"))) { if (stAb.checkMode(StaticAbilityMode.Continuous)) {
for (final StaticAbilityLayer layer : stAb.getLayers()) { for (final StaticAbilityLayer layer : stAb.getLayers()) {
StaticAbilityContinuous.applyContinuousAbility(stAb, new CardCollection(card), layer); StaticAbilityContinuous.applyContinuousAbility(stAb, new CardCollection(card), layer);
} }

View File

@@ -15,20 +15,23 @@ import forge.game.cost.Cost;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice; import forge.game.cost.CostSacrifice;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock; import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.MyRandom; import forge.util.MyRandom;
import org.apache.commons.lang3.ObjectUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -72,7 +75,7 @@ public class AttachAi extends SpellAbilityAi {
// prevent run-away activations - first time will always return true // prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return false;
} }
// Attach spells always have a target // Attach spells always have a target
@@ -130,7 +133,7 @@ public class AttachAi extends SpellAbilityAi {
int power = 0, toughness = 0; int power = 0, toughness = 0;
List<String> keywords = Lists.newArrayList(); List<String> keywords = Lists.newArrayList();
for (StaticAbility stAb : source.getStaticAbilities()) { for (StaticAbility stAb : source.getStaticAbilities()) {
if ("Continuous".equals(stAb.getParam("Mode"))) { if (stAb.checkMode(StaticAbilityMode.Continuous)) {
if (stAb.hasParam("AddPower")) { if (stAb.hasParam("AddPower")) {
power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb); power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
} }
@@ -307,9 +310,8 @@ public class AttachAi extends SpellAbilityAi {
String type = ""; String type = "";
for (final StaticAbility stAb : attachSource.getStaticAbilities()) { for (final StaticAbility stAb : attachSource.getStaticAbilities()) {
final Map<String, String> stab = stAb.getMapParams(); if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddType")) {
if (stab.get("Mode").equals("Continuous") && stab.containsKey("AddType")) { type = stAb.getParam("AddType");
type = stab.get("AddType");
} }
} }
@@ -371,9 +373,39 @@ public class AttachAi extends SpellAbilityAi {
*/ */
private static Card attachAIKeepTappedPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory, final Card attachSource) { private static Card attachAIKeepTappedPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory, final Card attachSource) {
// AI For Cards like Paralyzing Grasp and Glimmerdust Nap // AI For Cards like Paralyzing Grasp and Glimmerdust Nap
// check for ETB Trigger
boolean tapETB = isAuraSpell(sa) && attachSource.getTriggers().anyMatch(t -> {
if (t.getMode() != TriggerType.ChangesZone) {
return false;
}
if (!ZoneType.Battlefield.toString().equals(t.getParam("Destination"))) {
return false;
}
if (t.hasParam("ValidCard") && !t.getParam("ValidCard").contains("Self")) {
return false;
}
SpellAbility tSa = t.ensureAbility();
if (tSa == null) {
return false;
}
if (!ApiType.Tap.equals(tSa.getApi())) {
return false;
}
if (!"Enchanted".equals(tSa.getParam("Defined"))) {
return false;
}
return true;
});
final List<Card> prefList = CardLists.filter(list, c -> { final List<Card> prefList = CardLists.filter(list, c -> {
// Don't do Untapped Vigilance cards // Don't do Untapped Vigilance cards
if (c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) { if (!tapETB && c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
return false; return false;
} }
@@ -388,20 +420,9 @@ public class AttachAi extends SpellAbilityAi {
return false; return false;
} }
} }
// already affected
if (!c.isEnchanted()) { if (!c.canUntap(c.getController(), true)) {
return true; return false;
}
final Iterable<Card> auras = c.getEnchantedBy();
for (Card aura : auras) {
SpellAbility auraSA = aura.getSpells().get(0);
if (auraSA.getApi() == ApiType.Attach) {
if ("KeepTapped".equals(auraSA.getParam("AILogic"))) {
// Don't attach multiple KeepTapped Auras to one card
return false;
}
}
} }
return true; return true;
@@ -486,29 +507,29 @@ public class AttachAi extends SpellAbilityAi {
*/ */
private static Card attachAIAnimatePreference(final SpellAbility sa, final List<Card> list, final boolean mandatory, private static Card attachAIAnimatePreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource) { final Card attachSource) {
if (list.isEmpty()) { if (list.isEmpty()) {
return null; return null;
} }
Card card = null; Card card = null;
// AI For choosing a Card to Animate. // AI For choosing a Card to Animate.
List<Card> betterList = CardLists.getNotType(list, "Creature"); List<Card> betterList = CardLists.getNotType(list, "Creature");
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Animate Artifact")) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Animate Artifact")) {
betterList = CardLists.filter(betterList, c -> c.getCMC() > 0); betterList = CardLists.filter(betterList, c -> c.getCMC() > 0);
card = ComputerUtilCard.getMostExpensivePermanentAI(betterList); card = ComputerUtilCard.getMostExpensivePermanentAI(betterList);
} else { } else {
List<Card> evenBetterList = CardLists.filter(betterList, c -> c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.hasKeyword(Keyword.HEXPROOF)); List<Card> evenBetterList = CardLists.filter(betterList, c -> c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.hasKeyword(Keyword.HEXPROOF));
if (!evenBetterList.isEmpty()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; betterList = evenBetterList;
} }
evenBetterList = CardLists.filter(betterList, CardPredicates.UNTAPPED); evenBetterList = CardLists.filter(betterList, CardPredicates.UNTAPPED);
if (!evenBetterList.isEmpty()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; betterList = evenBetterList;
} }
evenBetterList = CardLists.filter(betterList, c -> c.getTurnInZone() != c.getGame().getPhaseHandler().getTurn()); evenBetterList = CardLists.filter(betterList, c -> c.getTurnInZone() != c.getGame().getPhaseHandler().getTurn());
if (!evenBetterList.isEmpty()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; betterList = evenBetterList;
} }
evenBetterList = CardLists.filter(betterList, c -> { evenBetterList = CardLists.filter(betterList, c -> {
for (final SpellAbility sa1 : c.getSpellAbilities()) { for (final SpellAbility sa1 : c.getSpellAbilities()) {
if (sa1.isAbility() && sa1.getPayCosts().hasTapCost()) { if (sa1.isAbility() && sa1.getPayCosts().hasTapCost()) {
return false; return false;
@@ -516,10 +537,10 @@ public class AttachAi extends SpellAbilityAi {
} }
return true; return true;
}); });
if (!evenBetterList.isEmpty()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; betterList = evenBetterList;
} }
card = ComputerUtilCard.getWorstAI(betterList); card = ComputerUtilCard.getWorstAI(betterList);
} }
@@ -549,28 +570,46 @@ public class AttachAi extends SpellAbilityAi {
final Card attachSource) { final Card attachSource) {
// AI For choosing a Card to Animate. // AI For choosing a Card to Animate.
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
final Card attachSourceLki = CardCopyService.getLKICopy(attachSource); Card attachSourceLki = null;
for (Trigger t : attachSource.getTriggers()) {
if (!t.getMode().equals(TriggerType.ChangesZone)) {
continue;
}
if (!"Battlefield".equals(t.getParam("Destination"))) {
continue;
}
if (!"Card.Self".equals(t.getParam("ValidCard"))) {
continue;
}
SpellAbility trigSa = t.ensureAbility();
SpellAbility animateSa = trigSa.findSubAbilityByType(ApiType.Animate);
if (animateSa == null) {
continue;
}
animateSa.setActivatingPlayer(sa.getActivatingPlayer());
attachSourceLki = AnimateAi.becomeAnimated(attachSource, animateSa);
}
if (attachSourceLki == null) {
return null;
}
attachSourceLki.setLastKnownZone(ai.getZone(ZoneType.Battlefield)); attachSourceLki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
// Suppress original attach Spell to replace it with another final Card finalAttachSourceLki = attachSourceLki;
attachSourceLki.getFirstAttachSpell().setSuppressed(true);
//TODO for Reanimate Auras i need the new Attach Spell, in later versions it might be part of the Enchant Keyword
attachSourceLki.addSpellAbility(AbilityFactory.getAbility(attachSourceLki, "NewAttach"));
List<Card> betterList = CardLists.filter(list, c -> { List<Card> betterList = CardLists.filter(list, c -> {
final Card lki = CardCopyService.getLKICopy(c); final Card lki = CardCopyService.getLKICopy(c);
// need to fake it as if lki would be on the battlefield // need to fake it as if lki would be on the battlefield
lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield)); lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
// Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered // Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered
attachSourceLki.clearRemembered(); finalAttachSourceLki.clearRemembered();
attachSourceLki.addRemembered(lki); finalAttachSourceLki.addRemembered(lki);
// need to check what the cards would be on the battlefield // need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events // do not attach yet, that would cause Events
CardCollection preList = new CardCollection(lki); CardCollection preList = new CardCollection(lki);
preList.add(attachSourceLki); preList.add(finalAttachSourceLki);
c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList); c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList);
boolean result = lki.canBeAttached(attachSourceLki, null); boolean result = lki.canBeAttached(finalAttachSourceLki, null);
//reset static abilities //reset static abilities
c.getGame().getAction().checkStaticAbilities(false); c.getGame().getAction().checkStaticAbilities(false);
@@ -795,27 +834,45 @@ public class AttachAi extends SpellAbilityAi {
int totPower = 0; int totPower = 0;
final List<String> keywords = new ArrayList<>(); final List<String> keywords = new ArrayList<>();
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) { boolean cantAttack = false;
final Map<String, String> stabMap = stAbility.getMapParams(); boolean cantBlock = false;
if (!stabMap.get("Mode").equals("Continuous")) { for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
if (stAbility.checkMode(StaticAbilityMode.CantAttack)) {
String valid = stAbility.getParam("ValidCard");
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
cantAttack = true;
}
} else if (stAbility.checkMode(StaticAbilityMode.CantBlock)) {
String valid = stAbility.getParam("ValidCard");
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
cantBlock = true;
}
} else if (stAbility.checkMode(StaticAbilityMode.CantBlockBy)) {
String valid = stAbility.getParam("ValidBlocker");
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
cantBlock = true;
}
}
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
continue; continue;
} }
final String affected = stabMap.get("Affected"); final String affected = stAbility.getParam("Affected");
if (affected == null) { if (affected == null) {
continue; continue;
} }
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) { if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa); totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa); totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), sa);
String kws = stabMap.get("AddKeyword"); String kws = stAbility.getParam("AddKeyword");
if (kws != null) { if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & "))); keywords.addAll(Arrays.asList(kws.split(" & ")));
} }
kws = stabMap.get("AddHiddenKeyword"); kws = stAbility.getParam("AddHiddenKeyword");
if (kws != null) { if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & "))); keywords.addAll(Arrays.asList(kws.split(" & ")));
} }
@@ -851,6 +908,12 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, c -> c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c)); prefList = CardLists.filter(prefList, c -> c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c));
} }
if (cantAttack) {
prefList = CardLists.filter(prefList, c -> c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c));
} else if (cantBlock) { // TODO better can block filter?
prefList = CardLists.filter(prefList, c -> c.isCreature() && !ComputerUtilCard.isUselessCreature(ai, c));
}
//some auras aren't useful in multiples //some auras aren't useful in multiples
if (attachSource.hasSVar("NonStackingAttachEffect")) { if (attachSource.hasSVar("NonStackingAttachEffect")) {
prefList = CardLists.filter(prefList, prefList = CardLists.filter(prefList,
@@ -925,6 +988,10 @@ public class AttachAi extends SpellAbilityAi {
return true; return true;
} }
private static boolean isAuraSpell(final SpellAbility sa) {
return sa.isSpell() && sa.getHostCard().isAura();
}
/** /**
* Attach preference. * Attach preference.
* *
@@ -940,7 +1007,23 @@ public class AttachAi extends SpellAbilityAi {
*/ */
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) { private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
GameObject o; GameObject o;
if (tgt.canTgtPlayer()) { boolean spellCanTargetPlayer = false;
if (isAuraSpell(sa)) {
Card source = sa.getHostCard();
if (!source.hasKeyword(Keyword.ENCHANT)) {
return false;
}
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
String ko = ki.getOriginal();
String m[] = ko.split(":");
String v = m[1];
if (v.contains("Player") || v.contains("Opponent")) {
spellCanTargetPlayer = true;
break;
}
}
}
if (tgt.canTgtPlayer() && (!isAuraSpell(sa) || spellCanTargetPlayer)) {
List<Player> targetable = new ArrayList<>(); List<Player> targetable = new ArrayList<>();
for (final Player player : sa.getHostCard().getGame().getPlayers()) { for (final Player player : sa.getHostCard().getGame().getPlayers()) {
if (sa.canTarget(player)) { if (sa.canTarget(player)) {
@@ -1005,9 +1088,8 @@ public class AttachAi extends SpellAbilityAi {
CardCollection toRemove = new CardCollection(); CardCollection toRemove = new CardCollection();
for (Trigger t : attachSource.getTriggers()) { for (Trigger t : attachSource.getTriggers()) {
if (t.getMode() == TriggerType.ChangesZone) { if (t.getMode() == TriggerType.ChangesZone) {
final Map<String, String> params = t.getMapParams(); if ("Card.Self".equals(t.getParam("ValidCard"))
if ("Card.Self".equals(params.get("ValidCard")) && "Battlefield".equals(t.getParam("Destination"))) {
&& "Battlefield".equals(params.get("Destination"))) {
SpellAbility trigSa = t.ensureAbility(); SpellAbility trigSa = t.ensureAbility();
if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) { if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
for (Card target : list) { for (Card target : list) {
@@ -1044,17 +1126,17 @@ public class AttachAi extends SpellAbilityAi {
// Probably want to "weight" the list by amount of Enchantments and // Probably want to "weight" the list by amount of Enchantments and
// choose the "lightest" // choose the "lightest"
List<Card> betterList = CardLists.filter(magnetList, c -> CombatUtil.canAttack(c, ai.getWeakestOpponent())); List<Card> betterList = CardLists.filter(magnetList, c -> CombatUtil.canAttack(c, ai.getWeakestOpponent()));
if (!betterList.isEmpty()) { if (!betterList.isEmpty()) {
return ComputerUtilCard.getBestAI(betterList); return ComputerUtilCard.getBestAI(betterList);
} }
// Magnet List should not be attached when they are useless // Magnet List should not be attached when they are useless
betterList = CardLists.filter(magnetList, c -> !ComputerUtilCard.isUselessCreature(ai, c)); betterList = CardLists.filter(magnetList, c -> !ComputerUtilCard.isUselessCreature(ai, c));
if (!betterList.isEmpty()) { if (!betterList.isEmpty()) {
return ComputerUtilCard.getBestAI(betterList); return ComputerUtilCard.getBestAI(betterList);
} }
//return ComputerUtilCard.getBestAI(magnetList); //return ComputerUtilCard.getBestAI(magnetList);
} }
@@ -1067,29 +1149,27 @@ public class AttachAi extends SpellAbilityAi {
boolean grantingExtraBlock = false; boolean grantingExtraBlock = false;
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) { for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
final Map<String, String> stabMap = stAbility.getMapParams(); if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
if (!"Continuous".equals(stabMap.get("Mode"))) {
continue; continue;
} }
final String affected = stabMap.get("Affected"); final String affected = stAbility.getParam("Affected");
if (affected == null) { if (affected == null) {
continue; continue;
} }
if (affected.contains(stCheck) || affected.contains("AttachedBy")) { if (affected.contains(stCheck) || affected.contains("AttachedBy")) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility); totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility); totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), stAbility);
grantingAbilities |= stabMap.containsKey("AddAbility"); grantingAbilities |= stAbility.hasParam("AddAbility");
grantingExtraBlock |= stabMap.containsKey("CanBlockAmount") || stabMap.containsKey("CanBlockAny"); grantingExtraBlock |= stAbility.hasParam("CanBlockAmount") || stAbility.hasParam("CanBlockAny");
String kws = stabMap.get("AddKeyword"); String kws = stAbility.getParam("AddKeyword");
if (kws != null) { if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & "))); keywords.addAll(Arrays.asList(kws.split(" & ")));
} }
kws = stabMap.get("AddHiddenKeyword"); kws = stAbility.getParam("AddHiddenKeyword");
if (kws != null) { if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & "))); keywords.addAll(Arrays.asList(kws.split(" & ")));
} }
@@ -1143,13 +1223,13 @@ public class AttachAi extends SpellAbilityAi {
prefList = ComputerUtil.getSafeTargets(ai, sa, prefList); prefList = ComputerUtil.getSafeTargets(ai, sa, prefList);
if (attachSource.isAura()) { if (attachSource.isAura()) {
if (!attachSource.getName().equals("Daybreak Coronet")) { if (!attachSource.getName().equals("Daybreak Coronet")) {
// TODO For Auras like Rancor, that aren't as likely to lead to // TODO For Auras like Rancor, that aren't as likely to lead to
// card disadvantage, this check should be skipped // card disadvantage, this check should be skipped
prefList = CardLists.filter(prefList, c -> !c.isEnchanted() || c.hasKeyword(Keyword.HEXPROOF)); prefList = CardLists.filter(prefList, c -> !c.isEnchanted() || c.hasKeyword(Keyword.HEXPROOF));
} }
// should not attach Auras to creatures that does leave the play // should not attach Auras to creatures that does leave the play
prefList = CardLists.filter(prefList, c -> !c.hasSVar("EndOfTurnLeavePlay")); prefList = CardLists.filter(prefList, c -> !c.hasSVar("EndOfTurnLeavePlay"));
} }
@@ -1158,10 +1238,15 @@ public class AttachAi extends SpellAbilityAi {
// TODO Somehow test for definitive advantage (e.g. opponent low on health, AI is attacking) // TODO Somehow test for definitive advantage (e.g. opponent low on health, AI is attacking)
// to be able to deal the final blow with an enchanted vehicle like that // to be able to deal the final blow with an enchanted vehicle like that
boolean canOnlyTargetCreatures = true; boolean canOnlyTargetCreatures = true;
for (String valid : ObjectUtils.firstNonNull(attachSource.getFirstAttachSpell(), sa).getTargetRestrictions().getValidTgts()) { if (attachSource.isAura()) {
if (!valid.startsWith("Creature")) { for (KeywordInterface ki : attachSource.getKeywords(Keyword.ENCHANT)) {
canOnlyTargetCreatures = false; String o = ki.getOriginal();
break; String m[] = o.split(":");
String v = m[1];
if (!v.startsWith("Creature")) {
canOnlyTargetCreatures = false;
break;
}
} }
} }
if (canOnlyTargetCreatures && (attachSource.isAura() || attachSource.isEquipment())) { if (canOnlyTargetCreatures && (attachSource.isAura() || attachSource.isEquipment())) {
@@ -1172,7 +1257,7 @@ public class AttachAi extends SpellAbilityAi {
// Probably prefer to Enchant Creatures that Can Attack // Probably prefer to Enchant Creatures that Can Attack
// Filter out creatures that can't Attack or have Defender // Filter out creatures that can't Attack or have Defender
if (keywords.isEmpty()) { if (keywords.isEmpty()) {
final int powerBonus = totPower; final int powerBonus = totPower;
prefList = CardLists.filter(prefList, c -> { prefList = CardLists.filter(prefList, c -> {
if (!c.isCreature()) { if (!c.isCreature()) {
return true; return true;
@@ -1387,8 +1472,6 @@ public class AttachAi extends SpellAbilityAi {
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource); c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
} else if ("ChangeType".equals(logic)) { } else if ("ChangeType".equals(logic)) {
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource); c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
} else if ("KeepTapped".equals(logic)) {
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
} else if ("Animate".equals(logic)) { } else if ("Animate".equals(logic)) {
c = attachAIAnimatePreference(sa, prefList, mandatory, attachSource); c = attachAIAnimatePreference(sa, prefList, mandatory, attachSource);
} else if ("Reanimate".equals(logic)) { } else if ("Reanimate".equals(logic)) {
@@ -1399,6 +1482,12 @@ public class AttachAi extends SpellAbilityAi {
c = attachAIHighestEvaluationPreference(prefList); c = attachAIHighestEvaluationPreference(prefList);
} }
if (isAuraSpell(sa)) {
if (attachSource.getReplacementEffects().anyMatch(re -> re.getMode().equals(ReplacementType.Untap) && re.getLayer().equals(ReplacementLayer.CantHappen))) {
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
}
}
// Consider exceptional cases which break the normal evaluation rules // Consider exceptional cases which break the normal evaluation rules
if (!isUsefulAttachAction(ai, c, sa)) { if (!isUsefulAttachAction(ai, c, sa)) {
return null; return null;
@@ -1551,8 +1640,6 @@ public class AttachAi extends SpellAbilityAi {
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.") } else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) { || keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card); return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
} }
return true; return true;
} }

View File

@@ -914,6 +914,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.isSpell()) { if (sa.isSpell()) {
list.remove(source); // spells can't target their own source, because it's actually in the stack zone list.remove(source); // spells can't target their own source, because it's actually in the stack zone
} }
// list = CardLists.canSubsequentlyTarget(list, sa);
if (sa.hasParam("AttachedTo")) { if (sa.hasParam("AttachedTo")) {
list = CardLists.filter(list, c -> { list = CardLists.filter(list, c -> {
for (Card card : game.getCardsIn(ZoneType.Battlefield)) { for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
@@ -1282,7 +1285,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
list.remove(choice); list.remove(choice);
sa.getTargets().add(choice); if (sa.canTarget(choice)) {
sa.getTargets().add(choice);
}
} }
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card. // Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
@@ -1448,6 +1453,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
// AI Targeting // AI Targeting
Card choice = null; Card choice = null;
// Filter out cards TargetsForEachPlayer
list = CardLists.canSubsequentlyTarget(list, sa);
if (!list.isEmpty()) { if (!list.isEmpty()) {
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list); Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list);
if (mostExpensivePermanent.isCreature() if (mostExpensivePermanent.isCreature()

View File

@@ -161,10 +161,10 @@ public class ChooseGenericAi extends SpellAbilityAi {
} }
} }
// FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat // FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "Draw")) { if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.DRAW)) {
return skipDraw; return skipDraw;
} }
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "BeginCombat")) { if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.COMBAT_BEGIN)) {
return skipCombat; return skipCombat;
} }

View File

@@ -14,7 +14,7 @@ public class CloakAi extends ManifestBaseAi {
// (e.g. Grafdigger's Cage) // (e.g. Grafdigger's Cage)
Card topCopy = CardCopyService.getLKICopy(card); Card topCopy = CardCopyService.getLKICopy(card);
topCopy.turnFaceDownNoUpdate(); topCopy.turnFaceDownNoUpdate();
topCopy.setCloaked(true); topCopy.setCloaked(sa);
if (ComputerUtil.isETBprevented(topCopy)) { if (ComputerUtil.isETBprevented(topCopy)) {
return false; return false;

View File

@@ -4,6 +4,7 @@ import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana; import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
@@ -18,6 +19,13 @@ public class ConniveAi extends SpellAbilityAi {
return false; // can't draw anything return false; // can't draw anything
} }
Card host = sa.getHostCard();
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
if (num == 0) {
return false; // Won't do anything
}
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
// Filter AI-specific targets if provided // Filter AI-specific targets if provided

View File

@@ -205,6 +205,9 @@ public class ControlGainAi extends SpellAbilityAi {
while (t == null) { while (t == null) {
// filter by MustTarget requirement // filter by MustTarget requirement
CardCollection originalList = new CardCollection(list); CardCollection originalList = new CardCollection(list);
list = CardLists.canSubsequentlyTarget(list, sa);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa); boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
if (planeswalkers > 0) { if (planeswalkers > 0) {

View File

@@ -152,6 +152,8 @@ public class CopyPermanentAi extends SpellAbilityAi {
// target loop // target loop
while (sa.canAddMoreTarget()) { while (sa.canAddMoreTarget()) {
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
sa.resetTargets(); sa.resetTargets();

View File

@@ -95,7 +95,7 @@ public class DamageDealAi extends DamageAiBase {
final String damage = sa.getParam("NumDmg"); final String damage = sa.getParam("NumDmg");
int dmg = calculateDamageAmount(sa, source, damage); int dmg = calculateDamageAmount(sa, source, damage);
if (damage.equals("X") || source.getSVar("X").equals("Count$xPaid")) { if (damage.equals("X") || (dmg == 0 && source.getSVar("X").equals("Count$xPaid"))) {
if (sa.getSVar("X").equals("Count$xPaid") || sa.getSVar(damage).equals("Count$xPaid")) { if (sa.getSVar("X").equals("Count$xPaid") || sa.getSVar(damage).equals("Count$xPaid")) {
dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());

View File

@@ -216,6 +216,8 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection originalList = new CardCollection(list); CardCollection originalList = new CardCollection(list);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa); boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets(); sa.resetTargets();
@@ -275,6 +277,7 @@ public class DestroyAi extends SpellAbilityAi {
choice = aura; choice = aura;
} }
} }
// TODO What about stolen permanents we're getting back at the end of the turn?
} }
} }
@@ -284,7 +287,9 @@ public class DestroyAi extends SpellAbilityAi {
} }
list.remove(choice); list.remove(choice);
sa.getTargets().add(choice); if (sa.canTarget(choice)) {
sa.getTargets().add(choice);
}
} }
} else if (sa.hasParam("Defined")) { } else if (sa.hasParam("Defined")) {
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
@@ -361,7 +366,10 @@ public class DestroyAi extends SpellAbilityAi {
} }
} else { } else {
Card c = ComputerUtilCard.getBestAI(preferred); Card c = ComputerUtilCard.getBestAI(preferred);
sa.getTargets().add(c);
if (sa.canTarget(c)) {
sa.getTargets().add(c);
}
preferred.remove(c); preferred.remove(c);
} }
} }
@@ -382,7 +390,9 @@ public class DestroyAi extends SpellAbilityAi {
} else { } else {
c = ComputerUtilCard.getCheapestPermanentAI(list, sa, false); c = ComputerUtilCard.getCheapestPermanentAI(list, sa, false);
} }
sa.getTargets().add(c); if (sa.canTarget(c)) {
sa.getTargets().add(c);
}
list.remove(c); list.remove(c);
} }
} }

View File

@@ -515,12 +515,17 @@ public class DrawAi extends SpellAbilityAi {
return false; return false;
} }
if ((computerHandSize + numCards > computerMaxHandSize) if ((computerHandSize + numCards > computerMaxHandSize)) {
&& game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger()
&& !assumeSafeX) {
// Don't draw too many cards and then risk discarding cards at EOT // Don't draw too many cards and then risk discarding cards at EOT
if (!drawback) { if (game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger()
&& !assumeSafeX
&& !drawback) {
return false;
}
if (computerHandSize > computerMaxHandSize) {
// Don't make my hand size get too big if already at max
return false; return false;
} }
} }

View File

@@ -0,0 +1,140 @@
package forge.ai.ability;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.Game;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.Map;
public class EndureAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// Support for possible targeted Endure (e.g. target creature endures X)
if (sa.usesTargeting()) {
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
if (bestCreature == null) {
return false;
}
sa.resetTargets();
sa.getTargets().add(bestCreature);
}
return true;
}
public static boolean shouldPutCounters(Player ai, SpellAbility sa) {
// TODO: adapted from Fabricate AI in TokenAi, maybe can be refactored to a single method
final Card source = sa.getHostCard();
final Game game = source.getGame();
final String num = sa.getParamOrDefault("Num", "1");
final int amount = AbilityUtils.calculateAmount(source, num, sa);
// if host would leave the play or if host is useless, create the token
if (source.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(ai, source)) {
return false;
}
// need a copy for one with extra +1/+1 counter boost,
// without causing triggers to run
final Card copy = CardCopyService.getLKICopy(source);
copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + amount);
copy.setZone(source.getZone());
// if host would put into the battlefield attacking
Combat combat = source.getGame().getCombat();
if (combat != null && combat.isAttacking(source)) {
final Player defender = combat.getDefenderPlayerByAttacker(source);
return defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true);
}
// if the host has haste and can attack
if (CombatUtil.canAttack(copy)) {
for (final Player opp : ai.getOpponents()) {
if (CombatUtil.canAttack(copy, opp) &&
opp.canLoseLife() &&
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true))
return true;
}
}
// TODO check for trigger to turn token ETB into +1/+1 counter for host
// TODO check for trigger to turn token ETB into damage or life loss for opponent
// in these cases token might be preferred even if they would not survive
// evaluate creature with counters
int evalCounter = ComputerUtilCard.evaluateCreature(copy);
// spawn the token so it's possible to evaluate it
final Card token = TokenInfo.getProtoType("w_x_x_spirit", sa, ai, false);
token.setController(ai, 0);
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
token.setTokenSpawningAbility(sa);
// evaluate the generated token
token.setBasePowerString(num);
token.setBasePower(amount);
token.setBaseToughnessString(num);
token.setBaseToughness(amount);
boolean result = true;
// need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events
CardCollection preList = new CardCollection(token);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
// token would not survive
if (!token.isCreature() || token.getNetToughness() < 1) {
result = false;
}
if (result) {
int evalToken = ComputerUtilCard.evaluateCreature(token);
result = evalToken < evalCounter;
}
//reset static abilities
game.getAction().checkStaticAbilities(false);
return result;
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return shouldPutCounters(player, sa);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// Support for possible targeted Endure (e.g. target creature endures X)
if (sa.usesTargeting()) {
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
if (!list.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
return true;
}
return false;
}
return canPlayAI(aiPlayer, sa) || mandatory;
}
}

View File

@@ -14,7 +14,7 @@ public class ManifestAi extends ManifestBaseAi {
// (e.g. Grafdigger's Cage) // (e.g. Grafdigger's Cage)
Card topCopy = CardCopyService.getLKICopy(card); Card topCopy = CardCopyService.getLKICopy(card);
topCopy.turnFaceDownNoUpdate(); topCopy.turnFaceDownNoUpdate();
topCopy.setManifested(true); topCopy.setManifested(sa);
if (ComputerUtil.isETBprevented(topCopy)) { if (ComputerUtil.isETBprevented(topCopy)) {
return false; return false;

View File

@@ -14,6 +14,7 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -47,7 +48,7 @@ public class PermanentCreatureAi extends PermanentAi {
if (sa.isDash()) { if (sa.isDash()) {
//only checks that the dashed creature will attack //only checks that the dashed creature will attack
if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat")) if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))
return false; return false;
if (ComputerUtilCost.canPayCost(sa.getHostCard().getSpellPermanent(), ai, false)) { if (ComputerUtilCost.canPayCost(sa.getHostCard().getSpellPermanent(), ai, false)) {
//do not dash if creature can be played normally //do not dash if creature can be played normally
@@ -70,7 +71,7 @@ public class PermanentCreatureAi extends PermanentAi {
// after attacking // after attacking
if (card.hasSVar("EndOfTurnLeavePlay") if (card.hasSVar("EndOfTurnLeavePlay")
&& (!ph.isPlayerTurn(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && (!ph.isPlayerTurn(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat"))) { || game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))) {
// AiPlayDecision.AnotherTime // AiPlayDecision.AnotherTime
return false; return false;
} }
@@ -154,7 +155,7 @@ public class PermanentCreatureAi extends PermanentAi {
boolean canCastAtOppTurn = true; boolean canCastAtOppTurn = true;
for (Card c : ai.getGame().getCardsIn(ZoneType.Battlefield)) { for (Card c : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
for (StaticAbility s : c.getStaticAbilities()) { for (StaticAbility s : c.getStaticAbilities()) {
if ("CantBeCast".equals(s.getParam("Mode")) && StringUtils.contains(s.getParam("Activator"), "NonActive") if (s.checkMode(StaticAbilityMode.CantBeCast) && StringUtils.contains(s.getParam("Activator"), "NonActive")
&& (!s.getParam("Activator").startsWith("You") || c.getController().equals(ai))) { && (!s.getParam("Activator").startsWith("You") || c.getController().equals(ai))) {
canCastAtOppTurn = false; canCastAtOppTurn = false;
break; break;

View File

@@ -4,7 +4,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.*; import forge.ai.*;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.*; import forge.game.card.*;
@@ -15,9 +14,6 @@ import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -61,6 +57,12 @@ public class PumpAi extends PumpAiBase {
return SpecialAiLogic.doAristocratLogic(ai, sa); return SpecialAiLogic.doAristocratLogic(ai, sa);
} else if (aiLogic.startsWith("AristocratCounters")) { } else if (aiLogic.startsWith("AristocratCounters")) {
return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa); return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa);
} else if (aiLogic.equals("SwitchPT")) {
// Some more AI would be even better, but this is a good start to prevent spamming
if (sa.isActivatedAbility() && sa.getActivationsThisTurn() > 0 && !sa.usesTargeting()) {
// Will prevent flipping back and forth
return false;
}
} }
return super.checkAiLogic(ai, sa, aiLogic); return super.checkAiLogic(ai, sa, aiLogic);
@@ -81,6 +83,11 @@ public class PumpAi extends PumpAiBase {
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !isThreatened) { if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !isThreatened) {
return false; return false;
} }
} else if (logic.equals("SwitchPT")) {
// Some more AI would be even better, but this is a good start to prevent spamming
if (ph.getPhase().isAfter(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE) || !ph.inCombat()) {
return false;
}
} }
return super.checkPhaseRestrictions(ai, sa, ph); return super.checkPhaseRestrictions(ai, sa, ph);
} }
@@ -97,12 +104,6 @@ public class PumpAi extends PumpAiBase {
return false; return false;
} }
} }
if (sa.hasParam("SwitchPT")) {
// Some more AI would be even better, but this is a good start to prevent spamming
if (ph.getPhase().isAfter(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE) || !ph.inCombat()) {
return false;
}
}
if (game.getStack().isEmpty() && (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN) if (game.getStack().isEmpty() && (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN)
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS))) { || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS))) {
// Instant-speed pumps should not be cast outside of combat when the // Instant-speed pumps should not be cast outside of combat when the
@@ -247,14 +248,6 @@ public class PumpAi extends PumpAiBase {
} }
} }
if (sa.hasParam("SwitchPT")) {
// Some more AI would be even better, but this is a good start to prevent spamming
if (sa.isActivatedAbility() && sa.getActivationsThisTurn() > 0 && !sa.usesTargeting()) {
// Will prevent flipping back and forth
return false;
}
}
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return false;
} }
@@ -363,7 +356,7 @@ public class PumpAi extends PumpAiBase {
} // pumpPlayAI() } // pumpPlayAI()
private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory, private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory,
boolean immediately) { boolean immediately) {
final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & "))
: Lists.newArrayList(); : Lists.newArrayList();
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -473,65 +466,6 @@ public class PumpAi extends PumpAiBase {
} }
} }
if (sa.hasParam("SwitchPT")) {
// Logic to kill opponent creatures
CardCollection oppCreatures = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
if (!oppCreatures.isEmpty()) {
CardCollection oppCreaturesFiltered = CardLists.filter(oppCreatures, card -> {
// don't care about useless creatures
if (ComputerUtilCard.isUselessCreature(ai, card)
|| card.hasSVar("EndOfTurnLeavePlay")) {
return false;
}
// dies by target
if (card.getSVar("Targeting").equals("Dies")) {
return true;
}
// dies by state based action
if (card.getNetPower() <= 0) {
return true;
}
if (card.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return false;
}
// check if switching PT causes it to be lethal
Card lki = CardCopyService.getLKICopy(card);
lki.addSwitchPT(-1, 0);
// check if creature could regenerate
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(card);
runParams.put(AbilityKey.Regeneration, true);
List<ReplacementEffect> repDestoryList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
// non-Regeneration one like Totem-Armor
// should do it anyway to destroy the aura?
if (repDestoryList.stream().anyMatch(r -> !r.hasParam("Regeneration"))) {
return false;
}
// TODO make it force to use regen?
// should check phase and make it before combat damage or better before blocker?
if (repDestoryList.stream().anyMatch(r -> r.hasParam("Regeneration")) && card.canBeShielded()) {
return false;
}
// maybe do it anyway to reduce its power?
if (card.getLethal() - card.getDamage() > 0) {
return false;
}
return true;
});
// the ones that die by switching PT
if (!oppCreaturesFiltered.isEmpty()) {
Card best = ComputerUtilCard.getBestCreatureAI(oppCreaturesFiltered);
if (best != null) {
sa.getTargets().add(best);
return true;
}
}
}
return false;
}
if (sa.isCurse()) { if (sa.isCurse()) {
for (final Player opp : ai.getOpponents()) { for (final Player opp : ai.getOpponents()) {
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
@@ -608,6 +542,8 @@ public class PumpAi extends PumpAiBase {
Card t = null; Card t = null;
// boolean goodt = false; // boolean goodt = false;
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (mandatory || ComputerUtil.activateForCost(sa, ai)) { if (mandatory || ComputerUtil.activateForCost(sa, ai)) {

View File

@@ -10,7 +10,6 @@ import forge.game.combat.CombatUtil;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.phase.Untap;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -137,7 +136,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return CombatUtil.canBlockAtLeastOne(card, attackers); return CombatUtil.canBlockAtLeastOne(card, attackers);
} else if (keyword.endsWith("This card doesn't untap during your next untap step.")) { } else if (keyword.endsWith("This card doesn't untap during your next untap step.")) {
return !ph.getPhase().isBefore(PhaseType.MAIN2) && !card.isUntapped() && ph.isPlayerTurn(ai) return !ph.getPhase().isBefore(PhaseType.MAIN2) && !card.isUntapped() && ph.isPlayerTurn(ai)
&& Untap.canUntap(card); && card.canUntap(card.getController(), true);
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.") } else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")
|| keyword.endsWith("Prevent all damage that would be dealt by CARDNAME.")) { || keyword.endsWith("Prevent all damage that would be dealt by CARDNAME.")) {
if (ph.isPlayerTurn(ai) && (!(CombatUtil.canBlock(card) || combat != null && combat.isBlocking(card)) if (ph.isPlayerTurn(ai) && (!(CombatUtil.canBlock(card) || combat != null && combat.isBlocking(card))

View File

@@ -367,8 +367,7 @@ public class TokenAi extends SpellAbilityAi {
} }
private boolean tgtRoleAura(final Player ai, final SpellAbility sa, final Card tok, final boolean mandatory) { private boolean tgtRoleAura(final Player ai, final SpellAbility sa, final Card tok, final boolean mandatory) {
boolean isCurse = "Curse".equals(sa.getParam("AILogic")) || boolean isCurse = "Curse".equals(sa.getParam("AILogic")) || "Curse".equals(tok.getSVar("AttachAILogic"));
tok.getFirstAttachSpell().getParamOrDefault("AILogic", "").equals("Curse");
List<Card> tgts = CardUtil.getValidCardsToTarget(sa); List<Card> tgts = CardUtil.getValidCardsToTarget(sa);
// look for card without role from ai // look for card without role from ai

View File

@@ -16,7 +16,6 @@ import forge.game.cost.CostTap;
import forge.game.mana.ManaCostBeingPaid; import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.phase.Untap;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -338,7 +337,7 @@ public class UntapAi extends SpellAbilityAi {
} }
// See if there's anything to untap that is tapped and that doesn't untap during the next untap step by itself // See if there's anything to untap that is tapped and that doesn't untap during the next untap step by itself
CardCollection noAutoUntap = CardLists.filter(untapList, Untap.CANUNTAP.negate()); CardCollection noAutoUntap = CardLists.filter(untapList, c -> !c.canUntap(c.getController(), true));
if (!noAutoUntap.isEmpty()) { if (!noAutoUntap.isEmpty()) {
return ComputerUtilCard.getBestAI(noAutoUntap); return ComputerUtilCard.getBestAI(noAutoUntap);
} }

View File

@@ -96,7 +96,7 @@ public class GameCopier {
newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn()); newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn());
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters())); newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
newPlayer.setSpeed(origPlayer.getSpeed()); newPlayer.setSpeed(origPlayer.getSpeed());
newPlayer.setBlessing(origPlayer.hasBlessing()); newPlayer.setBlessing(origPlayer.hasBlessing(), null);
newPlayer.setRevolt(origPlayer.hasRevolt()); newPlayer.setRevolt(origPlayer.hasRevolt());
newPlayer.setDescended(origPlayer.getDescended()); newPlayer.setDescended(origPlayer.getDescended());
newPlayer.setLibrarySearched(origPlayer.getLibrarySearched()); newPlayer.setLibrarySearched(origPlayer.getLibrarySearched());
@@ -356,7 +356,6 @@ public class GameCopier {
newCard.setPTCharacterDefiningTable(c.getSetPTCharacterDefiningTable()); newCard.setPTCharacterDefiningTable(c.getSetPTCharacterDefiningTable());
newCard.setPTBoost(c.getPTBoostTable()); newCard.setPTBoost(c.getPTBoostTable());
newCard.setSwitchPTTable(c.getSwitchPTTable());
// TODO copy by map // TODO copy by map
newCard.setDamage(c.getDamage()); newCard.setDamage(c.getDamage());
newCard.setDamageReceivedThisTurn(c.getDamageReceivedThisTurn()); newCard.setDamageReceivedThisTurn(c.getDamageReceivedThisTurn());
@@ -374,10 +373,10 @@ public class GameCopier {
if (c.isFaceDown()) { if (c.isFaceDown()) {
newCard.turnFaceDown(true); newCard.turnFaceDown(true);
if (c.isManifested()) { if (c.isManifested()) {
newCard.setManifested(true); newCard.setManifested(c.getManifestedSA());
} }
if (c.isCloaked()) { if (c.isCloaked()) {
newCard.setCloaked(true); newCard.setCloaked(c.getCloakedSA());
} }
} }
if (c.isMonstrous()) { if (c.isMonstrous()) {

View File

@@ -23,17 +23,16 @@ public final class ImageKeys {
public static final String HIDDEN_CARD = "hidden"; public static final String HIDDEN_CARD = "hidden";
public static final String MORPH_IMAGE = "morph"; public static final String MORPH_IMAGE = "morph";
public static final String DISGUISED_IMAGE = "disguised";
public static final String MANIFEST_IMAGE = "manifest"; public static final String MANIFEST_IMAGE = "manifest";
public static final String CLOAKED_IMAGE = "cloaked"; public static final String CLOAKED_IMAGE = "cloaked";
public static final String FORETELL_IMAGE = "foretell"; public static final String FORETELL_IMAGE = "foretell";
public static final String BLESSING_IMAGE = "blessing";
public static final String INITIATIVE_IMAGE = "initiative";
public static final String MONARCH_IMAGE = "monarch";
public static final String THE_RING_IMAGE = "the_ring";
public static final String RADIATION_IMAGE = "radiation";
public static final String BACKFACE_POSTFIX = "$alt"; public static final String BACKFACE_POSTFIX = "$alt";
public static final String SPECFACE_W = "$wspec";
public static final String SPECFACE_U = "$uspec";
public static final String SPECFACE_B = "$bspec";
public static final String SPECFACE_R = "$rspec";
public static final String SPECFACE_G = "$gspec";
private static String CACHE_CARD_PICS_DIR, CACHE_TOKEN_PICS_DIR, CACHE_ICON_PICS_DIR, CACHE_BOOSTER_PICS_DIR, private static String CACHE_CARD_PICS_DIR, CACHE_TOKEN_PICS_DIR, CACHE_ICON_PICS_DIR, CACHE_BOOSTER_PICS_DIR,
CACHE_FATPACK_PICS_DIR, CACHE_BOOSTERBOX_PICS_DIR, CACHE_PRECON_PICS_DIR, CACHE_TOURNAMENTPACK_PICS_DIR; CACHE_FATPACK_PICS_DIR, CACHE_BOOSTERBOX_PICS_DIR, CACHE_PRECON_PICS_DIR, CACHE_TOURNAMENTPACK_PICS_DIR;
@@ -93,13 +92,38 @@ public final class ImageKeys {
return cachedCards.get(key); return cachedCards.get(key);
} }
public static File getImageFile(String key) { public static File getImageFile(String key) {
return getImageFile(key, false);
}
public static File getImageFile(String key, boolean artCrop) {
if (StringUtils.isEmpty(key)) if (StringUtils.isEmpty(key))
return null; return null;
final String dir; final String dir;
final String filename; final String filename;
if (key.startsWith(ImageKeys.TOKEN_PREFIX)) { String[] tempdata = null;
filename = key.substring(ImageKeys.TOKEN_PREFIX.length()); if (key.startsWith(ImageKeys.CARD_PREFIX)) {
tempdata = key.substring(ImageKeys.CARD_PREFIX.length()).split("\\|");
String tokenname = tempdata[0];
if (tempdata.length > 1) {
tokenname += "_" + tempdata[1];
}
if (tempdata.length > 2) {
tokenname += "_" + tempdata[2];
}
filename = tokenname ;
dir = CACHE_CARD_PICS_DIR;
} else if (key.startsWith(ImageKeys.TOKEN_PREFIX)) {
tempdata = key.substring(ImageKeys.TOKEN_PREFIX.length()).split("\\|");
String tokenname = tempdata[0];
if (tempdata.length > 1) {
tokenname += "_" + tempdata[1];
}
if (tempdata.length > 2) {
tokenname += "_" + tempdata[2];
}
filename = tokenname;
dir = CACHE_TOKEN_PICS_DIR; dir = CACHE_TOKEN_PICS_DIR;
} else if (key.startsWith(ImageKeys.ICON_PREFIX)) { } else if (key.startsWith(ImageKeys.ICON_PREFIX)) {
filename = key.substring(ImageKeys.ICON_PREFIX.length()); filename = key.substring(ImageKeys.ICON_PREFIX.length());
@@ -140,6 +164,54 @@ public final class ImageKeys {
cachedCards.put(filename, file); cachedCards.put(filename, file);
return file; return file;
} }
if (tempdata != null && dir.equals(CACHE_CARD_PICS_DIR)) {
String setlessFilename = tempdata[0] + (artCrop ? ".artcrop" : ".fullborder");
String setCode = tempdata.length > 1 ? tempdata[1] : "";
String collectorNumber = tempdata.length > 2 ? tempdata[2] : "";
if (!setCode.isEmpty()) {
if (!collectorNumber.isEmpty()) {
file = findFile(dir, setCode + "/" + collectorNumber + "_" + setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
file = findFile(dir, setCode + "/" + setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
file = findFile(dir, setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
if (tempdata != null && dir.equals(CACHE_TOKEN_PICS_DIR)) {
String setlessFilename = tempdata[0];
String setCode = tempdata.length > 1 ? tempdata[1] : "";
String collectorNumber = tempdata.length > 2 ? tempdata[2] : "";
if (!setCode.isEmpty()) {
if (!collectorNumber.isEmpty()) {
file = findFile(dir, setCode + "/" + collectorNumber + "_" + setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
file = findFile(dir, setCode + "/" + setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
file = findFile(dir, setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
// AE -> Ae and Ae -> AE for older cards with different file names // AE -> Ae and Ae -> AE for older cards with different file names
// on case-sensitive file systems // on case-sensitive file systems
@@ -221,39 +293,7 @@ public final class ImageKeys {
return file; return file;
} }
} }
if (dir.equals(CACHE_TOKEN_PICS_DIR)) { if (filename.contains("/")) {
int index = filename.lastIndexOf('_');
if (index != -1) {
String setlessFilename = filename.substring(0, index);
String setCode = filename.substring(index + 1);
// try with upper case set
file = findFile(dir, setlessFilename + "_" + setCode.toUpperCase());
if (file != null) {
cachedCards.put(filename, file);
return file;
}
// try with lower case set
file = findFile(dir, setlessFilename + "_" + setCode.toLowerCase());
if (file != null) {
cachedCards.put(filename, file);
return file;
}
// try without set name
file = findFile(dir, setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
// if there's an art variant try without it
if (setlessFilename.matches(".*[0-9]*$")) {
file = findFile(dir, setlessFilename.replaceAll("[0-9]*$", ""));
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
}
} else if (filename.contains("/")) {
String setlessFilename = filename.substring(filename.indexOf('/') + 1); String setlessFilename = filename.substring(filename.indexOf('/') + 1);
file = findFile(dir, setlessFilename); file = findFile(dir, setlessFilename);
if (file != null) { if (file != null) {

View File

@@ -95,12 +95,12 @@ public class StaticData {
if (!loadNonLegalCards) { if (!loadNonLegalCards) {
for (CardEdition e : editions) { for (CardEdition e : editions) {
if (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER) { if (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER) {
List<CardEdition.CardInSet> eternalCards = e.getFunnyEternalCards(); List<CardEdition.EditionEntry> eternalCards = e.getFunnyEternalCards();
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) { for (CardEdition.EditionEntry cis : e.getAllCardsInSet()) {
if (eternalCards.contains(cis)) if (eternalCards.contains(cis))
continue; continue;
funnyCards.add(cis.name); funnyCards.add(cis.name());
} }
} }
} }
@@ -217,6 +217,9 @@ public class StaticData {
} }
public CardEdition getCardEdition(String setCode) { public CardEdition getCardEdition(String setCode) {
if (CardEdition.UNKNOWN_CODE.equals(setCode)) {
return CardEdition.UNKNOWN;
}
CardEdition edition = this.editions.get(setCode); CardEdition edition = this.editions.get(setCode);
return edition; return edition;
} }
@@ -248,6 +251,15 @@ public class StaticData {
} }
} }
/**
* Retrieve a PaperCard by looking at all available card databases for any matching print.
* @param cardName The name of the card
* @return PaperCard instance found in one of the available CardDb databases, or <code>null</code> if not found.
*/
public PaperCard fetchCard(final String cardName) {
return fetchCard(cardName, null, null);
}
/** /**
* Retrieve a PaperCard by looking at all available card databases; * Retrieve a PaperCard by looking at all available card databases;
* @param cardName The name of the card * @param cardName The name of the card
@@ -778,11 +790,11 @@ public class StaticData {
Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>(); Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>();
List<CompletableFuture<?>> futures = new ArrayList<>(); List<CompletableFuture<?>> futures = new ArrayList<>();
for (CardEdition.CardInSet c : e.getAllCardsInSet()) { for (CardEdition.EditionEntry c : e.getAllCardsInSet()) {
if (cardCount.containsKey(c.name)) { if (cardCount.containsKey(c.name())) {
cardCount.put(c.name, Pair.of(c.collectorNumber != null && c.collectorNumber.startsWith("F"), cardCount.get(c.name).getRight() + 1)); cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), cardCount.get(c.name()).getRight() + 1));
} else { } else {
cardCount.put(c.name, Pair.of(c.collectorNumber != null && c.collectorNumber.startsWith("F"), 1)); cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), 1));
} }
} }
@@ -844,9 +856,9 @@ public class StaticData {
futures.clear(); futures.clear();
// TODO: Audit token images here... // TODO: Audit token images here...
for(Map.Entry<String, Integer> tokenEntry : e.getTokens().entrySet()) { for(Map.Entry<String, Collection<CardEdition.EditionEntry>> tokenEntry : e.getTokens().asMap().entrySet()) {
final String name = tokenEntry.getKey(); final String name = tokenEntry.getKey();
final int artIndex = tokenEntry.getValue(); final int artIndex = tokenEntry.getValue().size();
try { try {
PaperToken token = getAllTokens().getToken(name, e.getCode()); PaperToken token = getAllTokens().getToken(name, e.getCode());
if (token == null) { if (token == null) {
@@ -983,4 +995,23 @@ public class StaticData {
} }
return false; return false;
} }
public String getOtherImageKey(String name, String set) {
if (this.editions.get(set) != null) {
String realSetCode = this.editions.get(set).getOtherSet(name);
if (realSetCode != null) {
CardEdition.EditionEntry ee = this.editions.get(realSetCode).findOther(name);
if (ee != null) { // TODO add collector Number and new ImageKey format
return ImageKeys.getTokenKey(String.format("%s|%s|%s", name, realSetCode, ee.collectorNumber()));
}
}
}
for (CardEdition e : this.editions) {
CardEdition.EditionEntry ee = e.findOther(name);
if (ee != null) { // TODO add collector Number and new ImageKey format
return ImageKeys.getTokenKey(String.format("%s|%s|%s", name, e.getCode(), ee.collectorNumber()));
}
}
// final fallback
return ImageKeys.getTokenKey(name);
}
} }

View File

@@ -22,7 +22,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps; import com.google.common.collect.Multimaps;
import forge.StaticData; import forge.StaticData;
import forge.card.CardEdition.CardInSet; import forge.card.CardEdition.EditionEntry;
import forge.card.CardEdition.Type; import forge.card.CardEdition.Type;
import forge.deck.generation.IDeckGenPool; import forge.deck.generation.IDeckGenPool;
import forge.item.IPaperCard; import forge.item.IPaperCard;
@@ -42,7 +42,8 @@ import java.util.stream.Stream;
public final class CardDb implements ICardDatabase, IDeckGenPool { public final class CardDb implements ICardDatabase, IDeckGenPool {
public final static String foilSuffix = "+"; public final static String foilSuffix = "+";
public final static char NameSetSeparator = '|'; public final static char NameSetSeparator = '|';
public final static String colorIDPrefix = "#"; public final static String FlagPrefix = "#";
public static final String FlagSeparator = "\t";
private final String exlcudedCardName = "Concentrate"; private final String exlcudedCardName = "Concentrate";
private final String exlcudedCardSet = "DS0"; private final String exlcudedCardSet = "DS0";
@@ -93,19 +94,19 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public int artIndex; public int artIndex;
public boolean isFoil; public boolean isFoil;
public String collectorNumber; public String collectorNumber;
public Set<String> colorID; public Map<String, String> flags;
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber) { private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber) {
this(name, edition, artIndex, isFoil, collectorNumber, null); this(name, edition, artIndex, isFoil, collectorNumber, null);
} }
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber, Set<String> colorID) { private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber, Map<String, String> flags) {
cardName = name; cardName = name;
this.edition = edition; this.edition = edition;
this.artIndex = artIndex; this.artIndex = artIndex;
this.isFoil = isFoil; this.isFoil = isFoil;
this.collectorNumber = collectorNumber; this.collectorNumber = collectorNumber;
this.colorID = colorID; this.flags = flags;
} }
public static boolean isFoilCardName(final String cardName){ public static boolean isFoilCardName(final String cardName){
@@ -120,7 +121,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
public static String compose(String cardName, String setCode) { public static String compose(String cardName, String setCode) {
setCode = setCode != null ? setCode : ""; if(setCode == null || StringUtils.isBlank(setCode) || setCode.equals(CardEdition.UNKNOWN_CODE))
setCode = "";
cardName = cardName != null ? cardName : ""; cardName = cardName != null ? cardName : "";
if (cardName.indexOf(NameSetSeparator) != -1) if (cardName.indexOf(NameSetSeparator) != -1)
// If cardName is another RequestString, just get card name and forget about the rest. // If cardName is another RequestString, just get card name and forget about the rest.
@@ -134,14 +136,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return requestInfo + NameSetSeparator + artIndex; return requestInfo + NameSetSeparator + artIndex;
} }
public static String compose(String cardName, String setCode, int artIndex, Set<String> colorID) {
String requestInfo = compose(cardName, setCode);
artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
String cid = colorID == null ? "" : NameSetSeparator +
colorID.toString().replace("[", colorIDPrefix).replace(", ", colorIDPrefix).replace("]", "");
return requestInfo + NameSetSeparator + artIndex + cid;
}
public static String compose(String cardName, String setCode, String collectorNumber) { public static String compose(String cardName, String setCode, String collectorNumber) {
String requestInfo = compose(cardName, setCode); String requestInfo = compose(cardName, setCode);
// CollectorNumber will be wrapped in square brackets // CollectorNumber will be wrapped in square brackets
@@ -149,6 +143,34 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return requestInfo + NameSetSeparator + collectorNumber; return requestInfo + NameSetSeparator + collectorNumber;
} }
public static String compose(String cardName, String setCode, int artIndex, Map<String, String> flags) {
String requestInfo = compose(cardName, setCode);
artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
if(flags == null)
return requestInfo + NameSetSeparator + artIndex;
return requestInfo + NameSetSeparator + artIndex + getFlagSegment(flags);
}
public static String compose(String cardName, String setCode, String collectorNumber, Map<String, String> flags) {
String requestInfo = compose(cardName, setCode);
collectorNumber = preprocessCollectorNumber(collectorNumber);
if(flags == null || flags.isEmpty())
return requestInfo + NameSetSeparator + collectorNumber;
return requestInfo + NameSetSeparator + collectorNumber + getFlagSegment(flags);
}
public static String compose(PaperCard card) {
String name = compose(card.getName(), card.isFoil());
return compose(name, card.getEdition(), card.getCollectorNumber(), card.getMarkedFlags().toMap());
}
public static String compose(String cardName, String setCode, int artIndex, String collectorNumber) {
String requestInfo = compose(cardName, setCode, artIndex);
// CollectorNumber will be wrapped in square brackets
collectorNumber = preprocessCollectorNumber(collectorNumber);
return requestInfo + NameSetSeparator + collectorNumber;
}
private static String preprocessCollectorNumber(String collectorNumber) { private static String preprocessCollectorNumber(String collectorNumber) {
if (collectorNumber == null) if (collectorNumber == null)
return ""; return "";
@@ -160,19 +182,21 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return collectorNumber; return collectorNumber;
} }
public static String compose(String cardName, String setCode, int artIndex, String collectorNumber) { private static String getFlagSegment(Map<String, String> flags) {
String requestInfo = compose(cardName, setCode, artIndex); if(flags == null)
// CollectorNumber will be wrapped in square brackets return "";
collectorNumber = preprocessCollectorNumber(collectorNumber); String flagText = flags.entrySet().stream()
return requestInfo + NameSetSeparator + collectorNumber; .map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(FlagSeparator));
return NameSetSeparator + FlagPrefix + "{" + flagText + "}";
} }
private static boolean isCollectorNumber(String s) { private static boolean isCollectorNumber(String s) {
return s.startsWith("[") && s.endsWith("]"); return s.startsWith("[") && s.endsWith("]");
} }
private static boolean isColorIDString(String s) { private static boolean isFlagSegment(String s) {
return s.startsWith(colorIDPrefix); return s.startsWith(FlagPrefix);
} }
private static boolean isArtIndex(String s) { private static boolean isArtIndex(String s) {
@@ -201,44 +225,36 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return null; return null;
String[] info = TextUtil.split(reqInfo, NameSetSeparator); String[] info = TextUtil.split(reqInfo, NameSetSeparator);
int setPos; int index = 1;
int artPos;
int cNrPos;
int clrPos;
if (info.length >= 4) { // name|set|artIndex|[collNr]
setPos = isSetCode(info[1]) ? 1 : -1;
artPos = isArtIndex(info[2]) ? 2 : -1;
cNrPos = isCollectorNumber(info[3]) ? 3 : -1;
int pos = cNrPos > 0 ? -1 : 3;
clrPos = pos > 0 ? isColorIDString(info[pos]) ? pos : -1 : -1;
} else if (info.length == 3) { // name|set|artIndex (or CollNr)
setPos = isSetCode(info[1]) ? 1 : -1;
artPos = isArtIndex(info[2]) ? 2 : -1;
cNrPos = isCollectorNumber(info[2]) ? 2 : -1;
int pos = cNrPos > 0 ? -1 : 2;
clrPos = pos > 0 ? isColorIDString(info[pos]) ? pos : -1 : -1;
} else if (info.length == 2) { // name|set (or artIndex, even if not possible via compose)
setPos = isSetCode(info[1]) ? 1 : -1;
artPos = isArtIndex(info[1]) ? 1 : -1;
cNrPos = -1;
clrPos = -1;
} else {
setPos = -1;
artPos = -1;
cNrPos = -1;
clrPos = -1;
}
String cardName = info[0]; String cardName = info[0];
boolean isFoil = false; boolean isFoil = false;
int artIndex = IPaperCard.NO_ART_INDEX;
String setCode = null;
String collectorNumber = IPaperCard.NO_COLLECTOR_NUMBER;
Map<String, String> flags = null;
if (isFoilCardName(cardName)) { if (isFoilCardName(cardName)) {
cardName = cardName.substring(0, cardName.length() - foilSuffix.length()); cardName = cardName.substring(0, cardName.length() - foilSuffix.length());
isFoil = true; isFoil = true;
} }
int artIndex = artPos > 0 ? Integer.parseInt(info[artPos]) : IPaperCard.NO_ART_INDEX; // default: no art index
String collectorNumber = cNrPos > 0 ? info[cNrPos].substring(1, info[cNrPos].length() - 1) : IPaperCard.NO_COLLECTOR_NUMBER; if(info.length > index && isSetCode(info[index])) {
String setCode = setPos > 0 ? info[setPos] : null; setCode = info[index];
Set<String> colorID = clrPos > 0 ? Arrays.stream(info[clrPos].substring(1).split(colorIDPrefix)).collect(Collectors.toSet()) : null; index++;
if (setCode != null && setCode.equals(CardEdition.UNKNOWN.getCode())) { // ??? }
if(info.length > index && isArtIndex(info[index])) {
artIndex = Integer.parseInt(info[index]);
index++;
}
if(info.length > index && isCollectorNumber(info[index])) {
collectorNumber = info[index].substring(1, info[index].length() - 1);
index++;
}
if (info.length > index && isFlagSegment(info[index])) {
String flagText = info[index].substring(FlagPrefix.length());
flags = parseRequestFlags(flagText);
}
if (CardEdition.UNKNOWN_CODE.equals(setCode)) { // ???
setCode = null; setCode = null;
} }
if (setCode == null) { if (setCode == null) {
@@ -253,7 +269,29 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// finally, check whether any between artIndex and CollectorNumber has been set // finally, check whether any between artIndex and CollectorNumber has been set
if (collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER) && artIndex == IPaperCard.NO_ART_INDEX) if (collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER) && artIndex == IPaperCard.NO_ART_INDEX)
artIndex = IPaperCard.DEFAULT_ART_INDEX; artIndex = IPaperCard.DEFAULT_ART_INDEX;
return new CardRequest(cardName, setCode, artIndex, isFoil, collectorNumber, colorID); return new CardRequest(cardName, setCode, artIndex, isFoil, collectorNumber, flags);
}
private static Map<String, String> parseRequestFlags(String flagText) {
flagText = flagText.trim();
if(flagText.isEmpty())
return null;
if(!flagText.startsWith("{")) {
//Legacy form for marked colors. They'll be of the form "W#B#R"
Map<String, String> flags = new HashMap<>();
String normalizedColorString = ColorSet.fromNames(flagText.split(FlagPrefix)).toString();
flags.put("markedColors", String.join("", normalizedColorString));
return flags;
}
flagText = flagText.substring(1, flagText.length() - 1); //Trim the braces.
//List of flags, a series of "key=value" text broken up by tabs.
return Arrays.stream(flagText.split(FlagSeparator))
.map(f -> f.split("=", 2))
.filter(f -> f.length > 0)
.collect(Collectors.toMap(
entry -> entry[0],
entry -> entry.length > 1 ? entry[1] : "true" //If there's no '=' in the entry, treat it as a boolean flag.
));
} }
} }
@@ -294,27 +332,27 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
} }
private void addSetCard(CardEdition e, CardInSet cis, CardRules cr) { private void addSetCard(CardEdition e, EditionEntry cis, CardRules cr) {
int artIdx = IPaperCard.DEFAULT_ART_INDEX; int artIdx = IPaperCard.DEFAULT_ART_INDEX;
String key = e.getCode() + "/" + cis.name; String key = e.getCode() + "/" + cis.name();
if (artIds.containsKey(key)) { if (artIds.containsKey(key)) {
artIdx = artIds.get(key) + 1; artIdx = artIds.get(key) + 1;
} }
artIds.put(key, artIdx); artIds.put(key, artIdx);
addCard(new PaperCard(cr, e.getCode(), cis.rarity, artIdx, false, cis.collectorNumber, cis.artistName, cis.functionalVariantName)); addCard(new PaperCard(cr, e.getCode(), cis.rarity(), artIdx, false, cis.collectorNumber(), cis.artistName(), cis.functionalVariantName()));
} }
private boolean addFromSetByName(String cardName, CardEdition ed, CardRules cr) { private boolean addFromSetByName(String cardName, CardEdition ed, CardRules cr) {
List<CardInSet> cardsInSet = ed.getCardInSet(cardName); // empty collection if not present List<EditionEntry> cardsInSet = ed.getCardInSet(cardName); // empty collection if not present
if (cr.hasFunctionalVariants()) { if (cr.hasFunctionalVariants()) {
cardsInSet = cardsInSet.stream().filter(c -> StringUtils.isEmpty(c.functionalVariantName) cardsInSet = cardsInSet.stream().filter(c -> StringUtils.isEmpty(c.functionalVariantName())
|| cr.getSupportedFunctionalVariants().contains(c.functionalVariantName) || cr.getSupportedFunctionalVariants().contains(c.functionalVariantName())
).collect(Collectors.toList()); ).collect(Collectors.toList());
} }
if (cardsInSet.isEmpty()) if (cardsInSet.isEmpty())
return false; return false;
for (CardInSet cis : cardsInSet) { for (EditionEntry cis : cardsInSet) {
addSetCard(ed, cis, cr); addSetCard(ed, cis, cr);
} }
return true; return true;
@@ -359,15 +397,15 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
upcomingSet = e; upcomingSet = e;
} }
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) { for (CardEdition.EditionEntry cis : e.getAllCardsInSet()) {
CardRules cr = rulesByName.get(cis.name); CardRules cr = rulesByName.get(cis.name());
if (cr == null) { if (cr == null) {
missingCards.add(cis.name); missingCards.add(cis.name());
continue; continue;
} }
if (cr.hasFunctionalVariants()) { if (cr.hasFunctionalVariants()) {
if (StringUtils.isNotEmpty(cis.functionalVariantName) if (StringUtils.isNotEmpty(cis.functionalVariantName())
&& !cr.getSupportedFunctionalVariants().contains(cis.functionalVariantName)) { && !cr.getSupportedFunctionalVariants().contains(cis.functionalVariantName())) {
//Supported card, unsupported variant. //Supported card, unsupported variant.
//Could note the card as missing but since these are often un-cards, //Could note the card as missing but since these are often un-cards,
//it's likely absent because it does something out of scope. //it's likely absent because it does something out of scope.
@@ -406,7 +444,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown)); addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown));
} else if (enableUnknownCards && !this.filtered.contains(cr.getName())) { } else if (enableUnknownCards && !this.filtered.contains(cr.getName())) {
System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. "); System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. ");
addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special)); addCard(new PaperCard(cr, CardEdition.UNKNOWN_CODE, CardRarity.Special));
} }
} else { } else {
System.err.println("The custom card " + cr.getName() + " was not assigned to any set. Adding it to custom USER set, and will try to load custom art from USER edition."); System.err.println("The custom card " + cr.getName() + " was not assigned to any set. Adding it to custom USER set, and will try to load custom art from USER edition.");
@@ -425,8 +463,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
lang = new LangEnglish(); lang = new LangEnglish();
} }
// for now just check Universes Within // for now just check Universes Within
for (CardInSet cis : editions.get("SLX").getCards()) { for (EditionEntry cis : editions.get("SLX").getCards()) {
String orgName = alternateName.get(cis.name); String orgName = alternateName.get(cis.name());
if (orgName != null) { if (orgName != null) {
// found original (beyond) print // found original (beyond) print
CardRules org = getRules(orgName); CardRules org = getRules(orgName);
@@ -456,7 +494,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
CardRules within = new CardRules(new ICardFace[] { renamedMain, renamedOther, null, null, null, null, null }, org.getSplitType(), org.getAiHints()); CardRules within = new CardRules(new ICardFace[] { renamedMain, renamedOther, null, null, null, null, null }, org.getSplitType(), org.getAiHints());
// so workshop can edit same script // so workshop can edit same script
within.setNormalizedName(org.getNormalizedName()); within.setNormalizedName(org.getNormalizedName());
rulesByName.put(cis.name, within); rulesByName.put(cis.name(), within);
} }
} }
} }
@@ -592,15 +630,15 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
@Override @Override
public PaperCard getCard(final String cardName, String setCode, int artIndex, String collectorNumber) { public PaperCard getCard(final String cardName, String setCode, int artIndex, Map<String, String> flags) {
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, collectorNumber); String reqInfo = CardRequest.compose(cardName, setCode, artIndex, flags);
CardRequest request = CardRequest.fromString(reqInfo); CardRequest request = CardRequest.fromString(reqInfo);
return tryGetCard(request); return tryGetCard(request);
} }
@Override @Override
public PaperCard getCard(final String cardName, String setCode, int artIndex, Set<String> colorID) { public PaperCard getCard(final String cardName, String setCode, String collectorNumber, Map<String, String> flags) {
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, colorID); String reqInfo = CardRequest.compose(cardName, setCode, collectorNumber, flags);
CardRequest request = CardRequest.fromString(reqInfo); CardRequest request = CardRequest.fromString(reqInfo);
return tryGetCard(request); return tryGetCard(request);
} }
@@ -611,14 +649,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return null; return null;
// 1. First off, try using all possible search parameters, to narrow down the actual cards looked for. // 1. First off, try using all possible search parameters, to narrow down the actual cards looked for.
String reqEditionCode = request.edition; String reqEditionCode = request.edition;
if (reqEditionCode != null && reqEditionCode.length() > 0) { if (reqEditionCode != null && !reqEditionCode.isEmpty()) {
// This get is robust even against expansion aliases (e.g. TE and TMP both valid for Tempest) - // This get is robust even against expansion aliases (e.g. TE and TMP both valid for Tempest) -
// MOST of the extensions have two short codes, 141 out of 221 (so far) // MOST of the extensions have two short codes, 141 out of 221 (so far)
// ALSO: Set Code are always UpperCase // ALSO: Set Code are always UpperCase
CardEdition edition = editions.get(reqEditionCode.toUpperCase()); CardEdition edition = editions.get(reqEditionCode.toUpperCase());
return this.getCardFromSet(request.cardName, edition, request.artIndex, PaperCard cardFromSet = this.getCardFromSet(request.cardName, edition, request.artIndex, request.collectorNumber, request.isFoil);
request.collectorNumber, request.isFoil, request.colorID); if(cardFromSet != null && request.flags != null)
cardFromSet = cardFromSet.copyWithFlags(request.flags);
return cardFromSet;
} }
// 2. Card lookup in edition with specified filter didn't work. // 2. Card lookup in edition with specified filter didn't work.
@@ -661,11 +702,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override @Override
public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil) { public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil) {
return getCardFromSet(cardName, edition, artIndex, collectorNumber, isFoil, null);
}
@Override
public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil, Set<String> colorID) {
if (edition == null || cardName == null) // preview cards if (edition == null || cardName == null) // preview cards
return null; // No cards will be returned return null; // No cards will be returned
@@ -674,18 +710,18 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
cardName = cardNameRequest.cardName; cardName = cardNameRequest.cardName;
isFoil = isFoil || cardNameRequest.isFoil; isFoil = isFoil || cardNameRequest.isFoil;
List<PaperCard> candidates = getAllCards(cardName, c -> { String code1 = edition.getCode(), code2 = edition.getCode2();
boolean artIndexFilter = true;
boolean collectorNumberFilter = true; Predicate<PaperCard> filter = (c) -> {
boolean setFilter = c.getEdition().equalsIgnoreCase(edition.getCode()) || String ed = c.getEdition();
c.getEdition().equalsIgnoreCase(edition.getCode2()); return ed.equalsIgnoreCase(code1) || ed.equalsIgnoreCase(code2);
if (artIndex > 0) };
artIndexFilter = (c.getArtIndex() == artIndex); if (artIndex > 0)
if ((collectorNumber != null) && (collectorNumber.length() > 0) filter = filter.and((c) -> artIndex == c.getArtIndex());
&& !(collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER))) if (collectorNumber != null && !collectorNumber.isEmpty() && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER))
collectorNumberFilter = (c.getCollectorNumber().equals(collectorNumber)); filter = filter.and((c) -> collectorNumber.equals(c.getCollectorNumber()));
return setFilter && artIndexFilter && collectorNumberFilter;
}); List<PaperCard> candidates = getAllCards(cardName, filter);
if (candidates.isEmpty()) if (candidates.isEmpty())
return null; return null;
@@ -699,7 +735,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
while (!candidate.hasImage() && candidatesIterator.hasNext()) while (!candidate.hasImage() && candidatesIterator.hasNext())
candidate = candidatesIterator.next(); candidate = candidatesIterator.next();
candidate = candidate.hasImage() ? candidate : firstCandidate; candidate = candidate.hasImage() ? candidate : firstCandidate;
return isFoil ? candidate.getFoiled().getColorIDVersion(colorID) : candidate.getColorIDVersion(colorID); return isFoil ? candidate.getFoiled() : candidate;
} }
/* /*
@@ -742,11 +778,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, filter); return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, filter);
} }
@Override
public PaperCard getCardFromEditions(final String cardInfo, final CardArtPreference artPreference, int artIndex, Set<String> colorID) {
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, null, false, null, colorID);
}
/* /*
* =============================================== * ===============================================
* 4. SPECIALISED CARD LOOKUP BASED ON * 4. SPECIALISED CARD LOOKUP BASED ON
@@ -820,12 +851,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex, private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter){ Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter) {
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, releaseDate, releasedBeforeFlag, filter, null);
}
private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter, Set<String> colorID){
if (cardInfo == null) if (cardInfo == null)
return null; return null;
final CardRequest cr = CardRequest.fromString(cardInfo); final CardRequest cr = CardRequest.fromString(cardInfo);
@@ -865,7 +891,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
for (PaperCard card : cards) { for (PaperCard card : cards) {
String setCode = card.getEdition(); String setCode = card.getEdition();
CardEdition ed; CardEdition ed;
if (setCode.equals(CardEdition.UNKNOWN.getCode())) if (setCode.equals(CardEdition.UNKNOWN_CODE))
ed = CardEdition.UNKNOWN; ed = CardEdition.UNKNOWN;
else else
ed = editions.get(card.getEdition()); ed = editions.get(card.getEdition());
@@ -906,7 +932,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
candidate = candidate.hasImage() ? candidate : firstCandidate; candidate = candidate.hasImage() ? candidate : firstCandidate;
//If any, we're sure that at least one candidate is always returned despite it having any image //If any, we're sure that at least one candidate is always returned despite it having any image
return cr.isFoil ? candidate.getFoiled().getColorIDVersion(colorID) : candidate.getColorIDVersion(colorID); return cr.isFoil ? candidate.getFoiled() : candidate;
} }
@Override @Override
@@ -1017,7 +1043,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public static final Predicate<PaperCard> EDITION_NON_PROMO = paperCard -> { public static final Predicate<PaperCard> EDITION_NON_PROMO = paperCard -> {
String code = paperCard.getEdition(); String code = paperCard.getEdition();
CardEdition edition = StaticData.instance().getCardEdition(code); CardEdition edition = StaticData.instance().getCardEdition(code);
if(edition == null && code.equals("???")) if(edition == null && code.equals(CardEdition.UNKNOWN_CODE))
return true; return true;
return edition != null && edition.getType() != Type.PROMO; return edition != null && edition.getType() != Type.PROMO;
}; };
@@ -1025,7 +1051,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public static final Predicate<PaperCard> EDITION_NON_REPRINT = paperCard -> { public static final Predicate<PaperCard> EDITION_NON_REPRINT = paperCard -> {
String code = paperCard.getEdition(); String code = paperCard.getEdition();
CardEdition edition = StaticData.instance().getCardEdition(code); CardEdition edition = StaticData.instance().getCardEdition(code);
if(edition == null && code.equals("???")) if(edition == null && code.equals(CardEdition.UNKNOWN_CODE))
return true; return true;
return edition != null && Type.REPRINT_SET_TYPES.contains(edition.getType()); return edition != null && Type.REPRINT_SET_TYPES.contains(edition.getType());
}; };
@@ -1081,8 +1107,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public Collection<PaperCard> getAllCards(CardEdition edition) { public Collection<PaperCard> getAllCards(CardEdition edition) {
List<PaperCard> cards = Lists.newArrayList(); List<PaperCard> cards = Lists.newArrayList();
for (CardInSet cis : edition.getAllCardsInSet()) { for (EditionEntry cis : edition.getAllCardsInSet()) {
PaperCard card = this.getCard(cis.name, edition.getCode()); PaperCard card = this.getCard(cis.name(), edition.getCode());
if (card == null) { if (card == null) {
// Just in case the card is listed in the edition file but Forge doesn't support it // Just in case the card is listed in the edition file but Forge doesn't support it
continue; continue;
@@ -1126,29 +1152,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
.anyMatch(rarity::equals); .anyMatch(rarity::equals);
} }
public StringBuilder appendCardToStringBuilder(PaperCard card, StringBuilder sb) {
final boolean hasBadSetInfo = card.getEdition().equals(CardEdition.UNKNOWN.getCode()) || StringUtils.isBlank(card.getEdition());
sb.append(card.getName());
if (card.isFoil()) {
sb.append(CardDb.foilSuffix);
}
if (!hasBadSetInfo) {
int artCount = getArtCount(card.getName(), card.getEdition(), card.getFunctionalVariant());
sb.append(CardDb.NameSetSeparator).append(card.getEdition());
if (artCount >= IPaperCard.DEFAULT_ART_INDEX) {
sb.append(CardDb.NameSetSeparator).append(card.getArtIndex()); // indexes start at 1 to match image file name conventions
}
if (card.getColorID() != null) {
sb.append(CardDb.NameSetSeparator);
for (String color : card.getColorID())
sb.append(CardDb.colorIDPrefix).append(color);
}
}
return sb;
}
public PaperCard createUnsupportedCard(String cardRequest) { public PaperCard createUnsupportedCard(String cardRequest) {
CardRequest request = CardRequest.fromString(cardRequest); CardRequest request = CardRequest.fromString(cardRequest);
CardEdition cardEdition = CardEdition.UNKNOWN; CardEdition cardEdition = CardEdition.UNKNOWN;
@@ -1157,10 +1160,10 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// May iterate over editions and find out if there is any card named 'cardRequest' but not implemented with Forge script. // May iterate over editions and find out if there is any card named 'cardRequest' but not implemented with Forge script.
if (StringUtils.isBlank(request.edition)) { if (StringUtils.isBlank(request.edition)) {
for (CardEdition edition : editions) { for (CardEdition edition : editions) {
for (CardInSet cardInSet : edition.getAllCardsInSet()) { for (EditionEntry cardInSet : edition.getAllCardsInSet()) {
if (cardInSet.name.equals(request.cardName)) { if (cardInSet.name().equals(request.cardName)) {
cardEdition = edition; cardEdition = edition;
cardRarity = cardInSet.rarity; cardRarity = cardInSet.rarity();
break; break;
} }
} }
@@ -1171,9 +1174,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} else { } else {
cardEdition = editions.get(request.edition); cardEdition = editions.get(request.edition);
if (cardEdition != null) { if (cardEdition != null) {
for (CardInSet cardInSet : cardEdition.getAllCardsInSet()) { for (EditionEntry cardInSet : cardEdition.getAllCardsInSet()) {
if (cardInSet.name.equals(request.cardName)) { if (cardInSet.name().equals(request.cardName)) {
cardRarity = cardInSet.rarity; cardRarity = cardInSet.rarity();
break; break;
} }
} }
@@ -1224,9 +1227,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// @leriomaggio: DONE! re-using here the same strategy implemented for lazy-loading! // @leriomaggio: DONE! re-using here the same strategy implemented for lazy-loading!
for (CardEdition e : editions.getOrderedEditions()) { for (CardEdition e : editions.getOrderedEditions()) {
int artIdx = IPaperCard.DEFAULT_ART_INDEX; int artIdx = IPaperCard.DEFAULT_ART_INDEX;
for (CardInSet cis : e.getCardInSet(cardName)) for (EditionEntry cis : e.getCardInSet(cardName))
paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity, artIdx++, false, paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity(), artIdx++, false,
cis.collectorNumber, cis.artistName, cis.functionalVariantName)); cis.collectorNumber(), cis.artistName(), cis.functionalVariantName()));
} }
} else { } else {
String lastEdition = null; String lastEdition = null;
@@ -1240,17 +1243,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
if (ed == null) { if (ed == null) {
continue; continue;
} }
List<CardInSet> cardsInSet = ed.getCardInSet(cardName); List<EditionEntry> cardsInSet = ed.getCardInSet(cardName);
if (cardsInSet.isEmpty()) if (cardsInSet.isEmpty())
continue; continue;
int cardInSetIndex = Math.max(artIdx-1, 0); // make sure doesn't go below zero int cardInSetIndex = Math.max(artIdx-1, 0); // make sure doesn't go below zero
CardInSet cds = cardsInSet.get(cardInSetIndex); // use ArtIndex to get the right Coll. Number EditionEntry cds = cardsInSet.get(cardInSetIndex); // use ArtIndex to get the right Coll. Number
paperCards.add(new PaperCard(rules, lastEdition, tuple.getValue(), artIdx++, false, paperCards.add(new PaperCard(rules, lastEdition, tuple.getValue(), artIdx++, false,
cds.collectorNumber, cds.artistName, cds.functionalVariantName)); cds.collectorNumber(), cds.artistName(), cds.functionalVariantName()));
} }
} }
if (paperCards.isEmpty()) { if (paperCards.isEmpty()) {
paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN.getCode(), CardRarity.Special)); paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN_CODE, CardRarity.Special));
} }
// 2. add them to db // 2. add them to db
for (PaperCard paperCard : paperCards) { for (PaperCard paperCard : paperCards) {

View File

@@ -18,6 +18,7 @@
package forge.card; package forge.card;
import com.google.common.collect.*; import com.google.common.collect.*;
import forge.StaticData; import forge.StaticData;
import forge.card.CardDb.CardArtPreference; import forge.card.CardDb.CardArtPreference;
import forge.deck.CardPool; import forge.deck.CardPool;
@@ -165,20 +166,49 @@ public final class CardEdition implements Comparable<CardEdition> {
} }
} }
public static class CardInSet implements Comparable<CardInSet> { private static final Map<String, String> sortableCollNumberLookup = new HashMap<>();
public final CardRarity rarity; /**
public final String collectorNumber; * This method implements the main strategy to allow for natural ordering of collectorNumber
public final String name; * (i.e. "1" < "10"), overloading the default lexicographic order (i.e. "10" < "1").
public final String artistName; * Any non-numerical parts in the input collectorNumber will be also accounted for, and attached to the
public final String functionalVariantName; * resulting sorting key, accordingly.
*
* @param collectorNumber: Input collectorNumber tro transform in a Sorting Key
* @return A 5-digits zero-padded collector number + any non-numerical parts attached.
*/
public static String getSortableCollectorNumber(final String collectorNumber){
String inputCollNumber = collectorNumber;
if (collectorNumber == null || collectorNumber.isEmpty())
inputCollNumber = "50000"; // very big number of 5 digits to have them in last positions
public CardInSet(final String name, final String collectorNumber, final CardRarity rarity, final String artistName, final String functionalVariantName) { String matchedCollNr = sortableCollNumberLookup.getOrDefault(inputCollNumber, null);
this.name = name; if (matchedCollNr != null)
this.collectorNumber = collectorNumber; return matchedCollNr;
this.rarity = rarity;
this.artistName = artistName; // Now, for proper sorting, let's zero-pad the collector number (if integer)
this.functionalVariantName = functionalVariantName; int collNr;
String sortableCollNr;
try {
collNr = Integer.parseInt(inputCollNumber);
sortableCollNr = String.format("%05d", collNr);
} catch (NumberFormatException ex) {
String nonNumSub = inputCollNumber.replaceAll("[0-9]", "");
String onlyNumSub = inputCollNumber.replaceAll("[^0-9]", "");
try {
collNr = Integer.parseInt(onlyNumSub);
} catch (NumberFormatException exon) {
collNr = 0; // this is the case of ONLY-letters collector numbers
}
if ((collNr > 0) && (inputCollNumber.startsWith(onlyNumSub))) // e.g. 12a, 37+, 2018f,
sortableCollNr = String.format("%05d", collNr) + nonNumSub;
else // e.g. WS6, S1
sortableCollNr = nonNumSub + String.format("%05d", collNr);
} }
sortableCollNumberLookup.put(inputCollNumber, sortableCollNr);
return sortableCollNr;
}
public record EditionEntry(String name, String collectorNumber, CardRarity rarity, String artistName, String functionalVariantName) implements Comparable<EditionEntry> {
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@@ -186,7 +216,7 @@ public final class CardEdition implements Comparable<CardEdition> {
sb.append(collectorNumber); sb.append(collectorNumber);
sb.append(' '); sb.append(' ');
} }
if (rarity != CardRarity.Unknown) { if (rarity != CardRarity.Unknown && rarity != CardRarity.Token) {
sb.append(rarity); sb.append(rarity);
sb.append(' '); sb.append(' ');
} }
@@ -202,50 +232,8 @@ public final class CardEdition implements Comparable<CardEdition> {
return sb.toString(); return sb.toString();
} }
private static final Map<String, String> sortableCollNumberLookup = new HashMap<>();
/**
* This method implements the main strategy to allow for natural ordering of collectorNumber
* (i.e. "1" < "10"), overloading the default lexicographic order (i.e. "10" < "1").
* Any non-numerical parts in the input collectorNumber will be also accounted for, and attached to the
* resulting sorting key, accordingly.
*
* @param collectorNumber: Input collectorNumber tro transform in a Sorting Key
* @return A 5-digits zero-padded collector number + any non-numerical parts attached.
*/
public static String getSortableCollectorNumber(final String collectorNumber){
String inputCollNumber = collectorNumber;
if (collectorNumber == null || collectorNumber.isEmpty())
inputCollNumber = "50000"; // very big number of 5 digits to have them in last positions
String matchedCollNr = sortableCollNumberLookup.getOrDefault(inputCollNumber, null);
if (matchedCollNr != null)
return matchedCollNr;
// Now, for proper sorting, let's zero-pad the collector number (if integer)
int collNr;
String sortableCollNr;
try {
collNr = Integer.parseInt(inputCollNumber);
sortableCollNr = String.format("%05d", collNr);
} catch (NumberFormatException ex) {
String nonNumSub = inputCollNumber.replaceAll("[0-9]", "");
String onlyNumSub = inputCollNumber.replaceAll("[^0-9]", "");
try {
collNr = Integer.parseInt(onlyNumSub);
} catch (NumberFormatException exon) {
collNr = 0; // this is the case of ONLY-letters collector numbers
}
if ((collNr > 0) && (inputCollNumber.startsWith(onlyNumSub))) // e.g. 12a, 37+, 2018f,
sortableCollNr = String.format("%05d", collNr) + nonNumSub;
else // e.g. WS6, S1
sortableCollNr = nonNumSub + String.format("%05d", collNr);
}
sortableCollNumberLookup.put(inputCollNumber, sortableCollNr);
return sortableCollNr;
}
@Override @Override
public int compareTo(CardInSet o) { public int compareTo(EditionEntry o) {
final int nameCmp = name.compareToIgnoreCase(o.name); final int nameCmp = name.compareToIgnoreCase(o.name);
if (0 != nameCmp) { if (0 != nameCmp) {
return nameCmp; return nameCmp;
@@ -262,11 +250,17 @@ public final class CardEdition implements Comparable<CardEdition> {
private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
public static final CardEdition UNKNOWN = new CardEdition("1990-01-01", "???", "??", Type.UNKNOWN, "Undefined", FoilType.NOT_SUPPORTED, new CardInSet[]{}); /**
* Equivalent to the set code of CardEdition.UNKNOWN
*/
public static final String UNKNOWN_CODE = "???";
public static final CardEdition UNKNOWN = new CardEdition("1990-01-01", UNKNOWN_CODE, "??", Type.UNKNOWN, "Undefined", FoilType.NOT_SUPPORTED, new EditionEntry[]{});
private Date date; private Date date;
private String code; private String code;
private String code2; private String code2;
private String scryfallCode; private String scryfallCode;
private String tokensCode;
private String tokenFallbackCode;
private String cardsLanguage; private String cardsLanguage;
private Type type; private Type type;
private String name; private String name;
@@ -296,31 +290,32 @@ public final class CardEdition implements Comparable<CardEdition> {
private String doublePickDuringDraft = ""; private String doublePickDuringDraft = "";
private String[] chaosDraftThemes = new String[0]; private String[] chaosDraftThemes = new String[0];
private final ListMultimap<String, CardInSet> cardMap; private final ListMultimap<String, EditionEntry> cardMap;
private final List<CardInSet> cardsInSet; private final List<EditionEntry> cardsInSet;
private final Map<String, Integer> tokenNormalized; private final ListMultimap<String, EditionEntry> tokenMap;
// custom print sheets that will be loaded lazily // custom print sheets that will be loaded lazily
private final Map<String, List<String>> customPrintSheetsToParse; private final Map<String, List<String>> customPrintSheetsToParse;
private ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create();
private int boosterArts = 1; private int boosterArts = 1;
private SealedTemplate boosterTpl = null; private SealedTemplate boosterTpl = null;
private final Map<String, SealedTemplate> boosterTemplates = new HashMap<>(); private final Map<String, SealedTemplate> boosterTemplates = new HashMap<>();
private CardEdition(ListMultimap<String, CardInSet> cardMap, Map<String, Integer> tokens, Map<String, List<String>> customPrintSheetsToParse) { private CardEdition(ListMultimap<String, EditionEntry> cardMap, ListMultimap<String, EditionEntry> tokens, Map<String, List<String>> customPrintSheetsToParse) {
this.cardMap = cardMap; this.cardMap = cardMap;
this.cardsInSet = new ArrayList<>(cardMap.values()); this.cardsInSet = new ArrayList<>(cardMap.values());
Collections.sort(cardsInSet); Collections.sort(cardsInSet);
this.tokenNormalized = tokens; this.tokenMap = tokens;
this.customPrintSheetsToParse = customPrintSheetsToParse; this.customPrintSheetsToParse = customPrintSheetsToParse;
} }
private CardEdition(CardInSet[] cards, Map<String, Integer> tokens) { private CardEdition(EditionEntry[] cards, ListMultimap<String, EditionEntry> tokens) {
List<CardInSet> cardsList = Arrays.asList(cards); List<EditionEntry> cardsList = Arrays.asList(cards);
this.cardMap = ArrayListMultimap.create(); this.cardMap = ArrayListMultimap.create();
this.cardMap.replaceValues("cards", cardsList); this.cardMap.replaceValues("cards", cardsList);
this.cardsInSet = new ArrayList<>(cardsList); this.cardsInSet = new ArrayList<>(cardsList);
Collections.sort(cardsInSet); Collections.sort(cardsInSet);
this.tokenNormalized = tokens; this.tokenMap = tokens;
this.customPrintSheetsToParse = new HashMap<>(); this.customPrintSheetsToParse = new HashMap<>();
} }
@@ -337,8 +332,8 @@ public final class CardEdition implements Comparable<CardEdition> {
* @param name the name of the set * @param name the name of the set
* @param cards the cards in the set * @param cards the cards in the set
*/ */
private CardEdition(String date, String code, String code2, Type type, String name, FoilType foil, CardInSet[] cards) { private CardEdition(String date, String code, String code2, Type type, String name, FoilType foil, EditionEntry[] cards) {
this(cards, new HashMap<>()); this(cards, ArrayListMultimap.create());
this.code = code; this.code = code;
this.code2 = code2; this.code2 = code2;
this.type = type; this.type = type;
@@ -361,6 +356,7 @@ public final class CardEdition implements Comparable<CardEdition> {
public String getCode() { return code; } public String getCode() { return code; }
public String getCode2() { return code2; } public String getCode2() { return code2; }
public String getScryfallCode() { return scryfallCode.toLowerCase(); } public String getScryfallCode() { return scryfallCode.toLowerCase(); }
public String getTokensCode() { return tokensCode.toLowerCase(); }
public String getCardsLangCode() { return cardsLanguage.toLowerCase(); } public String getCardsLangCode() { return cardsLanguage.toLowerCase(); }
public Type getType() { return type; } public Type getType() { return type; }
public String getName() { return name; } public String getName() { return name; }
@@ -385,14 +381,14 @@ public final class CardEdition implements Comparable<CardEdition> {
public String getSheetReplaceCardFromSheet2() { return sheetReplaceCardFromSheet2; } public String getSheetReplaceCardFromSheet2() { return sheetReplaceCardFromSheet2; }
public String[] getChaosDraftThemes() { return chaosDraftThemes; } public String[] getChaosDraftThemes() { return chaosDraftThemes; }
public List<CardInSet> getCards() { return cardMap.get(EditionSectionWithCollectorNumbers.CARDS.getName()); } public List<EditionEntry> getCards() { return cardMap.get(EditionSectionWithCollectorNumbers.CARDS.getName()); }
public List<CardInSet> getRebalancedCards() { return cardMap.get(EditionSectionWithCollectorNumbers.REBALANCED.getName()); } public List<EditionEntry> getRebalancedCards() { return cardMap.get(EditionSectionWithCollectorNumbers.REBALANCED.getName()); }
public List<CardInSet> getFunnyEternalCards() { return cardMap.get(EditionSectionWithCollectorNumbers.ETERNAL.getName()); } public List<EditionEntry> getFunnyEternalCards() { return cardMap.get(EditionSectionWithCollectorNumbers.ETERNAL.getName()); }
public List<CardInSet> getAllCardsInSet() { public List<EditionEntry> getAllCardsInSet() {
return cardsInSet; return cardsInSet;
} }
private ListMultimap<String, CardInSet> cardsInSetLookupMap = null; private ListMultimap<String, EditionEntry> cardsInSetLookupMap = null;
/** /**
* Get all the CardInSet instances with the input card name. * Get all the CardInSet instances with the input card name.
@@ -400,12 +396,12 @@ public final class CardEdition implements Comparable<CardEdition> {
* @return A List of all the CardInSet instances for a given name. * @return A List of all the CardInSet instances for a given name.
* If not fount, an Empty sequence (view) will be returned instead! * If not fount, an Empty sequence (view) will be returned instead!
*/ */
public List<CardInSet> getCardInSet(String cardName){ public List<EditionEntry> getCardInSet(String cardName){
if (cardsInSetLookupMap == null) { if (cardsInSetLookupMap == null) {
// initialise // initialise
cardsInSetLookupMap = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList); cardsInSetLookupMap = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
List<CardInSet> cardsInSet = this.getAllCardsInSet(); List<EditionEntry> cardsInSet = this.getAllCardsInSet();
for (CardInSet cis : cardsInSet){ for (EditionEntry cis : cardsInSet){
String key = cis.name; String key = cis.name;
cardsInSetLookupMap.put(key, cis); cardsInSetLookupMap.put(key, cis);
} }
@@ -413,8 +409,19 @@ public final class CardEdition implements Comparable<CardEdition> {
return this.cardsInSetLookupMap.get(cardName); return this.cardsInSetLookupMap.get(cardName);
} }
public EditionEntry getCardFromCollectorNumber(String collectorNumber) {
if(collectorNumber == null || collectorNumber.isEmpty())
return null;
for(EditionEntry c : this.cardsInSet) {
//Could build a map for this one too if it's used for more than one-offs.
if (c.collectorNumber.equalsIgnoreCase(collectorNumber))
return c;
}
return null;
}
public boolean isRebalanced(String cardName) { public boolean isRebalanced(String cardName) {
for (CardInSet cis : getRebalancedCards()) { for (EditionEntry cis : getRebalancedCards()) {
if (cis.name.equals(cardName)) { if (cis.name.equals(cardName)) {
return true; return true;
} }
@@ -424,7 +431,44 @@ public final class CardEdition implements Comparable<CardEdition> {
public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others
public Map<String, Integer> getTokens() { return tokenNormalized; } public Multimap<String, EditionEntry> getTokens() { return tokenMap; }
public EditionEntry getTokenFromCollectorNumber(String collectorNumber) {
if(collectorNumber == null || collectorNumber.isEmpty())
return null;
for(EditionEntry c : this.tokenMap.values()) {
//Could build a map for this one too if it's used for more than one-offs.
if (c.collectorNumber.equalsIgnoreCase(collectorNumber))
return c;
}
return null;
}
public String getTokenSet(String token) {
if (tokenMap.containsKey(token)) {
return this.getCode();
}
if (this.tokenFallbackCode != null) {
return StaticData.instance().getCardEdition(this.tokenFallbackCode).getTokenSet(token);
}
return null;
}
public String getOtherSet(String token) {
if (otherMap.containsKey(token)) {
return this.getCode();
}
if (this.tokenFallbackCode != null) {
return StaticData.instance().getCardEdition(this.tokenFallbackCode).getOtherSet(token);
}
return null;
}
public EditionEntry findOther(String name) {
if (otherMap.containsKey(name)) {
return Aggregates.random(otherMap.get(name));
}
return null;
}
@Override @Override
public int compareTo(final CardEdition o) { public int compareTo(final CardEdition o) {
@@ -508,8 +552,8 @@ public final class CardEdition implements Comparable<CardEdition> {
for (String sectionName : cardMap.keySet()) { for (String sectionName : cardMap.keySet()) {
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName)); PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName));
List<CardInSet> cards = cardMap.get(sectionName); List<EditionEntry> cards = cardMap.get(sectionName);
for (CardInSet card : cards) { for (EditionEntry card : cards) {
int index = 1; int index = 1;
if (cardToIndex.containsKey(card.name)) { if (cardToIndex.containsKey(card.name)) {
index = cardToIndex.get(card.name) + 1; index = cardToIndex.get(card.name) + 1;
@@ -562,6 +606,7 @@ public final class CardEdition implements Comparable<CardEdition> {
it should also match the Un-set and older alternate art cards it should also match the Un-set and older alternate art cards
like Merseine from FEM. like Merseine from FEM.
*/ */
// Collector numbers now should allow hyphens for Planeswalker Championship Promos
//"(^(?<cnum>[0-9]+.?) )?((?<rarity>[SCURML]) )?(?<name>.*)$" //"(^(?<cnum>[0-9]+.?) )?((?<rarity>[SCURML]) )?(?<name>.*)$"
/* Ideally we'd use the named group above, but Android 6 and /* Ideally we'd use the named group above, but Android 6 and
earlier don't appear to support named groups. earlier don't appear to support named groups.
@@ -575,12 +620,20 @@ public final class CardEdition implements Comparable<CardEdition> {
* functional variant name - grouping #9 * functional variant name - grouping #9
*/ */
// "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$" // "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$"
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$" "(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
); );
ListMultimap<String, CardInSet> cardMap = ArrayListMultimap.create(); final Pattern tokenPattern = Pattern.compile(
/*
* cnum - grouping #2
* name - grouping #3
* artist name - grouping #5
*/
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
);
ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create();
List<BoosterSlot> boosterSlots = null; List<BoosterSlot> boosterSlots = null;
Map<String, Integer> tokenNormalized = new HashMap<>();
Map<String, List<String>> customPrintSheetsToParse = new HashMap<>(); Map<String, List<String>> customPrintSheetsToParse = new HashMap<>();
List<String> editionSectionsWithCollectorNumbers = EditionSectionWithCollectorNumbers.getNames(); List<String> editionSectionsWithCollectorNumbers = EditionSectionWithCollectorNumbers.getNames();
@@ -611,7 +664,7 @@ public final class CardEdition implements Comparable<CardEdition> {
String cardName = matcher.group(5); String cardName = matcher.group(5);
String artistName = matcher.group(7); String artistName = matcher.group(7);
String functionalVariantName = matcher.group(9); String functionalVariantName = matcher.group(9);
CardInSet cis = new CardInSet(cardName, collectorNumber, r, artistName, functionalVariantName); EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
cardMap.put(sectionName, cis); cardMap.put(sectionName, cis);
} }
@@ -625,41 +678,59 @@ public final class CardEdition implements Comparable<CardEdition> {
} }
} }
ListMultimap<String, EditionEntry> tokenMap = ArrayListMultimap.create();
ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create();
// parse tokens section // parse tokens section
if (contents.containsKey("tokens")) { if (contents.containsKey("tokens")) {
for (String line : contents.get("tokens")) { for (String line : contents.get("tokens")) {
if (StringUtils.isBlank(line)) if (StringUtils.isBlank(line))
continue; continue;
Matcher matcher = tokenPattern.matcher(line);
if (!tokenNormalized.containsKey(line)) { if (!matcher.matches()) {
tokenNormalized.put(line, 1); continue;
} else {
tokenNormalized.put(line, tokenNormalized.get(line) + 1);
} }
String collectorNumber = matcher.group(2);
String cardName = matcher.group(3);
String artistName = matcher.group(5);
// rarity isn't used for this anyway
EditionEntry tis = new EditionEntry(cardName, collectorNumber, CardRarity.Token, artistName, null);
tokenMap.put(cardName, tis);
}
}
if (contents.containsKey("other")) {
for (String line : contents.get("other")) {
if (StringUtils.isBlank(line))
continue;
Matcher matcher = tokenPattern.matcher(line);
if (!matcher.matches()) {
continue;
}
String collectorNumber = matcher.group(2);
String cardName = matcher.group(3);
String artistName = matcher.group(5);
EditionEntry tis = new EditionEntry(cardName, collectorNumber, CardRarity.Unknown, artistName, null);
otherMap.put(cardName, tis);
} }
} }
CardEdition res = new CardEdition(cardMap, tokenNormalized, customPrintSheetsToParse); CardEdition res = new CardEdition(cardMap, tokenMap, customPrintSheetsToParse);
res.boosterSlots = boosterSlots; res.boosterSlots = boosterSlots;
// parse metadata section // parse metadata section
res.name = metadata.get("name"); res.name = metadata.get("name");
res.date = parseDate(metadata.get("date")); res.date = parseDate(metadata.get("date"));
res.code = metadata.get("code"); res.code = metadata.get("code");
res.code2 = metadata.get("code2"); res.code2 = metadata.get("code2", res.code);
if (res.code2 == null) { res.scryfallCode = metadata.get("ScryfallCode", res.code);
res.code2 = res.code; res.tokensCode = metadata.get("TokensCode", "T" + res.scryfallCode);
} res.tokenFallbackCode = metadata.get("TokenFallbackCode");
res.scryfallCode = metadata.get("ScryfallCode"); res.cardsLanguage = metadata.get("CardLang", "en");
if (res.scryfallCode == null) {
res.scryfallCode = res.code;
}
res.cardsLanguage = metadata.get("CardLang");
if (res.cardsLanguage == null) {
res.cardsLanguage = "en";
}
res.boosterArts = metadata.getInt("BoosterCovers", 1); res.boosterArts = metadata.getInt("BoosterCovers", 1);
res.otherMap = otherMap;
String boosterDesc = metadata.get("Booster"); String boosterDesc = metadata.get("Booster");
if (metadata.contains("Booster")) { if (metadata.contains("Booster")) {
@@ -778,7 +849,7 @@ public final class CardEdition implements Comparable<CardEdition> {
initAliases(E); //Made a method in case the system changes, so it's consistent. initAliases(E); //Made a method in case the system changes, so it's consistent.
} }
CardEdition customBucket = new CardEdition("2990-01-01", "USER", "USER", CardEdition customBucket = new CardEdition("2990-01-01", "USER", "USER",
Type.CUSTOM_SET, "USER", FoilType.NOT_SUPPORTED, new CardInSet[]{}); Type.CUSTOM_SET, "USER", FoilType.NOT_SUPPORTED, new EditionEntry[]{});
this.add(customBucket); this.add(customBucket);
initAliases(customBucket); initAliases(customBucket);
this.lock = true; //Consider it initialized and prevent from writing any more data. this.lock = true; //Consider it initialized and prevent from writing any more data.
@@ -810,7 +881,7 @@ public final class CardEdition implements Comparable<CardEdition> {
public CardEdition getEditionByCodeOrThrow(final String code) { public CardEdition getEditionByCodeOrThrow(final String code) {
final CardEdition set = this.get(code); final CardEdition set = this.get(code);
if (null == set && code.equals("???")) //Hardcoded set ??? is not with the others, needs special check. if (null == set && code.equals(UNKNOWN_CODE)) //Hardcoded set ??? is not with the others, needs special check.
return UNKNOWN; return UNKNOWN;
if (null == set) { if (null == set) {
throw new RuntimeException("Edition with code '" + code + "' not found"); throw new RuntimeException("Edition with code '" + code + "' not found");
@@ -941,4 +1012,12 @@ public final class CardEdition implements Comparable<CardEdition> {
} }
return 0; return 0;
} }
public boolean hasBasicLands() {
for(String landName : MagicColor.Constant.BASIC_LANDS) {
if (null == StaticData.instance().getCommonCards().getCard(landName, this.getCode(), 0))
return false;
}
return true;
}
} }

View File

@@ -20,6 +20,8 @@ package forge.card;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.StaticData;
import forge.card.mana.IParserManaCost; import forge.card.mana.IParserManaCost;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostShard; import forge.card.mana.ManaCostShard;
@@ -149,6 +151,10 @@ public final class CardRules implements ICardCharacteristics {
return splitType; return splitType;
} }
public boolean hasBackSide() {
return CardSplitType.DUAL_FACED_CARDS.contains(splitType) || splitType == CardSplitType.Flip;
}
public ICardFace getMainPart() { public ICardFace getMainPart() {
return mainPart; return mainPart;
} }
@@ -165,20 +171,32 @@ public final class CardRules implements ICardCharacteristics {
return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values()); return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values());
} }
public ICardFace getWSpecialize() { public String getImageName(CardStateName state) {
return specializedParts.get(CardStateName.SpecializeW); if (splitType == CardSplitType.Split) {
} return mainPart.getName() + otherPart.getName();
public ICardFace getUSpecialize() { } else if (state.equals(splitType.getChangedStateName())) {
return specializedParts.get(CardStateName.SpecializeU); if (otherPart != null) {
} return otherPart.getName();
public ICardFace getBSpecialize() { } else if (this.hasBackSide()) {
return specializedParts.get(CardStateName.SpecializeB); if (!getMeldWith().isEmpty()) {
} final CardDb db = StaticData.instance().getCommonCards();
public ICardFace getRSpecialize() { return db.getRules(getMeldWith()).getOtherPart().getName();
return specializedParts.get(CardStateName.SpecializeR); }
} return null;
public ICardFace getGSpecialize() { }
return specializedParts.get(CardStateName.SpecializeG); }
switch (state) {
case SpecializeW:
case SpecializeU:
case SpecializeB:
case SpecializeR:
case SpecializeG:
ICardFace face = specializedParts.get(state);
return face != null ? face.getName() : null;
default:
return getName();
}
} }
public String getName() { public String getName() {
@@ -396,6 +414,7 @@ public final class CardRules implements ICardCharacteristics {
} }
public int getSetColorID() { public int getSetColorID() {
//Could someday generalize this to support other kinds of markings.
return setColorID; return setColorID;
} }

View File

@@ -11,7 +11,8 @@ public enum CardSplitType
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld), Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit), Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped), Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped),
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Adventure), Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
Omen(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal), Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal),
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null); Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);

View File

@@ -5,12 +5,11 @@ public enum CardStateName {
Original, Original,
FaceDown, FaceDown,
Flipped, Flipped,
Converted,
Transformed, Transformed,
Meld, Meld,
LeftSplit, LeftSplit,
RightSplit, RightSplit,
Adventure, Secondary,
Modal, Modal,
EmptyRoom, EmptyRoom,
SpecializeW, SpecializeW,

View File

@@ -26,6 +26,7 @@ import org.apache.commons.lang3.StringUtils;
import java.util.*; import java.util.*;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors;
/** /**
* <p> * <p>
@@ -315,6 +316,12 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return landTypes; return landTypes;
} }
public Set<String> getBattleTypes() {
if(!isBattle())
return Set.of();
return subtypes.stream().filter(CardType::isABattleType).collect(Collectors.toSet());
}
@Override @Override
public boolean hasStringType(String t) { public boolean hasStringType(String t) {
if (t.isEmpty()) { if (t.isEmpty()) {

View File

@@ -16,6 +16,7 @@ public interface CardTypeView extends Iterable<String>, Serializable {
Set<String> getCreatureTypes(); Set<String> getCreatureTypes();
Set<String> getLandTypes(); Set<String> getLandTypes();
Set<String> getBattleTypes();
boolean hasStringType(String t); boolean hasStringType(String t);
boolean hasType(CoreType type); boolean hasType(CoreType type);

View File

@@ -25,6 +25,8 @@ import forge.util.BinaryUtil;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* <p>CardColor class.</p> * <p>CardColor class.</p>
@@ -291,14 +293,8 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
*/ */
@Override @Override
public String toString() { public String toString() {
if (this.orderWeight == -1) { final ManaCostShard[] orderedShards = getOrderedShards();
return "n/a"; return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
}
final String toReturn = MagicColor.toLongString(myColor);
if (toReturn.equals(MagicColor.Constant.COLORLESS) && myColor != 0) {
return "multi";
}
return toReturn;
} }
/** /**
@@ -376,6 +372,10 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
} }
} }
public Stream<MagicColor.Color> stream() {
return this.toEnumSet().stream();
}
//Get array of mana cost shards for color set in the proper order //Get array of mana cost shards for color set in the proper order
public ManaCostShard[] getOrderedShards() { public ManaCostShard[] getOrderedShards() {
return shardOrderLookup[myColor]; return shardOrderLookup[myColor];

View File

@@ -5,43 +5,42 @@ import forge.item.PaperCard;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.Set;
/**
* Magic Cards Database.
* --------------------
* This interface defines the general API for Database Access and Cards' Lookup.
* <p>
* Methods for single Card's lookup currently support three alternative strategies:
* 1. [getCard]: Card search based on a single card's attributes
* (i.e. name, edition, art, collectorNumber)
* <p>
* 2. [getCardFromSet]: Card Lookup from a single Expansion set.
* Particularly useful in Deck Editors when a specific Set is specified.
* <p>
* 3. [getCardFromEditions]: Card search considering a predefined `SetPreference` policy and/or a specified Date
* when no expansion is specified for a card.
* This method is particularly useful for Re-prints whenever no specific
* Expansion is specified (e.g. in Deck Import) and a decision should be made
* on which card to pick. This methods allows to adopt a SetPreference selection
* policy to make this decision.
* <p>
* The API also includes methods to fetch Collection of Card (i.e. PaperCard instances):
* - all cards (no filter)
* - all unique cards (by name)
* - all prints of a single card
* - all cards from a single Expansion Set
* - all cards compliant with a filter condition (i.e. Predicate)
* <p>
* Finally, various utility methods are supported:
* - Get the foil version of a Card (if Any)
* - Get the Order Number of a Card in an Expansion Set
* - Get the number of Print/Arts for a card in a Set (useful for those exp. having multiple arts)
* */
public interface ICardDatabase extends Iterable<PaperCard> { public interface ICardDatabase extends Iterable<PaperCard> {
/**
* Magic Cards Database.
* --------------------
* This interface defines the general API for Database Access and Cards' Lookup.
*
* Methods for single Card's lookup currently support three alternative strategies:
* 1. [getCard]: Card search based on a single card's attributes
* (i.e. name, edition, art, collectorNumber)
*
* 2. [getCardFromSet]: Card Lookup from a single Expansion set.
* Particularly useful in Deck Editors when a specific Set is specified.
*
* 3. [getCardFromEditions]: Card search considering a predefined `SetPreference` policy and/or a specified Date
* when no expansion is specified for a card.
* This method is particularly useful for Re-prints whenever no specific
* Expansion is specified (e.g. in Deck Import) and a decision should be made
* on which card to pick. This methods allows to adopt a SetPreference selection
* policy to make this decision.
*
* The API also includes methods to fetch Collection of Card (i.e. PaperCard instances):
* - all cards (no filter)
* - all unique cards (by name)
* - all prints of a single card
* - all cards from a single Expansion Set
* - all cards compliant with a filter condition (i.e. Predicate)
*
* Finally, various utility methods are supported:
* - Get the foil version of a Card (if Any)
* - Get the Order Number of a Card in an Expansion Set
* - Get the number of Print/Arts for a card in a Set (useful for those exp. having multiple arts)
* */
/* SINGLE CARD RETRIEVAL METHODS /* SINGLE CARD RETRIEVAL METHODS
* ============================= */ * ============================= */
// 1. Card Lookup by attributes // 1. Card Lookup by attributes
@@ -50,22 +49,20 @@ public interface ICardDatabase extends Iterable<PaperCard> {
PaperCard getCard(String cardName, String edition, int artIndex); PaperCard getCard(String cardName, String edition, int artIndex);
// [NEW Methods] Including the card CollectorNumber as criterion for DB lookup // [NEW Methods] Including the card CollectorNumber as criterion for DB lookup
PaperCard getCard(String cardName, String edition, String collectorNumber); PaperCard getCard(String cardName, String edition, String collectorNumber);
PaperCard getCard(String cardName, String edition, int artIndex, String collectorNumber); PaperCard getCard(String cardName, String edition, int artIndex, Map<String, String> flags);
PaperCard getCard(String cardName, String edition, int artIndex, Set<String> colorID); PaperCard getCard(String cardName, String edition, String collectorNumber, Map<String, String> flags);
// 2. Card Lookup from a single Expansion Set // 2. Card Lookup from a single Expansion Set
PaperCard getCardFromSet(String cardName, CardEdition edition, boolean isFoil); // NOT yet used, included for API symmetry PaperCard getCardFromSet(String cardName, CardEdition edition, boolean isFoil); // NOT yet used, included for API symmetry
PaperCard getCardFromSet(String cardName, CardEdition edition, String collectorNumber, boolean isFoil); PaperCard getCardFromSet(String cardName, CardEdition edition, String collectorNumber, boolean isFoil);
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, boolean isFoil); PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, boolean isFoil);
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil); PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil);
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil, Set<String> colorID);
// 3. Card lookup based on CardArtPreference Selection Policy // 3. Card lookup based on CardArtPreference Selection Policy
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference); PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference);
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, Predicate<PaperCard> filter); PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, Predicate<PaperCard> filter);
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex); PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex);
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Predicate<PaperCard> filter); PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Predicate<PaperCard> filter);
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Set<String> colorID);
// 4. Specialised Card Lookup on CardArtPreference Selection and Release Date // 4. Specialised Card Lookup on CardArtPreference Selection and Release Date
PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, Date releaseDate); PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, Date releaseDate);

View File

@@ -1,6 +1,7 @@
package forge.card; package forge.card;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import forge.deck.DeckRecognizer;
/** /**
* Holds byte values for each color magic has. * Holds byte values for each color magic has.
@@ -187,6 +188,12 @@ public final class MagicColor {
public String getName() { public String getName() {
return name; return name;
} }
public String getLocalizedName() {
//Should probably move some of this logic back here, or at least to a more general location.
return DeckRecognizer.getLocalisedMagicColorName(getName());
}
public byte getColormask() { public byte getColormask() {
return colormask; return colormask;
} }

View File

@@ -94,6 +94,7 @@ public enum ManaCostShard {
/** The cmpc. */ /** The cmpc. */
private final float cmpc; private final float cmpc;
private final String stringValue; private final String stringValue;
private final String shortStringValue;
/** The image key. */ /** The image key. */
private final String imageKey; private final String imageKey;
@@ -125,6 +126,7 @@ public enum ManaCostShard {
this.cmc = this.getCMC(); this.cmc = this.getCMC();
this.cmpc = this.getCmpCost(); this.cmpc = this.getCmpCost();
this.stringValue = "{" + sValue + "}"; this.stringValue = "{" + sValue + "}";
this.shortStringValue = sValue;
this.imageKey = imgKey; this.imageKey = imgKey;
} }
@@ -232,16 +234,21 @@ public enum ManaCostShard {
return ManaCostShard.valueOf(atoms); return ManaCostShard.valueOf(atoms);
} }
/* /**
* (non-Javadoc) * @return the string representation of this shard - e.g. "{W}" "{2/U}" "{G/P}"
*
* @see java.lang.Object#toString()
*/ */
@Override @Override
public final String toString() { public final String toString() {
return this.stringValue; return this.stringValue;
} }
/**
* @return The string representation of this shard without brackets - e.g. "W" "2/U" "G/P"
*/
public final String toShortString() {
return this.shortStringValue;
}
/** /**
* Gets the cmc. * Gets the cmc.
* *

View File

@@ -52,7 +52,10 @@ public class CardPool extends ItemPool<PaperCard> {
public void add(final String cardRequest, final int amount) { public void add(final String cardRequest, final int amount) {
CardDb.CardRequest request = CardDb.CardRequest.fromString(cardRequest); CardDb.CardRequest request = CardDb.CardRequest.fromString(cardRequest);
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.artIndex, amount, false, request.colorID); if(request.collectorNumber != null && !request.collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER))
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.collectorNumber, amount, false, request.flags);
else
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.artIndex, amount, false, request.flags);
} }
public void add(final String cardName, final String setCode) { public void add(final String cardName, final String setCode) {
@@ -71,7 +74,20 @@ public class CardPool extends ItemPool<PaperCard> {
public void add(String cardName, String setCode, int artIndex, final int amount) { public void add(String cardName, String setCode, int artIndex, final int amount) {
this.add(cardName, setCode, artIndex, amount, false, null); this.add(cardName, setCode, artIndex, amount, false, null);
} }
public void add(String cardName, String setCode, int artIndex, final int amount, boolean addAny, Set<String> colorID) { private void add(String cardName, String setCode, String collectorNumber, final int amount, boolean addAny, Map<String, String> flags) {
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
CardDb db = entry.getValue();
PaperCard paperCard = db.getCard(cardName, setCode, collectorNumber, flags);
if (paperCard != null) {
this.add(paperCard, amount);
return;
}
}
//Failed to find it. Fall back accordingly?
this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny, flags);
}
private void add(String cardName, String setCode, int artIndex, final int amount, boolean addAny, Map<String, String> flags) {
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases(); Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
PaperCard paperCard = null; PaperCard paperCard = null;
String selectedDbName = ""; String selectedDbName = "";
@@ -81,7 +97,7 @@ public class CardPool extends ItemPool<PaperCard> {
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){ for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
String dbName = entry.getKey(); String dbName = entry.getKey();
CardDb db = entry.getValue(); CardDb db = entry.getValue();
paperCard = db.getCard(cardName, setCode, artIndex, colorID); paperCard = db.getCard(cardName, setCode, artIndex, flags);
if (paperCard != null) { if (paperCard != null) {
selectedDbName = dbName; selectedDbName = dbName;
break; break;
@@ -123,7 +139,7 @@ public class CardPool extends ItemPool<PaperCard> {
int cnt = artGroups[i - 1]; int cnt = artGroups[i - 1];
if (cnt <= 0) if (cnt <= 0)
continue; continue;
PaperCard randomCard = cardDb.getCard(cardName, setCode, i, colorID); PaperCard randomCard = cardDb.getCard(cardName, setCode, i, flags);
this.add(randomCard, cnt); this.add(randomCard, cnt);
} }
} }
@@ -430,7 +446,6 @@ public class CardPool extends ItemPool<PaperCard> {
public String toCardList(String separator) { public String toCardList(String separator) {
List<Entry<PaperCard, Integer>> main2sort = Lists.newArrayList(this); List<Entry<PaperCard, Integer>> main2sort = Lists.newArrayList(this);
main2sort.sort(ItemPoolSorter.BY_NAME_THEN_SET); main2sort.sort(ItemPoolSorter.BY_NAME_THEN_SET);
final CardDb commonDb = StaticData.instance().getCommonCards();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
boolean isFirst = true; boolean isFirst = true;
@@ -441,10 +456,8 @@ public class CardPool extends ItemPool<PaperCard> {
else else
isFirst = false; isFirst = false;
CardDb db = !e.getKey().getRules().isVariant() ? commonDb : StaticData.instance().getVariantCards();
sb.append(e.getValue()).append(" "); sb.append(e.getValue()).append(" ");
db.appendCardToStringBuilder(e.getKey(), sb); sb.append(CardDb.CardRequest.compose(e.getKey()));
} }
return sb.toString(); return sb.toString();
} }
@@ -463,20 +476,4 @@ public class CardPool extends ItemPool<PaperCard> {
} }
return filteredPool; return filteredPool;
} }
/**
* Applies a predicate to this CardPool's cards.
* @param predicate the Predicate to apply to this CardPool
* @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate
*/
public CardPool getFilteredPoolWithCardsCount(Predicate<PaperCard> predicate) {
CardPool filteredPool = new CardPool();
for (Entry<PaperCard, Integer> entry : this.items.entrySet()) {
PaperCard pc = entry.getKey();
int count = entry.getValue();
if (predicate.test(pc))
filteredPool.add(pc, count);
}
return filteredPool;
}
} }

View File

@@ -247,7 +247,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
Map<String, List<String>> referenceDeckLoadingMap; Map<String, List<String>> referenceDeckLoadingMap;
if (deferredSections != null) { if (deferredSections != null) {
this.validateDeferredSections(); this.normalizeDeferredSections();
referenceDeckLoadingMap = new HashMap<>(this.deferredSections); referenceDeckLoadingMap = new HashMap<>(this.deferredSections);
} else } else
referenceDeckLoadingMap = new HashMap<>(loadedSections); referenceDeckLoadingMap = new HashMap<>(loadedSections);
@@ -267,7 +267,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
continue; continue;
final List<String> cardsInSection = s.getValue(); final List<String> cardsInSection = s.getValue();
ArrayList<String> cardNamesWithNoEdition = getAllCardNamesWithNoSpecifiedEdition(cardsInSection); ArrayList<String> cardNamesWithNoEdition = getAllCardNamesWithNoSpecifiedEdition(cardsInSection);
if (cardNamesWithNoEdition.size() > 0) { if (!cardNamesWithNoEdition.isEmpty()) {
includeCardsFromUnspecifiedSet = true; includeCardsFromUnspecifiedSet = true;
if (smartCardArtSelection) if (smartCardArtSelection)
cardsWithNoEdition.put(sec, cardNamesWithNoEdition); cardsWithNoEdition.put(sec, cardNamesWithNoEdition);
@@ -281,10 +281,10 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
optimiseCardArtSelectionInDeckSections(cardsWithNoEdition); optimiseCardArtSelectionInDeckSections(cardsWithNoEdition);
} }
private void validateDeferredSections() { private void normalizeDeferredSections() {
/* /*
Construct a temporary (DeckSection, CardPool) Maps, to be sanitised and finalised Construct a temporary (DeckSection, CardPool) Maps, to be sanitised and finalised
before copying into `this.parts`. This sanitisation is applied because of the before copying into `this.parts`. This sanitization is applied because of the
validation schema introduced in DeckSections. validation schema introduced in DeckSections.
*/ */
Map<String, List<String>> validatedSections = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); Map<String, List<String>> validatedSections = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
@@ -296,61 +296,33 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
} }
final List<String> cardsInSection = s.getValue(); final List<String> cardsInSection = s.getValue();
List<Pair<String, Integer>> originalCardRequests = CardPool.processCardList(cardsInSection);
CardPool pool = CardPool.fromCardList(cardsInSection); CardPool pool = CardPool.fromCardList(cardsInSection);
if (pool.countDistinct() == 0) if (pool.countDistinct() == 0)
continue; // pool empty, no card has been found! continue; // pool empty, no card has been found!
// Filter pool by applying DeckSection Validation schema for Card Types (to avoid inconsistencies) List<String> validatedSection = validatedSections.computeIfAbsent(s.getKey(), (k) -> new ArrayList<>());
CardPool filteredPool = pool.getFilteredPoolWithCardsCount(deckSection::validate); for (Entry<PaperCard, Integer> entry : pool) {
// Add all the cards from ValidPool anyway! PaperCard card = entry.getKey();
List<String> whiteList = validatedSections.getOrDefault(s.getKey(), null); String normalizedRequest = getPoolRequest(entry);
if (whiteList == null) if(deckSection.validate(card))
whiteList = new ArrayList<>(); validatedSection.add(normalizedRequest);
for (Entry<PaperCard, Integer> entry : filteredPool) { else {
String poolRequest = getPoolRequest(entry, originalCardRequests); // Card was in the wrong section. Move it to the right section.
whiteList.add(poolRequest); DeckSection cardSection = DeckSection.matchingSection(card);
assert(cardSection.validate(card)); //Card doesn't fit in the matchingSection?
List<String> sectionCardList = validatedSections.computeIfAbsent(cardSection.name(), (k) -> new ArrayList<>());
sectionCardList.add(normalizedRequest);
}
} }
validatedSections.put(s.getKey(), whiteList);
if (filteredPool.countDistinct() != pool.countDistinct()) {
CardPool blackList = pool.getFilteredPoolWithCardsCount(input -> !(deckSection.validate(input)));
for (Entry<PaperCard, Integer> entry : blackList) {
DeckSection cardSection = DeckSection.matchingSection(entry.getKey());
String poolRequest = getPoolRequest(entry, originalCardRequests);
List<String> sectionCardList = validatedSections.getOrDefault(cardSection.name(), null);
if (sectionCardList == null)
sectionCardList = new ArrayList<>();
sectionCardList.add(poolRequest);
validatedSections.put(cardSection.name(), sectionCardList);
} // end for blacklist
} // end if
} // end main for on deferredSections } // end main for on deferredSections
// Overwrite deferredSections // Overwrite deferredSections
this.deferredSections = validatedSections; this.deferredSections = validatedSections;
} }
private String getPoolRequest(Entry<PaperCard, Integer> entry, List<Pair<String, Integer>> originalCardRequests) { private String getPoolRequest(Entry<PaperCard, Integer> entry) {
PaperCard card = entry.getKey();
int amount = entry.getValue(); int amount = entry.getValue();
String poolCardRequest = CardDb.CardRequest.compose( String poolCardRequest = CardDb.CardRequest.compose(entry.getKey());
card.isFoil() ? CardDb.CardRequest.compose(card.getName(), true) : card.getName(),
card.getEdition(), card.getArtIndex(), card.getColorID());
String originalRequestCandidate = null;
for (Pair<String, Integer> originalRequest : originalCardRequests){
String cardRequest = originalRequest.getLeft();
if (!StringUtils.startsWithIgnoreCase(poolCardRequest, cardRequest))
continue;
originalRequestCandidate = cardRequest;
int cardAmount = originalRequest.getRight();
if (amount == cardAmount)
return String.format("%d %s", cardAmount, cardRequest);
}
// This is just in case, it should never happen as we're
if (originalRequestCandidate != null)
return String.format("%d %s", amount, originalRequestCandidate);
return String.format("%d %s", amount, poolCardRequest); return String.format("%d %s", amount, poolCardRequest);
} }

View File

@@ -987,7 +987,7 @@ public class DeckRecognizer {
private static String getMagicColourLabel(MagicColor.Color magicColor) { private static String getMagicColourLabel(MagicColor.Color magicColor) {
if (magicColor == null) // Multicolour if (magicColor == null) // Multicolour
return String.format("%s {W}{U}{B}{R}{G}", getLocalisedMagicColorName("Multicolour")); return String.format("%s {W}{U}{B}{R}{G}", getLocalisedMagicColorName("Multicolour"));
return String.format("%s %s", getLocalisedMagicColorName(magicColor.getName()), magicColor.getSymbol()); return String.format("%s %s", magicColor.getLocalizedName(), magicColor.getSymbol());
} }
private static final HashMap<Integer, String> manaSymbolsMap = new HashMap<Integer, String>() {{ private static final HashMap<Integer, String> manaSymbolsMap = new HashMap<Integer, String>() {{
@@ -1006,8 +1006,8 @@ public class DeckRecognizer {
if (magicColor2 == null || magicColor2 == MagicColor.Color.COLORLESS if (magicColor2 == null || magicColor2 == MagicColor.Color.COLORLESS
|| magicColor1 == MagicColor.Color.COLORLESS) || magicColor1 == MagicColor.Color.COLORLESS)
return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2)); return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2));
String localisedName1 = getLocalisedMagicColorName(magicColor1.getName()); String localisedName1 = magicColor1.getLocalizedName();
String localisedName2 = getLocalisedMagicColorName(magicColor2.getName()); String localisedName2 = magicColor2.getLocalizedName();
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask()); String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol); return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
} }

View File

@@ -88,7 +88,7 @@ public enum DeckSection {
CardType t = card.getRules().getType(); CardType t = card.getRules().getType();
// NOTE: Same rules applies to both Deck and Side, despite "Conspiracy cards" are allowed // NOTE: Same rules applies to both Deck and Side, despite "Conspiracy cards" are allowed
// in the SideBoard (see Rule 313.2) // in the SideBoard (see Rule 313.2)
// Those will be matched later, in case (see `Deck::validateDeferredSections`) // Those will be matched later, in case (see `Deck::normalizeDeferredSections`)
return !t.isConspiracy() && !t.isDungeon() && !t.isPhenomenon() && !t.isPlane() && !t.isScheme() && !t.isVanguard(); return !t.isConspiracy() && !t.isDungeon() && !t.isPhenomenon() && !t.isPlane() && !t.isScheme() && !t.isVanguard();
}; };

View File

@@ -61,6 +61,8 @@ public class DeckSerializer {
} }
for(Entry<DeckSection, CardPool> s : d) { for(Entry<DeckSection, CardPool> s : d) {
if(s.getValue().isEmpty())
continue;
out.add(TextUtil.enclosedBracket(s.getKey().toString())); out.add(TextUtil.enclosedBracket(s.getKey().toString()));
out.add(s.getValue().toCardList(System.lineSeparator())); out.add(s.getValue().toCardList(System.lineSeparator()));
} }

View File

@@ -2,10 +2,10 @@ package forge.item;
import forge.card.CardRarity; import forge.card.CardRarity;
import forge.card.CardRules; import forge.card.CardRules;
import forge.card.ColorSet;
import forge.card.ICardFace; import forge.card.ICardFace;
import java.io.Serializable; import java.io.Serializable;
import java.util.Set;
public interface IPaperCard extends InventoryItem, Serializable { public interface IPaperCard extends InventoryItem, Serializable {
@@ -20,7 +20,7 @@ public interface IPaperCard extends InventoryItem, Serializable {
String getEdition(); String getEdition();
String getCollectorNumber(); String getCollectorNumber();
String getFunctionalVariant(); String getFunctionalVariant();
Set<String> getColorID(); ColorSet getMarkedColors();
int getArtIndex(); int getArtIndex();
boolean isFoil(); boolean isFoil();
boolean isToken(); boolean isToken();

View File

@@ -24,12 +24,10 @@ import forge.util.CardTranslation;
import forge.util.ImageUtil; import forge.util.ImageUtil;
import forge.util.Localizer; import forge.util.Localizer;
import forge.util.TextUtil; import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException; import java.io.*;
import java.io.ObjectInputStream; import java.util.*;
import java.util.Optional; import java.util.stream.Collectors;
import java.util.Set;
/** /**
* A lightweight version of a card that matches real-world cards, to use outside of games (eg. inventory, decks, trade). * A lightweight version of a card that matches real-world cards, to use outside of games (eg. inventory, decks, trade).
@@ -39,6 +37,7 @@ import java.util.Set;
* @author Forge * @author Forge
*/ */
public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet, IPaperCard { public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet, IPaperCard {
@Serial
private static final long serialVersionUID = 2942081982620691205L; private static final long serialVersionUID = 2942081982620691205L;
// Reference to rules // Reference to rules
@@ -55,16 +54,15 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
private String artist; private String artist;
private final int artIndex; private final int artIndex;
private final boolean foil; private final boolean foil;
private Boolean hasImage; private final PaperCardFlags flags;
private final boolean noSell; private final String sortableName;
private Set<String> colorID;
private String sortableName;
private final String functionalVariant; private final String functionalVariant;
// Calculated fields are below: // Calculated fields are below:
private transient CardRarity rarity; // rarity is given in ctor when set is assigned private transient CardRarity rarity; // rarity is given in ctor when set is assigned
// Reference to a new instance of Self, but foiled! // Reference to a new instance of Self, but foiled!
private transient PaperCard foiledVersion, noSellVersion; private transient PaperCard foiledVersion, noSellVersion, flaglessVersion;
private transient Boolean hasImage;
@Override @Override
public String getName() { public String getName() {
@@ -89,8 +87,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
} }
@Override @Override
public Set<String> getColorID() { public ColorSet getMarkedColors() {
return colorID; return this.flags.markedColors;
} }
@Override @Override
@@ -147,32 +145,32 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return unFoiledVersion; return unFoiledVersion;
} }
public PaperCard getNoSellVersion() { public PaperCard getNoSellVersion() {
if (this.noSell) if (this.flags.noSellValue)
return this; return this;
if (this.noSellVersion == null) { if (this.noSellVersion == null)
this.noSellVersion = new PaperCard(this.rules, this.edition, this.rarity, this.noSellVersion = new PaperCard(this, this.flags.withNoSellValueFlag(true));
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, true);
}
return this.noSellVersion; return this.noSellVersion;
} }
public PaperCard getSellable() {
if (!this.noSell)
return this;
PaperCard sellable = new PaperCard(this.rules, this.edition, this.rarity, public PaperCard copyWithoutFlags() {
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, false); if(this.flaglessVersion == null) {
return sellable; if(this.flags == PaperCardFlags.IDENTITY_FLAGS)
this.flaglessVersion = this;
else
this.flaglessVersion = new PaperCard(this, null);
}
return flaglessVersion;
} }
public PaperCard getColorIDVersion(Set<String> colors) { public PaperCard copyWithFlags(Map<String, String> flags) {
if (colors == null && this.colorID == null) if(flags == null || flags.isEmpty())
return this.copyWithoutFlags();
return new PaperCard(this, new PaperCardFlags(flags));
}
public PaperCard copyWithMarkedColors(ColorSet colors) {
if(Objects.equals(colors, this.flags.markedColors))
return this; return this;
if (this.colorID != null && this.colorID.equals(colors)) return new PaperCard(this, this.flags.withMarkedColors(colors));
return this;
if (colors != null && colors.equals(this.colorID))
return this;
return new PaperCard(this.rules, this.edition, this.rarity,
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, this.noSell, colors);
} }
@Override @Override
public String getItemType() { public String getItemType() {
@@ -180,8 +178,12 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return localizer.getMessage("lblCard"); return localizer.getMessage("lblCard");
} }
public boolean isNoSell() { public PaperCardFlags getMarkedFlags() {
return noSell; return this.flags;
}
public boolean hasNoSellValue() {
return this.flags.noSellValue;
} }
public boolean hasImage() { public boolean hasImage() {
return hasImage(false); return hasImage(false);
@@ -198,38 +200,41 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME, IPaperCard.NO_FUNCTIONAL_VARIANT); IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME, IPaperCard.NO_FUNCTIONAL_VARIANT);
} }
public PaperCard(final PaperCard copyFrom, final PaperCardFlags flags) {
this(copyFrom.rules, copyFrom.edition, copyFrom.rarity, copyFrom.artIndex, copyFrom.foil, copyFrom.collectorNumber,
copyFrom.artist, copyFrom.functionalVariant, flags);
this.flaglessVersion = copyFrom.flaglessVersion;
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
final int artIndex0, final boolean foil0, final String collectorNumber0, final int artIndex0, final boolean foil0, final String collectorNumber0,
final String artist0, final String functionalVariant) { final String artist0, final String functionalVariant) {
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, false); this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, null);
} }
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, protected PaperCard(final CardRules rules, final String edition, final CardRarity rarity,
final int artIndex0, final boolean foil0, final String collectorNumber0, final int artIndex, final boolean foil, final String collectorNumber,
final String artist0, final String functionalVariant, final boolean noSell0) { final String artist, final String functionalVariant, final PaperCardFlags flags) {
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, noSell0, null); if (rules == null || edition == null || rarity == null) {
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
final int artIndex0, final boolean foil0, final String collectorNumber0,
final String artist0, final String functionalVariant, final boolean noSell0, final Set<String> colorID0) {
if (rules0 == null || edition0 == null || rarity0 == null) {
throw new IllegalArgumentException("Cannot create card without rules, edition or rarity"); throw new IllegalArgumentException("Cannot create card without rules, edition or rarity");
} }
rules = rules0; this.rules = rules;
name = rules0.getName(); name = rules.getName();
edition = edition0; this.edition = edition;
artIndex = Math.max(artIndex0, IPaperCard.DEFAULT_ART_INDEX); this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
foil = foil0; this.foil = foil;
rarity = rarity0; this.rarity = rarity;
artist = TextUtil.normalizeText(artist0); this.artist = TextUtil.normalizeText(artist);
collectorNumber = (collectorNumber0 != null) && (collectorNumber0.length() > 0) ? collectorNumber0 : IPaperCard.NO_COLLECTOR_NUMBER; this.collectorNumber = (collectorNumber != null && !collectorNumber.isEmpty()) ? collectorNumber : IPaperCard.NO_COLLECTOR_NUMBER;
// If the user changes the language this will make cards sort by the old language until they restart the game. // If the user changes the language this will make cards sort by the old language until they restart the game.
// This is a good tradeoff // This is a good tradeoff
sortableName = TextUtil.toSortableName(CardTranslation.getTranslatedName(rules0.getName())); sortableName = TextUtil.toSortableName(CardTranslation.getTranslatedName(rules.getName()));
this.functionalVariant = functionalVariant != null ? functionalVariant : IPaperCard.NO_FUNCTIONAL_VARIANT; this.functionalVariant = functionalVariant != null ? functionalVariant : IPaperCard.NO_FUNCTIONAL_VARIANT;
noSell = noSell0;
colorID = colorID0; if(flags == null || flags.equals(PaperCardFlags.IDENTITY_FLAGS))
this.flags = PaperCardFlags.IDENTITY_FLAGS;
else
this.flags = flags;
} }
public static PaperCard FAKE_CARD = new PaperCard(CardRules.getUnsupportedCardNamed("Fake Card"), "Fake Edition", CardRarity.Common); public static PaperCard FAKE_CARD = new PaperCard(CardRules.getUnsupportedCardNamed("Fake Card"), "Fake Edition", CardRarity.Common);
@@ -256,8 +261,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
} }
if (!getCollectorNumber().equals(other.getCollectorNumber())) if (!getCollectorNumber().equals(other.getCollectorNumber()))
return false; return false;
// colorID can be NULL if (!Objects.equals(flags, other.flags))
if (getColorID() != other.getColorID())
return false; return false;
return (other.foil == foil) && (other.artIndex == artIndex); return (other.foil == foil) && (other.artIndex == artIndex);
} }
@@ -269,13 +273,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
*/ */
@Override @Override
public int hashCode() { public int hashCode() {
final int code = (name.hashCode() * 11) + (edition.hashCode() * 59) + return Objects.hash(name, edition, collectorNumber, artIndex, foil, flags);
(artIndex * 2) + (getCollectorNumber().hashCode() * 383);
final int id = Optional.ofNullable(colorID).map(Set::hashCode).orElse(0);
if (foil) {
return code + id + 1;
}
return code + id;
} }
// FIXME: Check // FIXME: Check
@@ -307,7 +305,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
String collectorNumber = collectorNumber0; String collectorNumber = collectorNumber0;
if (collectorNumber.equals(NO_COLLECTOR_NUMBER)) if (collectorNumber.equals(NO_COLLECTOR_NUMBER))
collectorNumber = null; collectorNumber = null;
return CardEdition.CardInSet.getSortableCollectorNumber(collectorNumber); return CardEdition.getSortableCollectorNumber(collectorNumber);
} }
private String sortableCNKey = null; private String sortableCNKey = null;
@@ -339,6 +337,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return Integer.compare(artIndex, o.getArtIndex()); return Integer.compare(artIndex, o.getArtIndex());
} }
@Serial
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
// default deserialization // default deserialization
ois.defaultReadObject(); ois.defaultReadObject();
@@ -354,22 +353,24 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
rarity = pc.getRarity(); rarity = pc.getRarity();
} }
@Serial
private Object readResolve() throws ObjectStreamException {
//If we deserialize an old PaperCard with no flags, reinitialize as a fresh copy to set default flags.
if(this.flags == null)
return new PaperCard(this, null);
return this;
}
@Override @Override
public String getImageKey(boolean altState) { public String getImageKey(boolean altState) {
String noramlizedName = StringUtils.stripAccents(name); return altState ? this.getCardAltImageKey() : this.getCardImageKey();
String imageKey = ImageKeys.CARD_PREFIX + noramlizedName + CardDb.NameSetSeparator
+ edition + CardDb.NameSetSeparator + artIndex;
if (altState) {
imageKey += ImageKeys.BACKFACE_POSTFIX;
}
return imageKey;
} }
private String cardImageKey = null; private String cardImageKey = null;
@Override @Override
public String getCardImageKey() { public String getCardImageKey() {
if (this.cardImageKey == null) if (this.cardImageKey == null)
this.cardImageKey = ImageUtil.getImageKey(this, "", true); this.cardImageKey = ImageUtil.getImageKey(this, CardStateName.Original);
return cardImageKey; return cardImageKey;
} }
@@ -378,9 +379,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardAltImageKey() { public String getCardAltImageKey() {
if (this.cardAltImageKey == null){ if (this.cardAltImageKey == null){
if (this.hasBackFace()) if (this.hasBackFace())
this.cardAltImageKey = ImageUtil.getImageKey(this, "back", true); this.cardAltImageKey = ImageUtil.getImageKey(this, this.getRules().getSplitType().getChangedStateName());
else // altImageKey will be the same as cardImageKey else // altImageKey will be the same as cardImageKey
this.cardAltImageKey = ImageUtil.getImageKey(this, "", true); this.cardAltImageKey = getCardImageKey();
} }
return cardAltImageKey; return cardAltImageKey;
} }
@@ -390,9 +391,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardWSpecImageKey() { public String getCardWSpecImageKey() {
if (this.cardWSpecImageKey == null) { if (this.cardWSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize) if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardWSpecImageKey = ImageUtil.getImageKey(this, "white", true); this.cardWSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeW);
else // just use cardImageKey else // just use cardImageKey
this.cardWSpecImageKey = ImageUtil.getImageKey(this, "", true); this.cardWSpecImageKey = getCardImageKey();
} }
return cardWSpecImageKey; return cardWSpecImageKey;
} }
@@ -402,9 +403,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardUSpecImageKey() { public String getCardUSpecImageKey() {
if (this.cardUSpecImageKey == null) { if (this.cardUSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize) if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardUSpecImageKey = ImageUtil.getImageKey(this, "blue", true); this.cardUSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeU);
else // just use cardImageKey else // just use cardImageKey
this.cardUSpecImageKey = ImageUtil.getImageKey(this, "", true); this.cardUSpecImageKey = getCardImageKey();
} }
return cardUSpecImageKey; return cardUSpecImageKey;
} }
@@ -414,9 +415,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardBSpecImageKey() { public String getCardBSpecImageKey() {
if (this.cardBSpecImageKey == null) { if (this.cardBSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize) if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardBSpecImageKey = ImageUtil.getImageKey(this, "black", true); this.cardBSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeB);
else // just use cardImageKey else // just use cardImageKey
this.cardBSpecImageKey = ImageUtil.getImageKey(this, "", true); this.cardBSpecImageKey = getCardImageKey();
} }
return cardBSpecImageKey; return cardBSpecImageKey;
} }
@@ -426,9 +427,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardRSpecImageKey() { public String getCardRSpecImageKey() {
if (this.cardRSpecImageKey == null) { if (this.cardRSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize) if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardRSpecImageKey = ImageUtil.getImageKey(this, "red", true); this.cardRSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeR);
else // just use cardImageKey else // just use cardImageKey
this.cardRSpecImageKey = ImageUtil.getImageKey(this, "", true); this.cardRSpecImageKey = getCardImageKey();
} }
return cardRSpecImageKey; return cardRSpecImageKey;
} }
@@ -438,18 +439,16 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardGSpecImageKey() { public String getCardGSpecImageKey() {
if (this.cardGSpecImageKey == null) { if (this.cardGSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize) if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardGSpecImageKey = ImageUtil.getImageKey(this, "green", true); this.cardGSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeG);
else // just use cardImageKey else // just use cardImageKey
this.cardGSpecImageKey = ImageUtil.getImageKey(this, "", true); this.cardGSpecImageKey = getCardImageKey();
} }
return cardGSpecImageKey; return cardGSpecImageKey;
} }
@Override @Override
public boolean hasBackFace(){ public boolean hasBackFace(){
CardSplitType cst = this.rules.getSplitType(); return this.rules.hasBackSide();
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld
|| cst == CardSplitType.Modal;
} }
@Override @Override
@@ -493,4 +492,88 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public boolean isRebalanced() { public boolean isRebalanced() {
return StaticData.instance().isRebalanced(name); return StaticData.instance().isRebalanced(name);
} }
/**
* Contains properties of a card which distinguish it from an otherwise identical copy of the card with the same
* name, edition, and collector number. Examples include permanent markings on the card, and flags for Adventure
* mode.
*/
public static class PaperCardFlags implements Serializable {
@Serial
private static final long serialVersionUID = -3924720485840169336L;
/**
* Chosen colors, for cards like Cryptic Spires.
*/
public final ColorSet markedColors;
/**
* Removes the sell value of the card in Adventure mode.
*/
public final boolean noSellValue;
//TODO: Could probably move foil here.
static final PaperCardFlags IDENTITY_FLAGS = new PaperCardFlags(Map.of());
protected PaperCardFlags(Map<String, String> flags) {
if(flags.containsKey("markedColors"))
markedColors = ColorSet.fromNames(flags.get("markedColors").split(""));
else
markedColors = null;
noSellValue = flags.containsKey("noSellValue");
}
//Copy constructor. There are some better ways to do this, and they should be explored once we have more than 4
//or 5 fields here. Just need to ensure it's impossible to accidentally change a field while the PaperCardFlags
//object is in use.
private PaperCardFlags(PaperCardFlags copyFrom, ColorSet markedColors, Boolean noSellValue) {
if(markedColors == null)
markedColors = copyFrom.markedColors;
else if(markedColors.isColorless())
markedColors = null;
this.markedColors = markedColors;
this.noSellValue = noSellValue != null ? noSellValue : copyFrom.noSellValue;
}
public PaperCardFlags withMarkedColors(ColorSet markedColors) {
if(markedColors == null)
markedColors = ColorSet.getNullColor();
return new PaperCardFlags(this, markedColors, null);
}
public PaperCardFlags withNoSellValueFlag(boolean noSellValue) {
return new PaperCardFlags(this, null, noSellValue);
}
private Map<String, String> asMap;
public Map<String, String> toMap() {
if(asMap != null)
return asMap;
Map<String, String> out = new HashMap<>();
if(markedColors != null && !markedColors.isColorless())
out.put("markedColors", markedColors.toString());
if(noSellValue)
out.put("noSellValue", "true");
asMap = out;
return out;
}
@Override
public String toString() {
return this.toMap().entrySet().stream()
.map((e) -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("\t"));
}
@Override
public boolean equals(Object o) {
if (!(o instanceof PaperCardFlags that)) return false;
return noSellValue == that.noSellValue && Objects.equals(markedColors, that.markedColors);
}
@Override
public int hashCode() {
return Objects.hash(markedColors, noSellValue);
}
}
} }

View File

@@ -7,11 +7,12 @@ import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
public class PaperToken implements InventoryItemFromSet, IPaperCard { public class PaperToken implements InventoryItemFromSet, IPaperCard {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private String name; private String name;
private String collectorNumber;
private String artist;
private transient CardEdition edition; private transient CardEdition edition;
private ArrayList<String> imageFileName = new ArrayList<>(); private ArrayList<String> imageFileName = new ArrayList<>();
private transient CardRules cardRules; private transient CardRules cardRules;
@@ -54,75 +55,31 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
return makeTokenFileName(fileName); return makeTokenFileName(fileName);
} }
public static String makeTokenFileName(final CardRules rules, CardEdition edition) { public PaperToken(final CardRules c, CardEdition edition0, String imageFileName, String collectorNumber, String artist) {
ArrayList<String> build = new ArrayList<>();
String subtypes = StringUtils.join(rules.getType().getSubtypes(), " ");
if (!rules.getName().equals(subtypes)) {
return makeTokenFileName(rules.getName());
}
ColorSet colors = rules.getColor();
if (colors.isColorless()) {
build.add("C");
} else {
String color = "";
if (colors.hasWhite()) color += "W";
if (colors.hasBlue()) color += "U";
if (colors.hasBlack()) color += "B";
if (colors.hasRed()) color += "R";
if (colors.hasGreen()) color += "G";
build.add(color);
}
if (rules.getPower() != null && rules.getToughness() != null) {
build.add(rules.getPower());
build.add(rules.getToughness());
}
String cardTypes = "";
if (rules.getType().isArtifact()) cardTypes += "A";
if (rules.getType().isEnchantment()) cardTypes += "E";
if (!cardTypes.isEmpty()) {
build.add(cardTypes);
}
build.add(subtypes);
// Are these keywords sorted?
for (String keyword : rules.getMainPart().getKeywords()) {
build.add(keyword);
}
if (edition != null) {
build.add(edition.getCode());
}
return StringUtils.join(build, "_").replace('*', 'x').toLowerCase();
}
public PaperToken(final CardRules c, CardEdition edition0, String imageFileName) {
this.cardRules = c; this.cardRules = c;
this.name = c.getName(); this.name = c.getName();
this.edition = edition0; this.edition = edition0;
this.collectorNumber = collectorNumber;
this.artist = artist;
if (edition != null && edition.getTokens().containsKey(imageFileName)) { if (collectorNumber != null && !collectorNumber.isEmpty() && edition != null && edition.getTokens().containsKey(imageFileName)) {
this.artIndex = edition.getTokens().get(imageFileName); int idx = 0;
} // count the one with the same collectorNumber
for (CardEdition.EditionEntry t : edition.getTokens().get(imageFileName)) {
if (imageFileName == null) { ++idx;
// This shouldn't really happen. We can just use the normalized name again for the base image name if (!t.collectorNumber().equals(collectorNumber)) {
this.imageFileName.add(makeTokenFileName(c, edition0)); continue;
} else { }
String formatEdition = null == edition || CardEdition.UNKNOWN == edition ? "" : "_" + edition.getCode().toLowerCase(); // TODO make better image file names when collector number is known
// for the right index, we need to count the ones with wrong collector number too
this.imageFileName.add(String.format("%s%s", imageFileName, formatEdition)); this.imageFileName.add(String.format("%s|%s|%s|%d", imageFileName, edition.getCode(), collectorNumber, idx));
for (int idx = 2; idx <= this.artIndex; idx++) {
this.imageFileName.add(String.format("%s%d%s", imageFileName, idx, formatEdition));
} }
this.artIndex = this.imageFileName.size();
} else if (null == edition || CardEdition.UNKNOWN == edition) {
this.imageFileName.add(imageFileName);
} else {
// Fallback if CollectorNumber is not used
this.imageFileName.add(String.format("%s|%s", imageFileName, edition.getCode()));
} }
} }
@@ -138,12 +95,14 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
@Override @Override
public String getEdition() { public String getEdition() {
return edition != null ? edition.getCode() : "???"; return edition != null ? edition.getCode() : CardEdition.UNKNOWN_CODE;
} }
@Override @Override
public String getCollectorNumber() { public String getCollectorNumber() {
return IPaperCard.NO_COLLECTOR_NUMBER; if (collectorNumber.isEmpty())
return IPaperCard.NO_COLLECTOR_NUMBER;
return collectorNumber;
} }
@Override @Override
@@ -153,7 +112,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
} }
@Override @Override
public Set<String> getColorID() { public ColorSet getMarkedColors() {
return null; return null;
} }
@@ -178,13 +137,8 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
} }
@Override @Override
public String getArtist() { /*TODO*/ public String getArtist() {
return ""; return artist;
}
// Unfortunately this is a property of token, cannot move it outside of class
public String getImageFilename() {
return getImageFilename(1);
} }
public String getImageFilename(int idx) { public String getImageFilename(int idx) {
@@ -259,24 +213,21 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
// InventoryItem // InventoryItem
@Override @Override
public String getImageKey(boolean altState) { public String getImageKey(boolean altState) {
if (hasBackFace()) { String suffix = "";
String edCode = edition != null ? "_" + edition.getCode().toLowerCase() : ""; if (hasBackFace() && altState) {
if (altState) { if (collectorNumber != null && !collectorNumber.isEmpty() && edition != null) {
String name = ImageKeys.TOKEN_PREFIX + cardRules.getOtherPart().getName().toLowerCase().replace(" token", ""); String name = cardRules.getOtherPart().getName().toLowerCase().replace(" token", "").replace(" ", "_");
name.replace(" ", "_"); return ImageKeys.getTokenKey(String.format("%s|%s|%s%s", name, edition.getCode(), collectorNumber, ImageKeys.BACKFACE_POSTFIX));
return name + edCode;
} else { } else {
String name = ImageKeys.TOKEN_PREFIX + cardRules.getMainPart().getName().toLowerCase().replace(" token", ""); suffix = ImageKeys.BACKFACE_POSTFIX;
name.replace(" ", "_");
return name + edCode;
} }
} }
int idx = MyRandom.getRandom().nextInt(artIndex); int idx = MyRandom.getRandom().nextInt(artIndex);
return getImageKey(idx); return getImageKey(idx) + suffix;
} }
public String getImageKey(int artIndex) { public String getImageKey(int artIndex) {
return ImageKeys.TOKEN_PREFIX + imageFileName.get(artIndex).replace(" ", "_"); return ImageKeys.getTokenKey(imageFileName.get(artIndex).replace(" ", "_"));
} }
public boolean isRebalanced() { public boolean isRebalanced() {

View File

@@ -1,10 +1,15 @@
package forge.token; package forge.token;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.card.CardDb; import forge.card.CardDb;
import forge.card.CardEdition; import forge.card.CardEdition;
import forge.card.CardRules; import forge.card.CardRules;
import forge.item.IPaperCard;
import forge.item.PaperToken; import forge.item.PaperToken;
import forge.util.Aggregates;
import java.util.*; import java.util.*;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -23,8 +28,8 @@ public class TokenDb implements ITokenDatabase {
// The image names should be the same as the script name + _set // The image names should be the same as the script name + _set
// If that isn't found, consider falling back to the original token // If that isn't found, consider falling back to the original token
private final Multimap<String, PaperToken> allTokenByName = HashMultimap.create();
private final Map<String, PaperToken> tokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); private final Map<String, PaperToken> extraTokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
private final CardEdition.Collection editions; private final CardEdition.Collection editions;
private final Map<String, CardRules> rulesByName; private final Map<String, CardRules> rulesByName;
@@ -38,38 +43,87 @@ public class TokenDb implements ITokenDatabase {
return this.rulesByName.containsKey(rule); return this.rulesByName.containsKey(rule);
} }
@Override
public PaperToken getToken(String tokenName) {
return getToken(tokenName, CardEdition.UNKNOWN.getName());
}
public void preloadTokens() { public void preloadTokens() {
for (CardEdition edition : this.editions) { for (CardEdition edition : this.editions) {
for (String name : edition.getTokens().keySet()) { for (Map.Entry<String, Collection<CardEdition.EditionEntry>> inSet : edition.getTokens().asMap().entrySet()) {
try { String name = inSet.getKey();
getToken(name, edition.getCode()); String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
} catch(Exception e) { for (CardEdition.EditionEntry t : inSet.getValue()) {
System.out.println(name + "_" + edition.getCode() + " defined in Edition file, but not defined as a token script."); allTokenByName.put(fullName, addTokenInSet(edition, name, t));
} }
} }
} }
} }
protected boolean loadTokenFromSet(CardEdition edition, String name) {
String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
if (allTokenByName.containsKey(fullName)) {
return true;
}
if (!edition.getTokens().containsKey(name)) {
return false;
}
for (CardEdition.EditionEntry t : edition.getTokens().get(name)) {
allTokenByName.put(fullName, addTokenInSet(edition, name, t));
}
return true;
}
protected PaperToken addTokenInSet(CardEdition edition, String name, CardEdition.EditionEntry t) {
CardRules rules;
if (rulesByName.containsKey(name)) {
rules = rulesByName.get(name);
} else if ("w_2_2_spirit".equals(name) || "w_3_3_spirit".equals(name)) { // Hotfix for Endure Token
rules = rulesByName.get("w_x_x_spirit");
} else {
throw new RuntimeException("wrong token name:" + name);
}
return new PaperToken(rules, edition, name, t.collectorNumber(), t.artistName());
}
// try all editions to find token
protected PaperToken fallbackToken(String name) {
for (CardEdition edition : this.editions) {
String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
if (loadTokenFromSet(edition, name)) {
return Aggregates.random(allTokenByName.get(fullName));
}
}
return null;
}
@Override
public PaperToken getToken(String tokenName) {
return getToken(tokenName, CardEdition.UNKNOWN.getCode());
}
@Override @Override
public PaperToken getToken(String tokenName, String edition) { public PaperToken getToken(String tokenName, String edition) {
String fullName = String.format("%s_%s", tokenName, edition.toLowerCase()); CardEdition realEdition = editions.getEditionByCodeOrThrow(edition);
String fullName = String.format("%s_%s", tokenName, realEdition.getCode().toLowerCase());
if (!tokensByName.containsKey(fullName)) { // token exist in Set, return one at random
if (loadTokenFromSet(realEdition, tokenName)) {
return Aggregates.random(allTokenByName.get(fullName));
}
PaperToken fallback = this.fallbackToken(tokenName);
if (fallback != null) {
return fallback;
}
if (!extraTokensByName.containsKey(fullName)) {
try { try {
PaperToken pt = new PaperToken(rulesByName.get(tokenName), editions.get(edition), tokenName); PaperToken pt = new PaperToken(rulesByName.get(tokenName), realEdition, tokenName, "", IPaperCard.NO_ARTIST_NAME);
tokensByName.put(fullName, pt); extraTokensByName.put(fullName, pt);
return pt; return pt;
} catch(Exception e) { } catch(Exception e) {
throw e; throw e;
} }
} }
return tokensByName.get(fullName); return extraTokensByName.get(fullName);
} }
@Override @Override
@@ -119,7 +173,7 @@ public class TokenDb implements ITokenDatabase {
@Override @Override
public List<PaperToken> getAllTokens() { public List<PaperToken> getAllTokens() {
return new ArrayList<>(tokensByName.values()); return new ArrayList<>(allTokenByName.values());
} }
@Override @Override
@@ -139,6 +193,6 @@ public class TokenDb implements ITokenDatabase {
@Override @Override
public Iterator<PaperToken> iterator() { public Iterator<PaperToken> iterator() {
return tokensByName.values().iterator(); return allTokenByName.values().iterator();
} }
} }

View File

@@ -5,6 +5,7 @@ import forge.StaticData;
import forge.card.CardDb; import forge.card.CardDb;
import forge.card.CardRules; import forge.card.CardRules;
import forge.card.CardSplitType; import forge.card.CardSplitType;
import forge.card.CardStateName;
import forge.item.IPaperCard; import forge.item.IPaperCard;
import forge.item.PaperCard; import forge.item.PaperCard;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -24,20 +25,17 @@ public class ImageUtil {
key = imageKey.substring(ImageKeys.CARD_PREFIX.length()); key = imageKey.substring(ImageKeys.CARD_PREFIX.length());
else else
return null; return null;
if (key.endsWith(ImageKeys.BACKFACE_POSTFIX)) {
key = key.substring(0, key.length() - ImageKeys.BACKFACE_POSTFIX.length());
}
if (key.isEmpty()) if (key.isEmpty())
return null; return null;
CardDb db = StaticData.instance().getCommonCards(); String[] tempdata = key.split("\\|");
PaperCard cp = null; PaperCard cp = StaticData.instance().fetchCard(tempdata[0], tempdata[1], tempdata[2]);
//db shouldn't be null
if (db != null) {
cp = db.getCard(key);
if (cp == null) {
db = StaticData.instance().getVariantCards();
if (db != null)
cp = db.getCard(key);
}
}
if (cp == null) if (cp == null)
System.err.println("Can't find PaperCard from key: " + key); System.err.println("Can't find PaperCard from key: " + key);
// return cp regardless if it's null // return cp regardless if it's null
@@ -54,6 +52,21 @@ public class ImageUtil {
return key; return key;
} }
public static String getImageRelativePath(String name, String set, String collectorNumber, boolean artChop) {
StringBuilder sb = new StringBuilder();
sb.append(set).append("/");
if (!collectorNumber.isEmpty() && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)) {
sb.append(collectorNumber).append("_");
}
sb.append(StringUtils.stripAccents(name));
sb.append(artChop ? ".artcrop" : ".fullborder");
sb.append(".jpg");
return sb.toString();
}
public static String getImageRelativePath(PaperCard cp, String face, boolean includeSet, boolean isDownloadUrl) { public static String getImageRelativePath(PaperCard cp, String face, boolean includeSet, boolean isDownloadUrl) {
final String nameToUse = cp == null ? null : getNameToUse(cp, face); final String nameToUse = cp == null ? null : getNameToUse(cp, face);
if (nameToUse == null) { if (nameToUse == null) {
@@ -123,25 +136,15 @@ public class ImageUtil {
else else
return null; return null;
} else if (face.equals("white")) { } else if (face.equals("white")) {
if (card.getWSpecialize() != null) { return card.getImageName(CardStateName.SpecializeW);
return card.getWSpecialize().getName();
}
} else if (face.equals("blue")) { } else if (face.equals("blue")) {
if (card.getUSpecialize() != null) { return card.getImageName(CardStateName.SpecializeU);
return card.getUSpecialize().getName();
}
} else if (face.equals("black")) { } else if (face.equals("black")) {
if (card.getBSpecialize() != null) { return card.getImageName(CardStateName.SpecializeB);
return card.getBSpecialize().getName();
}
} else if (face.equals("red")) { } else if (face.equals("red")) {
if (card.getRSpecialize() != null) { return card.getImageName(CardStateName.SpecializeR);
return card.getRSpecialize().getName();
}
} else if (face.equals("green")) { } else if (face.equals("green")) {
if (card.getGSpecialize() != null) { return card.getImageName(CardStateName.SpecializeG);
return card.getGSpecialize().getName();
}
} else if (CardSplitType.Split == cp.getRules().getSplitType()) { } else if (CardSplitType.Split == cp.getRules().getSplitType()) {
return card.getMainPart().getName() + card.getOtherPart().getName(); return card.getMainPart().getName() + card.getOtherPart().getName();
} else if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(cp.getFunctionalVariant())) { } else if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(cp.getFunctionalVariant())) {
@@ -150,50 +153,95 @@ public class ImageUtil {
return cp.getName(); return cp.getName();
} }
public static String getNameToUse(PaperCard cp, CardStateName face) {
if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(cp.getFunctionalVariant())) {
return cp.getFunctionalVariant();
}
final CardRules card = cp.getRules();
return card.getImageName(face);
}
public static String getImageKey(PaperCard cp, String face, boolean includeSet) { public static String getImageKey(PaperCard cp, String face, boolean includeSet) {
return getImageRelativePath(cp, face, includeSet, false); return getImageRelativePath(cp, face, includeSet, false);
} }
public static String getImageKey(PaperCard cp, CardStateName face) {
String name = getNameToUse(cp, face);
String number = cp.getCollectorNumber();
String suffix = "";
switch (face) {
case SpecializeB:
number += "b";
break;
case SpecializeG:
number += "g";
break;
case SpecializeR:
number += "r";
break;
case SpecializeU:
number += "u";
break;
case SpecializeW:
number += "w";
break;
case Meld:
case Modal:
case Secondary:
case Transformed:
suffix = ImageKeys.BACKFACE_POSTFIX;
break;
case Flipped:
break; // add info to rotate the image?
default:
break;
};
return ImageKeys.CARD_PREFIX + name + CardDb.NameSetSeparator + cp.getEdition()
+ CardDb.NameSetSeparator + number + CardDb.NameSetSeparator + cp.getArtIndex() + suffix;
}
public static String getDownloadUrl(PaperCard cp, String face) { public static String getDownloadUrl(PaperCard cp, String face) {
return getImageRelativePath(cp, face, true, true); return getImageRelativePath(cp, face, true, true);
} }
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){ public static String getScryfallDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam, boolean useArtCrop){
return getScryfallDownloadUrl(cp, face, setCode, langCode, useArtCrop, false); return getScryfallDownloadUrl(collectorNumber, setCode, langCode, faceParam, useArtCrop, false);
} }
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){ public static String getScryfallDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam, boolean useArtCrop, boolean hyphenateAlchemy){
String editionCode;
if ((setCode != null) && (setCode.length() > 0))
editionCode = setCode;
else
editionCode = cp.getEdition().toLowerCase();
String cardCollectorNumber = cp.getCollectorNumber();
// Hack to account for variations in Arabian Nights // Hack to account for variations in Arabian Nights
cardCollectorNumber = cardCollectorNumber.replace("+", ""); collectorNumber = collectorNumber.replace("+", "");
// override old planechase sets from their modified id since scryfall move the planechase cards outside their original setcode // override old planechase sets from their modified id since scryfall move the planechase cards outside their original setcode
if (cardCollectorNumber.startsWith("OHOP")) { if (collectorNumber.startsWith("OHOP")) {
editionCode = "ohop"; setCode = "ohop";
cardCollectorNumber = cardCollectorNumber.substring("OHOP".length()); collectorNumber = collectorNumber.substring("OHOP".length());
} else if (cardCollectorNumber.startsWith("OPCA")) { } else if (collectorNumber.startsWith("OPCA")) {
editionCode = "opca"; setCode = "opca";
cardCollectorNumber = cardCollectorNumber.substring("OPCA".length()); collectorNumber = collectorNumber.substring("OPCA".length());
} else if (cardCollectorNumber.startsWith("OPC2")) { } else if (collectorNumber.startsWith("OPC2")) {
editionCode = "opc2"; setCode = "opc2";
cardCollectorNumber = cardCollectorNumber.substring("OPC2".length()); collectorNumber = collectorNumber.substring("OPC2".length());
} else if (hyphenateAlchemy) { } else if (hyphenateAlchemy) {
if (!cardCollectorNumber.startsWith("A")) { if (!collectorNumber.startsWith("A")) {
return null; return null;
} }
cardCollectorNumber = cardCollectorNumber.replace("A", "A-"); collectorNumber = collectorNumber.replace("A", "A-");
} }
String versionParam = useArtCrop ? "art_crop" : "normal"; String versionParam = useArtCrop ? "art_crop" : "normal";
String faceParam = ""; if (!faceParam.isEmpty()) {
if (cp.getRules().getOtherPart() != null) { faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
faceParam = (face.equals("back") ? "&face=back" : "&face=front");
} }
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, cardCollectorNumber, return String.format("%s/%s/%s?format=image&version=%s%s", setCode, collectorNumber,
langCode, versionParam, faceParam);
}
public static String getScryfallTokenDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam) {
String versionParam = "normal";
if (!faceParam.isEmpty()) {
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
}
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, collectorNumber,
langCode, versionParam, faceParam); langCode, versionParam, faceParam);
} }

View File

@@ -269,6 +269,13 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
// need not set out-of-sync: either remove did set, or nothing was removed // need not set out-of-sync: either remove did set, or nothing was removed
} }
public void removeIf(Predicate<T> test) {
for (final T item : items.keySet()) {
if (test.test(item))
remove(item);
}
}
public void clear() { public void clear() {
items.clear(); items.clear();
} }

View File

@@ -2,12 +2,14 @@ package forge.game;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Lists;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.MagicColor; import forge.card.MagicColor;
@@ -564,13 +566,23 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
return CardView.get(hostCard); return CardView.get(hostCard);
} }
protected IHasSVars getSVarFallback() { protected List<IHasSVars> getSVarFallback(final String name) {
List<IHasSVars> result = Lists.newArrayList();
if (this.getKeyword() != null && this.getKeyword().getStatic() != null) { if (this.getKeyword() != null && this.getKeyword().getStatic() != null) {
return this.getKeyword().getStatic(); // only do when the keyword has part of the SVar in ins original string
if (name == null || this.getKeyword().getOriginal().contains(name)) {
// TODO try to add the keyword instead if possible?
result.add(this.getKeyword().getStatic());
}
} }
if (getCardState() != null) if (getCardState() != null)
return getCardState(); result.add(getCardState());
return getHostCard(); result.add(getHostCard());
return result;
}
protected Optional<IHasSVars> findSVar(final String name) {
return getSVarFallback(name).stream().filter(f -> f.hasSVar(name)).findFirst();
} }
@Override @Override
@@ -578,12 +590,12 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
if (sVars.containsKey(name)) { if (sVars.containsKey(name)) {
return sVars.get(name); return sVars.get(name);
} }
return getSVarFallback().getSVar(name); return findSVar(name).map(o -> o.getSVar(name)).orElse("");
} }
@Override @Override
public boolean hasSVar(final String name) { public boolean hasSVar(final String name) {
return sVars.containsKey(name) || getSVarFallback().hasSVar(name); return sVars.containsKey(name) || findSVar(name).isPresent();
} }
public Integer getSVarInt(final String name) { public Integer getSVarInt(final String name) {
@@ -598,22 +610,21 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
} }
@Override @Override
public final void setSVar(final String name, final String value) { public void setSVar(final String name, final String value) {
sVars.put(name, value); sVars.put(name, value);
} }
@Override @Override
public Map<String, String> getSVars() { public Map<String, String> getSVars() {
Map<String, String> res = Maps.newHashMap(getSVarFallback().getSVars()); Map<String, String> res = Maps.newHashMap();
// TODO reverse the order
for (IHasSVars s : getSVarFallback(null)) {
res.putAll(s.getSVars());
}
res.putAll(sVars); res.putAll(sVars);
return res; return res;
} }
@Override
public Map<String, String> getDirectSVars() {
return sVars;
}
@Override @Override
public void setSVars(Map<String, String> newSVars) { public void setSVars(Map<String, String> newSVars) {
sVars = Maps.newTreeMap(); sVars = Maps.newTreeMap();

View File

@@ -122,30 +122,10 @@ public class ForgeScript {
} }
} }
return found; return found;
} else if (property.equals("hasActivatedAbilityWithTapCost")) { } else if (property.startsWith("hasAbility")) {
String valid = property.substring(11);
for (final SpellAbility sa : cardState.getSpellAbilities()) { for (final SpellAbility sa : cardState.getSpellAbilities()) {
if (sa.isActivatedAbility() && sa.getPayCosts().hasTapCost()) { if (sa.isValid(valid, sourceController, source, spellAbility)) {
return true;
}
}
return false;
} else if (property.equals("hasActivatedAbilityWithExhaust")) {
for (final SpellAbility sa : cardState.getSpellAbilities()) {
if (sa.isActivatedAbility() && sa.hasParam("Exhaust")) {
return true;
}
}
return false;
} else if (property.equals("hasActivatedAbility")) {
for (final SpellAbility sa : cardState.getSpellAbilities()) {
if (sa.isActivatedAbility()) {
return true;
}
}
return false;
} else if (property.equals("hasOtherActivatedAbility")) {
for (final SpellAbility sa : cardState.getSpellAbilities()) {
if (sa.isActivatedAbility() && !sa.equals(spellAbility)) {
return true; return true;
} }
} }
@@ -225,6 +205,8 @@ public class ForgeScript {
return sa.isEternalize(); return sa.isEternalize();
} else if (property.equals("Flashback")) { } else if (property.equals("Flashback")) {
return sa.isFlashback(); return sa.isFlashback();
} else if (property.equals("Harmonize")) {
return sa.isHarmonize();
} else if (property.equals("Jumpstart")) { } else if (property.equals("Jumpstart")) {
return sa.isJumpstart(); return sa.isJumpstart();
} else if (property.equals("Kicked")) { } else if (property.equals("Kicked")) {
@@ -243,6 +225,8 @@ public class ForgeScript {
return sa.isTurnFaceUp(); return sa.isTurnFaceUp();
} else if (property.equals("isCastFaceDown")) { } else if (property.equals("isCastFaceDown")) {
return sa.isCastFaceDown(); return sa.isCastFaceDown();
} else if (property.equals("Unearth")) {
return sa.isKeyword(Keyword.UNEARTH);
} else if (property.equals("Modular")) { } else if (property.equals("Modular")) {
return sa.isKeyword(Keyword.MODULAR); return sa.isKeyword(Keyword.MODULAR);
} else if (property.equals("Equip")) { } else if (property.equals("Equip")) {

View File

@@ -22,6 +22,7 @@ import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Table; import com.google.common.collect.Table;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import forge.GameCommand; import forge.GameCommand;
@@ -957,9 +958,9 @@ public class Game {
// if the player who lost was the Monarch, someone else will be the monarch // if the player who lost was the Monarch, someone else will be the monarch
// TODO need to check rules if it should try the next player if able // TODO need to check rules if it should try the next player if able
if (p.equals(getPhaseHandler().getPlayerTurn())) { if (p.equals(getPhaseHandler().getPlayerTurn())) {
getAction().becomeMonarch(getNextPlayerAfter(p), null); getAction().becomeMonarch(getNextPlayerAfter(p), p.getMonarchSet());
} else { } else {
getAction().becomeMonarch(getPhaseHandler().getPlayerTurn(), null); getAction().becomeMonarch(getPhaseHandler().getPlayerTurn(), p.getMonarchSet());
} }
} }
@@ -969,9 +970,9 @@ public class Game {
// If the player who has the initiative leaves the game on their own turn, // If the player who has the initiative leaves the game on their own turn,
// or the active player left the game at the same time, the next player in turn order takes the initiative. // or the active player left the game at the same time, the next player in turn order takes the initiative.
if (p.equals(getPhaseHandler().getPlayerTurn())) { if (p.equals(getPhaseHandler().getPlayerTurn())) {
getAction().takeInitiative(getNextPlayerAfter(p), null); getAction().takeInitiative(getNextPlayerAfter(p), p.getInitiativeSet());
} else { } else {
getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), null); getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), p.getInitiativeSet());
} }
} }
@@ -1206,29 +1207,43 @@ public class Game {
public int getCounterAddedThisTurn(CounterType cType, String validPlayer, String validCard, Card source, Player sourceController, CardTraitBase ctb) { public int getCounterAddedThisTurn(CounterType cType, String validPlayer, String validCard, Card source, Player sourceController, CardTraitBase ctb) {
int result = 0; int result = 0;
if (!countersAddedThisTurn.containsRow(cType)) { Set<CounterType> types = null;
if (cType == null) {
types = countersAddedThisTurn.rowKeySet();
} else if (!countersAddedThisTurn.containsRow(cType)) {
return result; return result;
} else {
types = Sets.newHashSet(cType);
} }
for (Map.Entry<Player, List<Pair<Card, Integer>>> e : countersAddedThisTurn.row(cType).entrySet()) { for (CounterType type : types) {
if (e.getKey().isValid(validPlayer.split(","), sourceController, source, ctb)) { for (Map.Entry<Player, List<Pair<Card, Integer>>> e : countersAddedThisTurn.row(type).entrySet()) {
for (Pair<Card, Integer> p : e.getValue()) { if (e.getKey().isValid(validPlayer.split(","), sourceController, source, ctb)) {
if (p.getKey().isValid(validCard.split(","), sourceController, source, ctb)) { for (Pair<Card, Integer> p : e.getValue()) {
result += p.getValue(); if (p.getKey().isValid(validCard.split(","), sourceController, source, ctb)) {
} result += p.getValue();
} }
} }
}
}
} }
return result; return result;
} }
public int getCounterAddedThisTurn(CounterType cType, Card card) { public int getCounterAddedThisTurn(CounterType cType, Card card) {
int result = 0; int result = 0;
if (!countersAddedThisTurn.containsRow(cType)) { Set<CounterType> types = null;
if (cType == null) {
types = countersAddedThisTurn.rowKeySet();
} else if (!countersAddedThisTurn.containsRow(cType)) {
return result; return result;
} else {
types = Sets.newHashSet(cType);
} }
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(cType).values()) { for (CounterType type : types) {
for (Pair<Card, Integer> p : l) { for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(type).values()) {
if (p.getKey().equalsWithGameTimestamp(card)) { for (Pair<Card, Integer> p : l) {
result += p.getValue(); if (p.getKey().equalsWithGameTimestamp(card)) {
result += p.getValue();
}
} }
} }
} }

View File

@@ -22,6 +22,7 @@ import forge.GameCommand;
import forge.StaticData; import forge.StaticData;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.CardType.Supertype; import forge.card.CardType.Supertype;
import forge.card.ColorSet;
import forge.card.GamePieceType; import forge.card.GamePieceType;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.deck.DeckSection; import forge.deck.DeckSection;
@@ -43,11 +44,10 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates; import forge.game.spellability.SpellAbilityPredicates;
import forge.game.spellability.SpellPermanent; import forge.game.spellability.SpellPermanent;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityContinuous; import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer; import forge.game.staticability.StaticAbilityLayer;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone; import forge.game.zone.PlayerZone;
import forge.game.zone.PlayerZoneBattlefield; import forge.game.zone.PlayerZoneBattlefield;
@@ -386,7 +386,7 @@ public class GameAction {
return moveToGraveyard(copied, cause, params); return moveToGraveyard(copied, cause, params);
} }
} }
attachAuraOnIndirectEnterBattlefield(copied, params); attachAuraOnIndirectETB(copied, params);
} }
// Handle merged permanent here so all replacement effects are already applied. // Handle merged permanent here so all replacement effects are already applied.
@@ -542,8 +542,8 @@ public class GameAction {
game.addLeftGraveyardThisTurn(lastKnownInfo); game.addLeftGraveyardThisTurn(lastKnownInfo);
} }
if (c.hasChosenColorSpire()) { if (c.hasMarkedColor()) {
copied.setChosenColorID(ImmutableSet.copyOf(c.getChosenColorID())); copied.setMarkedColors(c.getMarkedColors());
} }
copied.updateStateForView(); copied.updateStateForView();
@@ -797,7 +797,7 @@ public class GameAction {
continue; continue;
} }
if (stAb.checkMode("CantBlockBy")) { if (stAb.checkMode(StaticAbilityMode.CantBlockBy)) {
if (!stAb.hasParam("ValidAttacker") || (stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) { if (!stAb.hasParam("ValidAttacker") || (stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
continue; continue;
} }
@@ -807,7 +807,7 @@ public class GameAction {
} }
} }
} }
if (stAb.checkMode(StaticAbilityCantAttackBlock.MinMaxBlockerMode)) { if (stAb.checkMode(StaticAbilityMode.MinMaxBlocker)) {
for (Card creature : IterableUtil.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.CREATURES)) { for (Card creature : IterableUtil.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.CREATURES)) {
if (stAb.matchesValidParam("ValidCard", creature)) { if (stAb.matchesValidParam("ValidCard", creature)) {
creature.updateAbilityTextForView(); creature.updateAbilityTextForView();
@@ -1073,7 +1073,7 @@ public class GameAction {
public boolean hasStaticAbilityAffectingZone(ZoneType zone, StaticAbilityLayer layer) { public boolean hasStaticAbilityAffectingZone(ZoneType zone, StaticAbilityLayer layer) {
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) { for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions("Continuous")) { if (!stAb.checkConditions(StaticAbilityMode.Continuous)) {
continue; continue;
} }
if (layer != null && !stAb.getLayers().contains(layer)) { if (layer != null && !stAb.getLayers().contains(layer)) {
@@ -1105,10 +1105,6 @@ public class GameAction {
// remove old effects // remove old effects
game.getStaticEffects().clearStaticEffects(affectedCards); game.getStaticEffects().clearStaticEffects(affectedCards);
for (final Player p : game.getPlayers()) {
p.clearStaticAbilities();
}
// search for cards with static abilities // search for cards with static abilities
final FCollection<StaticAbility> staticAbilities = new FCollection<>(); final FCollection<StaticAbility> staticAbilities = new FCollection<>();
final CardCollection staticList = new CardCollection(); final CardCollection staticList = new CardCollection();
@@ -1123,7 +1119,7 @@ public class GameAction {
// need to get Card from preList if able // need to get Card from preList if able
final Card co = preList.get(c); final Card co = preList.get(c);
for (StaticAbility stAb : co.getStaticAbilities()) { for (StaticAbility stAb : co.getStaticAbilities()) {
if (stAb.checkMode("Continuous") && stAb.zonesCheck()) { if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.zonesCheck()) {
staticAbilities.add(stAb); staticAbilities.add(stAb);
} }
} }
@@ -1170,7 +1166,7 @@ public class GameAction {
if (affectedHere != null) { if (affectedHere != null) {
for (final Card c : affectedHere) { for (final Card c : affectedHere) {
for (final StaticAbility st2 : c.getStaticAbilities()) { for (final StaticAbility st2 : c.getStaticAbilities()) {
if (!staticAbilities.contains(st2) && st2.checkMode("Continuous") && st2.zonesCheck()) { if (!staticAbilities.contains(st2) && st2.checkMode(StaticAbilityMode.Continuous) && st2.zonesCheck()) {
toAdd.add(st2); toAdd.add(st2);
st2.applyContinuousAbilityBefore(layer, preList); st2.applyContinuousAbilityBefore(layer, preList);
} }
@@ -1467,7 +1463,7 @@ public class GameAction {
checkAgainCard |= stateBasedAction_Saga(c, sacrificeList); checkAgainCard |= stateBasedAction_Saga(c, sacrificeList);
checkAgainCard |= stateBasedAction_Battle(c, noRegCreats); checkAgainCard |= stateBasedAction_Battle(c, noRegCreats);
checkAgainCard |= stateBasedAction_Role(c, unAttachList); checkAgainCard |= stateBasedAction_Role(c, unAttachList);
checkAgainCard |= stateBasedAction704_attach(c, unAttachList); // Attachment checkAgainCard |= stateBasedAction704_attach(c, unAttachList);
checkAgainCard |= stateBasedAction_Contraption(c, noRegCreats); checkAgainCard |= stateBasedAction_Contraption(c, noRegCreats);
checkAgainCard |= stateBasedAction704_5q(c); // annihilate +1/+1 counters with -1/-1 ones checkAgainCard |= stateBasedAction704_5q(c); // annihilate +1/+1 counters with -1/-1 ones
@@ -1514,9 +1510,7 @@ public class GameAction {
if (!spaceSculptors.isEmpty() && !spaceSculptors.contains(p)) { if (!spaceSculptors.isEmpty() && !spaceSculptors.contains(p)) {
checkAgain |= stateBasedAction704_5u(p); checkAgain |= stateBasedAction704_5u(p);
} }
if (handleLegendRule(p, noRegCreats)) { checkAgain |= handleLegendRule(p, noRegCreats);
checkAgain = true;
}
if ((game.getRules().hasAppliedVariant(GameType.Commander) if ((game.getRules().hasAppliedVariant(GameType.Commander)
|| game.getRules().hasAppliedVariant(GameType.Brawl) || game.getRules().hasAppliedVariant(GameType.Brawl)
@@ -1535,13 +1529,12 @@ public class GameAction {
checkAgain = true; checkAgain = true;
} }
if (handlePlaneswalkerRule(p, noRegCreats)) { checkAgain |= handlePlaneswalkerRule(p, noRegCreats);
checkAgain = true;
}
} }
for (Player p : spaceSculptors) { for (Player p : spaceSculptors) {
checkAgain |= stateBasedAction704_5u(p); checkAgain |= stateBasedAction704_5u(p);
} }
// 704.5m World rule // 704.5m World rule
checkAgain |= handleWorldRule(noRegCreats); checkAgain |= handleWorldRule(noRegCreats);
@@ -1576,6 +1569,7 @@ public class GameAction {
orderedSacrificeList = true; orderedSacrificeList = true;
} }
sacrifice(sacrificeList, null, true, mapParams); sacrifice(sacrificeList, null, true, mapParams);
setHoldCheckingStaticAbilities(false); setHoldCheckingStaticAbilities(false);
table.triggerChangesZoneAll(game, null); table.triggerChangesZoneAll(game, null);
@@ -1634,7 +1628,7 @@ public class GameAction {
private boolean stateBasedAction_Saga(Card c, CardCollection sacrificeList) { private boolean stateBasedAction_Saga(Card c, CardCollection sacrificeList) {
boolean checkAgain = false; boolean checkAgain = false;
if (!c.isSaga()) { if (!c.isSaga() || !c.hasChapter()) {
return false; return false;
} }
// needs to be effect, because otherwise it might be a cost? // needs to be effect, because otherwise it might be a cost?
@@ -1656,10 +1650,27 @@ public class GameAction {
if (!c.isBattle()) { if (!c.isBattle()) {
return checkAgain; return checkAgain;
} }
if (((c.getProtectingPlayer() == null || !c.getProtectingPlayer().isInGame()) && Player battleController = c.getController();
Player battleProtector = c.getProtectingPlayer();
/*
704.5w If a battle has no player in the game designated as its protector and no attacking creatures are currently
attacking that battle, that battles controller chooses an appropriate player to be its protector based on its
battle type. If no player can be chosen this way, the battle is put into its owners graveyard.
704.5x If a Sieges controller is also its designated protector, that player chooses an opponent to become its
protector. If no player can be chosen this way, the battle is put into its owners graveyard.
*/
if (((battleProtector == null || !battleProtector.isInGame()) &&
(game.getCombat() == null || game.getCombat().getAttackersOf(c).isEmpty())) || (game.getCombat() == null || game.getCombat().getAttackersOf(c).isEmpty())) ||
(c.getType().hasStringType("Siege") && c.getController().equals(c.getProtectingPlayer()))) { (c.getType().hasStringType("Siege") && battleController.equals(battleProtector))) {
Player newProtector = c.getController().getController().chooseSingleEntityForEffect(c.getController().getOpponents(), null, "Choose an opponent to protect this battle", null); Player newProtector;
if (c.getType().getBattleTypes().contains("Siege"))
newProtector = battleController.getController().chooseSingleEntityForEffect(battleController.getOpponents(), new SpellAbility.EmptySa(ApiType.ChoosePlayer, c), "Choose an opponent to protect this battle", null);
else {
// Fall back to the controller. Technically should fall back to null per the above rules, but no official
// cards should use this branch. For now this better supports custom cards. May need to revise this later.
newProtector = battleController;
}
// seems unlikely unless range of influence gets implemented // seems unlikely unless range of influence gets implemented
if (newProtector == null) { if (newProtector == null) {
removeList.add(c); removeList.add(c);
@@ -1745,12 +1756,12 @@ public class GameAction {
} }
private boolean stateBasedAction_Contraption(Card c, CardCollection removeList) { private boolean stateBasedAction_Contraption(Card c, CardCollection removeList) {
if(!c.isContraption()) if (!c.isContraption())
return false; return false;
int currentSprocket = c.getSprocket(); int currentSprocket = c.getSprocket();
//A contraption that is in the battlefield without being assembled is put into the graveyard or junkyard. //A contraption that is in the battlefield without being assembled is put into the graveyard or junkyard.
if(currentSprocket == 0) { if (currentSprocket == 0) {
removeList.add(c); removeList.add(c);
return true; return true;
} }
@@ -1759,7 +1770,7 @@ public class GameAction {
//A reassemble effect can handle that on its own. But if it changed controller due to some other effect, //A reassemble effect can handle that on its own. But if it changed controller due to some other effect,
//we assign it here. A contraption uses sprocket -1 to signify it has been assembled previously but now needs //we assign it here. A contraption uses sprocket -1 to signify it has been assembled previously but now needs
//a new sprocket. //a new sprocket.
if(currentSprocket > 0 && currentSprocket <= 3) if (currentSprocket > 0 && currentSprocket <= 3)
return false; return false;
int sprocket = c.getController().getController().chooseSprocket(c); int sprocket = c.getController().getController().chooseSprocket(c);
@@ -2027,7 +2038,7 @@ public class GameAction {
} }
private boolean handleWorldRule(CardCollection noRegCreats) { private boolean handleWorldRule(CardCollection noRegCreats) {
final List<Card> worlds = CardLists.getType(game.getCardsIn(ZoneType.Battlefield), "World"); final List<Card> worlds = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), c -> c.getType().hasSupertype(Supertype.World));
if (worlds.size() <= 1) { if (worlds.size() <= 1) {
return false; return false;
} }
@@ -2036,7 +2047,7 @@ public class GameAction {
long ts = 0; long ts = 0;
for (final Card crd : worlds) { for (final Card crd : worlds) {
long crdTs = crd.getGameTimestamp(); long crdTs = crd.getWorldTimestamp();
if (crdTs > ts) { if (crdTs > ts) {
ts = crdTs; ts = crdTs;
toKeep.clear(); toKeep.clear();
@@ -2417,15 +2428,14 @@ public class GameAction {
for (Card c : spires) { for (Card c : spires) {
// TODO: only do this for the AI, for the player part, get the encoded color from the deck file and pass // TODO: only do this for the AI, for the player part, get the encoded color from the deck file and pass
// it to either player or the papercard object so it feels like rule based for the player side.. // it to either player or the papercard object so it feels like rule based for the player side..
if (!c.hasChosenColorSpire()) { if (!c.hasMarkedColor()) {
if (takesAction.isAI()) { if (takesAction.isAI()) {
List<String> colorChoices = new ArrayList<>(MagicColor.Constant.ONLY_COLORS);
String prompt = CardTranslation.getTranslatedName(c.getName()) + ": " + String prompt = CardTranslation.getTranslatedName(c.getName()) + ": " +
Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(2)); Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(2));
SpellAbility sa = new SpellAbility.EmptySa(ApiType.ChooseColor, c, takesAction); SpellAbility sa = new SpellAbility.EmptySa(ApiType.ChooseColor, c, takesAction);
sa.putParam("AILogic", "MostProminentInComputerDeck"); sa.putParam("AILogic", "MostProminentInComputerDeck");
Set<String> chosenColors = new HashSet<>(takesAction.getController().chooseColors(prompt, sa, 2, 2, colorChoices)); ColorSet chosenColors = ColorSet.fromNames(takesAction.getController().chooseColors(prompt, sa, 2, 2, MagicColor.Constant.ONLY_COLORS));
c.setChosenColorID(chosenColors); c.setMarkedColors(chosenColors);
} }
} }
} }
@@ -2774,21 +2784,35 @@ public class GameAction {
* the source * the source
* @return true, if successful * @return true, if successful
*/ */
public static boolean attachAuraOnIndirectEnterBattlefield(final Card source, Map<AbilityKey, Object> params) { private boolean attachAuraOnIndirectETB(final Card source, Map<AbilityKey, Object> params) {
// When an Aura ETB without being cast you can choose a valid card to // When an Aura ETB without being cast you can choose a valid card to attach it to
// attach it to if (!source.hasKeyword(Keyword.ENCHANT)) {
final SpellAbility aura = source.getFirstAttachSpell(); return false;
}
SpellAbility aura = source.getCurrentState().getAuraSpell();
if (aura == null) { if (aura == null) {
return false; return false;
} }
aura.setActivatingPlayer(source.getController());
final Game game = source.getGame();
final TargetRestrictions tgt = aura.getTargetRestrictions();
Set<ZoneType> zones = EnumSet.noneOf(ZoneType.class);
boolean canTargetPlayer = false;
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
String o = ki.getOriginal();
String m[] = o.split(":");
String v = m[1];
if (v.contains("inZone")) { // currently the only other zone is Graveyard
zones.add(ZoneType.Graveyard);
} else {
zones.add(ZoneType.Battlefield);
}
if (v.startsWith("Player") || v.startsWith("Opponent")) {
canTargetPlayer = true;
}
}
Player p = source.getController(); Player p = source.getController();
if (tgt.canTgtPlayer()) { if (canTargetPlayer) {
final FCollection<Player> players = game.getPlayers().filter(PlayerPredicates.canBeAttached(source, aura)); final FCollection<Player> players = game.getPlayers().filter(PlayerPredicates.canBeAttached(source, null));
final Player pa = p.getController().chooseSingleEntityForEffect(players, aura, final Player pa = p.getController().chooseSingleEntityForEffect(players, aura,
Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", CardTranslation.getTranslatedName(source.getName())), null); Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", CardTranslation.getTranslatedName(source.getName())), null);
@@ -2797,9 +2821,7 @@ public class GameAction {
return true; return true;
} }
} else { } else {
List<ZoneType> zones = Lists.newArrayList(tgt.getZone());
CardCollection list = new CardCollection(); CardCollection list = new CardCollection();
if (params != null) { if (params != null) {
if (zones.contains(ZoneType.Battlefield)) { if (zones.contains(ZoneType.Battlefield)) {
list.addAll((CardCollectionView) params.get(AbilityKey.LastStateBattlefield)); list.addAll((CardCollectionView) params.get(AbilityKey.LastStateBattlefield));
@@ -2812,7 +2834,7 @@ public class GameAction {
} }
list.addAll(game.getCardsIn(zones)); list.addAll(game.getCardsIn(zones));
list = CardLists.filter(list, CardPredicates.canBeAttached(source, aura)); list = CardLists.filter(list, CardPredicates.canBeAttached(source, null));
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return false;
} }

View File

@@ -43,6 +43,7 @@ import forge.game.spellability.*;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAlternativeCost; import forge.game.staticability.StaticAbilityAlternativeCost;
import forge.game.staticability.StaticAbilityLayer; import forge.game.staticability.StaticAbilityLayer;
import forge.game.staticability.StaticAbilityMode;
import forge.game.zone.Zone; import forge.game.zone.Zone;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
@@ -183,6 +184,34 @@ public final class GameActionUtil {
flashback.setKeyword(inst); flashback.setKeyword(inst);
flashback.setIntrinsic(inst.isIntrinsic()); flashback.setIntrinsic(inst.isIntrinsic());
alternatives.add(flashback); alternatives.add(flashback);
} else if (keyword.startsWith("Harmonize")) {
if (!source.isInZone(ZoneType.Graveyard)) {
continue;
}
if (keyword.equals("Harmonize") && source.getManaCost().isNoCost()) {
continue;
}
SpellAbility harmonize = null;
if (keyword.contains(":")) {
final String[] k = keyword.split(":");
harmonize = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
String extraParams = k.length > 2 ? k[2] : "";
if (!extraParams.isEmpty()) {
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
harmonize.putParam(param.getKey(), param.getValue());
}
}
} else {
harmonize = sa.copy(activator);
}
harmonize.setAlternativeCost(AlternativeCost.Harmonize);
harmonize.getRestrictions().setZone(ZoneType.Graveyard);
harmonize.setKeyword(inst);
harmonize.setIntrinsic(inst.isIntrinsic());
alternatives.add(harmonize);
} else if (keyword.startsWith("Foretell")) { } else if (keyword.startsWith("Foretell")) {
// Foretell cast only from Exile // Foretell cast only from Exile
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() || if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() ||
@@ -390,7 +419,7 @@ public final class GameActionUtil {
costSources.addAll(game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)); costSources.addAll(game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES));
for (final Card ca : costSources) { for (final Card ca : costSources) {
for (final StaticAbility stAb : ca.getStaticAbilities()) { for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions("OptionalCost")) { if (!stAb.checkConditions(StaticAbilityMode.OptionalCost)) {
continue; continue;
} }
@@ -582,9 +611,8 @@ public final class GameActionUtil {
" or greater>"; " or greater>";
final Cost cost = new Cost(casualtyCost, false); final Cost cost = new Cost(casualtyCost, false);
String str = "Pay for Casualty? " + cost.toSimpleString(); String str = "Pay for Casualty? " + cost.toSimpleString();
boolean v = pc.addKeywordCost(sa, cost, ki, str);
if (v) { if (pc.addKeywordCost(sa, cost, ki, str)) {
if (result == null) { if (result == null) {
result = sa.copy(); result = sa.copy();
} }
@@ -630,9 +658,7 @@ public final class GameActionUtil {
final Cost cost = new Cost(k[1], false); final Cost cost = new Cost(k[1], false);
String str = "Pay for Offspring? " + cost.toSimpleString(); String str = "Pay for Offspring? " + cost.toSimpleString();
boolean v = pc.addKeywordCost(sa, cost, ki, str); if (pc.addKeywordCost(sa, cost, ki, str)) {
if (v) {
if (result == null) { if (result == null) {
result = sa.copy(); result = sa.copy();
} }
@@ -679,6 +705,25 @@ public final class GameActionUtil {
} }
} }
if (sa.isHarmonize()) {
CardCollectionView creatures = activator.getCreaturesInPlay();
if (!creatures.isEmpty()) {
int max = Aggregates.max(creatures, Card::getNetPower);
int n = pc.chooseNumber(sa, "Choose power of creature to tap", 0, max);
final String harmonizeCost = "tapXType<1/Creature.powerEQ" + n + "/creature for Harmonize>";
final Cost cost = new Cost(harmonizeCost, false);
if (pc.addKeywordCost(sa, cost, sa.getKeyword(), "Tap creature?")) {
if (result == null) {
result = sa.copy();
}
result.getPayCosts().add(cost);
reset = true;
result.setOptionalKeywordAmount(sa.getKeyword(), n);
}
}
}
if (host.isCreature()) { if (host.isCreature()) {
String kw = "As an additional cost to cast creature spells," + String kw = "As an additional cost to cast creature spells," +
" you may pay any amount of mana. If you do, that creature enters " + " you may pay any amount of mana. If you do, that creature enters " +
@@ -902,14 +947,16 @@ public final class GameActionUtil {
} }
if (fromZone != null && !fromZone.is(ZoneType.None)) { // and not a copy if (fromZone != null && !fromZone.is(ZoneType.None)) { // and not a copy
// add back to where it came from, hopefully old state
// skip GameAction
oldCard.getZone().remove(oldCard);
// might have been an alternative lki host // might have been an alternative lki host
oldCard = ability.getCardState().getCard(); oldCard = ability.getCardState().getCard();
oldCard.setCastSA(null); oldCard.setCastSA(null);
oldCard.setCastFrom(null); oldCard.setCastFrom(null);
// add back to where it came from, hopefully old state
// skip GameAction
oldCard.getZone().remove(oldCard);
// in some rare cases the old position no longer exists (Panglacial Wurm + Selvala) // in some rare cases the old position no longer exists (Panglacial Wurm + Selvala)
Integer newPosition = zonePosition >= 0 ? Math.min(zonePosition, fromZone.size()) : null; Integer newPosition = zonePosition >= 0 ? Math.min(zonePosition, fromZone.size()) : null;
fromZone.add(oldCard, newPosition, null, true); fromZone.add(oldCard, newPosition, null, true);

View File

@@ -37,11 +37,11 @@ import forge.game.card.CounterEnumType;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.event.GameEventCardAttachment; import forge.game.event.GameEventCardAttachment;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityCantAttach; import forge.game.staticability.StaticAbilityCantAttach;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -267,15 +267,18 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
} }
protected boolean canBeEnchantedBy(final Card aura) { protected boolean canBeEnchantedBy(final Card aura) {
// TODO need to check for multiple Enchant Keywords if (!aura.hasKeyword(Keyword.ENCHANT)) {
return false;
SpellAbility sa = aura.getFirstAttachSpell();
TargetRestrictions tgt = null;
if (sa != null) {
tgt = sa.getTargetRestrictions();
} }
for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) {
return tgt != null && isValid(tgt.getValidTgts(), aura.getController(), aura, sa); String k = ki.getOriginal();
String m[] = k.split(":");
String v = m[1];
if (!isValid(v.split(","), aura.getController(), aura, null)) {
return false;
}
}
return true;
} }
public boolean hasCounters() { public boolean hasCounters() {

View File

@@ -159,12 +159,17 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
} }
// Add ETB flag // Add ETB flag
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Cause, cause); runParams.put(AbilityKey.Cause, cause);
if (params != null) { if (params != null) {
runParams.putAll(params); runParams.putAll(params);
} }
boolean firstTime = false;
if (gm.getKey() instanceof Card c) {
firstTime = game.getCounterAddedThisTurn(null, c) == 0;
}
// Apply counter after replacement effect // Apply counter after replacement effect
for (Map.Entry<Optional<Player>, Map<CounterType, Integer>> e : values.entrySet()) { for (Map.Entry<Optional<Player>, Map<CounterType, Integer>> e : values.entrySet()) {
boolean remember = cause != null && cause.hasParam("RememberPut"); boolean remember = cause != null && cause.hasParam("RememberPut");
@@ -182,6 +187,13 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
} }
} }
} }
if (result.containsColumn(gm.getKey())) {
runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Object, gm.getKey());
runParams.put(AbilityKey.FirstTime, firstTime);
game.getTriggerHandler().runTrigger(TriggerType.CounterTypeAddedAll, runParams, false);
}
} }
int totalAdded = totalValues(); int totalAdded = totalValues();

View File

@@ -21,7 +21,7 @@ import com.google.common.collect.Lists;
import forge.StaticData; import forge.StaticData;
import forge.card.CardDb; import forge.card.CardDb;
import forge.card.CardEdition; import forge.card.CardEdition;
import forge.card.CardEdition.CardInSet; import forge.card.CardEdition.EditionEntry;
import forge.card.CardRarity; import forge.card.CardRarity;
import forge.deck.CardPool; import forge.deck.CardPool;
import forge.deck.Deck; import forge.deck.Deck;
@@ -156,7 +156,7 @@ public class GameFormat implements Comparable<GameFormat> {
for (CardRarity cr: this.getAllowedRarities()) { for (CardRarity cr: this.getAllowedRarities()) {
crp.add(StaticData.instance().getCommonCards().wasPrintedAtRarity(cr)); crp.add(StaticData.instance().getCommonCards().wasPrintedAtRarity(cr));
} }
p = p.and(IterableUtil.or(crp)); p = p.and(IterableUtil.<PaperCard>or(crp));
} }
if (!this.getAdditionalCards().isEmpty()) { if (!this.getAdditionalCards().isEmpty()) {
p = p.or(PaperCardPredicates.names(this.getAdditionalCards())); p = p.or(PaperCardPredicates.names(this.getAdditionalCards()));
@@ -226,9 +226,9 @@ public class GameFormat implements Comparable<GameFormat> {
for (String setCode : allowedSetCodes_ro) { for (String setCode : allowedSetCodes_ro) {
CardEdition edition = StaticData.instance().getEditions().get(setCode); CardEdition edition = StaticData.instance().getEditions().get(setCode);
if (edition != null) { if (edition != null) {
for (CardInSet card : edition.getAllCardsInSet()) { for (EditionEntry card : edition.getAllCardsInSet()) {
if (!bannedCardNames_ro.contains(card.name)) { if (!bannedCardNames_ro.contains(card.name())) {
PaperCard pc = commonCards.getCard(card.name, setCode, card.collectorNumber); PaperCard pc = commonCards.getCard(card.name(), setCode, card.collectorNumber());
if (pc != null) { if (pc != null) {
cards.add(pc); cards.add(pc);
} }

View File

@@ -170,7 +170,7 @@ public class GameSnapshot {
newPlayer.setDamageReceivedThisTurn(origPlayer.getDamageReceivedThisTurn()); newPlayer.setDamageReceivedThisTurn(origPlayer.getDamageReceivedThisTurn());
newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn()); newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn());
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters())); newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
newPlayer.setBlessing(origPlayer.hasBlessing()); newPlayer.setBlessing(origPlayer.hasBlessing(), null);
newPlayer.setRevolt(origPlayer.hasRevolt()); newPlayer.setRevolt(origPlayer.hasRevolt());
newPlayer.setLibrarySearched(origPlayer.getLibrarySearched()); newPlayer.setLibrarySearched(origPlayer.getLibrarySearched());
newPlayer.setSpellsCastLastTurn(origPlayer.getSpellsCastLastTurn()); newPlayer.setSpellsCastLastTurn(origPlayer.getSpellsCastLastTurn());
@@ -322,7 +322,7 @@ public class GameSnapshot {
newCard.setLayerTimestamp(fromCard.getLayerTimestamp()); newCard.setLayerTimestamp(fromCard.getLayerTimestamp());
newCard.setTapped(fromCard.isTapped()); newCard.setTapped(fromCard.isTapped());
newCard.setFaceDown(fromCard.isFaceDown()); newCard.setFaceDown(fromCard.isFaceDown());
newCard.setManifested(fromCard.isManifested()); newCard.setManifested(fromCard.getManifestedSA());
newCard.setSickness(fromCard.hasSickness()); newCard.setSickness(fromCard.hasSickness());
newCard.setState(fromCard.getCurrentStateName(), false); newCard.setState(fromCard.getCurrentStateName(), false);
} }

View File

@@ -22,7 +22,7 @@ public enum GameType {
Winston (DeckFormat.Limited, true, true, true, "lblWinston", ""), Winston (DeckFormat.Limited, true, true, true, "lblWinston", ""),
Gauntlet (DeckFormat.Constructed, false, true, true, "lblGauntlet", ""), Gauntlet (DeckFormat.Constructed, false, true, true, "lblGauntlet", ""),
Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""), Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""),
CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommander", "lblCommanderDesc"), CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommanderGauntlet", "lblCommanderDesc"),
Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""), Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""),
QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""), QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""),
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""), PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""),

View File

@@ -15,7 +15,6 @@ public interface IHasSVars {
//public Set<String> getSVars(); //public Set<String> getSVars();
public Map<String, String> getSVars(); public Map<String, String> getSVars();
public Map<String, String> getDirectSVars();
public void removeSVar(final String var); public void removeSVar(final String var);
} }

View File

@@ -180,26 +180,19 @@ public final class AbilityFactory {
} }
public static Cost parseAbilityCost(final CardState state, Map<String, String> mapParams, AbilityRecordType type) { public static Cost parseAbilityCost(final CardState state, Map<String, String> mapParams, AbilityRecordType type) {
Cost abCost = null; if (type == AbilityRecordType.SubAbility) {
if (type != AbilityRecordType.SubAbility) { return null;
String cost = mapParams.get("Cost"); }
if (cost == null) { String cost = mapParams.get("Cost");
if (type == AbilityRecordType.Spell) { if (cost != null) {
SpellAbility firstAbility = state.getFirstAbility(); return new Cost(cost, type == AbilityRecordType.Ability);
if (firstAbility != null && firstAbility.isSpell()) { }
// TODO might remove when Enchant Keyword is refactored if (type == AbilityRecordType.Spell) {
System.err.println(state.getName() + " already has Spell using mana cost"); // for a Spell if no Cost is used, use the card states ManaCost
} return new Cost(state.getManaCost(), false);
// for a Spell if no Cost is used, use the card states ManaCost } else {
abCost = new Cost(state.getManaCost(), false); throw new RuntimeException("AbilityFactory : getAbility -- no Cost in " + state.getName());
} else {
throw new RuntimeException("AbilityFactory : getAbility -- no Cost in " + state.getName());
}
} else {
abCost = new Cost(cost, type == AbilityRecordType.Ability);
}
} }
return abCost;
} }
public static SpellAbility getAbility(AbilityRecordType type, ApiType api, Map<String, String> mapParams, public static SpellAbility getAbility(AbilityRecordType type, ApiType api, Map<String, String> mapParams,
@@ -216,15 +209,6 @@ public final class AbilityFactory {
} }
} }
else if (api == ApiType.PermanentCreature || api == ApiType.PermanentNoncreature) {
// If API is a permanent type, and creating AF Spell
// Clear out the auto created SpellPermanent spell
if (type == AbilityRecordType.Spell
&& !mapParams.containsKey("SubAbility") && !mapParams.containsKey("NonBasicSpell")) {
hostCard.clearFirstSpell();
}
}
if (abCost == null) { if (abCost == null) {
abCost = parseAbilityCost(state, mapParams, type); abCost = parseAbilityCost(state, mapParams, type);
} }
@@ -510,8 +494,9 @@ public final class AbilityFactory {
AbilityRecordType leftType = AbilityRecordType.getRecordType(leftMap); AbilityRecordType leftType = AbilityRecordType.getRecordType(leftMap);
ApiType leftApi = leftType.getApiTypeOf(leftMap); ApiType leftApi = leftType.getApiTypeOf(leftMap);
leftMap.put("StackDescription", leftMap.get("SpellDescription")); leftMap.put("StackDescription", leftMap.get("SpellDescription"));
leftMap.put("SpellDescription", "Fuse (you may cast both halves of this card from your hand)."); leftMap.put("SpellDescription", "Fuse (You may cast one or both halves of this card from your hand.)");
leftMap.put("ActivationZone", "Hand"); leftMap.put("ActivationZone", "Hand");
leftMap.put("Secondary", "True");
CardState rightState = card.getState(CardStateName.RightSplit); CardState rightState = card.getState(CardStateName.RightSplit);
SpellAbility rightAbility = rightState.getFirstAbility(); SpellAbility rightAbility = rightState.getFirstAbility();
@@ -526,8 +511,10 @@ public final class AbilityFactory {
totalCost.add(parseAbilityCost(rightState, rightMap, rightType)); totalCost.add(parseAbilityCost(rightState, rightMap, rightType));
final SpellAbility left = getAbility(leftType, leftApi, leftMap, totalCost, leftState, leftState); final SpellAbility left = getAbility(leftType, leftApi, leftMap, totalCost, leftState, leftState);
left.setOriginalAbility(leftAbility);
left.setCardState(card.getState(CardStateName.Original)); left.setCardState(card.getState(CardStateName.Original));
final AbilitySub right = (AbilitySub) getAbility(AbilityRecordType.SubAbility, rightApi, rightMap, null, rightState, rightState); final AbilitySub right = (AbilitySub) getAbility(AbilityRecordType.SubAbility, rightApi, rightMap, null, rightState, rightState);
right.setOriginalAbility(rightAbility);
left.appendSubAbility(right); left.appendSubAbility(right);
return left; return left;
} }

View File

@@ -38,7 +38,6 @@ public enum AbilityKey {
Causer("Causer"), Causer("Causer"),
Championed("Championed"), Championed("Championed"),
ClassLevel("ClassLevel"), ClassLevel("ClassLevel"),
Cost("Cost"),
CostStack("CostStack"), CostStack("CostStack"),
CounterAmount("CounterAmount"), CounterAmount("CounterAmount"),
CounteredSA("CounteredSA"), CounteredSA("CounteredSA"),
@@ -62,6 +61,7 @@ public enum AbilityKey {
DefendingPlayer("DefendingPlayer"), DefendingPlayer("DefendingPlayer"),
Destination("Destination"), Destination("Destination"),
Devoured("Devoured"), Devoured("Devoured"),
DicePTExchanges("DicePTExchanges"),
Discard("Discard"), Discard("Discard"),
DiscardedBefore("DiscardedBefore"), DiscardedBefore("DiscardedBefore"),
DividedShieldAmount("DividedShieldAmount"), DividedShieldAmount("DividedShieldAmount"),
@@ -72,7 +72,6 @@ public enum AbilityKey {
Explored("Explored"), Explored("Explored"),
Explorer("Explorer"), Explorer("Explorer"),
ExtraTurn("ExtraTurn"), ExtraTurn("ExtraTurn"),
Event("Event"),
ETB("ETB"), ETB("ETB"),
Fighter("Fighter"), Fighter("Fighter"),
Fighters("Fighters"), Fighters("Fighters"),
@@ -94,8 +93,8 @@ public enum AbilityKey {
Mana("Mana"), Mana("Mana"),
MergedCards("MergedCards"), MergedCards("MergedCards"),
Mode("Mode"), Mode("Mode"),
Modifier("Modifier"),
MonstrosityAmount("MonstrosityAmount"), MonstrosityAmount("MonstrosityAmount"),
NaturalResult("NaturalResult"),
NewCard("NewCard"), NewCard("NewCard"),
NewCounterAmount("NewCounterAmount"), NewCounterAmount("NewCounterAmount"),
NoPreventDamage("NoPreventDamage"), NoPreventDamage("NoPreventDamage"),

View File

@@ -1366,7 +1366,7 @@ public class AbilityUtils {
// do blessing there before condition checks // do blessing there before condition checks
if (source.hasKeyword(Keyword.ASCEND) && controller.getZone(ZoneType.Battlefield).size() >= 10) { if (source.hasKeyword(Keyword.ASCEND) && controller.getZone(ZoneType.Battlefield).size() >= 10) {
controller.setBlessing(true); controller.setBlessing(true, source.getSetCode());
} }
if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) { if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) {
@@ -1621,7 +1621,8 @@ public class AbilityUtils {
final String[] sq; final String[] sq;
sq = l[0].split("\\."); sq = l[0].split("\\.");
String[] paidparts = l[0].split("\\$", 2);
Iterable<Card> someCards = null;
final Game game = c.getGame(); final Game game = c.getGame();
if (ctb != null) { if (ctb != null) {
@@ -1786,11 +1787,10 @@ public class AbilityUtils {
} }
// Count$NumTimesChoseMode // Count$NumTimesChoseMode
if (sq[0].startsWith("NumTimesChoseMode")) { if (sq[0].startsWith("NumTimesChoseMode")) {
SpellAbility sub = sa.getRootAbility();
int amount = 0; int amount = 0;
while (sub != null) { SpellAbility tail = sa.getTailAbility();
if (sub.getDirectSVars().containsKey("CharmOrder")) amount++; if (tail.hasSVar("CharmOrder")) {
sub = sub.getSubAbility(); amount = tail.getSVarInt("CharmOrder");
} }
return doXMath(amount, expr, c, ctb); return doXMath(amount, expr, c, ctb);
} }
@@ -1809,27 +1809,25 @@ public class AbilityUtils {
} }
if (sq[0].startsWith("LastStateBattlefield")) { if (sq[0].startsWith("LastStateBattlefield")) {
final String[] k = l[0].split(" "); final String[] k = paidparts[0].split(" ");
CardCollectionView list;
// this is only for spells that were cast // this is only for spells that were cast
if (sq[0].contains("WithFallback")) { if (sq[0].contains("WithFallback")) {
if (!sa.getHostCard().wasCast()) { if (!sa.getHostCard().wasCast()) {
return doXMath(0, expr, c, ctb); return doXMath(0, expr, c, ctb);
} }
list = sa.getHostCard().getCastSA().getLastStateBattlefield(); someCards = sa.getHostCard().getCastSA().getLastStateBattlefield();
} else { } else {
list = sa.getLastStateBattlefield(); someCards = sa.getLastStateBattlefield();
} }
if (list == null || list.isEmpty()) { if (someCards == null || Iterables.isEmpty(someCards)) {
// LastState is Empty // LastState is Empty
if (sq[0].contains("WithFallback")) { if (sq[0].contains("WithFallback")) {
list = game.getCardsIn(ZoneType.Battlefield); someCards = game.getCardsIn(ZoneType.Battlefield);
} else { } else {
return doXMath(0, expr, c, ctb); return doXMath(0, expr, c, ctb);
} }
} }
list = CardLists.getValidCards(list, k[1], player, c, sa); someCards = CardLists.getValidCards(someCards, k[1], player, c, sa);
return doXMath(list.size(), expr, c, ctb);
} }
if (sq[0].startsWith("LastStateGraveyard")) { if (sq[0].startsWith("LastStateGraveyard")) {
@@ -1856,6 +1854,10 @@ public class AbilityUtils {
return doXMath(list.size(), expr, c, ctb); return doXMath(list.size(), expr, c, ctb);
} }
if (sq[0].equals("ActivatedThisGame")) {
return doXMath(sa.getActivationsThisGame(), expr, c, ctb);
}
if (sq[0].equals("ResolvedThisTurn")) { if (sq[0].equals("ResolvedThisTurn")) {
return doXMath(sa.getResolvedThisTurn(), expr, c, ctb); return doXMath(sa.getResolvedThisTurn(), expr, c, ctb);
} }
@@ -1956,9 +1958,6 @@ public class AbilityUtils {
return doXMath(sum, expr, c, ctb); return doXMath(sum, expr, c, ctb);
} }
String[] paidparts = l[0].split("\\$", 2);
Iterable<Card> someCards = null;
// count valid cards in any specified zone/s // count valid cards in any specified zone/s
if (sq[0].startsWith("Valid")) { if (sq[0].startsWith("Valid")) {
String[] lparts = paidparts[0].split(" ", 2); String[] lparts = paidparts[0].split(" ", 2);
@@ -2217,7 +2216,7 @@ public class AbilityUtils {
// Count$IfCastInOwnMainPhase.<numMain>.<numNotMain> // 7/10 // Count$IfCastInOwnMainPhase.<numMain>.<numNotMain> // 7/10
if (sq[0].contains("IfCastInOwnMainPhase")) { if (sq[0].contains("IfCastInOwnMainPhase")) {
final PhaseHandler cPhase = game.getPhaseHandler(); final PhaseHandler cPhase = game.getPhaseHandler();
final boolean isMyMain = cPhase.getPhase().isMain() && cPhase.isPlayerTurn(player) && c.getCastFrom() != null; final boolean isMyMain = cPhase.getPhase().isMain() && cPhase.isPlayerTurn(player) && c.wasCast();
return doXMath(Integer.parseInt(sq[isMyMain ? 1 : 2]), expr, c, ctb); return doXMath(Integer.parseInt(sq[isMyMain ? 1 : 2]), expr, c, ctb);
} }
@@ -2226,6 +2225,11 @@ public class AbilityUtils {
return doXMath(game.getPhaseHandler().getNumUpkeep() - (game.getPhaseHandler().is(PhaseType.UPKEEP) ? 1 : 0), expr, c, ctb); return doXMath(game.getPhaseHandler().getNumUpkeep() - (game.getPhaseHandler().is(PhaseType.UPKEEP) ? 1 : 0), expr, c, ctb);
} }
// Count$FinishedEndOfTurnsThisTurn
if (sq[0].startsWith("FinishedEndOfTurnsThisTurn")) {
return doXMath(game.getPhaseHandler().getNumEndOfTurn() - (game.getPhaseHandler().is(PhaseType.END_OF_TURN) ? 1 : 0), expr, c, ctb);
}
// Count$AttachedTo <restriction> // Count$AttachedTo <restriction>
if (sq[0].startsWith("AttachedTo")) { if (sq[0].startsWith("AttachedTo")) {
final String[] k = l[0].split(" "); final String[] k = l[0].split(" ");
@@ -2268,6 +2272,9 @@ public class AbilityUtils {
if (sq[0].equals("Delirium")) { if (sq[0].equals("Delirium")) {
return doXMath(calculateAmount(c, sq[player.hasDelirium() ? 1 : 2], ctb), expr, c, ctb); return doXMath(calculateAmount(c, sq[player.hasDelirium() ? 1 : 2], ctb), expr, c, ctb);
} }
if (sq[0].equals("MaxSpeed")) {
return doXMath(calculateAmount(c, sq[player.maxSpeed() ? 1 : 2], ctb), expr, c, ctb);
}
if (sq[0].equals("FatefulHour")) { if (sq[0].equals("FatefulHour")) {
return doXMath(calculateAmount(c, sq[player.getLife() <= 5 ? 1 : 2], ctb), expr, c, ctb); return doXMath(calculateAmount(c, sq[player.getLife() <= 5 ? 1 : 2], ctb), expr, c, ctb);
} }
@@ -2317,6 +2324,10 @@ public class AbilityUtils {
return doXMath(player.getNumDrawnLastTurn(), expr, c, ctb); return doXMath(player.getNumDrawnLastTurn(), expr, c, ctb);
} }
if (sq[0].equals("YouFlipThisTurn")) {
return doXMath(player.getNumFlipsThisTurn(), expr, c, ctb);
}
if (sq[0].equals("YouRollThisTurn")) { if (sq[0].equals("YouRollThisTurn")) {
return doXMath(player.getNumRollsThisTurn(), expr, c, ctb); return doXMath(player.getNumRollsThisTurn(), expr, c, ctb);
} }
@@ -2402,6 +2413,10 @@ public class AbilityUtils {
return doXMath(player.getMaxOpponentAssignedDamage(), expr, c, ctb); return doXMath(player.getMaxOpponentAssignedDamage(), expr, c, ctb);
} }
if (sq[0].equals("MaxCombatDamageThisTurn")) {
return doXMath(player.getMaxAssignedCombatDamage(), expr, c, ctb);
}
if (sq[0].contains("TotalDamageThisTurn")) { if (sq[0].contains("TotalDamageThisTurn")) {
String[] props = l[0].split(" "); String[] props = l[0].split(" ");
int sum = 0; int sum = 0;
@@ -2469,7 +2484,6 @@ public class AbilityUtils {
// But these aren't really things you count so they'll show up in properties most likely // But these aren't really things you count so they'll show up in properties most likely
} }
//Count$TypesSharedWith [defined] //Count$TypesSharedWith [defined]
if (sq[0].startsWith("TypesSharedWith")) { if (sq[0].startsWith("TypesSharedWith")) {
Set<CardType.CoreType> thisTypes = Sets.newHashSet(c.getType().getCoreTypes()); Set<CardType.CoreType> thisTypes = Sets.newHashSet(c.getType().getCoreTypes());
@@ -2699,24 +2713,6 @@ public class AbilityUtils {
return doXMath(calculateAmount(c, sq[res.size() > 0 ? 1 : 2], ctb), expr, c, ctb); return doXMath(calculateAmount(c, sq[res.size() > 0 ? 1 : 2], ctb), expr, c, ctb);
} }
if (sq[0].startsWith("CreatureType")) {
String[] sqparts = l[0].split(" ", 2);
final String[] rest = sqparts[1].split(",");
final CardCollectionView cardsInZones = sqparts[0].length() > 12
? game.getCardsIn(ZoneType.listValueOf(sqparts[0].substring(12)))
: game.getCardsIn(ZoneType.Battlefield);
CardCollection cards = CardLists.getValidCards(cardsInZones, rest, player, c, ctb);
final Set<String> creatTypes = Sets.newHashSet();
for (Card card : cards) {
creatTypes.addAll(card.getType().getCreatureTypes());
}
// filter out fun types?
return doXMath(creatTypes.size(), expr, c, ctb);
}
// Count$Chroma.<color name> // Count$Chroma.<color name>
if (sq[0].startsWith("Chroma")) { if (sq[0].startsWith("Chroma")) {
final CardCollectionView cards; final CardCollectionView cards;
@@ -2775,16 +2771,6 @@ public class AbilityUtils {
return game.getPhaseHandler().getPlanarDiceSpecialActionThisTurn(); return game.getPhaseHandler().getPlanarDiceSpecialActionThisTurn();
} }
if (sq[0].equals("AllTypes")) {
List<Card> cards = getDefinedCards(c, sq[1], ctb);
int amount = countCardTypesFromList(cards, false) +
countSuperTypesFromList(cards) +
countSubTypesFromList(cards);
return doXMath(amount, expr, c, ctb);
}
if (sq[0].equals("TotalTurns")) { if (sq[0].equals("TotalTurns")) {
return doXMath(game.getPhaseHandler().getTurn(), expr, c, ctb); return doXMath(game.getPhaseHandler().getTurn(), expr, c, ctb);
} }
@@ -2930,18 +2916,6 @@ public class AbilityUtils {
return doXMath(colorSize[colorSize.length - 2], expr, c, ctb); return doXMath(colorSize[colorSize.length - 2], expr, c, ctb);
} }
if (sq[0].startsWith("ColorsCtrl")) {
final String restriction = l[0].substring(11);
final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
return doXMath(CardUtil.getColorsFromCards(list).countColors(), expr, c, ctb);
}
if (sq[0].startsWith("ColorsDefined")) {
final String restriction = l[0].substring(14);
final CardCollection list = getDefinedCards(c, restriction, ctb);
return doXMath(CardUtil.getColorsFromCards(list).countColors(), expr, c, ctb);
}
// TODO move below to handlePaid // TODO move below to handlePaid
if (sq[0].startsWith("SumPower")) { if (sq[0].startsWith("SumPower")) {
final String[] restrictions = l[0].split("_"); final String[] restrictions = l[0].split("_");
@@ -3426,17 +3400,14 @@ public class AbilityUtils {
return doXMath(numTied, m, source, ctb); return doXMath(numTied, m, source, ctb);
} }
final String[] sq;
sq = l[0].split("\\.");
// the number of players passed in // the number of players passed in
if (sq[0].equals("Amount")) { if (l[0].equals("Amount")) {
return doXMath(players.size(), m, source, ctb); return doXMath(players.size(), m, source, ctb);
} }
if (sq[0].startsWith("HasProperty")) { if (l[0].startsWith("HasProperty")) {
int totPlayer = 0; int totPlayer = 0;
String property = sq[0].substring(11); String property = l[0].substring(11);
for (Player p : players) { for (Player p : players) {
if (p.hasProperty(property, controller, source, ctb)) { if (p.hasProperty(property, controller, source, ctb)) {
totPlayer++; totPlayer++;
@@ -3460,7 +3431,7 @@ public class AbilityUtils {
return doXMath(totPlayer, m, source, ctb); return doXMath(totPlayer, m, source, ctb);
} }
if (sq[0].contains("DamageThisTurn")) { if (l[0].contains("DamageThisTurn")) {
int totDmg = 0; int totDmg = 0;
for (Player p : players) { for (Player p : players) {
totDmg += p.getAssignedDamage(); totDmg += p.getAssignedDamage();
@@ -3788,6 +3759,10 @@ public class AbilityUtils {
return Aggregates.max(paidList, Card::getCMC); return Aggregates.max(paidList, Card::getCMC);
} }
if (string.equals("Colors")) {
return CardUtil.getColorsFromCards(paidList).countColors();
}
if (string.equals("DifferentColorPair")) { if (string.equals("DifferentColorPair")) {
final Set<ColorSet> diffPair = new HashSet<>(); final Set<ColorSet> diffPair = new HashSet<>();
for (final Card card : paidList) { for (final Card card : paidList) {
@@ -3817,10 +3792,25 @@ public class AbilityUtils {
return doXMath(num, splitString.length > 1 ? splitString[1] : null, source, ctb); return doXMath(num, splitString.length > 1 ? splitString[1] : null, source, ctb);
} }
if (string.startsWith("AllTypes")) {
return countCardTypesFromList(paidList, false) +
countSuperTypesFromList(paidList) +
countSubTypesFromList(paidList);
}
if (string.startsWith("CardTypes")) { if (string.startsWith("CardTypes")) {
return doXMath(countCardTypesFromList(paidList, string.startsWith("CardTypesPermanent")), CardFactoryUtil.extractOperators(string), source, ctb); return doXMath(countCardTypesFromList(paidList, string.startsWith("CardTypesPermanent")), CardFactoryUtil.extractOperators(string), source, ctb);
} }
if (string.startsWith("CreatureType")) {
final Set<String> creatTypes = Sets.newHashSet();
for (Card card : paidList) {
creatTypes.addAll(card.getType().getCreatureTypes());
}
// filter out fun types?
return doXMath(creatTypes.size(), CardFactoryUtil.extractOperators(string), source, ctb);
}
String filteredString = string; String filteredString = string;
Iterable<Card> filteredList = paidList; Iterable<Card> filteredList = paidList;
final String[] filter = filteredString.split("_"); final String[] filter = filteredString.split("_");

View File

@@ -85,12 +85,14 @@ public enum ApiType {
Encode (EncodeEffect.class), Encode (EncodeEffect.class),
EndCombatPhase (EndCombatPhaseEffect.class), EndCombatPhase (EndCombatPhaseEffect.class),
EndTurn (EndTurnEffect.class), EndTurn (EndTurnEffect.class),
Endure (EndureEffect.class),
ExchangeLife (LifeExchangeEffect.class), ExchangeLife (LifeExchangeEffect.class),
ExchangeLifeVariant (LifeExchangeVariantEffect.class), ExchangeLifeVariant (LifeExchangeVariantEffect.class),
ExchangeControl (ControlExchangeEffect.class), ExchangeControl (ControlExchangeEffect.class),
ExchangeControlVariant (ControlExchangeVariantEffect.class), ExchangeControlVariant (ControlExchangeVariantEffect.class),
ExchangePower (PowerExchangeEffect.class), ExchangePower (PowerExchangeEffect.class),
ExchangeZone (ZoneExchangeEffect.class), ExchangeZone (ZoneExchangeEffect.class),
ExchangeTextBox (TextBoxExchangeEffect.class),
Explore (ExploreEffect.class), Explore (ExploreEffect.class),
Fight (FightEffect.class), Fight (FightEffect.class),
FlipACoin (FlipCoinEffect.class), FlipACoin (FlipCoinEffect.class),

View File

@@ -83,8 +83,8 @@ public abstract class SpellAbilityEffect {
if ("SpellDescription".equalsIgnoreCase(stackDesc)) { if ("SpellDescription".equalsIgnoreCase(stackDesc)) {
if (params.containsKey("SpellDescription")) { if (params.containsKey("SpellDescription")) {
String rawSDesc = params.get("SpellDescription"); String rawSDesc = params.get("SpellDescription");
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replaceAll(",,,,,,", " "); if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replace(",,,,,,", " ");
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replaceAll(",,,", " "); if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replace(",,,", " ");
String spellDesc = CardTranslation.translateSingleDescriptionText(rawSDesc, sa.getHostCard()); String spellDesc = CardTranslation.translateSingleDescriptionText(rawSDesc, sa.getHostCard());
//trim reminder text from StackDesc //trim reminder text from StackDesc
@@ -356,6 +356,7 @@ public abstract class SpellAbilityEffect {
boolean intrinsic = sa.isIntrinsic(); boolean intrinsic = sa.isIntrinsic();
boolean your = location.startsWith("Your"); boolean your = location.startsWith("Your");
boolean combat = location.endsWith("Combat"); boolean combat = location.endsWith("Combat");
boolean upkeep = location.endsWith("Upkeep");
String desc = sa.getParamOrDefault("AtEOTDesc", ""); String desc = sa.getParamOrDefault("AtEOTDesc", "");
@@ -365,11 +366,16 @@ public abstract class SpellAbilityEffect {
if (combat) { if (combat) {
location = location.substring(0, location.length() - "Combat".length()); location = location.substring(0, location.length() - "Combat".length());
} }
if (upkeep) {
location = location.substring(0, location.length() - "Upkeep".length());
}
if (desc.isEmpty()) { if (desc.isEmpty()) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (location.equals("Hand")) { if (location.equals("Hand")) {
sb.append("Return "); sb.append("Return ");
} else if (location.equals("Library")) {
sb.append("Shuffle ");
} else if (location.equals("SacrificeCtrl")) { } else if (location.equals("SacrificeCtrl")) {
sb.append("Its controller sacrifices "); sb.append("Its controller sacrifices ");
} else { } else {
@@ -378,6 +384,8 @@ public abstract class SpellAbilityEffect {
sb.append(Lang.joinHomogenous(crds)); sb.append(Lang.joinHomogenous(crds));
if (location.equals("Hand")) { if (location.equals("Hand")) {
sb.append(" to your hand"); sb.append(" to your hand");
} else if (location.equals("Library")) {
sb.append(" into your library");
} }
sb.append(" at the "); sb.append(" at the ");
if (combat) { if (combat) {
@@ -385,14 +393,18 @@ public abstract class SpellAbilityEffect {
} else { } else {
sb.append("beginning of "); sb.append("beginning of ");
sb.append(your ? "your" : "the"); sb.append(your ? "your" : "the");
sb.append(" next end step."); if (upkeep) {
sb.append(" next upkeep.");
} else {
sb.append(" next end step.");
}
} }
desc = sb.toString(); desc = sb.toString();
} }
StringBuilder delTrig = new StringBuilder(); StringBuilder delTrig = new StringBuilder();
delTrig.append("Mode$ Phase | Phase$ "); delTrig.append("Mode$ Phase | Phase$ ");
delTrig.append(combat ? "EndCombat " : "End Of Turn "); delTrig.append(combat ? "EndCombat " : upkeep ? "Upkeep" : "End Of Turn ");
if (your) { if (your) {
delTrig.append("| ValidPlayer$ You "); delTrig.append("| ValidPlayer$ You ");
@@ -410,6 +422,8 @@ public abstract class SpellAbilityEffect {
String trigSA = ""; String trigSA = "";
if (location.equals("Hand")) { if (location.equals("Hand")) {
trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRememberedLKI | Origin$ Battlefield | Destination$ Hand"; trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRememberedLKI | Origin$ Battlefield | Destination$ Hand";
} else if (location.equals("Library")) {
trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRememberedLKI | Origin$ Battlefield | Destination$ Library | Shuffle$ True";
} else if (location.equals("SacrificeCtrl")) { } else if (location.equals("SacrificeCtrl")) {
trigSA = "DB$ SacrificeAll | Defined$ DelayTriggerRememberedLKI"; trigSA = "DB$ SacrificeAll | Defined$ DelayTriggerRememberedLKI";
} else if (location.equals("Sacrifice")) { } else if (location.equals("Sacrifice")) {
@@ -767,7 +781,11 @@ public abstract class SpellAbilityEffect {
return combatChanged; return combatChanged;
} }
protected static GameCommand untilHostLeavesPlayCommand(final CardZoneTable triggerList, final SpellAbility sa) { protected static void changeZoneUntilCommand(final CardZoneTable triggerList, final SpellAbility sa) {
if (!sa.hasParam("Duration")) {
return;
}
final Card hostCard = sa.getHostCard(); final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame(); final Game game = hostCard.getGame();
hostCard.addUntilLeavesBattlefield(triggerList.allCards()); hostCard.addUntilLeavesBattlefield(triggerList.allCards());
@@ -782,7 +800,7 @@ public abstract class SpellAbilityEffect {
lki = null; lki = null;
} }
return new GameCommand() { GameCommand gc = new GameCommand() {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -837,6 +855,13 @@ public abstract class SpellAbilityEffect {
} }
}; };
// corner case can lead to host exiling itself during the effect
if (sa.getParam("Duration").contains("UntilHostLeavesPlay") && !hostCard.isInPlay()) {
gc.run();
} else {
addUntilCommand(sa, gc);
}
} }
protected static void discard(SpellAbility sa, final boolean effect, Map<Player, CardCollectionView> discardedMap, Map<AbilityKey, Object> params) { protected static void discard(SpellAbility sa, final boolean effect, Map<Player, CardCollectionView> discardedMap, Map<AbilityKey, Object> params) {
@@ -897,6 +922,8 @@ public abstract class SpellAbilityEffect {
} else { } else {
game.getUpkeep().addUntilEnd(controller, until); game.getUpkeep().addUntilEnd(controller, until);
} }
} else if ("UntilTheEndOfYourNextUntap".equals(duration)) {
game.getUntap().addUntilEnd(controller, until);
} else if ("UntilNextEndStep".equals(duration)) { } else if ("UntilNextEndStep".equals(duration)) {
game.getEndOfTurn().addAt(until); game.getEndOfTurn().addAt(until);
} else if ("UntilYourNextEndStep".equals(duration)) { } else if ("UntilYourNextEndStep".equals(duration)) {
@@ -997,8 +1024,9 @@ public abstract class SpellAbilityEffect {
return true; return true;
} }
public static Player getNewChooser(final SpellAbility sa, final Player activator, final Player loser) { public static Player getNewChooser(final SpellAbility sa, final Player loser) {
// CR 800.4g // CR 800.4g
final Player activator = sa.getActivatingPlayer();
final PlayerCollection options; final PlayerCollection options;
if (loser.isOpponentOf(activator)) { if (loser.isOpponentOf(activator)) {
options = activator.getOpponents(); options = activator.getOpponents();

View File

@@ -165,14 +165,8 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
// remove abilities // remove abilities
final List<SpellAbility> removedAbilities = Lists.newArrayList(); final List<SpellAbility> removedAbilities = Lists.newArrayList();
boolean clearSpells = sa.hasParam("OverwriteSpells"); if (sa.hasParam("RemoveThisAbility")) {
removedAbilities.add(sa.getOriginalAbility());
if (clearSpells) {
removedAbilities.addAll(Lists.newArrayList(c.getSpells()));
}
if (sa.hasParam("RemoveThisAbility") && !removedAbilities.contains(sa)) {
removedAbilities.add(sa);
} }
// give abilities // give abilities
@@ -252,9 +246,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
} }
if (!"Permanent".equals(duration) && !perpetual) { if (!"Permanent".equals(duration) && !perpetual) {
if ("UntilControllerNextUntap".equals(duration)) { if ("UntilAnimatedFaceup".equals(duration)) {
game.getUntap().addUntil(c.getController(), unanimate);
} else if ("UntilAnimatedFaceup".equals(duration)) {
c.addFaceupCommand(unanimate); c.addFaceupCommand(unanimate);
} else { } else {
addUntilCommand(sa, unanimate); addUntilCommand(sa, unanimate);

View File

@@ -39,7 +39,7 @@ public class AscendEffect extends SpellAbilityEffect {
} }
// Player need 10+ permanents on the battlefield // Player need 10+ permanents on the battlefield
if (p.getZone(ZoneType.Battlefield).size() >= 10) { if (p.getZone(ZoneType.Battlefield).size() >= 10) {
p.setBlessing(true); p.setBlessing(true, sa.getOriginalHost().getSetCode());
} }
} }
} }

View File

@@ -24,7 +24,7 @@ public class BecomeMonarchEffect extends SpellAbilityEffect {
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
// TODO: improve ai and fix corner cases // TODO: improve ai and fix corner cases
final String set = sa.getHostCard().getSetCode(); final String set = sa.getOriginalHost().getSetCode();
for (final Player p : getTargetPlayers(sa)) { for (final Player p : getTargetPlayers(sa)) {
if (!p.isInGame()) { if (!p.isInGame()) {

View File

@@ -186,10 +186,12 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
triggerList.triggerChangesZoneAll(game, sa); triggerList.triggerChangesZoneAll(game, sa);
if (sa.hasParam("Duration")) { if (sa.hasParam("AtEOT") && !triggerList.isEmpty()) {
addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, sa)); registerDelayedTrigger(sa, sa.getParam("AtEOT"), triggerList.allCards());
} }
changeZoneUntilCommand(triggerList, sa);
// CR 701.20d If an effect would cause a player to shuffle a set of objects into a library, // CR 701.20d If an effect would cause a player to shuffle a set of objects into a library,
// that library is shuffled even if there are no objects in that set. // that library is shuffled even if there are no objects in that set.
if (sa.hasParam("Shuffle")) { if (sa.hasParam("Shuffle")) {

View File

@@ -663,9 +663,24 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
} }
if (sa.isKeyword(Keyword.UNEARTH) && movedCard.isInPlay()) { if (sa.isKeyword(Keyword.UNEARTH) && movedCard.isInPlay()) {
movedCard.setUnearthed(true); movedCard.setUnearthed(true);
movedCard.addChangedCardKeywords(Lists.newArrayList("Haste"), null, false, game.getNextTimestamp(), null);
final Card eff = createEffect(sa, sa.getActivatingPlayer(), "Unearth Effect", hostCard.getImageKey());
// It gains haste.
String s = "Mode$ Continuous | Affected$ Card.IsRemembered | EffectZone$ Command | AddKeyword$ Haste";
eff.addStaticAbility(s);
// If it would leave the battlefield, exile it instead of putting it anywhere else.
addLeaveBattlefieldReplacement(eff, "Exile");
eff.addRemembered(movedCard);
movedCard.addLeavesPlayCommand(exileEffectCommand(game, eff));
game.getAction().moveToCommand(eff, sa);
// Exile it at the beginning of the next end step.
registerDelayedTrigger(sa, "Exile", Lists.newArrayList(movedCard)); registerDelayedTrigger(sa, "Exile", Lists.newArrayList(movedCard));
addLeaveBattlefieldReplacement(movedCard, sa, "Exile");
} }
if (sa.hasParam("LeaveBattlefield")) { if (sa.hasParam("LeaveBattlefield")) {
addLeaveBattlefieldReplacement(movedCard, sa, sa.getParam("LeaveBattlefield")); addLeaveBattlefieldReplacement(movedCard, sa, sa.getParam("LeaveBattlefield"));
@@ -732,8 +747,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
if (sa.hasParam("ForetoldCost")) { if (sa.hasParam("ForetoldCost")) {
movedCard.setForetoldCostByEffect(true); movedCard.setForetoldCostByEffect(true);
} }
// look at the exiled card }
movedCard.addMayLookTemp(activator); // look at the exiled card
if (sa.hasParam("WithMayLook") || sa.hasParam("Foretold")) {
movedCard.addMayLookFaceDownExile(activator);
} }
// CR 400.7k // CR 400.7k
@@ -817,9 +834,8 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
if (sa.hasParam("AtEOT") && !triggerList.isEmpty()) { if (sa.hasParam("AtEOT") && !triggerList.isEmpty()) {
registerDelayedTrigger(sa, sa.getParam("AtEOT"), triggerList.allCards()); registerDelayedTrigger(sa, sa.getParam("AtEOT"), triggerList.allCards());
} }
if ("UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, sa)); changeZoneUntilCommand(triggerList, sa);
}
// might set after card is moved again if something has changed // might set after card is moved again if something has changed
if (destination.equals(ZoneType.Exile)) { if (destination.equals(ZoneType.Exile)) {
@@ -1024,6 +1040,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
handleCastWhileSearching(fetchList, decider); handleCastWhileSearching(fetchList, decider);
} }
if (sa.hasParam("RememberSearched")) {
source.addRemembered(player);
}
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(decider); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(decider);
runParams.put(AbilityKey.Target, player); runParams.put(AbilityKey.Target, player);
game.getTriggerHandler().runTrigger(TriggerType.SearchedLibrary, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.SearchedLibrary, runParams, false);
@@ -1129,7 +1148,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
} }
} }
// If we're choosing multiple cards, only need to show the reveal dialog the first time through. // If we're choosing multiple cards, only need to show the reveal dialog the first time through.
boolean shouldReveal = (i == 0); boolean shouldReveal = (i == 0);
Card c = null; Card c = null;
@@ -1364,8 +1382,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
if (sa.hasParam("ForetoldCost")) { if (sa.hasParam("ForetoldCost")) {
movedCard.setForetoldCostByEffect(true); movedCard.setForetoldCostByEffect(true);
} }
// look at the exiled card }
movedCard.addMayLookTemp(sa.getActivatingPlayer());
// look at the exiled card
if (sa.hasParam("WithMayLook") || sa.hasParam("Foretold")) {
movedCard.addMayLookFaceDownExile(sa.getActivatingPlayer());
} }
} }
else { else {
@@ -1461,9 +1482,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
} }
triggerList.triggerChangesZoneAll(game, sa); triggerList.triggerChangesZoneAll(game, sa);
if ("UntilHostLeavesPlay".equals(sa.getParam("Duration"))) { changeZoneUntilCommand(triggerList, sa);
addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, sa));
}
} }
private void handleCastWhileSearching(final CardCollection fetchList, final Player decider) { private void handleCastWhileSearching(final CardCollection fetchList, final Player decider) {

View File

@@ -11,6 +11,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -45,7 +46,7 @@ public class CharmEffect extends SpellAbilityEffect {
choices.removeAll(toRemove); choices.removeAll(toRemove);
} }
int indx = 0; int indx = 1;
// set CharmOrder // set CharmOrder
for (AbilitySub sub : choices) { for (AbilitySub sub : choices) {
sub.setSVar("CharmOrder", Integer.toString(indx)); sub.setSVar("CharmOrder", Integer.toString(indx));
@@ -89,12 +90,13 @@ public class CharmEffect extends SpellAbilityEffect {
boolean limit = sa.hasParam("ActivationLimit"); boolean limit = sa.hasParam("ActivationLimit");
boolean gameLimit = sa.hasParam("GameActivationLimit"); boolean gameLimit = sa.hasParam("GameActivationLimit");
boolean oppChooses = "Opponent".equals(sa.getParam("Chooser")); boolean oppChooses = "Opponent".equals(sa.getParam("Chooser"));
boolean spree = sa.hasParam("Spree"); boolean spree = source.hasKeyword(Keyword.SPREE);
boolean tiered = source.hasKeyword(Keyword.TIERED);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(sa.getCostDescription()); sb.append(sa.getCostDescription());
if (!spree) { if (!spree && !tiered) {
sb.append(oppChooses ? "An opponent chooses " : "Choose "); sb.append(oppChooses ? "An opponent chooses " : "Choose ");
if (isX) { if (isX) {
sb.append(sa.hasParam("MinCharmNum") && min == 0 ? "up to " : "").append("X"); sb.append(sa.hasParam("MinCharmNum") && min == 0 ? "up to " : "").append("X");
@@ -163,7 +165,7 @@ public class CharmEffect extends SpellAbilityEffect {
if (!includeChosen) { if (!includeChosen) {
sb.append(num == 1 ? " mode." : " modes."); sb.append(num == 1 ? " mode." : " modes.");
} else if (!list.isEmpty()) { } else if (!list.isEmpty()) {
if (!spree) { if (!spree && !tiered) {
if (!repeat && !additionalDesc && !limit && !gameLimit) { if (!repeat && !additionalDesc && !limit && !gameLimit) {
sb.append(" \u2014"); sb.append(" \u2014");
} }
@@ -171,7 +173,10 @@ public class CharmEffect extends SpellAbilityEffect {
} }
for (AbilitySub sub : list) { for (AbilitySub sub : list) {
if (spree) { if (spree) {
sb.append("+ " + new Cost(sub.getParam("SpreeCost"), false).toSimpleString() + " \u2014 "); sb.append("+ " + new Cost(sub.getParam("ModeCost"), false).toSimpleString() + " \u2014 ");
} else if (tiered) {
sb.append("\u2022 ").append(sub.getParam("PrecostDesc")).append(" \u2014 ");
sb.append(new Cost(sub.getParam("ModeCost"), false).toSimpleString() + " \u2014 ");
} else if (sub.hasParam("Pawprint")) { } else if (sub.hasParam("Pawprint")) {
sb.append(StringUtils.repeat("{P}", Integer.parseInt(sub.getParam("Pawprint"))) + " \u2014 "); sb.append(StringUtils.repeat("{P}", Integer.parseInt(sub.getParam("Pawprint"))) + " \u2014 ");
} else { } else {
@@ -271,10 +276,14 @@ public class CharmEffect extends SpellAbilityEffect {
// Sort Chosen by SA order // Sort Chosen by SA order
chosen.sort(Comparator.comparingInt(o -> o.getSVarInt("CharmOrder"))); chosen.sort(Comparator.comparingInt(o -> o.getSVarInt("CharmOrder")));
int indx = 1;
for (AbilitySub sub : chosen) { for (AbilitySub sub : chosen) {
// Clone the chosen, just in case the same subAb gets chosen multiple times // Clone the chosen, just in case the same subAb gets chosen multiple times
AbilitySub clone = (AbilitySub)sub.copy(sa.getActivatingPlayer()); AbilitySub clone = (AbilitySub)sub.copy(sa.getActivatingPlayer());
clone.setSVar("CharmOrder", Integer.toString(indx));
indx++;
// make StackDescription be the SpellDescription if it doesn't already have one // make StackDescription be the SpellDescription if it doesn't already have one
if (!clone.hasParam("StackDescription")) { if (!clone.hasParam("StackDescription")) {
clone.putParam("StackDescription", "SpellDescription"); clone.putParam("StackDescription", "SpellDescription");

View File

@@ -103,7 +103,7 @@ public class ChooseCardEffect extends SpellAbilityEffect {
CardCollectionView pChoices = choices; CardCollectionView pChoices = choices;
CardCollection chosen = new CardCollection(); CardCollection chosen = new CardCollection();
if (!p.isInGame()) { if (!p.isInGame()) {
p = getNewChooser(sa, activator, p); p = getNewChooser(sa, p);
} }
if (sa.hasParam("ControlledByPlayer")) { if (sa.hasParam("ControlledByPlayer")) {
final String param = sa.getParam("ControlledByPlayer"); final String param = sa.getParam("ControlledByPlayer");
@@ -131,7 +131,7 @@ public class ChooseCardEffect extends SpellAbilityEffect {
} }
} else if (sa.hasParam("ChooseEach")) { } else if (sa.hasParam("ChooseEach")) {
final String s = sa.getParam("ChooseEach"); final String s = sa.getParam("ChooseEach");
final String[] types = s.equals("Party") ? new String[]{"Cleric","Thief","Warrior","Wizard"} final String[] types = s.equals("Party") ? new String[]{"Cleric","Rogue","Warrior","Wizard"}
: s.split(" & "); : s.split(" & ");
for (final String type : types) { for (final String type : types) {
CardCollection valids = CardLists.filter(pChoices, CardPredicates.isType(type)); CardCollection valids = CardLists.filter(pChoices, CardPredicates.isType(type));

View File

@@ -1,9 +1,12 @@
package forge.game.ability.effects; package forge.game.ability.effects;
import forge.card.ColorSet;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.deck.DeckRecognizer; import forge.deck.DeckRecognizer;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardUtil;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.util.Aggregates; import forge.util.Aggregates;
@@ -41,6 +44,13 @@ public class ChooseColorEffect extends SpellAbilityEffect {
String[] restrictedChoices = sa.getParam("Choices").split(","); String[] restrictedChoices = sa.getParam("Choices").split(",");
colorChoices = Arrays.asList(restrictedChoices); colorChoices = Arrays.asList(restrictedChoices);
} }
if (sa.hasParam("ColorsFrom")) {
ColorSet cs = CardUtil.getColorsFromCards(AbilityUtils.getDefinedCards(card, sa.getParam("ColorsFrom"), sa));
if (cs.isColorless()) {
return;
}
colorChoices = cs.stream().map(Object::toString).collect(Collectors.toCollection(ArrayList::new));
}
if (sa.hasParam("Exclude")) { if (sa.hasParam("Exclude")) {
for (String s : sa.getParam("Exclude").split(",")) { for (String s : sa.getParam("Exclude").split(",")) {
colorChoices.remove(s); colorChoices.remove(s);
@@ -49,24 +59,22 @@ public class ChooseColorEffect extends SpellAbilityEffect {
for (Player p : getTargetPlayers(sa)) { for (Player p : getTargetPlayers(sa)) {
if (!p.isInGame()) { if (!p.isInGame()) {
p = getNewChooser(sa, sa.getActivatingPlayer(), p); p = getNewChooser(sa, p);
} }
List<String> chosenColors = new ArrayList<>(); List<String> chosenColors = new ArrayList<>();
int cntMin = sa.hasParam("TwoColors") ? 2 : 1; int cntMin = sa.hasParam("UpTo") ? 0 : sa.hasParam("TwoColors") ? 2 : 1;
int cntMax = sa.hasParam("TwoColors") ? 2 : sa.hasParam("OrColors") ? colorChoices.size() : 1; int cntMax = sa.hasParam("TwoColors") ? 2 : sa.hasParam("OrColors") ? colorChoices.size() : 1;
String prompt = null; String prompt = null;
if (cntMax == 1) { if (cntMax == 1) {
prompt = Localizer.getInstance().getMessage("lblChooseAColor"); prompt = Localizer.getInstance().getMessage("lblChooseAColor");
} else { } else if (cntMax > cntMin) {
if (cntMax > cntMin) { if (cntMax >= MagicColor.NUMBER_OR_COLORS) {
if (cntMax >= MagicColor.NUMBER_OR_COLORS) { prompt = Localizer.getInstance().getMessage("lblAtLastChooseNumColors", Lang.getNumeral(cntMin));
prompt = Localizer.getInstance().getMessage("lblAtLastChooseNumColors", Lang.getNumeral(cntMin));
} else {
prompt = Localizer.getInstance().getMessage("lblChooseSpecifiedRangeColors", Lang.getNumeral(cntMin), Lang.getNumeral(cntMax));
}
} else { } else {
prompt = Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(cntMax)); prompt = Localizer.getInstance().getMessage("lblChooseSpecifiedRangeColors", Lang.getNumeral(cntMin), Lang.getNumeral(cntMax));
} }
} else {
prompt = Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(cntMax));
} }
Player noNotify = p; Player noNotify = p;
if (sa.hasParam("Random")) { if (sa.hasParam("Random")) {

View File

@@ -62,23 +62,22 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
for (Player p : getDefinedPlayersOrTargeted(sa)) { for (Player p : getDefinedPlayersOrTargeted(sa)) {
if (!p.isInGame()) { if (!p.isInGame()) {
p = getNewChooser(sa, sa.getActivatingPlayer(), p); p = getNewChooser(sa, p);
} }
// determine if any of the choices are not valid
List<SpellAbility> saToRemove = Lists.newArrayList();
// determine if any of the choices are not valid
List<SpellAbility> availableSA = Lists.newArrayList(abilities);
for (SpellAbility saChoice : abilities) { for (SpellAbility saChoice : abilities) {
if (saChoice.getRestrictions() != null && !saChoice.getRestrictions().checkOtherRestrictions(host, saChoice, sa.getActivatingPlayer())) { if (saChoice.getRestrictions() != null && !saChoice.getRestrictions().checkOtherRestrictions(host, saChoice, sa.getActivatingPlayer())) {
saToRemove.add(saChoice); availableSA.remove(saChoice);
} else if (saChoice.hasParam("UnlessCost")) { } else if (saChoice.hasParam("UnlessCost")) {
// generic check for if the cost can be paid // generic check for if the cost can be paid
Cost unlessCost = new Cost(saChoice.getParam("UnlessCost"), false); Cost unlessCost = new Cost(saChoice.getParam("UnlessCost"), false);
if (!unlessCost.canPay(sa, p, true)) { if (!unlessCost.canPay(sa, p, true)) {
saToRemove.add(saChoice); availableSA.remove(saChoice);
} }
} }
} }
abilities.removeAll(saToRemove);
List<SpellAbility> chosenSAs = Lists.newArrayList(); List<SpellAbility> chosenSAs = Lists.newArrayList();
String prompt = sa.getParamOrDefault("ChoicePrompt", "Choose"); String prompt = sa.getParamOrDefault("ChoicePrompt", "Choose");
@@ -86,7 +85,7 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
if (sa.hasParam("AtRandom")) { if (sa.hasParam("AtRandom")) {
random = true; random = true;
chosenSAs = Aggregates.random(abilities, amount); chosenSAs = Aggregates.random(availableSA, amount);
int i = 0; int i = 0;
while (sa.getParam("AtRandom").equals("Urza") && i < chosenSAs.size()) { while (sa.getParam("AtRandom").equals("Urza") && i < chosenSAs.size()) {
@@ -99,8 +98,8 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
chosenSAs.set(i, Aggregates.random(abilities)); chosenSAs.set(i, Aggregates.random(abilities));
} }
} }
} else if (!abilities.isEmpty()) { } else if (!availableSA.isEmpty()) {
chosenSAs = p.getController().chooseSpellAbilitiesForEffect(abilities, sa, prompt, amount, ImmutableMap.of()); chosenSAs = p.getController().chooseSpellAbilitiesForEffect(availableSA, sa, prompt, amount, ImmutableMap.of());
} }
List<Object> oldRem = Lists.newArrayList(IterableUtil.filter(host.getRemembered(), Player.class)); List<Object> oldRem = Lists.newArrayList(IterableUtil.filter(host.getRemembered(), Player.class));
@@ -117,7 +116,6 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
} else if (secretly) { } else if (secretly) {
if (record.length() > 0) record.append("\r\n"); if (record.length() > 0) record.append("\r\n");
record.append(Localizer.getInstance().getMessage("lblPlayerChooseValue", p, chosenValue)); record.append(Localizer.getInstance().getMessage("lblPlayerChooseValue", p, chosenValue));
} }
if (sa.hasParam("SetChosenMode")) { if (sa.hasParam("SetChosenMode")) {
sa.getHostCard().setChosenMode(chosenValue); sa.getHostCard().setChosenMode(chosenValue);

View File

@@ -129,6 +129,8 @@ public class CloneEffect extends SpellAbilityEffect {
cloneTargets.remove(cardToCopy); cloneTargets.remove(cardToCopy);
} }
final long ts = game.getNextTimestamp();
for (Card tgtCard : cloneTargets) { for (Card tgtCard : cloneTargets) {
if (sa.hasParam("CloneZone") && if (sa.hasParam("CloneZone") &&
!tgtCard.isInZone(ZoneType.smartValueOf(sa.getParam("CloneZone")))) { !tgtCard.isInZone(ZoneType.smartValueOf(sa.getParam("CloneZone")))) {
@@ -141,7 +143,6 @@ public class CloneEffect extends SpellAbilityEffect {
game.getTriggerHandler().clearActiveTriggers(tgtCard, null); game.getTriggerHandler().clearActiveTriggers(tgtCard, null);
final long ts = game.getNextTimestamp();
tgtCard.addCloneState(CardFactory.getCloneStates(cardToCopy, tgtCard, sa), ts); tgtCard.addCloneState(CardFactory.getCloneStates(cardToCopy, tgtCard, sa), ts);
tgtCard.updateRooms(); tgtCard.updateRooms();
@@ -199,7 +200,7 @@ public class CloneEffect extends SpellAbilityEffect {
tgtCard.addRemembered(cardToCopy); tgtCard.addRemembered(cardToCopy);
} }
// spire // spire
tgtCard.setChosenColorID(cardToCopy.getChosenColorID()); tgtCard.setMarkedColors(cardToCopy.getMarkedColors());
game.fireEvent(new GameEventCardStatsChanged(tgtCard)); game.fireEvent(new GameEventCardStatsChanged(tgtCard));
} }

View File

@@ -132,10 +132,6 @@ public class ControlGainEffect extends SpellAbilityEffect {
tgtCards = getDefinedCards(sa); tgtCards = getDefinedCards(sa);
} }
if (tgtCards != null & sa.hasParam("ControlledByTarget")) {
tgtCards = CardLists.filterControlledBy(tgtCards, getTargetPlayers(sa));
}
// check for lose control criteria right away // check for lose control criteria right away
if (lose != null && lose.contains("LeavesPlay") && !source.isInPlay()) { if (lose != null && lose.contains("LeavesPlay") && !source.isInPlay()) {
return; return;
@@ -170,7 +166,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
tgtC.addTempController(newController, tStamp); tgtC.addTempController(newController, tStamp);
if (bUntap) { if (bUntap) {
if (tgtC.untap(true)) untapped.add(tgtC); if (tgtC.untap()) untapped.add(tgtC);
} }
if (keywords != null) { if (keywords != null) {

View File

@@ -26,10 +26,8 @@ public class ControlPlayerEffect extends SpellAbilityEffect {
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Player activator = sa.getActivatingPlayer(); final Player controller = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Controller"), sa).get(0);
final Game game = activator.getGame(); final Game game = controller.getGame();
final Player controller = sa.hasParam("Controller") ? AbilityUtils.getDefinedPlayers(
sa.getHostCard(), sa.getParam("Controller"), sa).get(0) : activator;
for (final Player pTarget: getTargetPlayers(sa)) { for (final Player pTarget: getTargetPlayers(sa)) {
// before next untap gain control // before next untap gain control

View File

@@ -32,7 +32,6 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.item.PaperCard; import forge.item.PaperCard;
import forge.util.PredicateString.StringOp;
public class CopyPermanentEffect extends TokenEffectBase { public class CopyPermanentEffect extends TokenEffectBase {
@@ -185,19 +184,15 @@ public class CopyPermanentEffect extends TokenEffectBase {
System.err.println("Copying random permanent(s): " + tgtCards.toString()); System.err.println("Copying random permanent(s): " + tgtCards.toString());
} }
} else if (sa.hasParam("DefinedName")) { } else if (sa.hasParam("DefinedName")) {
List<PaperCard> cards = Lists.newArrayList(StaticData.instance().getCommonCards().getUniqueCards());
String name = sa.getParam("DefinedName"); String name = sa.getParam("DefinedName");
if (name.equals("NamedCard")) { if (name.equals("NamedCard")) {
if (!host.getNamedCard().isEmpty()) { if (!host.getNamedCard().isEmpty()) {
name = host.getNamedCard(); name = host.getNamedCard();
} }
} }
PaperCard pc = StaticData.instance().getCommonCards().getUniqueByName(name);
Predicate<PaperCard> cpp = PaperCardPredicates.fromRules(CardRulesPredicates.name(StringOp.EQUALS, name)); if (pc != null) {
cards = Lists.newArrayList(IterableUtil.filter(cards, cpp)); tgtCards.add(Card.fromPaperCard(pc, controller));
if (!cards.isEmpty()) {
tgtCards.add(Card.fromPaperCard(cards.get(0), controller));
} }
} else if (sa.hasParam("Choices")) { } else if (sa.hasParam("Choices")) {
Player chooser = activator; Player chooser = activator;
@@ -314,7 +309,7 @@ public class CopyPermanentEffect extends TokenEffectBase {
} }
} }
// spire // spire
copy.setChosenColorID(original.getChosenColorID()); copy.setMarkedColors(original.getMarkedColors());
copy.setTokenSpawningAbility(sa); copy.setTokenSpawningAbility(sa);
copy.setGamePieceType(GamePieceType.TOKEN); copy.setGamePieceType(GamePieceType.TOKEN);

View File

@@ -13,7 +13,6 @@ import forge.game.card.CardFactory;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantBeCopied;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.*; import forge.util.*;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
@@ -66,7 +65,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
List<SpellAbility> tgtSpells = getTargetSpells(sa); List<SpellAbility> tgtSpells = getTargetSpells(sa);
tgtSpells.removeIf(tgtSA -> StaticAbilityCantBeCopied.cantBeCopied(tgtSA.getHostCard())); tgtSpells.removeIf(SpellAbility::cantBeCopied);
if (tgtSpells.isEmpty() || amount == 0) { if (tgtSpells.isEmpty() || amount == 0) {
return; return;

View File

@@ -7,6 +7,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import forge.card.MagicColor;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.GameEntityCounterTable; import forge.game.GameEntityCounterTable;
@@ -631,7 +632,19 @@ public class CountersPutEffect extends SpellAbilityEffect {
return; return;
} }
} }
resolvePerType(sa, placer, counterType, counterAmount, table, true); if (sa.hasParam("ForColor")) {
Iterable<String> oldColors = card.getChosenColors();
for (String color : MagicColor.Constant.ONLY_COLORS) {
card.setChosenColors(Lists.newArrayList(color));
if (sa.getOriginalParam("ChoiceTitle") != null) {
sa.getMapParams().put("ChoiceTitle", sa.getOriginalParam("ChoiceTitle").replace("chosenColor", color));
}
resolvePerType(sa, placer, counterType, counterAmount, table, true);
}
card.setChosenColors(Lists.newArrayList(oldColors));
} else {
resolvePerType(sa, placer, counterType, counterAmount, table, true);
}
} }
table.replaceCounterEffect(game, sa, true); table.replaceCounterEffect(game, sa, true);

View File

@@ -65,8 +65,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
ctype = CounterType.getType(sa.getParam("CounterType")); ctype = CounterType.getType(sa.getParam("CounterType"));
} }
final Player pl = !sa.hasParam("DefinedPlayer") ? sa.getActivatingPlayer() : final Player pl = AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
final boolean eachExisting = sa.hasParam("EachExistingCounter"); final boolean eachExisting = sa.hasParam("EachExistingCounter");
GameEntityCounterTable table = new GameEntityCounterTable(); GameEntityCounterTable table = new GameEntityCounterTable();
@@ -79,7 +78,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) { if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) {
continue; continue;
} }
if (!eachExisting && sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null, if (sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblWouldYouLikePutRemoveCounters", ctype.getName(), Localizer.getInstance().getMessage("lblWouldYouLikePutRemoveCounters", ctype.getName(),
CardTranslation.getTranslatedName(gameCard.getName())), null)) { CardTranslation.getTranslatedName(gameCard.getName())), null)) {
continue; continue;
@@ -114,8 +113,6 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
String prompt = Localizer.getInstance().getMessage("lblSelectCounterTypeToAddOrRemove"); String prompt = Localizer.getInstance().getMessage("lblSelectCounterTypeToAddOrRemove");
CounterType chosenType = pc.chooseCounterType(list, sa, prompt, params); CounterType chosenType = pc.chooseCounterType(list, sa, prompt, params);
params.put("CounterType", chosenType);
prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " ";
boolean putCounter; boolean putCounter;
if (sa.hasParam("RemoveConditionSVar")) { if (sa.hasParam("RemoveConditionSVar")) {
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
@@ -137,6 +134,8 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
} else if (!canReceive && canRemove) { } else if (!canReceive && canRemove) {
putCounter = false; putCounter = false;
} else { } else {
params.put("CounterType", chosenType);
prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " ";
putCounter = pc.chooseBinary(sa, prompt, BinaryChoiceType.AddOrRemove, params); putCounter = pc.chooseBinary(sa, prompt, BinaryChoiceType.AddOrRemove, params);
} }
} }

View File

@@ -101,34 +101,12 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
boolean rememberAmount = sa.hasParam("RememberAmount"); boolean rememberAmount = sa.hasParam("RememberAmount");
int totalRemoved = 0; int totalRemoved = 0;
for (final Player tgtPlayer : getTargetPlayers(sa)) {
if (!tgtPlayer.isInGame()) {
continue;
}
// Removing energy
if (type.equals("All")) {
for (Map.Entry<CounterType, Integer> e : Lists.newArrayList(tgtPlayer.getCounters().entrySet())) {
totalRemoved += tgtPlayer.subtractCounter(e.getKey(), e.getValue(), activator);
}
} else {
if (num.equals("All")) {
cntToRemove = tgtPlayer.getCounters(counterType);
}
if (type.equals("Any")) {
totalRemoved += removeAnyType(tgtPlayer, cntToRemove, sa);
} else {
totalRemoved += tgtPlayer.subtractCounter(counterType, cntToRemove, activator);
}
}
}
CardCollectionView srcCards; CardCollectionView srcCards;
String typeforPrompt = counterType == null ? "" : counterType.getName(); String typeforPrompt = counterType == null ? "" : counterType.getName();
String title = Localizer.getInstance().getMessage("lblChooseCardsToTakeTargetCounters", typeforPrompt); String title = Localizer.getInstance().getMessage("lblChooseCardsToTakeTargetCounters", typeforPrompt);
title = title.replace(" ", " "); title = title.replace(" ", " ");
if (sa.hasParam("Choices") && counterType != null) { if (sa.hasParam("Choices")) {
ZoneType choiceZone = sa.hasParam("ChoiceZone") ? ZoneType.smartValueOf(sa.getParam("ChoiceZone")) ZoneType choiceZone = sa.hasParam("ChoiceZone") ? ZoneType.smartValueOf(sa.getParam("ChoiceZone"))
: ZoneType.Battlefield; : ZoneType.Battlefield;
@@ -145,6 +123,27 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
params.put("CounterType", counterType); params.put("CounterType", counterType);
srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, params); srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, params);
} else { } else {
for (final Player tgtPlayer : getTargetPlayers(sa)) {
if (!tgtPlayer.isInGame()) {
continue;
}
// Removing energy
if (type.equals("All")) {
for (Map.Entry<CounterType, Integer> e : Lists.newArrayList(tgtPlayer.getCounters().entrySet())) {
totalRemoved += tgtPlayer.subtractCounter(e.getKey(), e.getValue(), activator);
}
} else {
if (num.equals("All")) {
cntToRemove = tgtPlayer.getCounters(counterType);
}
if (type.equals("Any")) {
totalRemoved += removeAnyType(tgtPlayer, cntToRemove, sa);
} else {
totalRemoved += tgtPlayer.subtractCounter(counterType, cntToRemove, activator);
}
}
}
srcCards = getTargetCards(sa); srcCards = getTargetCards(sa);
} }

View File

@@ -41,7 +41,6 @@ public class DamageEachEffect extends DamageBaseEffect {
return sb.toString(); return sb.toString();
} }
/* (non-Javadoc) /* (non-Javadoc)
* @see forge.card.abilityfactory.SpellEffect#resolve(java.util.Map, forge.card.spellability.SpellAbility) * @see forge.card.abilityfactory.SpellEffect#resolve(java.util.Map, forge.card.spellability.SpellAbility)
*/ */

View File

@@ -127,8 +127,8 @@ public class DigEffect extends SpellAbilityEffect {
final boolean skipReorder = sa.hasParam("SkipReorder"); final boolean skipReorder = sa.hasParam("SkipReorder");
// A hack for cards like Explorer's Scope that need to ensure that a card is revealed to the player activating the ability // A hack for cards like Explorer's Scope that need to ensure that a card is revealed to the player activating the ability
final boolean forceReveal = sa.hasParam("ForceRevealToController") || final boolean forceReveal = sa.hasParam("ForceRevealToController")
sa.hasParam("ForceReveal"); || sa.hasParam("ForceReveal") || sa.hasParam("WithMayLook");
// These parameters are used to indicate that a dialog box must be show to the player asking if the player wants to proceed // These parameters are used to indicate that a dialog box must be show to the player asking if the player wants to proceed
// with an optional ability, otherwise the optional ability is skipped. // with an optional ability, otherwise the optional ability is skipped.

View File

@@ -3,7 +3,6 @@ package forge.game.ability.effects;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
@@ -124,11 +123,9 @@ public class DiscardEffect extends SpellAbilityEffect {
final List<Player> targets = getTargetPlayers(sa), final List<Player> targets = getTargetPlayers(sa),
discarders; discarders;
Player firstTarget = null;
if (mode.equals("RevealTgtChoose")) { if (mode.equals("RevealTgtChoose")) {
// In this case the target need not be the discarding player // In this case the target need not be the discarding player
discarders = getDefinedPlayersOrTargeted(sa); discarders = getDefinedPlayersOrTargeted(sa);
firstTarget = Iterables.getFirst(targets, null);
} else { } else {
discarders = targets; discarders = targets;
} }
@@ -140,125 +137,123 @@ public class DiscardEffect extends SpellAbilityEffect {
} }
CardCollectionView toBeDiscarded = new CardCollection(); CardCollectionView toBeDiscarded = new CardCollection();
if ((mode.equals("RevealTgtChoose") && firstTarget != null) || !sa.usesTargeting() || p.canBeTargetedBy(sa)) { final int numCardsInHand = p.getCardsIn(ZoneType.Hand).size();
final int numCardsInHand = p.getCardsIn(ZoneType.Hand).size(); if (mode.equals("Defined")) {
if (mode.equals("Defined")) { if (!p.canDiscardBy(sa, true)) {
if (!p.canDiscardBy(sa, true)) { continue;
continue;
}
boolean runDiscard = !sa.hasParam("Optional")
|| p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, sa.getParam("DiscardMessage"), null);
if (runDiscard) {
toBeDiscarded = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa);
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
}
} }
if (mode.equals("Hand")) { boolean runDiscard = !sa.hasParam("Optional")
toBeDiscarded = p.getCardsIn(ZoneType.Hand); || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, sa.getParam("DiscardMessage"), null);
if (runDiscard) {
// Empty hand can still be discarded toBeDiscarded = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa);
if (!toBeDiscarded.isEmpty() && !p.canDiscardBy(sa, true)) {
continue;
}
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
} }
}
int numCards = 1; if (mode.equals("Hand")) {
if (sa.hasParam("NumCards")) { toBeDiscarded = p.getCardsIn(ZoneType.Hand);
numCards = AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa);
numCards = Math.min(numCards, numCardsInHand); // Empty hand can still be discarded
if (!toBeDiscarded.isEmpty() && !p.canDiscardBy(sa, true)) {
continue;
} }
if (mode.equals("Random")) { toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
if (!p.canDiscardBy(sa, true)) { }
continue;
}
String message = Localizer.getInstance().getMessage("lblWouldYouLikeRandomDiscardTargetCard", String.valueOf(numCards));
boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message, null);
if (runDiscard) { int numCards = 1;
final String valid = sa.getParamOrDefault("DiscardValid", "Card"); if (sa.hasParam("NumCards")) {
List<Card> list = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), valid, source.getController(), source, sa); numCards = AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa);
numCards = Math.min(numCards, numCardsInHand);
}
toBeDiscarded = new CardCollection(Aggregates.random(list, numCards)); if (mode.equals("Random")) {
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); if (!p.canDiscardBy(sa, true)) {
} continue;
} }
else if (mode.equals("TgtChoose") && sa.hasParam("UnlessType")) { String message = Localizer.getInstance().getMessage("lblWouldYouLikeRandomDiscardTargetCard", String.valueOf(numCards));
if (!p.canDiscardBy(sa, true)) { boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message, null);
continue;
}
if (numCardsInHand > 0) {
CardCollectionView hand = p.getCardsIn(ZoneType.Hand);
toBeDiscarded = p.getController().chooseCardsToDiscardUnlessType(Math.min(numCards, numCardsInHand), hand, sa.getParam("UnlessType"), sa);
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game,toBeDiscarded, ZoneType.Graveyard, sa);
}
}
else if (mode.equals("RevealDiscardAll")) {
// Reveal
final CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand);
for (final Player opp : p.getAllOtherPlayers()) {
opp.getController().reveal(dPHand, ZoneType.Hand, p, Localizer.getInstance().getMessage("lblReveal") + " ");
}
if (!p.canDiscardBy(sa, true)) {
continue;
}
String valid = sa.getParamOrDefault("DiscardValid", "Card");
if (valid.contains("X")) {
valid = TextUtil.fastReplace(valid,
"X", Integer.toString(AbilityUtils.calculateAmount(source, "X", sa)));
}
toBeDiscarded = CardLists.getValidCards(dPHand, valid, source.getController(), source, sa);
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
} else if (mode.endsWith("YouChoose") || mode.endsWith("TgtChoose")) {
CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand);
if (dPHand.isEmpty())
continue; // for loop over players
if (sa.hasParam("RevealNumber")) {
int amount = AbilityUtils.calculateAmount(source, sa.getParam("RevealNumber"), sa);
dPHand = p.getController().chooseCardsToRevealFromHand(amount, amount, dPHand);
}
Player chooser = p;
if (mode.endsWith("YouChoose")) {
chooser = source.getController();
} else if (mode.equals("RevealTgtChoose")) {
chooser = firstTarget;
}
if (mode.startsWith("Reveal")) {
game.getAction().reveal(dPHand, p);
}
if (mode.startsWith("Look") && p != chooser) {
game.getAction().revealTo(dPHand, chooser);
}
if (!p.canDiscardBy(sa, true)) {
continue;
}
if (runDiscard) {
final String valid = sa.getParamOrDefault("DiscardValid", "Card"); final String valid = sa.getParamOrDefault("DiscardValid", "Card");
CardCollection validCards = CardLists.getValidCards(dPHand, valid, source.getController(), source, sa); List<Card> list = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), valid, source.getController(), source, sa);
int min = sa.hasParam("AnyNumber") || sa.hasParam("Optional") ? 0 : Math.min(validCards.size(), numCards);
int max = sa.hasParam("AnyNumber") ? validCards.size() : Math.min(validCards.size(), numCards);
toBeDiscarded = max == 0 ? CardCollection.EMPTY : chooser.getController().chooseCardsToDiscardFrom(p, sa, validCards, min, max);
toBeDiscarded = new CardCollection(Aggregates.random(list, numCards));
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
}
}
else if (mode.equals("TgtChoose") && sa.hasParam("UnlessType")) {
if (!p.canDiscardBy(sa, true)) {
continue;
}
if (numCardsInHand > 0) {
CardCollectionView hand = p.getCardsIn(ZoneType.Hand);
toBeDiscarded = p.getController().chooseCardsToDiscardUnlessType(Math.min(numCards, numCardsInHand), hand, sa.getParam("UnlessType"), sa);
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game,toBeDiscarded, ZoneType.Graveyard, sa);
}
}
else if (mode.equals("RevealDiscardAll")) {
// Reveal
final CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand);
if (mode.startsWith("Reveal") && p != chooser) { for (final Player opp : p.getAllOtherPlayers()) {
p.getController().reveal(toBeDiscarded, ZoneType.Hand, p, Localizer.getInstance().getMessage("lblPlayerHasChosenCardsFrom", chooser.getName())); opp.getController().reveal(dPHand, ZoneType.Hand, p, Localizer.getInstance().getMessage("lblReveal") + " ");
} }
if (!p.canDiscardBy(sa, true)) {
continue;
}
String valid = sa.getParamOrDefault("DiscardValid", "Card");
if (valid.contains("X")) {
valid = TextUtil.fastReplace(valid,
"X", Integer.toString(AbilityUtils.calculateAmount(source, "X", sa)));
}
toBeDiscarded = CardLists.getValidCards(dPHand, valid, source.getController(), source, sa);
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
} else if (mode.endsWith("YouChoose") || mode.endsWith("TgtChoose")) {
CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand);
if (dPHand.isEmpty())
continue; // for loop over players
if (sa.hasParam("RevealNumber")) {
int amount = AbilityUtils.calculateAmount(source, sa.getParam("RevealNumber"), sa);
dPHand = p.getController().chooseCardsToRevealFromHand(amount, amount, dPHand);
}
Player chooser = p;
if (mode.endsWith("YouChoose")) {
chooser = sa.getActivatingPlayer();
} else if (mode.equals("RevealTgtChoose")) {
chooser = targets.get(0);
}
if (mode.startsWith("Reveal")) {
game.getAction().reveal(dPHand, p);
}
if (mode.startsWith("Look") && p != chooser) {
game.getAction().revealTo(dPHand, chooser);
}
if (!p.canDiscardBy(sa, true)) {
continue;
}
final String valid = sa.getParamOrDefault("DiscardValid", "Card");
CardCollection validCards = CardLists.getValidCards(dPHand, valid, source.getController(), source, sa);
int min = sa.hasParam("AnyNumber") || sa.hasParam("Optional") ? 0 : Math.min(validCards.size(), numCards);
int max = sa.hasParam("AnyNumber") ? validCards.size() : Math.min(validCards.size(), numCards);
toBeDiscarded = max == 0 ? CardCollection.EMPTY : chooser.getController().chooseCardsToDiscardFrom(p, sa, validCards, min, max);
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
if (mode.startsWith("Reveal") && p != chooser) {
p.getController().reveal(toBeDiscarded, ZoneType.Hand, p, Localizer.getInstance().getMessage("lblPlayerHasChosenCardsFrom", chooser.getName()));
} }
} }
discardedMap.put(p, toBeDiscarded); discardedMap.put(p, toBeDiscarded);

View File

@@ -57,7 +57,7 @@ import java.util.*;
// Cardnames that include "," must use ";" instead in Spellbook$ (i.e. Tovolar; Dire Overlord) // Cardnames that include "," must use ";" instead in Spellbook$ (i.e. Tovolar; Dire Overlord)
name = name.replace(";", ","); name = name.replace(";", ",");
Card cardOption = Card.fromPaperCard(StaticData.instance().getCommonCards().getUniqueByName(name), player); Card cardOption = Card.fromPaperCard(StaticData.instance().getCommonCards().getUniqueByName(name), player);
cardOption.setTokenCard(true); cardOption.setTokenCard(sa.hasParam("TokenCard"));
draftOptions.add(cardOption); draftOptions.add(cardOption);
} }

View File

@@ -22,7 +22,7 @@ public class EndTurnEffect extends SpellAbilityEffect {
final List<Player> enders = getDefinedPlayersOrTargeted(sa, "Defined"); final List<Player> enders = getDefinedPlayersOrTargeted(sa, "Defined");
Player ender = enders.isEmpty() ? sa.getActivatingPlayer() : enders.get(0); Player ender = enders.isEmpty() ? sa.getActivatingPlayer() : enders.get(0);
if (!ender.isInGame()) { if (!ender.isInGame()) {
ender = getNewChooser(sa, sa.getActivatingPlayer(), ender); ender = getNewChooser(sa, ender);
} }
if (sa.hasParam("Optional") && !ender.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantEndTurn"), null)) { if (sa.hasParam("Optional") && !ender.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantEndTurn"), null)) {

View File

@@ -0,0 +1,105 @@
package forge.game.ability.effects;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.mutable.MutableBoolean;
import com.google.common.collect.Maps;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardZoneTable;
import forge.game.card.CounterEnumType;
import forge.game.card.TokenCreateTable;
import forge.game.card.token.TokenInfo;
import forge.game.event.GameEventCombatChanged;
import forge.game.event.GameEventTokenCreated;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.CardTranslation;
import forge.util.Lang;
import forge.util.Localizer;
public class EndureEffect extends TokenEffectBase {
@Override
protected String getStackDescription(SpellAbility sa) {
final Card host = sa.getHostCard();
final StringBuilder sb = new StringBuilder();
List<Card> tgt = getTargetCards(sa);
sb.append(Lang.joinHomogenous(tgt));
sb.append(" ");
sb.append(tgt.size() > 1 ? "endure" : "endures");
int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
sb.append(" ").append(amount);
sb.append(". ");
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
final Game game = host.getGame();
String num = sa.getParamOrDefault("Num", "1");
int amount = AbilityUtils.calculateAmount(host, num, sa);
if (amount < 1) {
return;
}
GameEntityCounterTable table = new GameEntityCounterTable();
TokenCreateTable tokenTable = new TokenCreateTable();
for (final Card c : GameActionUtil.orderCardsByTheirOwners(game, getTargetCards(sa), ZoneType.Battlefield, sa)) {
final Player pl = c.getController();
Card gamec = game.getCardState(c, null);
Map<String, Object> params = Maps.newHashMap();
params.put("RevealedCard", c);
params.put("Amount", amount);
if (gamec != null && gamec.isInPlay() && gamec.equalsWithGameTimestamp(c) && gamec.canReceiveCounters(CounterEnumType.P1P1)
&& pl.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblEndureAction", CardTranslation.getTranslatedName(c.getName()), amount),
gamec, params)) {
gamec.addCounter(CounterEnumType.P1P1, amount, pl, table);
} else {
final Card result = TokenInfo.getProtoType("w_x_x_spirit", sa, pl, false);
// set PT
result.setBasePowerString(num);
result.setBasePower(amount);
result.setBaseToughnessString(num);
result.setBaseToughness(amount);
tokenTable.put(pl, result, 1);
}
}
table.replaceCounterEffect(game, sa, true);
if (!tokenTable.isEmpty()) {
CardZoneTable triggerList = new CardZoneTable();
MutableBoolean combatChanged = new MutableBoolean(false);
makeTokenTable(tokenTable, false, triggerList, combatChanged, sa);
triggerList.triggerChangesZoneAll(game, sa);
game.fireEvent(new GameEventTokenCreated());
if (combatChanged.isTrue()) {
game.updateCombatForView();
game.fireEvent(new GameEventCombatChanged());
}
}
}
}

View File

@@ -16,6 +16,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.player.PlayerController; import forge.game.player.PlayerController;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityFlipCoinMod;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.util.Localizer; import forge.util.Localizer;
import forge.util.MyRandom; import forge.util.MyRandom;
@@ -48,56 +49,26 @@ public class FlipCoinEffect extends SpellAbilityEffect {
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
final Player player = host.getController();
int flipMultiplier = 1; // For multiple copies of Krark's Thumb
final List<Player> playersToFlip = AbilityUtils.getDefinedPlayers(host, sa.getParam("Flipper"), sa); final List<Player> playersToFlip = AbilityUtils.getDefinedPlayers(host, sa.getParam("Flipper"), sa);
if (playersToFlip.isEmpty()) { //final List<Player> caller = AbilityUtils.getDefinedPlayers(host, sa.getParam("Caller"), sa);
playersToFlip.add(sa.getActivatingPlayer());
}
final List<Player> caller = AbilityUtils.getDefinedPlayers(host, sa.getParam("Caller"), sa);
if (caller.isEmpty()) {
caller.add(player);
}
final boolean noCall = sa.hasParam("NoCall"); final boolean noCall = sa.hasParam("NoCall");
final boolean forEachPlayer = sa.hasParam("ForEachPlayer"); final boolean forEachPlayer = sa.hasParam("ForEachPlayer");
String varName = sa.getParamOrDefault("SaveNumFlipsToSVar", "X"); String varName = sa.getParamOrDefault("SaveNumFlipsToSVar", "X");
boolean victory = false;
int amount = 1; int amount = 1;
if (sa.hasParam("Amount")) { if (sa.hasParam("Amount")) {
amount = AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa); amount = AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa);
} }
if (!noCall && !forEachPlayer && amount == 1) {
flipMultiplier = getFlipMultiplier(caller.get(0));
victory = flipCoinCall(caller.get(0), sa, flipMultiplier, varName);
}
final boolean rememberResult = sa.hasParam("RememberResult");
for (final Player flipper : playersToFlip) { for (final Player flipper : playersToFlip) {
if (noCall) { if (noCall) {
flipMultiplier = getFlipMultiplier(flipper); int countHeads = flipCoins(flipper, sa, amount);
int countTails = Math.abs(countHeads - amount);
int countHeads = 0;
int countTails = 0;
for (int i = 0; i < amount; ++i) {
final boolean resultIsHeads = flipCoinNoCall(sa, flipper, flipMultiplier, varName);
if (resultIsHeads) {
countHeads++;
} else {
countTails++;
}
if (rememberResult) {
host.addFlipResult(flipper, resultIsHeads ? "Heads" : "Tails");
}
}
if (countHeads > 0) { if (countHeads > 0) {
if (sa.hasParam("RememberResult")) {
host.addFlipResult(flipper, "Heads");
}
SpellAbility sub = sa.getAdditionalAbility("HeadsSubAbility"); SpellAbility sub = sa.getAdditionalAbility("HeadsSubAbility");
if (sub != null) { if (sub != null) {
if (sa.hasParam("Amount")) { if (sa.hasParam("Amount")) {
@@ -107,6 +78,9 @@ public class FlipCoinEffect extends SpellAbilityEffect {
} }
} }
if (countTails > 0) { if (countTails > 0) {
if (sa.hasParam("RememberResult")) {
host.addFlipResult(flipper, "Tails");
}
SpellAbility sub = sa.getAdditionalAbility("TailsSubAbility"); SpellAbility sub = sa.getAdditionalAbility("TailsSubAbility");
if (sub != null) { if (sub != null) {
if (sa.hasParam("Amount")) { if (sa.hasParam("Amount")) {
@@ -115,46 +89,7 @@ public class FlipCoinEffect extends SpellAbilityEffect {
AbilityUtils.resolve(sub); AbilityUtils.resolve(sub);
} }
} }
} else if (amount > 1) {
flipMultiplier = getFlipMultiplier(flipper);
int countWins = 0;
int countLosses = 0;
for (int i = 0; i < amount; ++i) {
final boolean win = flipCoinCall(caller.get(0), sa, flipMultiplier, varName);
if (win) {
countWins++;
} else {
countLosses++;
}
}
if (countWins > 0) {
SpellAbility sub = sa.getAdditionalAbility("WinSubAbility");
if (sub != null) {
sub.setSVar("Wins", "Number$" + countWins);
AbilityUtils.resolve(sub);
}
}
if (countLosses > 0) {
SpellAbility sub = sa.getAdditionalAbility("LoseSubAbility");
if (sub != null) {
sub.setSVar("Losses", "Number$" + countLosses);
AbilityUtils.resolve(sub);
}
}
if (sa.hasParam("RememberNumber")) {
String toRemember = sa.getParam("RememberNumber");
if (toRemember.startsWith("Win")) {
host.addRemembered(countWins);
} else if (toRemember.startsWith("Loss")) {
host.addRemembered(countLosses);
}
}
} else if (forEachPlayer) { } else if (forEachPlayer) {
flipMultiplier = getFlipMultiplier(flipper);
int countWins = 0; int countWins = 0;
int countLosses = 0; int countLosses = 0;
PlayerCollection wonFor = new PlayerCollection(); PlayerCollection wonFor = new PlayerCollection();
@@ -162,9 +97,9 @@ public class FlipCoinEffect extends SpellAbilityEffect {
for (final Player p : AbilityUtils.getDefinedPlayers(host, sa.getParam("ForEachPlayer"), sa)) { for (final Player p : AbilityUtils.getDefinedPlayers(host, sa.getParam("ForEachPlayer"), sa)) {
final String info = " (" + p.getName() +")"; final String info = " (" + p.getName() +")";
final boolean win = flipCoinCall(caller.get(0), sa, flipMultiplier, varName, info); final int win = flipCoins(flipper, sa, 1, info);
if (win) { if (win > 0) {
countWins++; countWins++;
wonFor.add(p); wonFor.add(p);
} else { } else {
@@ -196,57 +131,59 @@ public class FlipCoinEffect extends SpellAbilityEffect {
host.addRemembered(tempRemembered); host.addRemembered(tempRemembered);
} }
} }
} else if (victory) {
if (sa.hasParam("RememberWinner")) {
host.addRemembered(flipper);
}
if (sa.hasAdditionalAbility("WinSubAbility")) {
AbilityUtils.resolve(sa.getAdditionalAbility("WinSubAbility"));
}
} else { } else {
if (sa.hasParam("RememberLoser")) { int countWins = flipCoins(flipper, sa, amount);
host.addRemembered(flipper); int countLosses = Math.abs(countWins - amount);
if (countWins > 0) {
if (sa.hasParam("RememberWinner")) {
host.addRemembered(flipper);
}
SpellAbility sub = sa.getAdditionalAbility("WinSubAbility");
if (sub != null) {
sub.setSVar("Wins", "Number$" + countWins);
AbilityUtils.resolve(sub);
}
} }
if (countLosses > 0) {
if (sa.hasAdditionalAbility("LoseSubAbility")) { if (sa.hasParam("RememberLoser")) {
AbilityUtils.resolve(sa.getAdditionalAbility("LoseSubAbility")); host.addRemembered(flipper);
}
SpellAbility sub = sa.getAdditionalAbility("LoseSubAbility");
if (sub != null) {
sub.setSVar("Losses", "Number$" + countLosses);
AbilityUtils.resolve(sub);
}
}
if (sa.hasParam("RememberNumber")) {
String toRemember = sa.getParam("RememberNumber");
if (toRemember.startsWith("Win")) {
host.addRemembered(countWins);
} else if (toRemember.startsWith("Loss")) {
host.addRemembered(countLosses);
}
} }
} }
} }
} }
/** public static int flipCoins(final Player flipper, final SpellAbility sa, final int amount) {
* <p> return flipCoins(flipper, sa, amount, "");
* flipCoinNoCall Flip a coin without any call. }
* </p> public static int flipCoins(final Player flipper, final SpellAbility sa, final int amount, final String info) {
* int multiplier = getFlipMultiplier(flipper);
* @param sa the source card. int result = 0;
* @param flipper the player flipping the coin. boolean won = false;
* @param multiplier
* @return a boolean.
*/
public boolean flipCoinNoCall(final SpellAbility sa, final Player flipper, final int multiplier, final String varName) {
boolean result = false;
int numSuccesses = 0;
do { do {
Set<Boolean> flipResults = new HashSet<>(); Boolean fixedResult = StaticAbilityFlipCoinMod.fixedResult(flipper);
for (int i = 0; i < multiplier; i++) { for (int i = 0; i < amount; i++) {
flipResults.add(MyRandom.getRandom().nextBoolean()); won = flipCoin(flipper, sa, multiplier, fixedResult, info);
if (won) {
result++;
}
} }
flipper.getGame().fireEvent(new GameEventFlipCoin()); // until is sequential
result = flipResults.size() == 1 ? flipResults.iterator().next() : flipper.getController().chooseFlipResult(sa, flipper, BOTH_CHOICES, false);
if (result) {
numSuccesses++;
}
flipper.getGame().getAction().notifyOfValue(sa, flipper, result ? Localizer.getInstance().getMessage("lblHeads") : Localizer.getInstance().getMessage("lblTails"), null);
} while (sa.hasParam("FlipUntilYouLose") && result != false);
if (sa.hasParam("FlipUntilYouLose") && sa.hasAdditionalAbility("LoseSubAbility")) {
sa.getAdditionalAbility("LoseSubAbility").setSVar(varName, "Number$" + numSuccesses);
} }
while (sa.hasParam("FlipUntilYouLose") && won);
return result; return result;
} }
@@ -255,50 +192,50 @@ public class FlipCoinEffect extends SpellAbilityEffect {
* flipCoinCall. * flipCoinCall.
* </p> * </p>
* *
* @param caller * @param flipper
* @param sa * @param sa
* @param multiplier * @param multiplier
* @return a boolean. * @return a boolean.
*/ */
public static boolean flipCoinCall(final Player caller, final SpellAbility sa, final int multiplier) { private static boolean flipCoin(final Player flipper, final SpellAbility sa, int multiplier, final Boolean fixedResult, final String info) {
String varName = sa.getParamOrDefault("SaveNumFlipsToSVar", "X"); Set<Boolean> flipResults = new HashSet<>();
return flipCoinCall(caller, sa, multiplier, varName, ""); boolean noCall = sa.hasParam("NoCall");
} boolean choice = true;
public static boolean flipCoinCall(final Player caller, final SpellAbility sa, final int multiplier, final String varName) { if (fixedResult != null) {
return flipCoinCall(caller, sa, multiplier, varName, ""); flipResults.add(fixedResult);
} } else {
public static boolean flipCoinCall(final Player caller, final SpellAbility sa, final int multiplier, final String varName, final String info) { // no reason to ask if result is fixed anyway
boolean wonFlip = false; if (!noCall) {
int numSuccesses = 0; choice = flipper.getController().chooseBinary(sa, sa.getHostCard().getName() + " - " + Localizer.getInstance().getMessage("lblCallCoinFlip") + info, PlayerController.BinaryChoiceType.HeadsOrTails);
}
do {
Set<Boolean> flipResults = new HashSet<>();
final boolean choice = caller.getController().chooseBinary(sa, sa.getHostCard().getName() + " - " + Localizer.getInstance().getMessage("lblCallCoinFlip") + info, PlayerController.BinaryChoiceType.HeadsOrTails);
for (int i = 0; i < multiplier; i++) { for (int i = 0; i < multiplier; i++) {
flipResults.add(MyRandom.getRandom().nextBoolean()); flipResults.add(MyRandom.getRandom().nextBoolean());
} }
// Play the Flip A Coin sound
caller.getGame().fireEvent(new GameEventFlipCoin());
boolean result = flipResults.size() == 1 ? flipResults.iterator().next() : caller.getController().chooseFlipResult(sa, caller, BOTH_CHOICES, true);
wonFlip = result == choice;
if (wonFlip) {
numSuccesses++;
}
caller.getGame().getAction().notifyOfValue(sa, caller, wonFlip ? Localizer.getInstance().getMessage("lblWin") : Localizer.getInstance().getMessage("lblLose"), null);
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(caller);
runParams.put(AbilityKey.Result, wonFlip);
caller.getGame().getTriggerHandler().runTrigger(TriggerType.FlippedCoin, runParams, false);
} while (sa.hasParam("FlipUntilYouLose") && wonFlip);
if (sa.hasParam("FlipUntilYouLose") && sa.hasAdditionalAbility("LoseSubAbility")) {
sa.getAdditionalAbility("LoseSubAbility").setSVar(varName, "Number$" + numSuccesses);
} }
return wonFlip; boolean result = flipResults.size() == 1 ? flipResults.iterator().next() : flipper.getController().chooseFlipResult(sa, flipper, BOTH_CHOICES, true);
boolean wonOrHeads = result == choice;
String outcome;
if (noCall) {
outcome = wonOrHeads ? Localizer.getInstance().getMessage("lblHeads") : Localizer.getInstance().getMessage("lblTails");
} else {
outcome = wonOrHeads ? Localizer.getInstance().getMessage("lblWin") : Localizer.getInstance().getMessage("lblLose");
}
// Play the Flip A Coin sound
flipper.getGame().fireEvent(new GameEventFlipCoin());
flipper.getGame().getAction().notifyOfValue(sa, flipper, outcome, null);
flipper.flip();
if (!noCall || fixedResult != null) {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(flipper);
runParams.put(AbilityKey.Result, wonOrHeads);
flipper.getGame().getTriggerHandler().runTrigger(TriggerType.FlippedCoin, runParams, false);
}
return wonOrHeads;
} }
public static int getFlipMultiplier(final Player flipper) { public static int getFlipMultiplier(final Player flipper) {

View File

@@ -60,18 +60,21 @@ public class ManaEffect extends SpellAbilityEffect {
if (abMana.isComboMana()) { if (abMana.isComboMana()) {
int amount = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(card, sa.getParam("Amount"), sa) : 1; int amount = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(card, sa.getParam("Amount"), sa) : 1;
if(amount <= 0) if (amount <= 0)
continue; continue;
String express = abMana.getExpressChoice(); String combo = abMana.getComboColors(sa);
String[] colorsProduced = abMana.getComboColors(sa).split(" "); if (combo.isBlank()) {
return;
final StringBuilder choiceString = new StringBuilder(); }
final StringBuilder choiceSymbols = new StringBuilder(); String[] colorsProduced = combo.split(" ");
ColorSet colorOptions = ColorSet.fromNames(colorsProduced); ColorSet colorOptions = ColorSet.fromNames(colorsProduced);
String express = abMana.getExpressChoice();
String[] colorsNeeded = express.isEmpty() ? null : express.split(" "); String[] colorsNeeded = express.isEmpty() ? null : express.split(" ");
boolean differentChoice = abMana.getOrigProduced().contains("Different"); boolean differentChoice = abMana.getOrigProduced().contains("Different");
ColorSet fullOptions = colorOptions; ColorSet fullOptions = colorOptions;
final StringBuilder choiceString = new StringBuilder();
final StringBuilder choiceSymbols = new StringBuilder();
// Use specifyManaCombo if possible // Use specifyManaCombo if possible
if (colorsNeeded == null && amount > 1 && !sa.hasParam("TwoEach")) { if (colorsNeeded == null && amount > 1 && !sa.hasParam("TwoEach")) {
Map<Byte, Integer> choices = chooser.getController().specifyManaCombo(sa, colorOptions, amount, differentChoice); Map<Byte, Integer> choices = chooser.getController().specifyManaCombo(sa, colorOptions, amount, differentChoice);

View File

@@ -1,6 +1,6 @@
package forge.game.ability.effects; package forge.game.ability.effects;
import forge.game.card.Card; import forge.game.card.CardState;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.util.CardTranslation; import forge.util.CardTranslation;
import forge.util.Localizer; import forge.util.Localizer;
@@ -13,10 +13,10 @@ public class PermanentCreatureEffect extends PermanentEffect {
@Override @Override
public String getStackDescription(final SpellAbility sa) { public String getStackDescription(final SpellAbility sa) {
final Card sourceCard = sa.getHostCard(); final CardState source = sa.getCardState();
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
sb.append(CardTranslation.getTranslatedName(sourceCard.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" ").append(sourceCard.getNetPower()); sb.append(CardTranslation.getTranslatedName(source.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" ").append(source.getBasePowerString());
sb.append(" / ").append(sourceCard.getNetToughness()); sb.append(" / ").append(source.getBaseToughnessString());
return sb.toString(); return sb.toString();
} }
} }

Some files were not shown because too many files have changed in this diff Show More