Compare commits

..

860 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
Hans Mackowiak
137d87c3df use StaticAbility for Topsy Turvy (#7143) 2025-03-12 09:24:53 +01:00
Paul Hammerton
fbff1fe10a Merge pull request #7144 from paulsnoops/edition-updates
Edition updates: CC2, FIC, PMEI, SLD, TDM
2025-03-11 08:31:42 +00:00
Paul Hammerton
3acd4490c5 Edition updates: CC2, FIC, PMEI, SLD, TDM 2025-03-11 08:28:25 +00:00
Hans Mackowiak
2952ed79f8 ~ lf 2025-03-11 07:07:08 +01:00
Fulgur14
cb23eda5a6 Stormplain Detainment [TDM] 2025-03-10 17:10:07 +00:00
Renato Filipe Vidal Santos
ced87c8aea FIC: Celes, Rune Knight 2025-03-10 15:49:40 +00:00
Jetz72
daf87e26ad Fix importing "Ice Tunnel"; Max Speed being flipped instead of transformed (#7138) 2025-03-10 14:31:35 +00:00
Chris H
0c30c4e32c Restrict conspiracy drafts in Adventure mode for now 2025-03-08 18:55:00 -05:00
Paul Hammerton
bf5f9f69ca Merge pull request #7133 from paulsnoops/edition-updates
Edition updates: PWCS
2025-03-08 20:50:08 +00:00
Paul Hammerton
b4828d3b4d Edition updates: PWCS 2025-03-08 20:35:44 +00:00
Renato Filipe Vidal Santos
617df8e07c Fixing Chimeric Mass and Svogthos (#7130) 2025-03-08 08:15:03 +00:00
Simisays
deaba89f46 jace update 2025-03-07 21:23:23 -05:00
Hans Mackowiak
d5b13d56cc Update jackdaw.txt
Closes #7127
2025-03-07 09:26:32 +01:00
Chris H
f893c7ddf8 Balance some item costs 2025-03-06 12:14:00 -05:00
Paul Hammerton
c6cf450ac4 Merge pull request #7121 from paulsnoops/edition-updates
Edition updates: TDM
2025-03-04 18:40:10 +00:00
Paul Hammerton
fa123023c7 Edition updates: TDM 2025-03-04 18:24:38 +00:00
Fulgur14
0b285fa045 Rally the Monastery (#7120) 2025-03-04 17:03:31 +00:00
Paul Hammerton
fe6c4243ee Merge pull request #7119 from paulsnoops/ydft-formats
Add YDFT to formats
2025-03-04 14:06:36 +00:00
Paul Hammerton
1a2c18d25e Add YDFT to formats 2025-03-04 14:03:04 +00:00
Renato Filipe Vidal Santos
12e6de4697 Highway Reaver fix (#7118) 2025-03-04 14:58:02 +01:00
Paul Hammerton
f2b72d4234 Merge pull request #7090 from dracontes/rb-ydft-3
YDFT: 12 cards
2025-03-04 11:22:35 +00:00
Renato Filipe Vidal Santos
25a84e9aee Update venom_deadly_devourer.txt (#7117) 2025-03-04 11:47:54 +01:00
Hans Mackowiak
488171b02b Dream counters rework (#7116)
* Dream Counters: moved to extra getCounterMax function

* Update Rasputin Dreamweaver State-Based-Action with a StaticAbility
2025-03-04 11:33:57 +01:00
Renato Filipe Vidal Santos
eb22a449f4 Update underworld_sentinel.txt 2025-03-04 09:25:14 +00:00
Renato Filipe Vidal Santos
c21c043f5f Add files via upload 2025-03-04 06:05:23 +00:00
Simisays
94808ea73e Update vampirecastle_4.tmx 2025-03-03 18:57:32 -05:00
Simisays
7fe486cba6 Update zedruu.tmx 2025-03-03 18:57:32 -05:00
Simisays
a3517e260c update 2025-03-03 18:57:32 -05:00
Paul Hammerton
377f1fad41 Update MagicFest 2025.txt 2025-03-03 13:30:44 -05:00
Chris H
17448f99c9 Update MagicFest 2025.txt 2025-03-03 13:30:44 -05:00
Hans Mackowiak
8c83886c2e Update legions_to_ashes.txt
fix missing OppCtrl
2025-03-03 16:10:56 +01:00
Renato Filipe Vidal Santos
9cef38af60 SPE: Sensational Spider-Man (#7112) 2025-03-03 15:54:00 +01:00
tool4ever
288ecc0d72 Support Sensational Spider-Man (#7110) 2025-03-03 15:52:35 +01:00
Renato Filipe Vidal Santos
bb514f6d08 Update fallaji_antiquarian.txt 2025-03-03 10:06:50 +00:00
Chris H
ecb21abb9a Delay rendering the next dialog by a short bit, to allow for the TouchUp keypress to clear 2025-03-02 21:04:07 -05:00
Chris H
9445093d68 Fix Wanderlust from breaking other quests 2025-03-02 21:04:07 -05:00
Renato Filipe Vidal Santos
8f3f83051b SPE: 5 cards (#7107) 2025-03-02 21:49:24 +00:00
Renato Filipe Vidal Santos
5750892edf Add files via upload 2025-03-02 17:47:48 +00:00
Renato Filipe Vidal Santos
db74b2b70b Update shops.json 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
a5c036be05 Update quests.json 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
b60ee73ce5 Add files via upload 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
b743f1cbc5 Update golem.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
0f0bb56f7d Update goblins.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
8b593f8356 Update bluewizard_apprentice_2.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
79ac73fb15 Update banditarcher_damage.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
da0bb4c0ce Update lorthos.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
9d7617add9 Update emrakul.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
f38ed39c87 Update shops.json 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
8ff5f45449 Update ChooseCardNameEffect.java 2025-03-02 17:19:14 +00:00
Hans Mackowiak
0447299e4f StaticAbility InfectDamage for Phyrexian Unlife 2025-03-02 11:42:37 +01:00
Paul Hammerton
b797317f1a Merge pull request #7106 from paulsnoops/edition-updates
Edition updates: PLG25, PSLDSC, SLD, SPE, YDFT
2025-03-01 22:27:41 +00:00
Paul Hammerton
388f334fd3 Edition updates: PLG25, PSLDSC, SLD, SPE, YDFT 2025-03-01 22:22:15 +00:00
Chris H
e47f623b0a Update argent_dais.txt (#7103) 2025-03-01 18:28:00 +00:00
Renato Filipe Vidal Santos
93e71bded8 Add files via upload 2025-03-01 16:09:56 +00:00
Renato Filipe Vidal Santos
8a93283398 Add files via upload 2025-03-01 11:43:07 +00:00
Renato Filipe Vidal Santos
aafd9b8f49 Update fear_of_change.txt 2025-03-01 08:52:38 +00:00
Renato Filipe Vidal Santos
32fec183d5 Add files via upload 2025-03-01 08:51:56 +00:00
Agetian
d1751262df Make canBlock check if the blocker is a creature. (#7098)
- TokenAi also checks if the spawned token is a creature before running checks.
2025-02-28 19:09:37 +00:00
tool4ever
d05523360d Fix missing SpellDescription (#7099) 2025-02-28 19:08:53 +00:00
Hans Mackowiak
a32f9a3c1c Update a-uurg_spawn_of_turg.txt 2025-02-28 14:38:32 +01:00
Hans Mackowiak
0468247c5a Update uurg_spawn_of_turg.txt 2025-02-28 14:38:07 +01:00
Hans Mackowiak
d02dd67016 Hidden and Double Agenda better as Keyword (#7093)
* Hidden and Double Agenda better as Keyword
2025-02-28 10:22:13 +01:00
tool4ever
27d5766abb Update aetherflux_conduit.txt 2025-02-28 09:13:02 +01:00
Renato Filipe Vidal Santos
be88c63414 Update decoy_gambit.txt (#7095) 2025-02-28 09:08:36 +01:00
Renato Filipe Vidal Santos
c0d965857a Add files via upload 2025-02-28 07:42:50 +00:00
Renato Filipe Vidal Santos
e1763c45af Update quicksilver_lapidary.txt 2025-02-28 04:28:43 +00:00
Renato Filipe Vidal Santos
177f7f64ac Update euru_acorn_scrounger.txt 2025-02-28 04:28:04 +00:00
Renato Filipe Vidal Santos
a2096aa753 Update sala_deck_boss.txt 2025-02-27 21:29:40 +00:00
Renato Filipe Vidal Santos
bc9fac1da6 Add files via upload 2025-02-27 21:23:18 +00:00
Renato Filipe Vidal Santos
e0dcf24c90 Update banquet_guests.txt 2025-02-27 19:36:39 +00:00
Paul Hammerton
db5eb095aa Merge pull request #7094 from Agetian/lda-update-dft
Update LDA deck generation information for Aetherdrift
2025-02-27 18:11:39 +00:00
marthinwurer
16b87f20ca Delegate to piles all the time 2025-02-27 11:07:44 -05:00
Agetian
00b82fe9f9 - Update LDA information (Aetherdrift) 2025-02-27 14:06:23 +03:00
Renato Filipe Vidal Santos
6af0ad100e Update marina_vendrell.txt 2025-02-27 06:17:25 +01:00
Renato Filipe Vidal Santos
f14fda5d1b Update ForgeScript.java 2025-02-27 02:13:01 +00:00
Renato Filipe Vidal Santos
1ef027e7e2 Add files via upload 2025-02-27 02:11:08 +00:00
Renato Filipe Vidal Santos
61aaba268f Update agent_of_masks.txt 2025-02-26 19:34:40 +00:00
Renato Filipe Vidal Santos
b041eee060 Update a-blood_artist.txt 2025-02-26 19:33:52 +00:00
Renato Filipe Vidal Santos
8ed65b95f2 Update blood_artist.txt 2025-02-26 19:32:41 +00:00
Renato Filipe Vidal Santos
3fe53f601c Add files via upload 2025-02-26 19:31:38 +00:00
Paul Hammerton
5ce0374426 Merge pull request #7089 from paulsnoops/edition-updates
Edition updates: YDFT
2025-02-26 19:29:38 +00:00
Paul Hammerton
e4aba70090 Edition updates: YDFT 2025-02-26 19:21:22 +00:00
Renato Filipe Vidal Santos
b17824c20a YDFT: Support Skyforge 2025-02-26 18:16:20 +01:00
Chris H
976dd18fa2 Update negan_the_cold_blooded.txt (#7085) 2025-02-26 18:15:58 +01:00
Renato Filipe Vidal Santos
56bfcec656 Incidental cleanup pass #4 (#7084) 2025-02-26 18:15:43 +01:00
Chris H
f935706d22 Disable upload workflows to ftp forge 2025-02-26 08:52:36 -05:00
Renato Filipe Vidal Santos
e2322ee7ef YDFT: 3 cards (#7077) 2025-02-26 09:53:15 +01:00
Chris H
d553a7cac4 Update stairs_to_infinity.txt (#7082) 2025-02-26 06:00:24 +01:00
tool4ever
f75b2ad9ee Update neriv_crackling_vanguard.txt 2025-02-25 19:42:22 +00:00
tool4ever
333b25eeaf Update yannik_scavenging_sentinel.txt 2025-02-25 19:09:32 +00:00
Hans Mackowiak
0db70261f9 Update TypeLists.txt
Add missing Glimmer
2025-02-25 19:29:53 +01:00
tool4ever
504db590db Fix All in Good Time (#7081)
* Fix for Ketramose
2025-02-25 18:25:35 +00:00
Agetian
650b667148 - Add an AI hint for Arid Archway. (#7080) 2025-02-25 17:17:21 +03:00
Paul Hammerton
4afdd0c264 Merge pull request #7079 from paulsnoops/edition-updates
Edition updates
2025-02-25 10:22:17 +00:00
Paul Hammerton
2816bdef85 Edition updates: SLD, TDC, YDFT 2025-02-25 10:18:25 +00:00
Paul Hammerton
855fe70281 Edition updates: SLD, TDC, YDFT 2025-02-25 10:17:04 +00:00
Fulgur14
e4071a4f4e Neriv and Elsha (TDC) (#7078) 2025-02-25 11:06:36 +01:00
Hans Mackowiak
18ee17f7c8 Update TypeLists.txt
Fix missing Balloon type
2025-02-25 09:26:47 +01:00
Chris H
f84f694351 Reroute card images to scryfall.
Don't try to redownload if it fails during a session
2025-02-24 22:50:14 -05:00
tool4ever
0a15a0352d Fix battles attacking/blocking (#7075)
* Add battle SBA

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.59>
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-24 16:03:58 +01:00
tool4ever
0e46d436de Update mimeoplasm_revered_one.txt 2025-02-24 08:00:48 +01:00
Hans Mackowiak
a16b4ffe75 CostBehold: add Special Reveal Cost (#7072)
* CostBehold: add Special Reveal Cost

----

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-02-23 16:52:24 +01:00
tool4ever
608d4c5bda Don't update view of LKI instead of real Card (#7070)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-23 15:18:40 +03:00
Paul Hammerton
dbb8d8c93a Merge pull request #7071 from paulsnoops/update-tdc
Edition updates: TDC
2025-02-23 10:36:42 +00:00
Paul Hammerton
e30f9a6cb1 Edition updates: TDC 2025-02-23 10:22:21 +00:00
Renato Filipe Vidal Santos
bd4f5a2aa4 TDC: Betor, Ancestor's Voice (#7069) 2025-02-23 09:43:54 +00:00
Renato Filipe Vidal Santos
2d352b110b Negation cleanup: IsUnsolved (#7068) 2025-02-22 22:04:05 +00:00
Renato Filipe Vidal Santos
2802c61abd Negation cleanup: notAttackedThisTurn (#7066) 2025-02-22 21:37:02 +00:00
Paul Hammerton
62c27f9142 Replace net deck links 2025-02-22 15:48:16 -05:00
Renato Filipe Vidal Santos
c358f4e71f Negation cleanup: IsNotCommander (#7064) 2025-02-22 20:38:15 +00:00
Chris H
a05ecbc810 Update teval_the_balanced_scale.txt (#7065) 2025-02-22 20:34:55 +00:00
tool4ever
6b299693ca Finish nonToken cleanup (#7063) 2025-02-22 19:26:48 +00:00
Renato Filipe Vidal Santos
bb3413c1e5 Negation cleanup: nonToken, part 3 (#6952) 2025-02-22 17:44:28 +00:00
Renato Filipe Vidal Santos
854267d521 Negation cleanup: nonToken, part 2 (#6951) 2025-02-22 17:42:28 +00:00
Renato Filipe Vidal Santos
c4e05a5d9b Negation cleanup: nonToken, part 1 (#6950) 2025-02-22 17:42:17 +00:00
Renato Filipe Vidal Santos
27b61a79c8 Negation cleanup: nonToken, part 4 (#6953) 2025-02-22 17:42:03 +00:00
Chris H
70183bcc85 Update snapshot-both-pc-android.yml 2025-02-22 11:19:33 -05:00
Hans Mackowiak
94da663287 ~ lf 2025-02-22 15:00:59 +01:00
Paul Hammerton
3c7c1cc4c7 Merge pull request #7060 from paulsnoops/edition-updates
Edition updates: SLD, TDC, TDM
2025-02-22 10:39:27 +00:00
Paul Hammerton
3b773da60d Edition updates: SLD, TDC, TDM 2025-02-22 10:34:19 +00:00
Renato Filipe Vidal Santos
afee15cf44 TDM: 5 cards (#7058) 2025-02-22 10:17:39 +00:00
Fulgur14
a424aa65df 2 TDM and 1 TDC card (#7056) 2025-02-22 09:03:40 +00:00
tool4EvEr
fa93a7dfdd Fix Well-Laid Plans 2025-02-21 10:46:14 -05:00
Renato Filipe Vidal Santos
446f60b331 Add files via upload 2025-02-21 15:22:33 +01:00
tool4ever
e136368ce3 Update narset_jeskai_waymaster.txt 2025-02-21 10:33:06 +01:00
Fulgur14
5f1b54860c Narset, Jeskai Waymaster (TDM) (#7052) 2025-02-21 11:59:24 +03:00
Chris H
23eb008d5f Trigger edition change if playing 10E draft on mobile 2025-02-20 20:13:21 -05:00
Chris H
40882c20d6 Send URL to github snapshots 2025-02-20 20:13:04 -05:00
tool4ever
9054e01273 Fix corner case with Jace, Wielder of Mysteries (#7050) 2025-02-20 19:03:24 +00:00
Chris H
c5fe9b2667 Snapshot direct to GitHub (#7049)
Upload snapshots directly to a prerelease on GH
2025-02-20 13:14:53 -05:00
tool4ever
0b382d3a9a Update boom_scholar.txt 2025-02-20 09:07:06 +01:00
Hans Mackowiak
45319ddf73 ~ lf 2025-02-20 06:54:18 +01:00
Paul Hammerton
76c725843d Merge pull request #7046 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, SLD
2025-02-19 17:59:37 +00:00
Paul Hammerton
ee09d6ca6a Edition updates: FCA, FIC, FIN, SLD 2025-02-19 17:56:27 +00:00
Fulgur14
02173ce357 8 FIN cards (#7040) 2025-02-19 16:42:24 +00:00
tool4ever
105bfdc489 Script fixes (#7045) 2025-02-19 16:19:57 +01:00
Hans Mackowiak
9b90d04376 Update boommobile.txt
fix Exhaust Cost
2025-02-19 15:22:43 +01:00
Hans Mackowiak
6233fc09af Update Aetherdrift.txt
fix Skysovereign, Consul Flagship
2025-02-19 14:09:15 +01:00
tool4ever
ec20b59ff3 Dependency tab (#7013) 2025-02-19 10:31:59 +01:00
Northmoc
049eb19be4 fix issue #7007 (Double Team) (#7038) 2025-02-19 10:08:12 +01:00
Renato Filipe Vidal Santos
e5e8fa4cdd FIN: 4 cards (#7042) 2025-02-19 10:07:09 +01:00
tool4ever
80c11b9f11 Speed again (#7035)
* Speed again

* Fix NPE

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-18 19:52:43 +03:00
Renato Filipe Vidal Santos
af055f37dc Add files via upload (#7003) 2025-02-18 19:52:25 +03:00
Agetian
49c0db5280 Add puzzle PS_DFT1 - Possibility Storm - Aetherdrift 01 (#7037)
* - Add DFT/DRC achievements by Marek14.

* - Add puzzle PS_DFT1.
2025-02-18 19:52:03 +03:00
Agetian
8478835c4d - Add DFT/DRC achievements by Marek14. (#7036) 2025-02-18 19:39:29 +03:00
Paul Hammerton
804a9d9f20 Merge pull request #7034 from paulsnoops/edition-updates
Edition updates: FIC, FIN, SLD, SLP, SLX, TDM
2025-02-18 11:48:29 +00:00
Paul Hammerton
9060dd786f Edition updates: FIC, FIN, SLD, SLP, SLX, TDM 2025-02-18 11:38:30 +00:00
Renato Filipe Vidal Santos
802fee2e86 FIC: 4 cards (#7032) 2025-02-18 11:22:26 +00:00
Hans Mackowiak
8f6fc751dd Make AI start their engine in Main1 (#7033) 2025-02-18 10:06:37 +03:00
Simisays
770dbc31cd [ADVENTURE] Sorin dungeon cleanup (#7026)
* Update vampirecastle_4.tmx

* 2 more cleanups
2025-02-18 07:30:36 +03:00
Chris H
a49ab150f9 Fix imports 2025-02-17 20:19:41 -05:00
Chris H
4bc07e5311 Disable bulk images from the UI 2025-02-17 20:19:41 -05:00
tool4ever
8706ba7b68 Update sphere_of_annihilation.txt (#7030)
Closes #7029
2025-02-17 12:38:13 +01:00
Hans Mackowiak
99bc83ae84 Add GameEventDoorChanged for log 2025-02-17 10:04:55 +01:00
tool4ever
16e871be7b Fix Arvinox sometimes failing (#7023) 2025-02-16 14:16:42 +01:00
tool4ever
afc4024287 Effects don't need Exiled trigger (#7022) 2025-02-16 09:43:36 +00:00
tool4ever
0da1681c96 checkStaticAbilities: skip wrong zone earlier for less looping (#7018)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-16 09:50:37 +03:00
tool4ever
da19214754 Radiant Lotus revisited (#7017)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-16 08:00:41 +03:00
Chris H
44fca5ee5e Restore flatten library 2025-02-15 18:00:19 -05:00
GitHub Actions
2f33c24414 [maven-release-plugin] prepare for next development iteration 2025-02-15 18:00:19 -05:00
GitHub Actions
380f289887 [maven-release-plugin] prepare release forge-2.0.02 2025-02-15 18:00:19 -05:00
Chris H
a5ab069f5b Temporarily remove flatten library for release 2025-02-15 18:00:19 -05:00
Jetz
e880a83df2 Let SVars in functional variants overwrite original script 2025-02-15 12:34:00 -05:00
Jetz
80cc7218a3 URLs for token image keys for speed and max_speed 2025-02-15 12:34:00 -05:00
Renato Filipe Vidal Santos
4809fb858a Update gale_conduit_of_the_arcane.txt 2025-02-15 12:19:58 -05:00
Jetz
3af62888dc Remove wake lock on desktop 2025-02-15 07:50:06 +01:00
Jetz
79e1d0a0f0 Count destroyed attractions for "number of attractions you've visited this turn" 2025-02-15 07:50:06 +01:00
Jetz
c447dfc888 Support "Affinitycycling" and "Affinity for Affinity" 2025-02-15 07:50:06 +01:00
Jetz
8149966915 Fix Tyrranax Rex importing 2025-02-15 07:50:06 +01:00
Hans Mackowiak
472f9481e8 Update radiant_lotus.txt 2025-02-15 07:49:21 +01:00
Hans Mackowiak
2209ce3cee Update sanguine_soothsayer.txt
fix mana cost
2025-02-14 14:57:40 +01:00
Hans Mackowiak
258c89e65d Update CounterEffect.java (#7014) 2025-02-14 09:06:25 +01:00
tool4ever
11913085ef Misc cleanup (#7009) 2025-02-13 15:51:19 +01:00
Chris H
53fca12a57 Fix Radiant lotus 2025-02-12 21:14:01 -05:00
Chris H
8e8a795f19 Migrate upcoming 2025-02-12 20:34:55 -05:00
Renato Filipe Vidal Santos
a4b27321ac Oracle update: During your turn (#7005) 2025-02-12 10:55:57 +01:00
Renato Filipe Vidal Santos
ead83d932f Oracle update: Affinity (#7001) 2025-02-12 10:34:50 +01:00
tool4ever
900bd4327d Update pride_of_the_road.txt 2025-02-12 10:21:39 +01:00
Paul Hammerton
1a2bb054f4 Merge pull request #7000 from paulsnoops/fix-sld
Edition updates: SLD
2025-02-11 19:09:54 +00:00
Paul Hammerton
f908df46c8 Edition updates: SLD 2025-02-11 19:07:12 +00:00
Paul Hammerton
f8c97842c4 Merge pull request #6999 from paulsnoops/edition-updates
Edition updates: SLD
2025-02-11 17:08:59 +00:00
Paul Hammerton
c324b45025 Edition updates: SLD 2025-02-11 17:02:00 +00:00
tool4ever
5061ceda0e Clean up (#6995) 2025-02-10 21:43:04 +00:00
Chris H
d1e677eb4f Update rangers_refueler.txt 2025-02-10 17:37:26 +01:00
Jetz72
493a8f351b Add Speed Tracker to Command Zone (#6982)
* Add command zone effect displaying speed

* Remove enum counter type for speed.

* Make Start Your Engines an SBA.

* LifeLost -> LifeLostAll per speed rules.

* Use same game event for all speed changes.

* Fix keyword not appearing in detail text

* Cleanup extra createSpeedEffect.

* Add support for arbitrary overlay text. Remove fake counters.

* Text styling.

* Remove extra SBA check.

* Remove speed from PlayerView; localization support.

---------

Co-authored-by: Jetz <Jetz722@gmail.com>
2025-02-10 07:43:48 +03:00
Hans Mackowiak
04172eead0 suspected as Static Effect (#6991) 2025-02-09 13:02:59 +00:00
tool4ever
f0ed9288b3 Remove outdated logic (#6989) 2025-02-09 13:01:15 +00:00
Hans Mackowiak
03fe3d63ea Start your Engines as StaticAction (#6987)
* Start your Engines as StaticAction

* ~ fix trigger desc

* ~ moved keyword to better place
2025-02-09 10:52:17 +01:00
Hans Mackowiak
83438ef72b StaticAbilityContinous now have AffectedDefined for card list (#6986)
* StaticAbilityContinous now have AffectedDefined for card list
2025-02-09 10:49:19 +01:00
Paul Hammerton
cf18808a70 Merge pull request #6988 from paulsnoops/dft-rank
Update draft rankings: DFT, INR, PIO
2025-02-08 23:13:53 +00:00
Paul Hammerton
49dc2c1c42 Update draft rankings: DFT, INR, PIO 2025-02-08 23:10:29 +00:00
Northmoc
692400db2a finish up Max speed refactor (#6958)
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-02-08 14:42:55 +01:00
Northmoc
751c31b226 refactor Max Speed round 1 (#6957) 2025-02-08 14:35:26 +01:00
tool4ever
7be252c509 Fix Elvish Refueler (#6984) 2025-02-08 08:04:30 +00:00
tool4ever
288eac743c Fix Blessing applying too late for triggers (#6983) 2025-02-07 18:17:00 +00:00
tool4ever
d0bd80f158 Update spire_mechcycle.txt 2025-02-07 09:28:38 +01:00
Paul Hammerton
7fe8154bcb Merge pull request #6979 from paulsnoops/edition-updates
Edition updates: PL25
2025-02-06 18:38:42 +00:00
Paul Hammerton
87cd5c90a3 Edition updates: PL25 2025-02-06 18:34:33 +00:00
tool4ever
12399fca48 Update elvish_refueler.txt 2025-02-06 17:42:04 +00:00
Chris H
aaf17553c1 Update syphon_fuel.txt (#6978) 2025-02-06 17:20:17 +01:00
tool4ever
9dedd24d3e Fix Manifest Dread vs. Grafdigger's Cage (#6977) 2025-02-06 17:19:40 +01:00
tool4ever
2443f1486d Fix Primal Wellspring trigger not working with Chun-Li (#6971)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-06 09:40:29 +03:00
Simisays
cfd1822198 update 2025-02-05 19:05:00 -05:00
Paul Hammerton
7930c4949b Merge pull request #6975 from paulsnoops/fix-token
Edition updates: Fix token name in DFT
2025-02-05 17:50:39 +00:00
Paul Hammerton
ef6d0707ac Edition updates: Fix token name in DFT 2025-02-05 17:47:17 +00:00
Paul Hammerton
cfd792cb69 Merge pull request #6974 from paulsnoops/edition-updates
Edition updates: DFT, DRC, SLD, SPG
2025-02-05 17:14:52 +00:00
Paul Hammerton
f5352662cd Edition updates: DFT, DRC, SLD, SPG 2025-02-05 17:09:00 +00:00
Justin C
bf1192f80d instanceof Pattern variable changes (#6972) 2025-02-05 16:52:14 +03:00
tool4ever
dee2150cf9 Fix trigger targeting itself (#6973)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-05 16:52:04 +03:00
Hans Mackowiak
c44b105d9f ~ lf 2025-02-04 07:03:51 +01:00
loud1990
b624fb3cf8 DFT Card Cleanup
Changed mana cost for Sundial, trigger for Necroregent
2025-02-03 20:48:11 -05:00
Chris H
45396c1bf4 Fix sellfactor duplication 2025-02-03 18:55:24 -05:00
tool4ever
f562ae6fdb More view cleanup (#6967) 2025-02-03 21:12:03 +00:00
Chris H
6615090bda Allow jumpstart/"nosell" cards to be sold for 0 credits 2025-02-02 12:55:09 -05:00
Chris H
eaf6f117a2 Don't try to load battle if entering village simultaneously 2025-02-02 12:54:38 -05:00
tool4ever
a8488502e7 Reduce some View updates (#6966) 2025-02-02 17:52:20 +00:00
Chris H
309e36827c Fix draftable cards 2025-02-01 22:24:36 -05:00
Chris H
fe7883ddd8 Add ManaCost for Wretched Doll 2025-02-01 22:08:14 -05:00
Chris H
06e5ff5174 DFT edition cards update 2025-02-01 22:08:14 -05:00
Chris H
2c31dd01dd Add booster info for Aetherdrift 2025-02-01 21:40:31 -05:00
Hans Mackowiak
d8a92c4879 ~ lf 2025-02-01 11:55:46 +01:00
Fulgur14
16baeadf0c Final DFT push (2 of 3) (#6948) 2025-02-01 09:34:31 +00:00
Hans Mackowiak
88ed81f75f ~ lf 2025-02-01 10:17:29 +01:00
Fulgur14
70d9df1db2 Final DFT push (3 of 3) (#6949) 2025-02-01 08:33:19 +00:00
Hans Mackowiak
aaa04570f2 Update howlers_heavy.txt 2025-02-01 07:10:20 +01:00
Fulgur14
9365d55964 Final DFT push (1 of 3) (#6947) 2025-01-31 23:04:52 +00:00
Renato Filipe Vidal Santos
0c61139f51 Add files via upload (#6945) 2025-01-31 17:36:16 +03:00
Hans Mackowiak
34bd623e45 Update kataki_wars_wage.txt
Closes #6943
2025-01-31 11:07:00 +01:00
Fulgur14
0e31bb8565 7 DFT cards (Waxen Shapethief and its models) (#6937) 2025-01-31 08:33:59 +01:00
Chris H
0b87094f96 Fix NPE with POEReference 2025-01-30 21:32:17 -05:00
Chris H
42e53c66f6 Update ravenous_amulet.txt 2025-01-30 16:58:11 -05:00
Renato Filipe Vidal Santos
25a7d80146 Fixing mistaken references to defending player in triggered abilities 2025-01-30 21:36:48 +00:00
Simisays
a6170745b1 Update config.json 2025-01-30 15:04:49 -05:00
Simisays
db32547a6e Update eldrazilarge.dck 2025-01-30 15:04:49 -05:00
Simisays
4df6d9998b Update config.json 2025-01-30 15:04:49 -05:00
Simisays
2f42f6ca28 Update config.json 2025-01-30 15:04:49 -05:00
Simisays
6617c10946 update 2025-01-30 15:04:49 -05:00
Northmoc
1d34e02957 grim_bauble.txt and some fixes (#6935) 2025-01-30 18:34:06 +00:00
Northmoc
132f8d3d4f another round of whitespace and other fixes (#6934) 2025-01-30 18:32:41 +00:00
Northmoc
d3961b1a53 outpace_oblivion.txt (#6936) 2025-01-30 18:31:40 +00:00
Fulgur14
2c04ef9e1f Howlsquad Heavy (DFT) (#6933) 2025-01-30 13:37:02 +01:00
Fulgur14
f599e3ead6 4 DFT cards (Oviya and her projects) (#6932) 2025-01-30 12:07:42 +01:00
Northmoc
e16da84a75 gonti_night_minister.txt + support (#6924) 2025-01-30 11:28:04 +03:00
Fulgur14
0a622f5282 19 DFT cards (Mimeoplasm and its imprints) (#6922)
* Add files via upload

* Update mimeoplasm_revered_one.txt

* Update radiant_lotus.txt

* Add files via upload

* Update radiant_lotus.txt

* Update ripclaw_wrangler.txt
2025-01-30 08:07:40 +03:00
Fulgur14
bb40138c52 8 DFT cards (Adrenaline Jockey and groupies) (#6925)
* 7 DFT cards (Adrenaline Jockey and groupies)

* Add files via upload
2025-01-30 08:07:31 +03:00
tool4ever
2a7bd8bbd2 Clean up logic (#6928) 2025-01-29 18:53:39 +00:00
Fulgur14
e6fc666012 Rover Blades and Gastal Thrillroller (#6927) 2025-01-29 18:21:24 +00:00
Fulgur14
a1297e593c Salvation Engine (#6926) 2025-01-29 17:38:50 +00:00
Renato Filipe Vidal Santos
2026c7eca0 Edit pile cleanup: Trigger effects formatted as activated abilities 2025-01-29 14:13:15 +00:00
Fulgur14
137076f224 9 DFT cards (3 Tyrants and their food) (#6912) 2025-01-29 11:08:51 +00:00
Fulgur14
5538650681 7 DFT cards (Gastal Blockbuster and friends) (#6909) 2025-01-29 11:08:39 +00:00
tool4ever
0e36e6b6d9 Fix room fully unlocked when turned up from manifest (#6921) 2025-01-29 08:46:57 +00:00
Fulgur14
f4c786763a Update bitter_chill.txt (#6920) 2025-01-29 07:42:41 +00:00
5761 changed files with 74279 additions and 41033 deletions

View File

@@ -2,10 +2,21 @@ name: Publish Desktop Forge
on:
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:
build:
if: github.repository_owner == 'Card-Forge'
runs-on: ubuntu-latest
permissions:
contents: write
@@ -32,10 +43,94 @@ jobs:
run: |
git config user.email "actions@github.com"
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: |
export DISPLAY=":1"
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:
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

@@ -8,6 +8,9 @@ on:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 18 * * *'
jobs:
build:
@@ -109,16 +112,21 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
- name: Upload snapshot to GitHub Prerelease
uses: ncipollo/release-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
server: ftp.cardforge.org
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: izpack/
server-dir: downloads/dailysnapshots/
state-name: .ftp-deploy-both-sync-state.json
exclude: |
*.pom
*.repositories
*.xml
name: Daily Snapshot
tag: daily-snapshots
prerelease: true
artifacts: izpack/*
allowUpdates: true
removeArtifacts: 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\": \"🔴 Snapshot 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

@@ -13,10 +13,6 @@ on:
# description: 'Upload the completed Android package'
# required: false
# default: true
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 19 * * *'
jobs:
build:

View File

@@ -8,9 +8,6 @@ on:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '30 18 * * *'
jobs:
build:

View File

@@ -26,13 +26,13 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
### 📥 Desktop Installation
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.
3. **User Data Management:** Previous players data is preserved during upgrades.
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
### 📱 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

@@ -45,17 +45,16 @@ public class BiomeStructureDataMappingEditor extends JComponent {
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping))
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping biomeData))
return label;
BiomeStructureData.BiomeStructureDataMapping data=(BiomeStructureData.BiomeStructureDataMapping) value;
// Get the renderer component from parent class
label.setText(data.name);
label.setText(biomeData.name);
if(editor.data!=null)
{
SwingAtlas itemAtlas=new SwingAtlas(Config.instance().getFile(editor.data.structureAtlasPath));
if(itemAtlas.has(data.name))
label.setIcon(itemAtlas.get(data.name));
if(itemAtlas.has(biomeData.name))
label.setIcon(itemAtlas.get(biomeData.name));
else
{
ImageIcon img=itemAtlas.getAny();

View File

@@ -25,9 +25,8 @@ public class DialogOptionEditor extends JComponent{
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof DialogData))
if(!(value instanceof DialogData dialog))
return label;
DialogData dialog=(DialogData) value;
StringBuilder builder=new StringBuilder();
if(dialog.name==null||dialog.name.isEmpty())
builder.append("[[Blank Option]]");

View File

@@ -27,17 +27,16 @@ public class ItemsEditor extends JComponent {
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof ItemData))
if(!(value instanceof ItemData item))
return label;
ItemData Item=(ItemData) value;
// Get the renderer component from parent class
label.setText(Item.name);
label.setText(item.name);
if(itemAtlas==null)
itemAtlas=new SwingAtlas(Config.instance().getFile(Paths.ITEMS_ATLAS));
if(itemAtlas.has(Item.iconName))
label.setIcon(itemAtlas.get(Item.iconName));
if(itemAtlas.has(item.iconName))
label.setIcon(itemAtlas.get(item.iconName));
else
{
ImageIcon img=itemAtlas.getAny();

View File

@@ -26,9 +26,8 @@ public class QuestEditor extends JComponent {
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof AdventureQuestData))
if(!(value instanceof AdventureQuestData quest))
return label;
AdventureQuestData quest=(AdventureQuestData) value;
// Get the renderer component from parent class
label.setText(quest.name);

View File

@@ -26,9 +26,8 @@ public class QuestStageEditor extends JComponent{
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof AdventureQuestStage))
if(!(value instanceof AdventureQuestStage stageData))
return label;
AdventureQuestStage stageData=(AdventureQuestStage) value;
label.setText(stageData.name);
//label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually?
return label;

View File

@@ -43,9 +43,8 @@ public class WorldEditor extends JComponent {
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeData))
if(!(value instanceof BiomeData biome))
return label;
BiomeData biome=(BiomeData) value;
// Get the renderer component from parent class
label.setText(biome.name);

View File

@@ -37,6 +37,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
@@ -115,8 +116,8 @@ public class AiAttackController {
} // overloaded constructor to evaluate single specified attacker
private void refreshCombatants(GameEntity defender) {
if (defender instanceof Card && ((Card) defender).isBattle()) {
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
if (defender instanceof Card card && card.isBattle()) {
this.oppList = getOpponentCreatures(card.getProtectingPlayer());
} else {
this.oppList = getOpponentCreatures(defendingOpponent);
}
@@ -312,7 +313,8 @@ public class AiAttackController {
}
}
// Poison opponent if unblocked
if (defender instanceof Player && ComputerUtilCombat.poisonIfUnblocked(attacker, (Player) defender) > 0) {
if (defender instanceof Player player
&& ComputerUtilCombat.poisonIfUnblocked(attacker, player) > 0) {
return true;
}
@@ -849,10 +851,9 @@ public class AiAttackController {
// decided to attack another defender so related lists need to be updated
// (though usually rather try to avoid this situation for performance reasons)
if (defender != defendingOpponent) {
if (defender instanceof Player) {
defendingOpponent = (Player) defender;
} else if (defender instanceof Card) {
Card defCard = (Card) defender;
if (defender instanceof Player p) {
defendingOpponent = p;
} else if (defender instanceof Card defCard) {
if (defCard.isBattle()) {
defendingOpponent = defCard.getProtectingPlayer();
} else {
@@ -946,8 +947,8 @@ public class AiAttackController {
return 1;
}
// or weakest player
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) {
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife();
if (r1.getKey() instanceof Player p1 && r2.getKey() instanceof Player p2) {
return p1.getLife() - p2.getLife();
}
}
return r2.getValue() - r1.getValue();
@@ -1314,7 +1315,7 @@ public class AiAttackController {
attackersAssigned.add(attacker);
// check if attackers are enough to finish the attacked planeswalker
if (i < left.size() - 1 && defender instanceof Card) {
if (i < left.size() - 1 && defender instanceof Card card) {
final int blockNum = this.blockers.size();
int attackNum = 0;
int damage = 0;
@@ -1328,7 +1329,7 @@ public class AiAttackController {
}
}
// if enough damage: switch to next planeswalker
if (damage >= ComputerUtilCombat.getDamageToKill((Card) defender, true)) {
if (damage >= ComputerUtilCombat.getDamageToKill(card, true)) {
break;
}
}
@@ -1587,7 +1588,7 @@ public class AiAttackController {
// but there are no creatures it can target, no need to exert with it
boolean missTarget = false;
for (StaticAbility st : c.getStaticAbilities()) {
if (!"OptionalAttackCost".equals(st.getParam("Mode"))) {
if (!st.checkMode(StaticAbilityMode.OptionalAttackCost)) {
continue;
}
SpellAbility sa = st.getPayingTrigSA();
@@ -1754,10 +1755,12 @@ public class AiAttackController {
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final Queue<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
// TODO: detect Revenge of Ravens by the trigger instead of by name
boolean revengeOfRavens = false;
if (defender instanceof Player) {
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card) {
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
if (defender instanceof Player player) {
revengeOfRavens = !CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card card) {
revengeOfRavens = !CardLists.filter(card.getController().getCardsIn(ZoneType.Battlefield),
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
}
if (!revengeOfRavens) {

View File

@@ -161,12 +161,12 @@ public class AiBlockController {
// defend battles with fewer defense counters before battles with more defense counters,
// if planeswalker/battle will be too difficult to defend don't even bother
for (GameEntity defender : defenders) {
if ((defender instanceof Card && ((Card) defender).getController().equals(ai))
|| (defender instanceof Card && ((Card) defender).isBattle() && ((Card) defender).getProtectingPlayer().equals(ai))) {
final CardCollection attackers = combat.getAttackersOf(defender);
if ((defender instanceof Card card1 && card1.getController().equals(ai))
|| (defender instanceof Card card2 && card2.isBattle() && card2.getProtectingPlayer().equals(ai))) {
final CardCollection ccAttackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers);
sortedAttackers.addAll(attackers);
CardLists.sortByPowerDesc(ccAttackers);
sortedAttackers.addAll(ccAttackers);
} else if (defender instanceof Player && defender.equals(ai)) {
firstAttacker = combat.getAttackersOf(defender);
CardLists.sortByPowerDesc(firstAttacker);
@@ -872,9 +872,9 @@ public class AiBlockController {
CardCollection threatenedPWs = new CardCollection();
for (final Card attacker : attackers) {
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card) {
if (def instanceof Card card) {
if (!onlyIfLethal) {
threatenedPWs.add((Card) def);
threatenedPWs.add(card);
} else {
int damageToPW = 0;
for (final Card pwatkr : combat.getAttackersOf(def)) {
@@ -906,12 +906,12 @@ public class AiBlockController {
continue;
}
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card && threatenedPWs.contains(def)) {
if (def instanceof Card card && threatenedPWs.contains(def)) {
Card blockerDecided = null;
for (final Card blocker : chumpPWDefenders) {
if (CombatUtil.canBlock(attacker, blocker, combat)) {
combat.addBlocker(attacker, blocker);
pwsWithChumpBlocks.add((Card) def);
pwsWithChumpBlocks.add(card);
chosenChumpBlockers.add(blocker);
blockerDecided = blocker;
blockersLeft.remove(blocker);
@@ -1346,8 +1346,8 @@ public class AiBlockController {
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.CREATURES)
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
&& combat.getDefenderByAttacker(attacker) instanceof Card
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
&& combat.getDefenderByAttacker(attacker) instanceof Card card
&& card.isPlaneswalker();
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
return ((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.

View File

@@ -54,6 +54,7 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityDisableTriggers;
import forge.game.staticability.StaticAbilityMode;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -68,8 +69,10 @@ import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
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.TimeoutException;
@@ -292,7 +295,7 @@ public class AiController {
}
// can't fetch partner isn't problematic
if (tr.getKeyword() != null && tr.getKeyword().getOriginal().startsWith("Partner")) {
if (tr.isKeyword(Keyword.PARTNER)) {
continue;
}
@@ -689,7 +692,6 @@ public class AiController {
}
}
// TODO handle fetchlands and what they can fetch for
// determine new color pips
int[] card_counts = new int[6]; // in WUBRGC order
@@ -1128,7 +1130,7 @@ public class AiController {
// Memory Crystal-like effects need special handling
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
for (StaticAbility s : c.getStaticAbilities()) {
if ("ReduceCost".equals(s.getParam("Mode"))
if (s.checkMode(StaticAbilityMode.ReduceCost)
&& "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
}
@@ -1708,7 +1710,8 @@ public class AiController {
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...
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
@@ -1788,11 +1791,9 @@ public class AiController {
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
try {
if (game.AI_CAN_USE_TIMEOUT)
return future.completeOnTimeout(null, game.getAITimeout(), TimeUnit.SECONDS).get();
else
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
future.cancel(true);
return null;
}
}
@@ -2368,7 +2369,7 @@ public class AiController {
// TODO move to more common place
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) {

View File

@@ -46,6 +46,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.number(c);
}
@Override
public PaymentDecision visit(CostBehold cost) {
final String type = cost.getType();
CardCollectionView hand = player.getCardsIn(cost.getRevealFrom());
hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability);
return hand.isEmpty() ? null : PaymentDecision.card(getBestCreatureAI(hand));
}
@Override
public PaymentDecision visit(CostChooseColor cost) {
int c = cost.getAbilityAmount(ability);

View File

@@ -48,6 +48,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
@@ -1099,6 +1100,11 @@ public class ComputerUtil {
}
}
// if AI has no speed, play start your engines on Main1
if (ai.noSpeed() && cardState.hasKeyword(Keyword.START_YOUR_ENGINES)) {
return true;
}
// cast Blitz in main 1 if the creature attacks
if (sa.isBlitz() && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, card)) {
return true;
@@ -1408,9 +1414,7 @@ public class ComputerUtil {
}
}
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part;
if (part instanceof CostSacrifice sac) {
final String type = sac.getType();
if (type.equals("CARDNAME")) {
@@ -1455,15 +1459,14 @@ public class ComputerUtil {
// check for Continuous abilities that grant Haste
for (final Card c : all) {
for (StaticAbility stAb : c.getStaticAbilities()) {
Map<String, String> params = stAb.getMapParams();
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
&& stAb.getParam("AddKeyword").contains("Haste")) {
if (c.isEquipment() && c.getEquipping() == null) {
return true;
}
final String affected = params.get("Affected");
final String affected = stAb.getParam("Affected");
if (affected.contains("Creature.YouCtrl")
|| affected.contains("Other+YouCtrl")) {
return true;
@@ -1516,11 +1519,10 @@ public class ComputerUtil {
for (final Card c : opp) {
for (StaticAbility stAb : c.getStaticAbilities()) {
Map<String, String> params = stAb.getMapParams();
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
&& stAb.getParam("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")) {
return true;
}
@@ -1776,9 +1778,7 @@ public class ComputerUtil {
noRegen = true;
}
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// indestructible
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
@@ -1842,9 +1842,7 @@ public class ComputerUtil {
if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c, false)) {
threatened.add(c);
}
} else if (o instanceof Player) {
final Player p = (Player) o;
} else if (o instanceof Player p) {
if (source.hasKeyword(Keyword.INFECT)) {
if (p.canReceiveCounters(CounterEnumType.POISON) && ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= 10 - p.getPoisonCounters()) {
threatened.add(p);
@@ -1862,8 +1860,7 @@ public class ComputerUtil {
|| saviourApi == null)) {
final int dmg = -AbilityUtils.calculateAmount(source, topStack.getParam("NumDef"), topStack);
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
final boolean canRemove = (c.getNetToughness() <= dmg)
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && dmg >= ComputerUtilCombat.getDamageToKill(c, false));
if (!canRemove) {
@@ -1909,9 +1906,7 @@ public class ComputerUtil {
|| saviourApi == ApiType.Protection || saviourApi == null
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
// indestructible
if (o instanceof Card c) {
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
}
@@ -1960,8 +1955,7 @@ public class ComputerUtil {
&& topStack.hasParam("Destination")
&& topStack.getParam("Destination").equals("Exile")) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -1988,8 +1982,7 @@ public class ComputerUtil {
&& (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|| saviourApi == ApiType.Protection || saviourApi == null)) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -2011,8 +2004,7 @@ public class ComputerUtil {
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
if (enableCurseAuraRemoval) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -2436,7 +2428,7 @@ public class ComputerUtil {
// Are we picking a type to reduce costs for that type?
boolean reducingCost = false;
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;
break;
}
@@ -2898,7 +2890,7 @@ public class ComputerUtil {
// Iceberg does use Ice as Storage
|| (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName()))
// 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,
// and also on Chronozoa
|| (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.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
@@ -691,6 +692,8 @@ public class ComputerUtilCard {
public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) {
AiBlockController aiBlk = new AiBlockController(ai, checkingOther);
Combat combat = new Combat(ai);
// avoid removing original attacker
attacker.setCombatLKI(null);
combat.addAttacker(attacker, ai);
final List<Card> attackers = Lists.newArrayList(attacker);
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,
// assume it either benefits the player or disrupts the opponent
for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams();
if (params.get("Mode").equals("Continuous") && stAb.isIntrinsic()) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.isIntrinsic()) {
priority = true;
break;
}
@@ -1243,17 +1245,16 @@ public class ComputerUtilCard {
}
} else {
for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams();
//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;
if (params.containsKey("AddPower")) {
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
if (stAb.hasParam("AddPower")) {
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
}
if (params.containsKey("AddToughness")) {
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
if (stAb.hasParam("AddToughness")) {
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
}
String kws = params.get("AddKeyword");
String kws = stAb.getParam("AddKeyword");
if (kws != null) {
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
}
@@ -1784,7 +1785,7 @@ public class ComputerUtilCard {
// remove old boost that might be copied
for (final StaticAbility stAb : c.getStaticAbilities()) {
vCard.removePTBoost(c.getLayerTimestamp(), stAb.getId());
if (!stAb.checkMode("Continuous")) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
if (!stAb.hasParam("Affected")) {
@@ -1862,7 +1863,7 @@ public class ComputerUtilCard {
if (!c.isCreature()) {
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 false;

View File

@@ -31,7 +31,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword;
import forge.game.phase.Untap;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
@@ -39,6 +39,7 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMode;
import forge.game.staticability.StaticAbilityMustAttack;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -101,7 +102,7 @@ public class ComputerUtilCombat {
return false;
}
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), "BeginCombat")) {
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), PhaseType.COMBAT_BEGIN)) {
return false;
}
@@ -118,7 +119,7 @@ public class ComputerUtilCombat {
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
// || attacker.hasSVar("EndOfTurnLeavePlay"));
// 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));
}
/**
@@ -176,7 +177,7 @@ public class ComputerUtilCombat {
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
int damage = attacker.getNetCombatDamage();
int sum = 0;
if (attacked instanceof Player && !((Player) attacked).canLoseLife()) {
if (attacked instanceof Player player && !player.canLoseLife()) {
return 0;
}
@@ -214,7 +215,7 @@ public class ComputerUtilCombat {
int damage = attacker.getNetCombatDamage();
int poison = 0;
damage += predictPowerBonusOfAttacker(attacker, null, null, false);
if (attacker.hasKeyword(Keyword.INFECT)) {
if (attacker.isInfectDamage(attacked)) {
int pd = predictDamageTo(attacked, damage, attacker, true);
// opponent can always order it so that he gets 0
if (pd == 1 && attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
@@ -357,7 +358,7 @@ public class ComputerUtilCombat {
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
if (trampleDamage > 0) {
if (attacker.hasKeyword(Keyword.INFECT)) {
if (attacker.isInfectDamage(ai)) {
poison += trampleDamage;
}
poison += predictExtraPoisonWithDamage(attacker, ai, trampleDamage);
@@ -900,7 +901,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!stAb.checkMode("Continuous")) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
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));
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!stAb.checkMode("Continuous")) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
@@ -1387,7 +1388,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!"Continuous".equals(stAb.getParam("Mode"))) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
if (!stAb.hasParam("Affected")) {
@@ -1734,6 +1735,7 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
// AI should be less worried about Deathtouch
if (blocker.hasDoubleStrike()) {
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
return true;
@@ -1963,6 +1965,7 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
// AI should be less worried about deathtouch
if (attacker.hasDoubleStrike()) {
if (attackerDamage >= defenderLife) {
return true;
@@ -2539,20 +2542,20 @@ public class ComputerUtilCombat {
if (combat != null) {
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
// 1. If the card that spawned the attacker was sent at a card, attack the same. Consider improving.
if (def instanceof Card && Iterables.contains(defenders, def)) {
if (((Card) def).isPlaneswalker()) {
if (def instanceof Card card && Iterables.contains(defenders, def)) {
if (card.isPlaneswalker()) {
return def;
}
if (((Card) def).isBattle()) {
if (card.isBattle()) {
return def;
}
}
// 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably.
for (GameEntity p : defenders) {
if (p instanceof Player && !ComputerUtilCard.canBeBlockedProfitably((Player)p, attacker, true)) {
if (p instanceof Player p1 && !ComputerUtilCard.canBeBlockedProfitably(p1, attacker, true)) {
return p;
}
if (p instanceof Card && !ComputerUtilCard.canBeBlockedProfitably(((Card)p).getController(), attacker, true)) {
if (p instanceof Card card && !ComputerUtilCard.canBeBlockedProfitably(card.getController(), attacker, true)) {
return p;
}
}

View File

@@ -50,8 +50,7 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPutCounter) {
final CostPutCounter addCounter = (CostPutCounter) part;
if (part instanceof CostPutCounter addCounter) {
final CounterType type = addCounter.getCounter();
if (type.is(CounterEnumType.M1M1)) {
@@ -77,9 +76,7 @@ public class ComputerUtilCost {
}
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
if (part instanceof CostRemoveCounter remCounter) {
final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) {
if (type.is(CounterEnumType.P1P1)) {
@@ -106,9 +103,7 @@ public class ComputerUtilCost {
&& !source.hasKeyword(Keyword.UNDYING)) {
return false;
}
} else if (part instanceof CostRemoveAnyCounter) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
} else if (part instanceof CostRemoveAnyCounter remCounter) {
PaymentDecision pay = decision.visit(remCounter);
return pay != null;
}
@@ -133,9 +128,7 @@ public class ComputerUtilCost {
CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand));
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDiscard) {
final CostDiscard disc = (CostDiscard) part;
if (part instanceof CostDiscard disc) {
final String type = disc.getType();
final CardCollection typeList;
int num;
@@ -187,8 +180,7 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDamage) {
final CostDamage pay = (CostDamage) part;
if (part instanceof CostDamage pay) {
int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false);
if (ai.getLife() - realDamage < remainingLife
&& realDamage > 0 && !ai.cantLoseForZeroOrLessLife()
@@ -220,9 +212,7 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
final CostPayLife payLife = (CostPayLife) part;
if (part instanceof CostPayLife payLife) {
int amount = payLife.getAbilityAmount(sourceAbility);
// check if there's override for the remainingLife threshold
@@ -296,8 +286,7 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part;
if (part instanceof CostSacrifice sac) {
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
if (sac.payCostFromSource() && source.isCreature()) {
@@ -346,12 +335,11 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
if (part instanceof CostSacrifice sac) {
if (suppressRecursiveSacCostCheck) {
return false;
}
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
String type = sac.getType();
@@ -620,7 +608,7 @@ public class ComputerUtilCost {
}
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) {

View File

@@ -158,7 +158,7 @@ public class ComputerUtilMana {
}
// 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 payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana);
@@ -642,7 +642,8 @@ public class ComputerUtilMana {
List<SpellAbility> paymentList = Lists.newArrayList();
final ManaPool manapool = ai.getManaPool();
// Apply the color/type conversion matrix if necessary
// Apply color/type conversion matrix if necessary (already done via autopay)
if (ai.getControllingPlayer() == null) {
manapool.restoreColorReplacements();
CardPlayOption mayPlay = sa.getMayPlayOption();
if (!effect) {
@@ -656,10 +657,13 @@ public class ComputerUtilMana {
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)) {
CostPayment.handleOfferings(sa, test, cost.isPaid());
return true; // paid all from floating mana
// paid all from floating mana
return true;
}
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
@@ -1326,7 +1330,9 @@ public class ComputerUtilMana {
}
}
if (!effect) {
CostAdjustment.adjust(manaCost, sa, null, test);
}
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
ManaCost mkCost = sa.getPayCosts().getTotalMana();

View File

@@ -160,12 +160,6 @@ public class CreatureEvaluator implements Function<Card, Integer> {
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
if (c.isPaired()) {
value += addValue(14, "paired");
@@ -213,11 +207,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(1, "untapped");
}
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
if (!c.canUntap(c.getController(), true)) {
if (c.isTapped()) {
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
} else {
@@ -226,6 +216,17 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} else {
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
if (c.hasKeyword(Keyword.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.GameEntity;
import forge.game.ability.AbilityFactory;
import forge.game.ability.ApiType;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
@@ -1305,10 +1306,10 @@ public abstract class GameState {
} else if (info.startsWith("FaceDown")) {
c.turnFaceDown(true);
if (info.endsWith("Manifested")) {
c.setManifested(true);
c.setManifested(new SpellAbility.EmptySa(ApiType.Manifest, c));
}
if (info.endsWith("Cloaked")) {
c.setCloaked(true);
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
}
} else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true);
@@ -1408,7 +1409,7 @@ public abstract class GameState {
} else if (info.equals("Foretold")) {
c.setForetold(true);
c.turnFaceDown(true);
c.addMayLookTemp(c.getOwner());
c.addMayLookFaceDownExile(c.getOwner());
} else if (info.equals("ForetoldThisTurn")) {
c.setTurnInZone(turn);
} else if (info.equals("IsToken")) {

View File

@@ -15,6 +15,7 @@ import forge.game.*;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
@@ -745,6 +746,30 @@ public class PlayerControllerAi extends PlayerController {
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
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
return !ComputerUtil.wantMulligan(player, cardsToReturn);
@@ -1207,6 +1232,11 @@ public class PlayerControllerAi extends PlayerController {
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
public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) {
for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) {
@@ -1265,8 +1295,7 @@ public class PlayerControllerAi extends PlayerController {
public boolean playSaFromPlayEffect(SpellAbility tgtSA) {
boolean optional = !tgtSA.getPayCosts().isMandatory();
boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
Spell spell = (Spell) tgtSA;
if (tgtSA instanceof Spell spell) { // Isn't it ALWAYS a spell?
// TODO if mandatory AI is only forced to use mana when it's already in the pool
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
return ComputerUtil.playStack(tgtSA, player, getGame());
@@ -1390,11 +1419,11 @@ public class PlayerControllerAi extends PlayerController {
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa);
}
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
if (source != null && source.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
// If any Conspiracies are present, try not to choose the same name twice
// (otherwise the AI will spam the same name)
for (Card consp : player.getCardsIn(ZoneType.Command)) {
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
if (consp.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
String chosenName = consp.getNamedCard();
if (!chosenName.isEmpty()) {
aiLibrary = CardLists.filter(aiLibrary, CardPredicates.nameNotEquals(chosenName));

View File

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

View File

@@ -258,7 +258,7 @@ public abstract class SpellAbilityAi {
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (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));
}
@@ -342,9 +342,9 @@ public abstract class SpellAbilityAi {
for (T ent : options) {
if (ent instanceof Player) {
hasPlayer = true;
} else if (ent instanceof Card) {
} else if (ent instanceof Card card) {
hasCard = true;
if (((Card)ent).isPlaneswalker() || ((Card)ent).isBattle()) {
if (card.isPlaneswalker() || card.isBattle()) {
hasAttackableCard = true;
}
}

View File

@@ -88,6 +88,7 @@ public enum SpellApiToAi {
.put(ApiType.EachDamage, DamageEachAi.class)
.put(ApiType.Effect, EffectAi.class)
.put(ApiType.Encode, EncodeAi.class)
.put(ApiType.Endure, EndureAi.class)
.put(ApiType.EndCombatPhase, EndTurnAi.class)
.put(ApiType.EndTurn, EndTurnAi.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.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.staticability.StaticAbilityMode;
import forge.game.zone.ZoneType;
import forge.util.FileSection;
import forge.util.collect.FCollectionView;
@@ -562,7 +563,7 @@ public class AnimateAi extends SpellAbilityAi {
CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0);
if (traits != null) {
for (StaticAbility stAb : traits.getStaticAbilities()) {
if ("Continuous".equals(stAb.getParam("Mode"))) {
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
for (final StaticAbilityLayer layer : stAb.getLayers()) {
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.CostSacrifice;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import org.apache.commons.lang3.ObjectUtils;
import java.util.ArrayList;
import java.util.Arrays;
@@ -130,7 +133,7 @@ public class AttachAi extends SpellAbilityAi {
int power = 0, toughness = 0;
List<String> keywords = Lists.newArrayList();
for (StaticAbility stAb : source.getStaticAbilities()) {
if ("Continuous".equals(stAb.getParam("Mode"))) {
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
if (stAb.hasParam("AddPower")) {
power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
}
@@ -307,9 +310,8 @@ public class AttachAi extends SpellAbilityAi {
String type = "";
for (final StaticAbility stAb : attachSource.getStaticAbilities()) {
final Map<String, String> stab = stAb.getMapParams();
if (stab.get("Mode").equals("Continuous") && stab.containsKey("AddType")) {
type = stab.get("AddType");
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddType")) {
type = stAb.getParam("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) {
// 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 -> {
// 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;
}
@@ -388,21 +420,10 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
}
if (!c.isEnchanted()) {
return true;
}
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
// already affected
if (!c.canUntap(c.getController(), true)) {
return false;
}
}
}
return true;
});
@@ -549,28 +570,46 @@ public class AttachAi extends SpellAbilityAi {
final Card attachSource) {
// AI For choosing a Card to Animate.
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));
// Suppress original attach Spell to replace it with another
attachSourceLki.getFirstAttachSpell().setSuppressed(true);
final Card finalAttachSourceLki = attachSourceLki;
//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 -> {
final Card lki = CardCopyService.getLKICopy(c);
// need to fake it as if lki would be on the battlefield
lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
// Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered
attachSourceLki.clearRemembered();
attachSourceLki.addRemembered(lki);
finalAttachSourceLki.clearRemembered();
finalAttachSourceLki.addRemembered(lki);
// need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events
CardCollection preList = new CardCollection(lki);
preList.add(attachSourceLki);
preList.add(finalAttachSourceLki);
c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList);
boolean result = lki.canBeAttached(attachSourceLki, null);
boolean result = lki.canBeAttached(finalAttachSourceLki, null);
//reset static abilities
c.getGame().getAction().checkStaticAbilities(false);
@@ -795,27 +834,45 @@ public class AttachAi extends SpellAbilityAi {
int totPower = 0;
final List<String> keywords = new ArrayList<>();
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
final Map<String, String> stabMap = stAbility.getMapParams();
boolean cantAttack = false;
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;
}
final String affected = stabMap.get("Affected");
final String affected = stAbility.getParam("Affected");
if (affected == null) {
continue;
}
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), sa);
String kws = stabMap.get("AddKeyword");
String kws = stAbility.getParam("AddKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
kws = stabMap.get("AddHiddenKeyword");
kws = stAbility.getParam("AddHiddenKeyword");
if (kws != null) {
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));
}
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
if (attachSource.hasSVar("NonStackingAttachEffect")) {
prefList = CardLists.filter(prefList,
@@ -925,6 +988,10 @@ public class AttachAi extends SpellAbilityAi {
return true;
}
private static boolean isAuraSpell(final SpellAbility sa) {
return sa.isSpell() && sa.getHostCard().isAura();
}
/**
* Attach preference.
*
@@ -940,7 +1007,23 @@ public class AttachAi extends SpellAbilityAi {
*/
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
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<>();
for (final Player player : sa.getHostCard().getGame().getPlayers()) {
if (sa.canTarget(player)) {
@@ -1005,9 +1088,8 @@ public class AttachAi extends SpellAbilityAi {
CardCollection toRemove = new CardCollection();
for (Trigger t : attachSource.getTriggers()) {
if (t.getMode() == TriggerType.ChangesZone) {
final Map<String, String> params = t.getMapParams();
if ("Card.Self".equals(params.get("ValidCard"))
&& "Battlefield".equals(params.get("Destination"))) {
if ("Card.Self".equals(t.getParam("ValidCard"))
&& "Battlefield".equals(t.getParam("Destination"))) {
SpellAbility trigSa = t.ensureAbility();
if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
for (Card target : list) {
@@ -1067,29 +1149,27 @@ public class AttachAi extends SpellAbilityAi {
boolean grantingExtraBlock = false;
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
final Map<String, String> stabMap = stAbility.getMapParams();
if (!"Continuous".equals(stabMap.get("Mode"))) {
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
final String affected = stabMap.get("Affected");
final String affected = stAbility.getParam("Affected");
if (affected == null) {
continue;
}
if (affected.contains(stCheck) || affected.contains("AttachedBy")) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), stAbility);
grantingAbilities |= stabMap.containsKey("AddAbility");
grantingExtraBlock |= stabMap.containsKey("CanBlockAmount") || stabMap.containsKey("CanBlockAny");
grantingAbilities |= stAbility.hasParam("AddAbility");
grantingExtraBlock |= stAbility.hasParam("CanBlockAmount") || stAbility.hasParam("CanBlockAny");
String kws = stabMap.get("AddKeyword");
String kws = stAbility.getParam("AddKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
kws = stabMap.get("AddHiddenKeyword");
kws = stAbility.getParam("AddHiddenKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
@@ -1158,12 +1238,17 @@ public class AttachAi extends SpellAbilityAi {
// 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
boolean canOnlyTargetCreatures = true;
for (String valid : ObjectUtils.firstNonNull(attachSource.getFirstAttachSpell(), sa).getTargetRestrictions().getValidTgts()) {
if (!valid.startsWith("Creature")) {
if (attachSource.isAura()) {
for (KeywordInterface ki : attachSource.getKeywords(Keyword.ENCHANT)) {
String o = ki.getOriginal();
String m[] = o.split(":");
String v = m[1];
if (!v.startsWith("Creature")) {
canOnlyTargetCreatures = false;
break;
}
}
}
if (canOnlyTargetCreatures && (attachSource.isAura() || attachSource.isEquipment())) {
prefList = CardLists.filter(prefList, c -> c.getTimesCrewedThisTurn() == 0 || (attachSource.isEquipment() && attachSource.getGame().getPhaseHandler().is(PhaseType.MAIN1, ai)));
}
@@ -1387,8 +1472,6 @@ public class AttachAi extends SpellAbilityAi {
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
} else if ("ChangeType".equals(logic)) {
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
} else if ("KeepTapped".equals(logic)) {
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
} else if ("Animate".equals(logic)) {
c = attachAIAnimatePreference(sa, prefList, mandatory, attachSource);
} else if ("Reanimate".equals(logic)) {
@@ -1399,6 +1482,12 @@ public class AttachAi extends SpellAbilityAi {
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
if (!isUsefulAttachAction(ai, c, sa)) {
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.")
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
}
return true;
}

View File

@@ -22,7 +22,6 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType;
@@ -138,8 +137,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (aiLogic != null) {
if (aiLogic.equals("Always")) {
return true;
} else if (aiLogic.startsWith("ExileSpell")) {
return doExileSpellLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
@@ -878,6 +875,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
origin.addAll(ZoneType.listValueOf(sa.getParam("TgtZone")));
}
if (origin.contains(ZoneType.Stack) && doExileSpellLogic(ai, sa, mandatory)) {
return true;
}
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final Game game = ai.getGame();
@@ -902,7 +903,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true);
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
@@ -914,6 +914,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.isSpell()) {
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")) {
list = CardLists.filter(list, c -> {
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
@@ -1282,8 +1285,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
list.remove(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.
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
@@ -1448,6 +1453,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
// AI Targeting
Card choice = null;
// Filter out cards TargetsForEachPlayer
list = CardLists.canSubsequentlyTarget(list, sa);
if (!list.isEmpty()) {
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list);
if (mostExpensivePermanent.isCreature()
@@ -2061,33 +2069,26 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
private boolean doExileSpellLogic(final Player aiPlayer, final SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
SpellAbilityStackInstance top = aiPlayer.getGame().getStack().peek();
List<ApiType> dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
int manaCost = 0;
int minCost = 0;
if (aiLogic.contains(".")) {
minCost = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1));
private static boolean doExileSpellLogic(final Player ai, final SpellAbility sa, final boolean mandatory) {
List<ApiType> dangerousApi = null;
CardCollection spells = new CardCollection(ai.getGame().getStackZone().getCards());
Collections.reverse(spells);
if (!mandatory && !spells.isEmpty()) {
spells = spells.subList(0, 1);
spells = ComputerUtil.filterAITgts(sa, ai, spells, true);
dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
}
if (top != null) {
SpellAbility topSA = top.getSpellAbility();
if (topSA != null) {
if (topSA.getPayCosts().hasManaCost()) {
manaCost = topSA.getPayCosts().getTotalMana().getCMC();
}
if ((manaCost >= minCost || dangerousApi.contains(topSA.getApi()))
&& topSA.getActivatingPlayer().isOpponentOf(aiPlayer)
&& sa.canTargetSpellAbility(topSA)) {
for (Card c : spells) {
SpellAbility topSA = ai.getGame().getStack().getSpellMatchingHost(c);
if (topSA != null && (dangerousApi == null ||
(dangerousApi.contains(topSA.getApi()) && topSA.getActivatingPlayer().isOpponentOf(ai)))
&& sa.canTarget(topSA)) {
sa.resetTargets();
sa.getTargets().add(topSA);
return sa.isTargetNumberValid();
}
}
}
return false;
}

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
@@ -18,6 +19,13 @@ public class ConniveAi extends SpellAbilityAi {
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);
// Filter AI-specific targets if provided

View File

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

View File

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

View File

@@ -95,7 +95,7 @@ public class DamageDealAi extends DamageAiBase {
final String damage = sa.getParam("NumDmg");
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")) {
dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());

View File

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

View File

@@ -515,12 +515,17 @@ public class DrawAi extends SpellAbilityAi {
return false;
}
if ((computerHandSize + numCards > computerMaxHandSize)
&& game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger()
&& !assumeSafeX) {
if ((computerHandSize + numCards > computerMaxHandSize)) {
// 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;
}
}

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)
Card topCopy = CardCopyService.getLKICopy(card);
topCopy.turnFaceDownNoUpdate();
topCopy.setManifested(true);
topCopy.setManifested(sa);
if (ComputerUtil.isETBprevented(topCopy)) {
return false;

View File

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

View File

@@ -542,6 +542,8 @@ public class PumpAi extends PumpAiBase {
Card t = null;
// boolean goodt = false;
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
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.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.phase.Untap;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
@@ -137,7 +136,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return CombatUtil.canBlockAtLeastOne(card, attackers);
} 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)
&& Untap.canUntap(card);
&& card.canUntap(card.getController(), true);
} 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.")) {
if (ph.isPlayerTurn(ai) && (!(CombatUtil.canBlock(card) || combat != null && combat.isBlocking(card))

View File

@@ -6,6 +6,7 @@ import forge.ai.SpellAbilityAi;
import forge.card.CardStateName;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -142,7 +143,7 @@ public class SetStateAi extends SpellAbilityAi {
return false;
}
// hidden agenda
if (card.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")
if (card.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)
&& card.isInZone(ZoneType.Command)) {
String chosenName = card.getNamedCard();
for (Card cast : ai.getGame().getStack().getSpellsCastThisTurn()) {

View File

@@ -206,7 +206,8 @@ public class TokenAi extends SpellAbilityAi {
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
&& game.getCombat() != null
&& !game.getCombat().getAttackers().isEmpty()
&& alwaysOnOppAttack) {
&& alwaysOnOppAttack
&& actualToken.isCreature()) {
for (Card attacker : game.getCombat().getAttackers()) {
if (CombatUtil.canBlock(attacker, actualToken)) {
return true;
@@ -366,8 +367,7 @@ public class TokenAi extends SpellAbilityAi {
}
private boolean tgtRoleAura(final Player ai, final SpellAbility sa, final Card tok, final boolean mandatory) {
boolean isCurse = "Curse".equals(sa.getParam("AILogic")) ||
tok.getFirstAttachSpell().getParamOrDefault("AILogic", "").equals("Curse");
boolean isCurse = "Curse".equals(sa.getParam("AILogic")) || "Curse".equals(tok.getSVar("AttachAILogic"));
List<Card> tgts = CardUtil.getValidCardsToTarget(sa);
// 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.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.phase.Untap;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
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
CardCollection noAutoUntap = CardLists.filter(untapList, Untap.CANUNTAP.negate());
CardCollection noAutoUntap = CardLists.filter(untapList, c -> !c.canUntap(c.getController(), true));
if (!noAutoUntap.isEmpty()) {
return ComputerUtilCard.getBestAI(noAutoUntap);
}

View File

@@ -96,7 +96,7 @@ public class GameCopier {
newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn());
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
newPlayer.setSpeed(origPlayer.getSpeed());
newPlayer.setBlessing(origPlayer.hasBlessing());
newPlayer.setBlessing(origPlayer.hasBlessing(), null);
newPlayer.setRevolt(origPlayer.hasRevolt());
newPlayer.setDescended(origPlayer.getDescended());
newPlayer.setLibrarySearched(origPlayer.getLibrarySearched());
@@ -373,10 +373,10 @@ public class GameCopier {
if (c.isFaceDown()) {
newCard.turnFaceDown(true);
if (c.isManifested()) {
newCard.setManifested(true);
newCard.setManifested(c.getManifestedSA());
}
if (c.isCloaked()) {
newCard.setCloaked(true);
newCard.setCloaked(c.getCloakedSA());
}
}
if (c.isMonstrous()) {

View File

@@ -23,17 +23,16 @@ public final class ImageKeys {
public static final String HIDDEN_CARD = "hidden";
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 CLOAKED_IMAGE = "cloaked";
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 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,
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);
}
public static File getImageFile(String key) {
return getImageFile(key, false);
}
public static File getImageFile(String key, boolean artCrop) {
if (StringUtils.isEmpty(key))
return null;
final String dir;
final String filename;
if (key.startsWith(ImageKeys.TOKEN_PREFIX)) {
filename = key.substring(ImageKeys.TOKEN_PREFIX.length());
String[] tempdata = null;
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;
} else if (key.startsWith(ImageKeys.ICON_PREFIX)) {
filename = key.substring(ImageKeys.ICON_PREFIX.length());
@@ -140,6 +164,54 @@ public final class ImageKeys {
cachedCards.put(filename, 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
// on case-sensitive file systems
@@ -221,39 +293,7 @@ public final class ImageKeys {
return file;
}
}
if (dir.equals(CACHE_TOKEN_PICS_DIR)) {
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("/")) {
if (filename.contains("/")) {
String setlessFilename = filename.substring(filename.indexOf('/') + 1);
file = findFile(dir, setlessFilename);
if (file != null) {

View File

@@ -95,12 +95,12 @@ public class StaticData {
if (!loadNonLegalCards) {
for (CardEdition e : editions) {
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))
continue;
funnyCards.add(cis.name);
funnyCards.add(cis.name());
}
}
}
@@ -217,6 +217,9 @@ public class StaticData {
}
public CardEdition getCardEdition(String setCode) {
if (CardEdition.UNKNOWN_CODE.equals(setCode)) {
return CardEdition.UNKNOWN;
}
CardEdition edition = this.editions.get(setCode);
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;
* @param cardName The name of the card
@@ -778,11 +790,11 @@ public class StaticData {
Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>();
List<CompletableFuture<?>> futures = new ArrayList<>();
for (CardEdition.CardInSet c : e.getAllCardsInSet()) {
if (cardCount.containsKey(c.name)) {
cardCount.put(c.name, Pair.of(c.collectorNumber != null && c.collectorNumber.startsWith("F"), cardCount.get(c.name).getRight() + 1));
for (CardEdition.EditionEntry c : e.getAllCardsInSet()) {
if (cardCount.containsKey(c.name())) {
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), cardCount.get(c.name()).getRight() + 1));
} 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();
// 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 int artIndex = tokenEntry.getValue();
final int artIndex = tokenEntry.getValue().size();
try {
PaperToken token = getAllTokens().getToken(name, e.getCode());
if (token == null) {
@@ -983,4 +995,23 @@ public class StaticData {
}
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.Multimaps;
import forge.StaticData;
import forge.card.CardEdition.CardInSet;
import forge.card.CardEdition.EditionEntry;
import forge.card.CardEdition.Type;
import forge.deck.generation.IDeckGenPool;
import forge.item.IPaperCard;
@@ -42,7 +42,8 @@ import java.util.stream.Stream;
public final class CardDb implements ICardDatabase, IDeckGenPool {
public final static String foilSuffix = "+";
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 exlcudedCardSet = "DS0";
@@ -93,19 +94,19 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public int artIndex;
public boolean isFoil;
public String collectorNumber;
public Set<String> colorID;
public Map<String, String> flags;
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber) {
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;
this.edition = edition;
this.artIndex = artIndex;
this.isFoil = isFoil;
this.collectorNumber = collectorNumber;
this.colorID = colorID;
this.flags = flags;
}
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) {
setCode = setCode != null ? setCode : "";
if(setCode == null || StringUtils.isBlank(setCode) || setCode.equals(CardEdition.UNKNOWN_CODE))
setCode = "";
cardName = cardName != null ? cardName : "";
if (cardName.indexOf(NameSetSeparator) != -1)
// 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;
}
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) {
String requestInfo = compose(cardName, setCode);
// CollectorNumber will be wrapped in square brackets
@@ -149,6 +143,34 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
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) {
if (collectorNumber == null)
return "";
@@ -160,19 +182,21 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return collectorNumber;
}
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 getFlagSegment(Map<String, String> flags) {
if(flags == null)
return "";
String flagText = flags.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(FlagSeparator));
return NameSetSeparator + FlagPrefix + "{" + flagText + "}";
}
private static boolean isCollectorNumber(String s) {
return s.startsWith("[") && s.endsWith("]");
}
private static boolean isColorIDString(String s) {
return s.startsWith(colorIDPrefix);
private static boolean isFlagSegment(String s) {
return s.startsWith(FlagPrefix);
}
private static boolean isArtIndex(String s) {
@@ -201,44 +225,36 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return null;
String[] info = TextUtil.split(reqInfo, NameSetSeparator);
int setPos;
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;
}
int index = 1;
String cardName = info[0];
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)) {
cardName = cardName.substring(0, cardName.length() - foilSuffix.length());
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;
String setCode = setPos > 0 ? info[setPos] : null;
Set<String> colorID = clrPos > 0 ? Arrays.stream(info[clrPos].substring(1).split(colorIDPrefix)).collect(Collectors.toSet()) : null;
if (setCode != null && setCode.equals(CardEdition.UNKNOWN.getCode())) { // ???
if(info.length > index && isSetCode(info[index])) {
setCode = info[index];
index++;
}
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;
}
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
if (collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER) && artIndex == IPaperCard.NO_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;
String key = e.getCode() + "/" + cis.name;
String key = e.getCode() + "/" + cis.name();
if (artIds.containsKey(key)) {
artIdx = artIds.get(key) + 1;
}
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) {
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()) {
cardsInSet = cardsInSet.stream().filter(c -> StringUtils.isEmpty(c.functionalVariantName)
|| cr.getSupportedFunctionalVariants().contains(c.functionalVariantName)
cardsInSet = cardsInSet.stream().filter(c -> StringUtils.isEmpty(c.functionalVariantName())
|| cr.getSupportedFunctionalVariants().contains(c.functionalVariantName())
).collect(Collectors.toList());
}
if (cardsInSet.isEmpty())
return false;
for (CardInSet cis : cardsInSet) {
for (EditionEntry cis : cardsInSet) {
addSetCard(ed, cis, cr);
}
return true;
@@ -359,15 +397,15 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
upcomingSet = e;
}
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) {
CardRules cr = rulesByName.get(cis.name);
for (CardEdition.EditionEntry cis : e.getAllCardsInSet()) {
CardRules cr = rulesByName.get(cis.name());
if (cr == null) {
missingCards.add(cis.name);
missingCards.add(cis.name());
continue;
}
if (cr.hasFunctionalVariants()) {
if (StringUtils.isNotEmpty(cis.functionalVariantName)
&& !cr.getSupportedFunctionalVariants().contains(cis.functionalVariantName)) {
if (StringUtils.isNotEmpty(cis.functionalVariantName())
&& !cr.getSupportedFunctionalVariants().contains(cis.functionalVariantName())) {
//Supported card, unsupported variant.
//Could note the card as missing but since these are often un-cards,
//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));
} 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. ");
addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special));
addCard(new PaperCard(cr, CardEdition.UNKNOWN_CODE, CardRarity.Special));
}
} 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.");
@@ -425,8 +463,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
lang = new LangEnglish();
}
// for now just check Universes Within
for (CardInSet cis : editions.get("SLX").getCards()) {
String orgName = alternateName.get(cis.name);
for (EditionEntry cis : editions.get("SLX").getCards()) {
String orgName = alternateName.get(cis.name());
if (orgName != null) {
// found original (beyond) print
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());
// so workshop can edit same script
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
public PaperCard getCard(final String cardName, String setCode, int artIndex, String collectorNumber) {
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, collectorNumber);
public PaperCard getCard(final String cardName, String setCode, int artIndex, Map<String, String> flags) {
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, flags);
CardRequest request = CardRequest.fromString(reqInfo);
return tryGetCard(request);
}
@Override
public PaperCard getCard(final String cardName, String setCode, int artIndex, Set<String> colorID) {
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, colorID);
public PaperCard getCard(final String cardName, String setCode, String collectorNumber, Map<String, String> flags) {
String reqInfo = CardRequest.compose(cardName, setCode, collectorNumber, flags);
CardRequest request = CardRequest.fromString(reqInfo);
return tryGetCard(request);
}
@@ -611,14 +649,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return null;
// 1. First off, try using all possible search parameters, to narrow down the actual cards looked for.
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) -
// MOST of the extensions have two short codes, 141 out of 221 (so far)
// ALSO: Set Code are always UpperCase
CardEdition edition = editions.get(reqEditionCode.toUpperCase());
return this.getCardFromSet(request.cardName, edition, request.artIndex,
request.collectorNumber, request.isFoil, request.colorID);
PaperCard cardFromSet = this.getCardFromSet(request.cardName, edition, request.artIndex, request.collectorNumber, request.isFoil);
if(cardFromSet != null && request.flags != null)
cardFromSet = cardFromSet.copyWithFlags(request.flags);
return cardFromSet;
}
// 2. Card lookup in edition with specified filter didn't work.
@@ -661,11 +702,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override
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
return null; // No cards will be returned
@@ -674,18 +710,18 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
cardName = cardNameRequest.cardName;
isFoil = isFoil || cardNameRequest.isFoil;
List<PaperCard> candidates = getAllCards(cardName, c -> {
boolean artIndexFilter = true;
boolean collectorNumberFilter = true;
boolean setFilter = c.getEdition().equalsIgnoreCase(edition.getCode()) ||
c.getEdition().equalsIgnoreCase(edition.getCode2());
String code1 = edition.getCode(), code2 = edition.getCode2();
Predicate<PaperCard> filter = (c) -> {
String ed = c.getEdition();
return ed.equalsIgnoreCase(code1) || ed.equalsIgnoreCase(code2);
};
if (artIndex > 0)
artIndexFilter = (c.getArtIndex() == artIndex);
if ((collectorNumber != null) && (collectorNumber.length() > 0)
&& !(collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)))
collectorNumberFilter = (c.getCollectorNumber().equals(collectorNumber));
return setFilter && artIndexFilter && collectorNumberFilter;
});
filter = filter.and((c) -> artIndex == c.getArtIndex());
if (collectorNumber != null && !collectorNumber.isEmpty() && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER))
filter = filter.and((c) -> collectorNumber.equals(c.getCollectorNumber()));
List<PaperCard> candidates = getAllCards(cardName, filter);
if (candidates.isEmpty())
return null;
@@ -699,7 +735,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
while (!candidate.hasImage() && candidatesIterator.hasNext())
candidate = candidatesIterator.next();
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);
}
@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
@@ -820,12 +851,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
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){
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter) {
if (cardInfo == null)
return null;
final CardRequest cr = CardRequest.fromString(cardInfo);
@@ -865,7 +891,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
for (PaperCard card : cards) {
String setCode = card.getEdition();
CardEdition ed;
if (setCode.equals(CardEdition.UNKNOWN.getCode()))
if (setCode.equals(CardEdition.UNKNOWN_CODE))
ed = CardEdition.UNKNOWN;
else
ed = editions.get(card.getEdition());
@@ -906,7 +932,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
candidate = candidate.hasImage() ? candidate : firstCandidate;
//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
@@ -1017,7 +1043,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public static final Predicate<PaperCard> EDITION_NON_PROMO = paperCard -> {
String code = paperCard.getEdition();
CardEdition edition = StaticData.instance().getCardEdition(code);
if(edition == null && code.equals("???"))
if(edition == null && code.equals(CardEdition.UNKNOWN_CODE))
return true;
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 -> {
String code = paperCard.getEdition();
CardEdition edition = StaticData.instance().getCardEdition(code);
if(edition == null && code.equals("???"))
if(edition == null && code.equals(CardEdition.UNKNOWN_CODE))
return true;
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) {
List<PaperCard> cards = Lists.newArrayList();
for (CardInSet cis : edition.getAllCardsInSet()) {
PaperCard card = this.getCard(cis.name, edition.getCode());
for (EditionEntry cis : edition.getAllCardsInSet()) {
PaperCard card = this.getCard(cis.name(), edition.getCode());
if (card == null) {
// Just in case the card is listed in the edition file but Forge doesn't support it
continue;
@@ -1126,29 +1152,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
.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) {
CardRequest request = CardRequest.fromString(cardRequest);
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.
if (StringUtils.isBlank(request.edition)) {
for (CardEdition edition : editions) {
for (CardInSet cardInSet : edition.getAllCardsInSet()) {
if (cardInSet.name.equals(request.cardName)) {
for (EditionEntry cardInSet : edition.getAllCardsInSet()) {
if (cardInSet.name().equals(request.cardName)) {
cardEdition = edition;
cardRarity = cardInSet.rarity;
cardRarity = cardInSet.rarity();
break;
}
}
@@ -1171,9 +1174,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} else {
cardEdition = editions.get(request.edition);
if (cardEdition != null) {
for (CardInSet cardInSet : cardEdition.getAllCardsInSet()) {
if (cardInSet.name.equals(request.cardName)) {
cardRarity = cardInSet.rarity;
for (EditionEntry cardInSet : cardEdition.getAllCardsInSet()) {
if (cardInSet.name().equals(request.cardName)) {
cardRarity = cardInSet.rarity();
break;
}
}
@@ -1224,9 +1227,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// @leriomaggio: DONE! re-using here the same strategy implemented for lazy-loading!
for (CardEdition e : editions.getOrderedEditions()) {
int artIdx = IPaperCard.DEFAULT_ART_INDEX;
for (CardInSet cis : e.getCardInSet(cardName))
paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity, artIdx++, false,
cis.collectorNumber, cis.artistName, cis.functionalVariantName));
for (EditionEntry cis : e.getCardInSet(cardName))
paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity(), artIdx++, false,
cis.collectorNumber(), cis.artistName(), cis.functionalVariantName()));
}
} else {
String lastEdition = null;
@@ -1240,17 +1243,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
if (ed == null) {
continue;
}
List<CardInSet> cardsInSet = ed.getCardInSet(cardName);
List<EditionEntry> cardsInSet = ed.getCardInSet(cardName);
if (cardsInSet.isEmpty())
continue;
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,
cds.collectorNumber, cds.artistName, cds.functionalVariantName));
cds.collectorNumber(), cds.artistName(), cds.functionalVariantName()));
}
}
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
for (PaperCard paperCard : paperCards) {

View File

@@ -18,6 +18,7 @@
package forge.card;
import com.google.common.collect.*;
import forge.StaticData;
import forge.card.CardDb.CardArtPreference;
import forge.deck.CardPool;
@@ -165,43 +166,6 @@ public final class CardEdition implements Comparable<CardEdition> {
}
}
public static class CardInSet implements Comparable<CardInSet> {
public final CardRarity rarity;
public final String collectorNumber;
public final String name;
public final String artistName;
public final String functionalVariantName;
public CardInSet(final String name, final String collectorNumber, final CardRarity rarity, final String artistName, final String functionalVariantName) {
this.name = name;
this.collectorNumber = collectorNumber;
this.rarity = rarity;
this.artistName = artistName;
this.functionalVariantName = functionalVariantName;
}
public String toString() {
StringBuilder sb = new StringBuilder();
if (collectorNumber != null) {
sb.append(collectorNumber);
sb.append(' ');
}
if (rarity != CardRarity.Unknown) {
sb.append(rarity);
sb.append(' ');
}
sb.append(name);
if (artistName != null) {
sb.append(" @");
sb.append(artistName);
}
if (functionalVariantName != null) {
sb.append(" $");
sb.append(functionalVariantName);
}
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
@@ -244,8 +208,32 @@ public final class CardEdition implements Comparable<CardEdition> {
return sortableCollNr;
}
public record EditionEntry(String name, String collectorNumber, CardRarity rarity, String artistName, String functionalVariantName) implements Comparable<EditionEntry> {
public String toString() {
StringBuilder sb = new StringBuilder();
if (collectorNumber != null) {
sb.append(collectorNumber);
sb.append(' ');
}
if (rarity != CardRarity.Unknown && rarity != CardRarity.Token) {
sb.append(rarity);
sb.append(' ');
}
sb.append(name);
if (artistName != null) {
sb.append(" @");
sb.append(artistName);
}
if (functionalVariantName != null) {
sb.append(" $");
sb.append(functionalVariantName);
}
return sb.toString();
}
@Override
public int compareTo(CardInSet o) {
public int compareTo(EditionEntry o) {
final int nameCmp = name.compareToIgnoreCase(o.name);
if (0 != nameCmp) {
return nameCmp;
@@ -262,11 +250,17 @@ public final class CardEdition implements Comparable<CardEdition> {
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 String code;
private String code2;
private String scryfallCode;
private String tokensCode;
private String tokenFallbackCode;
private String cardsLanguage;
private Type type;
private String name;
@@ -296,31 +290,32 @@ public final class CardEdition implements Comparable<CardEdition> {
private String doublePickDuringDraft = "";
private String[] chaosDraftThemes = new String[0];
private final ListMultimap<String, CardInSet> cardMap;
private final List<CardInSet> cardsInSet;
private final Map<String, Integer> tokenNormalized;
private final ListMultimap<String, EditionEntry> cardMap;
private final List<EditionEntry> cardsInSet;
private final ListMultimap<String, EditionEntry> tokenMap;
// custom print sheets that will be loaded lazily
private final Map<String, List<String>> customPrintSheetsToParse;
private ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create();
private int boosterArts = 1;
private SealedTemplate boosterTpl = null;
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.cardsInSet = new ArrayList<>(cardMap.values());
Collections.sort(cardsInSet);
this.tokenNormalized = tokens;
this.tokenMap = tokens;
this.customPrintSheetsToParse = customPrintSheetsToParse;
}
private CardEdition(CardInSet[] cards, Map<String, Integer> tokens) {
List<CardInSet> cardsList = Arrays.asList(cards);
private CardEdition(EditionEntry[] cards, ListMultimap<String, EditionEntry> tokens) {
List<EditionEntry> cardsList = Arrays.asList(cards);
this.cardMap = ArrayListMultimap.create();
this.cardMap.replaceValues("cards", cardsList);
this.cardsInSet = new ArrayList<>(cardsList);
Collections.sort(cardsInSet);
this.tokenNormalized = tokens;
this.tokenMap = tokens;
this.customPrintSheetsToParse = new HashMap<>();
}
@@ -337,8 +332,8 @@ public final class CardEdition implements Comparable<CardEdition> {
* @param name the name of 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) {
this(cards, new HashMap<>());
private CardEdition(String date, String code, String code2, Type type, String name, FoilType foil, EditionEntry[] cards) {
this(cards, ArrayListMultimap.create());
this.code = code;
this.code2 = code2;
this.type = type;
@@ -361,6 +356,7 @@ public final class CardEdition implements Comparable<CardEdition> {
public String getCode() { return code; }
public String getCode2() { return code2; }
public String getScryfallCode() { return scryfallCode.toLowerCase(); }
public String getTokensCode() { return tokensCode.toLowerCase(); }
public String getCardsLangCode() { return cardsLanguage.toLowerCase(); }
public Type getType() { return type; }
public String getName() { return name; }
@@ -385,14 +381,14 @@ public final class CardEdition implements Comparable<CardEdition> {
public String getSheetReplaceCardFromSheet2() { return sheetReplaceCardFromSheet2; }
public String[] getChaosDraftThemes() { return chaosDraftThemes; }
public List<CardInSet> getCards() { return cardMap.get(EditionSectionWithCollectorNumbers.CARDS.getName()); }
public List<CardInSet> getRebalancedCards() { return cardMap.get(EditionSectionWithCollectorNumbers.REBALANCED.getName()); }
public List<CardInSet> getFunnyEternalCards() { return cardMap.get(EditionSectionWithCollectorNumbers.ETERNAL.getName()); }
public List<CardInSet> getAllCardsInSet() {
public List<EditionEntry> getCards() { return cardMap.get(EditionSectionWithCollectorNumbers.CARDS.getName()); }
public List<EditionEntry> getRebalancedCards() { return cardMap.get(EditionSectionWithCollectorNumbers.REBALANCED.getName()); }
public List<EditionEntry> getFunnyEternalCards() { return cardMap.get(EditionSectionWithCollectorNumbers.ETERNAL.getName()); }
public List<EditionEntry> getAllCardsInSet() {
return cardsInSet;
}
private ListMultimap<String, CardInSet> cardsInSetLookupMap = null;
private ListMultimap<String, EditionEntry> cardsInSetLookupMap = null;
/**
* 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.
* 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) {
// initialise
cardsInSetLookupMap = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
List<CardInSet> cardsInSet = this.getAllCardsInSet();
for (CardInSet cis : cardsInSet){
List<EditionEntry> cardsInSet = this.getAllCardsInSet();
for (EditionEntry cis : cardsInSet){
String key = cis.name;
cardsInSetLookupMap.put(key, cis);
}
@@ -413,8 +409,19 @@ public final class CardEdition implements Comparable<CardEdition> {
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) {
for (CardInSet cis : getRebalancedCards()) {
for (EditionEntry cis : getRebalancedCards()) {
if (cis.name.equals(cardName)) {
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 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
public int compareTo(final CardEdition o) {
@@ -508,8 +552,8 @@ public final class CardEdition implements Comparable<CardEdition> {
for (String sectionName : cardMap.keySet()) {
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName));
List<CardInSet> cards = cardMap.get(sectionName);
for (CardInSet card : cards) {
List<EditionEntry> cards = cardMap.get(sectionName);
for (EditionEntry card : cards) {
int index = 1;
if (cardToIndex.containsKey(card.name)) {
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
like Merseine from FEM.
*/
// Collector numbers now should allow hyphens for Planeswalker Championship Promos
//"(^(?<cnum>[0-9]+.?) )?((?<rarity>[SCURML]) )?(?<name>.*)$"
/* Ideally we'd use the named group above, but Android 6 and
earlier don't appear to support named groups.
@@ -575,12 +620,20 @@ public final class CardEdition implements Comparable<CardEdition> {
* functional variant name - grouping #9
*/
// "(^(.?[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;
Map<String, Integer> tokenNormalized = new HashMap<>();
Map<String, List<String>> customPrintSheetsToParse = new HashMap<>();
List<String> editionSectionsWithCollectorNumbers = EditionSectionWithCollectorNumbers.getNames();
@@ -611,7 +664,7 @@ public final class CardEdition implements Comparable<CardEdition> {
String cardName = matcher.group(5);
String artistName = matcher.group(7);
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);
}
@@ -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
if (contents.containsKey("tokens")) {
for (String line : contents.get("tokens")) {
if (StringUtils.isBlank(line))
continue;
Matcher matcher = tokenPattern.matcher(line);
if (!tokenNormalized.containsKey(line)) {
tokenNormalized.put(line, 1);
} else {
tokenNormalized.put(line, tokenNormalized.get(line) + 1);
if (!matcher.matches()) {
continue;
}
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;
// parse metadata section
res.name = metadata.get("name");
res.date = parseDate(metadata.get("date"));
res.code = metadata.get("code");
res.code2 = metadata.get("code2");
if (res.code2 == null) {
res.code2 = res.code;
}
res.scryfallCode = metadata.get("ScryfallCode");
if (res.scryfallCode == null) {
res.scryfallCode = res.code;
}
res.cardsLanguage = metadata.get("CardLang");
if (res.cardsLanguage == null) {
res.cardsLanguage = "en";
}
res.code2 = metadata.get("code2", res.code);
res.scryfallCode = metadata.get("ScryfallCode", res.code);
res.tokensCode = metadata.get("TokensCode", "T" + res.scryfallCode);
res.tokenFallbackCode = metadata.get("TokenFallbackCode");
res.cardsLanguage = metadata.get("CardLang", "en");
res.boosterArts = metadata.getInt("BoosterCovers", 1);
res.otherMap = otherMap;
String boosterDesc = metadata.get("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.
}
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);
initAliases(customBucket);
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) {
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;
if (null == set) {
throw new RuntimeException("Edition with code '" + code + "' not found");
@@ -941,4 +1012,12 @@ public final class CardEdition implements Comparable<CardEdition> {
}
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

@@ -222,7 +222,7 @@ final class CardFace implements ICardFace, Cloneable {
else variant.replacements.addAll(0, this.replacements);
if(variant.variables == null) variant.variables = this.variables;
else variant.variables.putAll(this.variables);
else this.variables.forEach((k, v) -> variant.variables.putIfAbsent(k, v));
if(variant.nonAbilityText == null) variant.nonAbilityText = this.nonAbilityText;
if(variant.draftActions == null) variant.draftActions = this.draftActions;

View File

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

View File

@@ -11,7 +11,8 @@ public enum CardSplitType
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
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),
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,8 @@ import forge.util.BinaryUtil;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* <p>CardColor class.</p>
@@ -291,14 +293,8 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
*/
@Override
public String toString() {
if (this.orderWeight == -1) {
return "n/a";
}
final String toReturn = MagicColor.toLongString(myColor);
if (toReturn.equals(MagicColor.Constant.COLORLESS) && myColor != 0) {
return "multi";
}
return toReturn;
final ManaCostShard[] orderedShards = getOrderedShards();
return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
}
/**
@@ -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
public ManaCostShard[] getOrderedShards() {
return shardOrderLookup[myColor];

View File

@@ -5,43 +5,42 @@ import forge.item.PaperCard;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.Set;
public interface ICardDatabase extends Iterable<PaperCard> {
/**
/**
* 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> {
/* SINGLE CARD RETRIEVAL METHODS
* ============================= */
// 1. Card Lookup by attributes
@@ -50,22 +49,20 @@ public interface ICardDatabase extends Iterable<PaperCard> {
PaperCard getCard(String cardName, String edition, int artIndex);
// [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, int artIndex, String collectorNumber);
PaperCard getCard(String cardName, String edition, int artIndex, Set<String> colorID);
PaperCard getCard(String cardName, String edition, int artIndex, Map<String, String> flags);
PaperCard getCard(String cardName, String edition, String collectorNumber, Map<String, String> flags);
// 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, String collectorNumber, 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, Set<String> colorID);
// 3. Card lookup based on CardArtPreference Selection Policy
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference);
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, Predicate<PaperCard> filter);
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Set<String> colorID);
// 4. Specialised Card Lookup on CardArtPreference Selection and Release Date
PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, Date releaseDate);

View File

@@ -1,6 +1,7 @@
package forge.card;
import com.google.common.collect.ImmutableList;
import forge.deck.DeckRecognizer;
/**
* Holds byte values for each color magic has.
@@ -187,6 +188,12 @@ public final class MagicColor {
public String getName() {
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() {
return colormask;
}

View File

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

View File

@@ -52,7 +52,10 @@ public class CardPool extends ItemPool<PaperCard> {
public void add(final String cardRequest, final int amount) {
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) {
@@ -71,7 +74,20 @@ public class CardPool extends ItemPool<PaperCard> {
public void add(String cardName, String setCode, int artIndex, final int amount) {
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();
PaperCard paperCard = null;
String selectedDbName = "";
@@ -81,7 +97,7 @@ public class CardPool extends ItemPool<PaperCard> {
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
String dbName = entry.getKey();
CardDb db = entry.getValue();
paperCard = db.getCard(cardName, setCode, artIndex, colorID);
paperCard = db.getCard(cardName, setCode, artIndex, flags);
if (paperCard != null) {
selectedDbName = dbName;
break;
@@ -123,7 +139,7 @@ public class CardPool extends ItemPool<PaperCard> {
int cnt = artGroups[i - 1];
if (cnt <= 0)
continue;
PaperCard randomCard = cardDb.getCard(cardName, setCode, i, colorID);
PaperCard randomCard = cardDb.getCard(cardName, setCode, i, flags);
this.add(randomCard, cnt);
}
}
@@ -430,7 +446,6 @@ public class CardPool extends ItemPool<PaperCard> {
public String toCardList(String separator) {
List<Entry<PaperCard, Integer>> main2sort = Lists.newArrayList(this);
main2sort.sort(ItemPoolSorter.BY_NAME_THEN_SET);
final CardDb commonDb = StaticData.instance().getCommonCards();
StringBuilder sb = new StringBuilder();
boolean isFirst = true;
@@ -441,10 +456,8 @@ public class CardPool extends ItemPool<PaperCard> {
else
isFirst = false;
CardDb db = !e.getKey().getRules().isVariant() ? commonDb : StaticData.instance().getVariantCards();
sb.append(e.getValue()).append(" ");
db.appendCardToStringBuilder(e.getKey(), sb);
sb.append(CardDb.CardRequest.compose(e.getKey()));
}
return sb.toString();
}
@@ -463,20 +476,4 @@ public class CardPool extends ItemPool<PaperCard> {
}
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;
if (deferredSections != null) {
this.validateDeferredSections();
this.normalizeDeferredSections();
referenceDeckLoadingMap = new HashMap<>(this.deferredSections);
} else
referenceDeckLoadingMap = new HashMap<>(loadedSections);
@@ -267,7 +267,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
continue;
final List<String> cardsInSection = s.getValue();
ArrayList<String> cardNamesWithNoEdition = getAllCardNamesWithNoSpecifiedEdition(cardsInSection);
if (cardNamesWithNoEdition.size() > 0) {
if (!cardNamesWithNoEdition.isEmpty()) {
includeCardsFromUnspecifiedSet = true;
if (smartCardArtSelection)
cardsWithNoEdition.put(sec, cardNamesWithNoEdition);
@@ -281,10 +281,10 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
optimiseCardArtSelectionInDeckSections(cardsWithNoEdition);
}
private void validateDeferredSections() {
private void normalizeDeferredSections() {
/*
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.
*/
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();
List<Pair<String, Integer>> originalCardRequests = CardPool.processCardList(cardsInSection);
CardPool pool = CardPool.fromCardList(cardsInSection);
if (pool.countDistinct() == 0)
continue; // pool empty, no card has been found!
// Filter pool by applying DeckSection Validation schema for Card Types (to avoid inconsistencies)
CardPool filteredPool = pool.getFilteredPoolWithCardsCount(deckSection::validate);
// Add all the cards from ValidPool anyway!
List<String> whiteList = validatedSections.getOrDefault(s.getKey(), null);
if (whiteList == null)
whiteList = new ArrayList<>();
for (Entry<PaperCard, Integer> entry : filteredPool) {
String poolRequest = getPoolRequest(entry, originalCardRequests);
whiteList.add(poolRequest);
List<String> validatedSection = validatedSections.computeIfAbsent(s.getKey(), (k) -> new ArrayList<>());
for (Entry<PaperCard, Integer> entry : pool) {
PaperCard card = entry.getKey();
String normalizedRequest = getPoolRequest(entry);
if(deckSection.validate(card))
validatedSection.add(normalizedRequest);
else {
// Card was in the wrong section. Move it to the right section.
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
// Overwrite deferredSections
this.deferredSections = validatedSections;
}
private String getPoolRequest(Entry<PaperCard, Integer> entry, List<Pair<String, Integer>> originalCardRequests) {
PaperCard card = entry.getKey();
private String getPoolRequest(Entry<PaperCard, Integer> entry) {
int amount = entry.getValue();
String poolCardRequest = CardDb.CardRequest.compose(
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);
String poolCardRequest = CardDb.CardRequest.compose(entry.getKey());
return String.format("%d %s", amount, poolCardRequest);
}
@@ -645,9 +617,8 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
/** {@inheritDoc} */
@Override
public boolean equals(final Object o) {
if (o instanceof Deck) {
final DeckBase dbase = (DeckBase) o;
boolean deckBaseEquals = super.equals(dbase);
if (o instanceof DeckBase deckBase) {
boolean deckBaseEquals = super.equals(deckBase);
if (!deckBaseEquals)
return false;
// ok so far we made sure they do have the same name. Now onto comparing parts

View File

@@ -472,7 +472,8 @@ public class DeckRecognizer {
"side", "sideboard", "sb",
"main", "card", "mainboard",
"avatar", "commander", "schemes",
"conspiracy", "planes", "deck", "dungeon"};
"conspiracy", "planes", "deck", "dungeon",
"attractions", "contraptions"};
private static CharSequence[] allCardTypes(){
List<String> cardTypesList = new ArrayList<>();
@@ -671,7 +672,8 @@ public class DeckRecognizer {
return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine,
currentDeckSection, true);
// UNKNOWN card as in the Counterspell|FEM case
return Token.UnknownCard(cardName, setCode, cardCount);
unknownCardToken = Token.UnknownCard(cardName, setCode, cardCount);
continue;
}
// ok so we can simply ignore everything but card name - as set code does not exist
// At this stage, we know the card name exists in the DB so a Card MUST be found
@@ -985,7 +987,7 @@ public class DeckRecognizer {
private static String getMagicColourLabel(MagicColor.Color magicColor) {
if (magicColor == null) // 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>() {{
@@ -1004,8 +1006,8 @@ public class DeckRecognizer {
if (magicColor2 == null || magicColor2 == MagicColor.Color.COLORLESS
|| magicColor1 == MagicColor.Color.COLORLESS)
return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2));
String localisedName1 = getLocalisedMagicColorName(magicColor1.getName());
String localisedName2 = getLocalisedMagicColorName(magicColor2.getName());
String localisedName1 = magicColor1.getLocalizedName();
String localisedName2 = magicColor2.getLocalizedName();
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
}

View File

@@ -88,7 +88,7 @@ public enum DeckSection {
CardType t = card.getRules().getType();
// NOTE: Same rules applies to both Deck and Side, despite "Conspiracy cards" are allowed
// 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();
};

View File

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

View File

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

View File

@@ -24,12 +24,10 @@ import forge.util.CardTranslation;
import forge.util.ImageUtil;
import forge.util.Localizer;
import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Optional;
import java.util.Set;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* 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
*/
public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet, IPaperCard {
@Serial
private static final long serialVersionUID = 2942081982620691205L;
// Reference to rules
@@ -55,16 +54,15 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
private String artist;
private final int artIndex;
private final boolean foil;
private Boolean hasImage;
private final boolean noSell;
private Set<String> colorID;
private String sortableName;
private final PaperCardFlags flags;
private final String sortableName;
private final String functionalVariant;
// Calculated fields are below:
private transient CardRarity rarity; // rarity is given in ctor when set is assigned
// 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
public String getName() {
@@ -89,8 +87,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
}
@Override
public Set<String> getColorID() {
return colorID;
public ColorSet getMarkedColors() {
return this.flags.markedColors;
}
@Override
@@ -147,32 +145,32 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return unFoiledVersion;
}
public PaperCard getNoSellVersion() {
if (this.noSell)
if (this.flags.noSellValue)
return this;
if (this.noSellVersion == null) {
this.noSellVersion = new PaperCard(this.rules, this.edition, this.rarity,
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, true);
}
if (this.noSellVersion == null)
this.noSellVersion = new PaperCard(this, this.flags.withNoSellValueFlag(true));
return this.noSellVersion;
}
public PaperCard getSellable() {
if (!this.noSell)
return this;
PaperCard sellable = new PaperCard(this.rules, this.edition, this.rarity,
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, false);
return sellable;
public PaperCard copyWithoutFlags() {
if(this.flaglessVersion == null) {
if(this.flags == PaperCardFlags.IDENTITY_FLAGS)
this.flaglessVersion = this;
else
this.flaglessVersion = new PaperCard(this, null);
}
public PaperCard getColorIDVersion(Set<String> colors) {
if (colors == null && this.colorID == null)
return flaglessVersion;
}
public PaperCard copyWithFlags(Map<String, String> flags) {
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;
if (this.colorID != null && this.colorID.equals(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);
return new PaperCard(this, this.flags.withMarkedColors(colors));
}
@Override
public String getItemType() {
@@ -180,8 +178,12 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return localizer.getMessage("lblCard");
}
public boolean isNoSell() {
return noSell;
public PaperCardFlags getMarkedFlags() {
return this.flags;
}
public boolean hasNoSellValue() {
return this.flags.noSellValue;
}
public boolean hasImage() {
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);
}
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,
final int artIndex0, final boolean foil0, final String collectorNumber0,
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,
final int artIndex0, final boolean foil0, final String collectorNumber0,
final String artist0, final String functionalVariant, final boolean noSell0) {
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, noSell0, 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) {
protected PaperCard(final CardRules rules, final String edition, final CardRarity rarity,
final int artIndex, final boolean foil, final String collectorNumber,
final String artist, final String functionalVariant, final PaperCardFlags flags) {
if (rules == null || edition == null || rarity == null) {
throw new IllegalArgumentException("Cannot create card without rules, edition or rarity");
}
rules = rules0;
name = rules0.getName();
edition = edition0;
artIndex = Math.max(artIndex0, IPaperCard.DEFAULT_ART_INDEX);
foil = foil0;
rarity = rarity0;
artist = TextUtil.normalizeText(artist0);
collectorNumber = (collectorNumber0 != null) && (collectorNumber0.length() > 0) ? collectorNumber0 : IPaperCard.NO_COLLECTOR_NUMBER;
this.rules = rules;
name = rules.getName();
this.edition = edition;
this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
this.foil = foil;
this.rarity = rarity;
this.artist = TextUtil.normalizeText(artist);
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.
// 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;
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);
@@ -256,8 +261,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
}
if (!getCollectorNumber().equals(other.getCollectorNumber()))
return false;
// colorID can be NULL
if (getColorID() != other.getColorID())
if (!Objects.equals(flags, other.flags))
return false;
return (other.foil == foil) && (other.artIndex == artIndex);
}
@@ -269,13 +273,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
*/
@Override
public int hashCode() {
final int code = (name.hashCode() * 11) + (edition.hashCode() * 59) +
(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;
return Objects.hash(name, edition, collectorNumber, artIndex, foil, flags);
}
// FIXME: Check
@@ -307,7 +305,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
String collectorNumber = collectorNumber0;
if (collectorNumber.equals(NO_COLLECTOR_NUMBER))
collectorNumber = null;
return CardEdition.CardInSet.getSortableCollectorNumber(collectorNumber);
return CardEdition.getSortableCollectorNumber(collectorNumber);
}
private String sortableCNKey = null;
@@ -339,6 +337,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return Integer.compare(artIndex, o.getArtIndex());
}
@Serial
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
// default deserialization
ois.defaultReadObject();
@@ -354,22 +353,24 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
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
public String getImageKey(boolean altState) {
String noramlizedName = StringUtils.stripAccents(name);
String imageKey = ImageKeys.CARD_PREFIX + noramlizedName + CardDb.NameSetSeparator
+ edition + CardDb.NameSetSeparator + artIndex;
if (altState) {
imageKey += ImageKeys.BACKFACE_POSTFIX;
}
return imageKey;
return altState ? this.getCardAltImageKey() : this.getCardImageKey();
}
private String cardImageKey = null;
@Override
public String getCardImageKey() {
if (this.cardImageKey == null)
this.cardImageKey = ImageUtil.getImageKey(this, "", true);
this.cardImageKey = ImageUtil.getImageKey(this, CardStateName.Original);
return cardImageKey;
}
@@ -378,9 +379,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardAltImageKey() {
if (this.cardAltImageKey == null){
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
this.cardAltImageKey = ImageUtil.getImageKey(this, "", true);
this.cardAltImageKey = getCardImageKey();
}
return cardAltImageKey;
}
@@ -390,9 +391,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardWSpecImageKey() {
if (this.cardWSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardWSpecImageKey = ImageUtil.getImageKey(this, "white", true);
this.cardWSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeW);
else // just use cardImageKey
this.cardWSpecImageKey = ImageUtil.getImageKey(this, "", true);
this.cardWSpecImageKey = getCardImageKey();
}
return cardWSpecImageKey;
}
@@ -402,9 +403,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardUSpecImageKey() {
if (this.cardUSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardUSpecImageKey = ImageUtil.getImageKey(this, "blue", true);
this.cardUSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeU);
else // just use cardImageKey
this.cardUSpecImageKey = ImageUtil.getImageKey(this, "", true);
this.cardUSpecImageKey = getCardImageKey();
}
return cardUSpecImageKey;
}
@@ -414,9 +415,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardBSpecImageKey() {
if (this.cardBSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardBSpecImageKey = ImageUtil.getImageKey(this, "black", true);
this.cardBSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeB);
else // just use cardImageKey
this.cardBSpecImageKey = ImageUtil.getImageKey(this, "", true);
this.cardBSpecImageKey = getCardImageKey();
}
return cardBSpecImageKey;
}
@@ -426,9 +427,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardRSpecImageKey() {
if (this.cardRSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardRSpecImageKey = ImageUtil.getImageKey(this, "red", true);
this.cardRSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeR);
else // just use cardImageKey
this.cardRSpecImageKey = ImageUtil.getImageKey(this, "", true);
this.cardRSpecImageKey = getCardImageKey();
}
return cardRSpecImageKey;
}
@@ -438,18 +439,16 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardGSpecImageKey() {
if (this.cardGSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardGSpecImageKey = ImageUtil.getImageKey(this, "green", true);
this.cardGSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeG);
else // just use cardImageKey
this.cardGSpecImageKey = ImageUtil.getImageKey(this, "", true);
this.cardGSpecImageKey = getCardImageKey();
}
return cardGSpecImageKey;
}
@Override
public boolean hasBackFace(){
CardSplitType cst = this.rules.getSplitType();
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld
|| cst == CardSplitType.Modal;
return this.rules.hasBackSide();
}
@Override
@@ -493,4 +492,88 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public boolean isRebalanced() {
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.Locale;
import java.util.Set;
public class PaperToken implements InventoryItemFromSet, IPaperCard {
private static final long serialVersionUID = 1L;
private String name;
private String collectorNumber;
private String artist;
private transient CardEdition edition;
private ArrayList<String> imageFileName = new ArrayList<>();
private transient CardRules cardRules;
@@ -54,75 +55,31 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
return makeTokenFileName(fileName);
}
public static String makeTokenFileName(final CardRules rules, CardEdition edition) {
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) {
public PaperToken(final CardRules c, CardEdition edition0, String imageFileName, String collectorNumber, String artist) {
this.cardRules = c;
this.name = c.getName();
this.edition = edition0;
this.collectorNumber = collectorNumber;
this.artist = artist;
if (edition != null && edition.getTokens().containsKey(imageFileName)) {
this.artIndex = edition.getTokens().get(imageFileName);
if (collectorNumber != null && !collectorNumber.isEmpty() && edition != null && edition.getTokens().containsKey(imageFileName)) {
int idx = 0;
// count the one with the same collectorNumber
for (CardEdition.EditionEntry t : edition.getTokens().get(imageFileName)) {
++idx;
if (!t.collectorNumber().equals(collectorNumber)) {
continue;
}
if (imageFileName == null) {
// This shouldn't really happen. We can just use the normalized name again for the base image name
this.imageFileName.add(makeTokenFileName(c, edition0));
// 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|%s|%d", imageFileName, edition.getCode(), collectorNumber, idx));
}
this.artIndex = this.imageFileName.size();
} else if (null == edition || CardEdition.UNKNOWN == edition) {
this.imageFileName.add(imageFileName);
} else {
String formatEdition = null == edition || CardEdition.UNKNOWN == edition ? "" : "_" + edition.getCode().toLowerCase();
this.imageFileName.add(String.format("%s%s", imageFileName, formatEdition));
for (int idx = 2; idx <= this.artIndex; idx++) {
this.imageFileName.add(String.format("%s%d%s", imageFileName, idx, formatEdition));
}
// 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
public String getEdition() {
return edition != null ? edition.getCode() : "???";
return edition != null ? edition.getCode() : CardEdition.UNKNOWN_CODE;
}
@Override
public String getCollectorNumber() {
if (collectorNumber.isEmpty())
return IPaperCard.NO_COLLECTOR_NUMBER;
return collectorNumber;
}
@Override
@@ -153,7 +112,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
}
@Override
public Set<String> getColorID() {
public ColorSet getMarkedColors() {
return null;
}
@@ -178,13 +137,8 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
}
@Override
public String getArtist() { /*TODO*/
return "";
}
// Unfortunately this is a property of token, cannot move it outside of class
public String getImageFilename() {
return getImageFilename(1);
public String getArtist() {
return artist;
}
public String getImageFilename(int idx) {
@@ -259,24 +213,21 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
// InventoryItem
@Override
public String getImageKey(boolean altState) {
if (hasBackFace()) {
String edCode = edition != null ? "_" + edition.getCode().toLowerCase() : "";
if (altState) {
String name = ImageKeys.TOKEN_PREFIX + cardRules.getOtherPart().getName().toLowerCase().replace(" token", "");
name.replace(" ", "_");
return name + edCode;
String suffix = "";
if (hasBackFace() && altState) {
if (collectorNumber != null && !collectorNumber.isEmpty() && edition != null) {
String name = cardRules.getOtherPart().getName().toLowerCase().replace(" token", "").replace(" ", "_");
return ImageKeys.getTokenKey(String.format("%s|%s|%s%s", name, edition.getCode(), collectorNumber, ImageKeys.BACKFACE_POSTFIX));
} else {
String name = ImageKeys.TOKEN_PREFIX + cardRules.getMainPart().getName().toLowerCase().replace(" token", "");
name.replace(" ", "_");
return name + edCode;
suffix = ImageKeys.BACKFACE_POSTFIX;
}
}
int idx = MyRandom.getRandom().nextInt(artIndex);
return getImageKey(idx);
return getImageKey(idx) + suffix;
}
public String getImageKey(int artIndex) {
return ImageKeys.TOKEN_PREFIX + imageFileName.get(artIndex).replace(" ", "_");
return ImageKeys.getTokenKey(imageFileName.get(artIndex).replace(" ", "_"));
}
public boolean isRebalanced() {

View File

@@ -65,8 +65,8 @@ public class BoosterGenerator {
}
public static List<PaperCard> getBoosterPack(SealedTemplate template) {
if (template instanceof SealedTemplateWithSlots) {
return BoosterGenerator.getBoosterPack((SealedTemplateWithSlots) template);
if (template instanceof SealedTemplateWithSlots slots) {
return BoosterGenerator.getBoosterPack(slots);
}
List<PaperCard> result = new ArrayList<>();

View File

@@ -1,10 +1,15 @@
package forge.token;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.card.CardDb;
import forge.card.CardEdition;
import forge.card.CardRules;
import forge.item.IPaperCard;
import forge.item.PaperToken;
import forge.util.Aggregates;
import java.util.*;
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
// If that isn't found, consider falling back to the original token
private final Map<String, PaperToken> tokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
private final Multimap<String, PaperToken> allTokenByName = HashMultimap.create();
private final Map<String, PaperToken> extraTokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
private final CardEdition.Collection editions;
private final Map<String, CardRules> rulesByName;
@@ -38,38 +43,87 @@ public class TokenDb implements ITokenDatabase {
return this.rulesByName.containsKey(rule);
}
@Override
public PaperToken getToken(String tokenName) {
return getToken(tokenName, CardEdition.UNKNOWN.getName());
}
public void preloadTokens() {
for (CardEdition edition : this.editions) {
for (String name : edition.getTokens().keySet()) {
try {
getToken(name, edition.getCode());
} catch(Exception e) {
System.out.println(name + "_" + edition.getCode() + " defined in Edition file, but not defined as a token script.");
for (Map.Entry<String, Collection<CardEdition.EditionEntry>> inSet : edition.getTokens().asMap().entrySet()) {
String name = inSet.getKey();
String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
for (CardEdition.EditionEntry t : inSet.getValue()) {
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
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 {
PaperToken pt = new PaperToken(rulesByName.get(tokenName), editions.get(edition), tokenName);
tokensByName.put(fullName, pt);
PaperToken pt = new PaperToken(rulesByName.get(tokenName), realEdition, tokenName, "", IPaperCard.NO_ARTIST_NAME);
extraTokensByName.put(fullName, pt);
return pt;
} catch(Exception e) {
throw e;
}
}
return tokensByName.get(fullName);
return extraTokensByName.get(fullName);
}
@Override
@@ -119,7 +173,7 @@ public class TokenDb implements ITokenDatabase {
@Override
public List<PaperToken> getAllTokens() {
return new ArrayList<>(tokensByName.values());
return new ArrayList<>(allTokenByName.values());
}
@Override
@@ -139,6 +193,6 @@ public class TokenDb implements ITokenDatabase {
@Override
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.CardRules;
import forge.card.CardSplitType;
import forge.card.CardStateName;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import org.apache.commons.lang3.StringUtils;
@@ -24,20 +25,17 @@ public class ImageUtil {
key = imageKey.substring(ImageKeys.CARD_PREFIX.length());
else
return null;
if (key.endsWith(ImageKeys.BACKFACE_POSTFIX)) {
key = key.substring(0, key.length() - ImageKeys.BACKFACE_POSTFIX.length());
}
if (key.isEmpty())
return null;
CardDb db = StaticData.instance().getCommonCards();
PaperCard cp = null;
//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);
}
}
String[] tempdata = key.split("\\|");
PaperCard cp = StaticData.instance().fetchCard(tempdata[0], tempdata[1], tempdata[2]);
if (cp == null)
System.err.println("Can't find PaperCard from key: " + key);
// return cp regardless if it's null
@@ -54,6 +52,21 @@ public class ImageUtil {
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) {
final String nameToUse = cp == null ? null : getNameToUse(cp, face);
if (nameToUse == null) {
@@ -123,25 +136,15 @@ public class ImageUtil {
else
return null;
} else if (face.equals("white")) {
if (card.getWSpecialize() != null) {
return card.getWSpecialize().getName();
}
return card.getImageName(CardStateName.SpecializeW);
} else if (face.equals("blue")) {
if (card.getUSpecialize() != null) {
return card.getUSpecialize().getName();
}
return card.getImageName(CardStateName.SpecializeU);
} else if (face.equals("black")) {
if (card.getBSpecialize() != null) {
return card.getBSpecialize().getName();
}
return card.getImageName(CardStateName.SpecializeB);
} else if (face.equals("red")) {
if (card.getRSpecialize() != null) {
return card.getRSpecialize().getName();
}
return card.getImageName(CardStateName.SpecializeR);
} else if (face.equals("green")) {
if (card.getGSpecialize() != null) {
return card.getGSpecialize().getName();
}
return card.getImageName(CardStateName.SpecializeG);
} else if (CardSplitType.Split == cp.getRules().getSplitType()) {
return card.getMainPart().getName() + card.getOtherPart().getName();
} else if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(cp.getFunctionalVariant())) {
@@ -150,50 +153,95 @@ public class ImageUtil {
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) {
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) {
return getImageRelativePath(cp, face, true, true);
}
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){
return getScryfallDownloadUrl(cp, face, setCode, langCode, useArtCrop, false);
public static String getScryfallDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam, boolean useArtCrop){
return getScryfallDownloadUrl(collectorNumber, setCode, langCode, faceParam, useArtCrop, false);
}
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){
String editionCode;
if ((setCode != null) && (setCode.length() > 0))
editionCode = setCode;
else
editionCode = cp.getEdition().toLowerCase();
String cardCollectorNumber = cp.getCollectorNumber();
public static String getScryfallDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam, boolean useArtCrop, boolean hyphenateAlchemy){
// 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
if (cardCollectorNumber.startsWith("OHOP")) {
editionCode = "ohop";
cardCollectorNumber = cardCollectorNumber.substring("OHOP".length());
} else if (cardCollectorNumber.startsWith("OPCA")) {
editionCode = "opca";
cardCollectorNumber = cardCollectorNumber.substring("OPCA".length());
} else if (cardCollectorNumber.startsWith("OPC2")) {
editionCode = "opc2";
cardCollectorNumber = cardCollectorNumber.substring("OPC2".length());
if (collectorNumber.startsWith("OHOP")) {
setCode = "ohop";
collectorNumber = collectorNumber.substring("OHOP".length());
} else if (collectorNumber.startsWith("OPCA")) {
setCode = "opca";
collectorNumber = collectorNumber.substring("OPCA".length());
} else if (collectorNumber.startsWith("OPC2")) {
setCode = "opc2";
collectorNumber = collectorNumber.substring("OPC2".length());
} else if (hyphenateAlchemy) {
if (!cardCollectorNumber.startsWith("A")) {
if (!collectorNumber.startsWith("A")) {
return null;
}
cardCollectorNumber = cardCollectorNumber.replace("A", "A-");
collectorNumber = collectorNumber.replace("A", "A-");
}
String versionParam = useArtCrop ? "art_crop" : "normal";
String faceParam = "";
if (cp.getRules().getOtherPart() != null) {
faceParam = (face.equals("back") ? "&face=back" : "&face=front");
if (!faceParam.isEmpty()) {
faceParam = (faceParam.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);
}

View File

@@ -269,13 +269,20 @@ 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
}
public void removeIf(Predicate<T> test) {
for (final T item : items.keySet()) {
if (test.test(item))
remove(item);
}
}
public void clear() {
items.clear();
}
@Override
public boolean equals(final Object obj) {
return (obj instanceof ItemPool) &&
(this.items.equals(((ItemPool)obj).items));
return (obj instanceof ItemPool ip) &&
(this.items.equals(ip.items));
}
}

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

View File

@@ -2,12 +2,14 @@ package forge.game;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Lists;
import forge.card.CardStateName;
import forge.card.MagicColor;
@@ -337,9 +339,6 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
if (params.containsKey("Blessing")) {
if ("True".equalsIgnoreCase(params.get("Blessing")) != hostController.hasBlessing()) return false;
}
if (params.containsKey("MaxSpeed")) {
if ("True".equalsIgnoreCase(params.get("MaxSpeed")) != hostController.maxSpeed()) return false;
}
if (params.containsKey("DayTime")) {
if ("Day".equalsIgnoreCase(params.get("DayTime"))) {
@@ -567,13 +566,23 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
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) {
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)
return getCardState();
return getHostCard();
result.add(getCardState());
result.add(getHostCard());
return result;
}
protected Optional<IHasSVars> findSVar(final String name) {
return getSVarFallback(name).stream().filter(f -> f.hasSVar(name)).findFirst();
}
@Override
@@ -581,12 +590,12 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
if (sVars.containsKey(name)) {
return sVars.get(name);
}
return getSVarFallback().getSVar(name);
return findSVar(name).map(o -> o.getSVar(name)).orElse("");
}
@Override
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) {
@@ -601,22 +610,21 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
}
@Override
public final void setSVar(final String name, final String value) {
public void setSVar(final String name, final String value) {
sVars.put(name, value);
}
@Override
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);
return res;
}
@Override
public Map<String, String> getDirectSVars() {
return sVars;
}
@Override
public void setSVars(Map<String, String> newSVars) {
sVars = Maps.newTreeMap();

View File

@@ -122,23 +122,10 @@ public class ForgeScript {
}
}
return found;
} else if (property.equals("hasActivatedAbilityWithTapCost")) {
} else if (property.startsWith("hasAbility")) {
String valid = property.substring(11);
for (final SpellAbility sa : cardState.getSpellAbilities()) {
if (sa.isActivatedAbility() && sa.getPayCosts().hasTapCost()) {
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)) {
if (sa.isValid(valid, sourceController, source, spellAbility)) {
return true;
}
}
@@ -218,6 +205,8 @@ public class ForgeScript {
return sa.isEternalize();
} else if (property.equals("Flashback")) {
return sa.isFlashback();
} else if (property.equals("Harmonize")) {
return sa.isHarmonize();
} else if (property.equals("Jumpstart")) {
return sa.isJumpstart();
} else if (property.equals("Kicked")) {
@@ -236,6 +225,8 @@ public class ForgeScript {
return sa.isTurnFaceUp();
} else if (property.equals("isCastFaceDown")) {
return sa.isCastFaceDown();
} else if (property.equals("Unearth")) {
return sa.isKeyword(Keyword.UNEARTH);
} else if (property.equals("Modular")) {
return sa.isKeyword(Keyword.MODULAR);
} 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.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.eventbus.EventBus;
import forge.GameCommand;
@@ -261,7 +262,6 @@ public class Game {
return null;
}
public void addPlayer(int id, Player player) {
playerCache.put(id, player);
}
@@ -523,7 +523,7 @@ public class Game {
* The Direction in which the turn order of this Game currently proceeds.
*/
public final Direction getTurnOrder() {
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().getAmountOfKeyword("The turn order is reversed.") % 2 == 1) {
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().isTurnOrderReversed()) {
return turnOrder.getOtherDirection();
}
return turnOrder;
@@ -958,9 +958,9 @@ public class Game {
// 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
if (p.equals(getPhaseHandler().getPlayerTurn())) {
getAction().becomeMonarch(getNextPlayerAfter(p), null);
getAction().becomeMonarch(getNextPlayerAfter(p), p.getMonarchSet());
} else {
getAction().becomeMonarch(getPhaseHandler().getPlayerTurn(), null);
getAction().becomeMonarch(getPhaseHandler().getPlayerTurn(), p.getMonarchSet());
}
}
@@ -970,9 +970,9 @@ public class Game {
// 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.
if (p.equals(getPhaseHandler().getPlayerTurn())) {
getAction().takeInitiative(getNextPlayerAfter(p), null);
getAction().takeInitiative(getNextPlayerAfter(p), p.getInitiativeSet());
} else {
getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), null);
getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), p.getInitiativeSet());
}
}
@@ -1185,6 +1185,12 @@ public class Game {
for (Player player : getRegisteredPlayers()) {
player.onCleanupPhase();
}
for (final Card c : getCardsIncludePhasingIn(ZoneType.Battlefield)) {
c.onCleanupPhase(getPhaseHandler().getPlayerTurn());
}
for (final Card card : getCardsInGame()) {
card.resetActivationsPerTurn();
}
}
public void addCounterAddedThisTurn(Player putter, CounterType cType, Card card, Integer value) {
@@ -1201,10 +1207,16 @@ public class Game {
public int getCounterAddedThisTurn(CounterType cType, String validPlayer, String validCard, Card source, Player sourceController, CardTraitBase ctb) {
int result = 0;
if (!countersAddedThisTurn.containsRow(cType)) {
Set<CounterType> types = null;
if (cType == null) {
types = countersAddedThisTurn.rowKeySet();
} else if (!countersAddedThisTurn.containsRow(cType)) {
return result;
} else {
types = Sets.newHashSet(cType);
}
for (Map.Entry<Player, List<Pair<Card, Integer>>> e : countersAddedThisTurn.row(cType).entrySet()) {
for (CounterType type : types) {
for (Map.Entry<Player, List<Pair<Card, Integer>>> e : countersAddedThisTurn.row(type).entrySet()) {
if (e.getKey().isValid(validPlayer.split(","), sourceController, source, ctb)) {
for (Pair<Card, Integer> p : e.getValue()) {
if (p.getKey().isValid(validCard.split(","), sourceController, source, ctb)) {
@@ -1213,20 +1225,28 @@ public class Game {
}
}
}
}
return result;
}
public int getCounterAddedThisTurn(CounterType cType, Card card) {
int result = 0;
if (!countersAddedThisTurn.containsRow(cType)) {
Set<CounterType> types = null;
if (cType == null) {
types = countersAddedThisTurn.rowKeySet();
} else if (!countersAddedThisTurn.containsRow(cType)) {
return result;
} else {
types = Sets.newHashSet(cType);
}
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(cType).values()) {
for (CounterType type : types) {
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(type).values()) {
for (Pair<Card, Integer> p : l) {
if (p.getKey().equalsWithGameTimestamp(card)) {
result += p.getValue();
}
}
}
}
return result;
}

View File

@@ -22,6 +22,7 @@ import forge.GameCommand;
import forge.StaticData;
import forge.card.CardStateName;
import forge.card.CardType.Supertype;
import forge.card.ColorSet;
import forge.card.GamePieceType;
import forge.card.MagicColor;
import forge.deck.DeckSection;
@@ -43,11 +44,10 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.spellability.SpellPermanent;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
import forge.game.zone.PlayerZoneBattlefield;
@@ -82,12 +82,6 @@ public class GameAction {
game = game0;
}
public final void resetActivationsPerTurn() {
for (final Card card : game.getCardsInGame()) {
card.resetActivationsPerTurn();
}
}
public Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer position, SpellAbility cause) {
return changeZone(zoneFrom, zoneTo, c, position, cause, null);
}
@@ -107,6 +101,8 @@ public class GameAction {
}
return c;
}
// dev mode
if (zoneFrom == null && !c.isToken()) {
zoneTo.add(c, position, CardCopyService.getLKICopy(c));
checkStaticAbilities();
@@ -314,37 +310,34 @@ public class GameAction {
c.getOwner().setCommanderReplacementSuppressed(true);
}
// in addition to actual tokens, cards "made" by digital-only mechanics
// are also added to inbound tokens so their etb replacements will work
if (zoneFrom == null || zoneFrom.is(ZoneType.None)) {
copied.getOwner().addInboundToken(copied);
}
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(copied);
repParams.put(AbilityKey.CardLKI, lastKnownInfo);
repParams.put(AbilityKey.Cause, cause);
repParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType() : null);
repParams.put(AbilityKey.Destination, zoneTo.getZoneType());
if (toBattlefield) {
repParams.put(AbilityKey.EffectOnly, true);
repParams.put(AbilityKey.CounterTable, table);
repParams.put(AbilityKey.CounterMap, table.column(copied));
}
if (params != null) {
repParams.putAll(params);
}
// in addition to actual tokens, cards "made" by digital-only mechanics
// are also added to inbound tokens so their etb replacements will work
if (zoneFrom == null || zoneFrom.is(ZoneType.None)) {
copied.getOwner().addInboundToken(copied);
}
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams);
copied.getOwner().removeInboundToken(copied);
if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) {
// reset failed manifested Cards back to original
if ((c.isManifested() || c.isCloaked()) && !c.isInPlay()) {
c.forceTurnFaceUp();
}
copied.getOwner().removeInboundToken(copied);
if (repres == ReplacementResult.Prevented) {
c.clearControllers();
cleanStaticEffect(staticEff, copied);
@@ -359,10 +352,6 @@ public class GameAction {
if (c.isInZone(ZoneType.Stack) && !zoneTo.is(ZoneType.Graveyard)) {
return moveToGraveyard(c, cause, params);
}
copied.clearDevoured();
copied.clearDelved();
copied.clearExploited();
} else if (toBattlefield && !c.isInPlay()) {
// was replaced with another Zone Change
if (c.removeChangedState()) {
@@ -379,8 +368,6 @@ public class GameAction {
copied.setGameTimestamp(game.getNextTimestamp());
}
copied.getOwner().removeInboundToken(copied);
// Aura entering as Copy from stack
// without targets it is sent to graveyard
if (copied.isAura() && !copied.isAttachedToEntity() && toBattlefield) {
@@ -399,7 +386,7 @@ public class GameAction {
return moveToGraveyard(copied, cause, params);
}
}
attachAuraOnIndirectEnterBattlefield(copied, params);
attachAuraOnIndirectETB(copied, params);
}
// Handle merged permanent here so all replacement effects are already applied.
@@ -432,10 +419,6 @@ public class GameAction {
}
}
if (suppress) {
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
}
if (zoneFrom != null) {
if (fromBattlefield && game.getCombat() != null) {
if (!toBattlefield) {
@@ -549,29 +532,25 @@ public class GameAction {
// order here is important so it doesn't unattach cards that might have returned from UntilHostLeavesPlay
unattachCardLeavingBattlefield(copied, c);
c.runLeavesPlayCommands();
if (copied.isTapped()) {
copied.setTapped(false); //untap card after it leaves the battlefield if needed
game.fireEvent(new GameEventCardTapped(c, false));
}
}
if (fromGraveyard) {
game.addLeftGraveyardThisTurn(lastKnownInfo);
}
// do ETB counters after zone add
if (!suppress && toBattlefield && !table.isEmpty()) {
game.getTriggerHandler().registerActiveTrigger(copied, false);
}
if (c.hasChosenColorSpire()) {
copied.setChosenColorID(ImmutableSet.copyOf(c.getChosenColorID()));
if (c.hasMarkedColor()) {
copied.setMarkedColors(c.getMarkedColors());
}
copied.updateStateForView();
if (fromBattlefield) {
copied.setDamage(0); //clear damage after a card leaves the battlefield
copied.setHasBeenDealtDeathtouchDamage(false);
if (copied.isTapped()) {
copied.setTapped(false); //untap card after it leaves the battlefield if needed
game.fireEvent(new GameEventCardTapped(c, false));
}
// needed for counters + ascend
if (!suppress && toBattlefield) {
game.getTriggerHandler().registerActiveTrigger(copied, false);
}
if (!table.isEmpty()) {
@@ -579,12 +558,12 @@ public class GameAction {
game.getTriggerHandler().suppressMode(TriggerType.Always);
// Need to apply any static effects to produce correct triggers
checkStaticAbilities();
// do ETB counters after zone add
table.replaceCounterEffect(game, null, true, true, params);
game.getTriggerHandler().clearSuppression(TriggerType.Always);
}
table.replaceCounterEffect(game, null, true, true, params);
// update static abilities after etb counters have been placed
game.getTriggerHandler().clearSuppression(TriggerType.Always);
checkStaticAbilities();
// 400.7g try adding keyword back into card if it doesn't already have it
@@ -607,12 +586,13 @@ public class GameAction {
c.cleanupExiledWith();
}
game.getTriggerHandler().clearActiveTriggers(copied, null);
game.getTriggerHandler().registerActiveTrigger(copied, false);
// play the change zone sound
game.fireEvent(new GameEventCardChangeZone(c, zoneFrom, zoneTo));
game.getTriggerHandler().clearActiveTriggers(copied, null);
game.getTriggerHandler().registerActiveTrigger(copied, false);
if (!suppress) {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(copied);
runParams.put(AbilityKey.CardLKI, lastKnownInfo);
runParams.put(AbilityKey.Cause, cause);
@@ -620,12 +600,12 @@ public class GameAction {
runParams.put(AbilityKey.Destination, zoneTo.getZoneType().name());
runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek());
runParams.put(AbilityKey.MergedCards, mergedCards);
if (params != null) {
runParams.putAll(params);
}
game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, true);
}
if (fromBattlefield && !zoneFrom.getPlayer().equals(zoneTo.getPlayer())) {
final Map<AbilityKey, Object> runParams2 = AbilityKey.mapFromCard(lastKnownInfo);
runParams2.put(AbilityKey.OriginalController, zoneFrom.getPlayer());
@@ -635,31 +615,18 @@ public class GameAction {
game.getTriggerHandler().runTrigger(TriggerType.ChangesController, runParams2, false);
}
if (suppress) {
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
}
if (zoneFrom == null) {
return copied;
}
if (!c.isRealToken() && !toBattlefield) {
copied.clearDevoured();
copied.clearDelved();
copied.clearExploited();
}
// rule 504.6: reveal a face-down card leaving the stack
if (zoneFrom != null && zoneTo != null && zoneFrom.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield) && wasFacedown) {
// CR 708.9 reveal face-down card leaving
if (wasFacedown && (fromBattlefield || (zoneFrom.is(ZoneType.Stack) && !toBattlefield))) {
Card revealLKI = CardCopyService.getLKICopy(c);
revealLKI.forceTurnFaceUp();
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card moves from the stack: ");
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card leaves the " + zoneFrom.toString() + ": ");
}
if (fromBattlefield) {
if (!c.isRealToken() && !c.isSpecialized()) {
copied.setState(CardStateName.Original, true);
}
// Soulbond unpairing
if (c.isPaired()) {
c.getPairedWith().setPairedWith(null);
@@ -680,27 +647,12 @@ public class GameAction {
}
changeZone(null, zoneTo, unmeld, position, cause, params);
}
// Reveal if face-down
if (wasFacedown) {
Card revealLKI = CardCopyService.getLKICopy(c);
revealLKI.forceTurnFaceUp();
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card leaves the battlefield: ");
copied.setState(CardStateName.Original, true);
}
} else if (toBattlefield) {
for (Player p : game.getPlayers()) {
copied.getDamageHistory().setNotAttackedSinceLastUpkeepOf(p);
copied.getDamageHistory().setNotBlockedSinceLastUpkeepOf(p);
copied.getDamageHistory().setNotBeenBlockedSinceLastUpkeepOf(p);
}
} else if (zoneTo.is(ZoneType.Graveyard)
|| zoneTo.is(ZoneType.Hand)
|| zoneTo.is(ZoneType.Library)
|| zoneTo.is(ZoneType.Exile)) {
if (copied.isFaceDown()) {
copied.setState(CardStateName.Original, true);
}
}
// Cards not on the battlefield / stack should not have controller
@@ -748,14 +700,14 @@ public class GameAction {
eff.setLayerTimestamp(timestamp);
} else {
// otherwise create effect first
eff = SpellAbilityEffect.createEffect(cause, cause.getActivatingPlayer(), name, source.getImageKey(), timestamp);
eff = SpellAbilityEffect.createEffect(cause, cause.getHostCard(), cause.getActivatingPlayer(), name, source.getImageKey(), timestamp);
eff.setRenderForUI(false);
StaticAbility stAb = eff.addStaticAbility(AbilityUtils.getSVar(cause, cause.getParam("StaticEffect")));
stAb.setActiveZone(EnumSet.of(ZoneType.Command));
// needed for ETB lookahead like Bronzehide Lion
stAb.putParam("AffectedZone", "Battlefield,Hand,Graveyard,Exile,Stack,Library,Command");
stAb.putParam("AffectedZone", "All");
SpellAbilityEffect.addForgetOnMovedTrigger(eff, "Battlefield");
game.getAction().moveToCommand(eff, cause);
eff.getOwner().getZone(ZoneType.Command).add(eff);
}
eff.addRemembered(copied);
@@ -772,7 +724,6 @@ public class GameAction {
}
}
private void storeChangesZoneAll(Card c, Zone zoneFrom, Zone zoneTo, Map<AbilityKey, Object> params) {
if (params != null && params.containsKey(AbilityKey.InternalTriggerTable)) {
((CardZoneTable) params.get(AbilityKey.InternalTriggerTable)).put(zoneFrom != null ? zoneFrom.getZoneType() : null, zoneTo.getZoneType(), c);
@@ -846,7 +797,7 @@ public class GameAction {
continue;
}
if (stAb.checkMode("CantBlockBy")) {
if (stAb.checkMode(StaticAbilityMode.CantBlockBy)) {
if (!stAb.hasParam("ValidAttacker") || (stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
continue;
}
@@ -856,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)) {
if (stAb.matchesValidParam("ValidCard", creature)) {
creature.updateAbilityTextForView();
@@ -982,6 +933,10 @@ public class GameAction {
final PlayerZone removed = c.getOwner().getZone(ZoneType.Exile);
final Card copied = moveTo(removed, c, cause, params);
if (c.isImmutable()) {
return copied;
}
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c);
runParams.put(AbilityKey.Cause, cause);
if (origin != null) { // is generally null when adding via dev mode
@@ -1038,7 +993,8 @@ public class GameAction {
lki = CardCopyService.getLKICopy(c);
}
game.addChangeZoneLKIInfo(lki);
if (lki.isInPlay()) {
// CR 702.26k
if (lki.isInPlay() && !lki.isPhasedOut()) {
if (game.getCombat() != null) {
game.getCombat().saveLKI(lki);
game.getCombat().removeFromCombat(c);
@@ -1117,7 +1073,7 @@ public class GameAction {
public boolean hasStaticAbilityAffectingZone(ZoneType zone, StaticAbilityLayer layer) {
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions("Continuous")) {
if (!stAb.checkConditions(StaticAbilityMode.Continuous)) {
continue;
}
if (layer != null && !stAb.getLayers().contains(layer)) {
@@ -1149,13 +1105,13 @@ public class GameAction {
// remove old effects
game.getStaticEffects().clearStaticEffects(affectedCards);
for (final Player p : game.getPlayers()) {
p.clearStaticAbilities();
}
// search for cards with static abilities
final FCollection<StaticAbility> staticAbilities = new FCollection<>();
final CardCollection staticList = new CardCollection();
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies = null;
if (preList.isEmpty()) {
dependencies = HashBasedTable.create();
}
game.forEachCardInGame(new Visitor<Card>() {
@Override
@@ -1163,7 +1119,7 @@ public class GameAction {
// need to get Card from preList if able
final Card co = preList.get(c);
for (StaticAbility stAb : co.getStaticAbilities()) {
if (stAb.checkMode("Continuous")) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.zonesCheck()) {
staticAbilities.add(stAb);
}
}
@@ -1190,7 +1146,7 @@ public class GameAction {
StaticAbility stAb = staticsForLayer.get(0);
// dependency with CDA seems unlikely
if (!stAb.isCharacteristicDefining()) {
stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility);
stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility, dependencies);
}
staticsForLayer.remove(stAb);
final CardCollectionView previouslyAffected = affectedPerAbility.get(stAb);
@@ -1210,7 +1166,7 @@ public class GameAction {
if (affectedHere != null) {
for (final Card c : affectedHere) {
for (final StaticAbility st2 : c.getStaticAbilities()) {
if (!staticAbilities.contains(st2)) {
if (!staticAbilities.contains(st2) && st2.checkMode(StaticAbilityMode.Continuous) && st2.zonesCheck()) {
toAdd.add(st2);
st2.applyContinuousAbilityBefore(layer, preList);
}
@@ -1277,14 +1233,16 @@ public class GameAction {
game.getTriggerHandler().runTrigger(TriggerType.Always, runParams, false);
game.getTriggerHandler().runTrigger(TriggerType.Immediate, runParams, false);
game.getView().setDependencies(dependencies);
}
// Update P/T and type in the view only once after all the cards have been processed, to avoid flickering
for (Card c : affectedCards) {
c.updateNameforView();
c.updatePowerToughnessForView();
c.updatePTforView();
c.updateTypesForView();
c.updateAbilityTextForView(); // only update keywords and text for view to avoid flickering
c.updateKeywords();
}
// TODO filter out old copies from zone change
@@ -1295,7 +1253,8 @@ public class GameAction {
game.getTracker().unfreeze();
}
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility) {
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility,
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
if (staticsForLayer.size() == 1) {
return staticsForLayer.get(0);
}
@@ -1309,12 +1268,11 @@ public class GameAction {
dependencyGraph.addVertex(stAb);
boolean exists = stAb.getHostCard().getStaticAbilities().contains(stAb);
boolean compareAffected = true;
boolean compareAffected = false;
CardCollectionView affectedHere = affectedPerAbility.get(stAb);
if (affectedHere == null) {
affectedHere = StaticAbilityContinuous.getAffectedCards(stAb, preList);
} else {
compareAffected = false;
compareAffected = true;
}
List<Object> effectResults = generateStaticAbilityResult(layer, stAb);
@@ -1342,21 +1300,24 @@ public class GameAction {
// ...what it applies to...
if (!dependency && compareAffected) {
CardCollectionView affectedAfterOther = StaticAbilityContinuous.getAffectedCards(stAb, preList);
if (!Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator())) {
dependency = true;
}
dependency = !Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator());
}
// ...or what it does to any of the things it applies to
if (!dependency) {
List<Object> effectResultsAfterOther = generateStaticAbilityResult(layer, stAb);
if (!effectResults.equals(effectResultsAfterOther)) {
dependency = true;
}
dependency = !effectResults.equals(effectResultsAfterOther);
}
if (dependency) {
dependencyGraph.addVertex(otherStAb);
dependencyGraph.addEdge(stAb, otherStAb);
if (dependencies != null) {
if (dependencies.contains(stAb, otherStAb)) {
dependencies.get(stAb, otherStAb).add(layer);
} else {
dependencies.put(stAb, otherStAb, EnumSet.of(layer));
}
}
}
// undo changes and check next pair
@@ -1389,7 +1350,7 @@ public class GameAction {
}
dependencyGraph.removeAllVertices(toRemove);
// now the earlist one left is the correct choice
// now the earliest one left is the correct choice
List<StaticAbility> statics = Lists.newArrayList(dependencyGraph.vertexSet());
statics.sort(Comparator.comparing(s -> s.getHostCard().getLayerTimestamp()));
@@ -1502,17 +1463,12 @@ public class GameAction {
checkAgainCard |= stateBasedAction_Saga(c, sacrificeList);
checkAgainCard |= stateBasedAction_Battle(c, noRegCreats);
checkAgainCard |= stateBasedAction_Role(c, unAttachList);
checkAgainCard |= stateBasedAction704_attach(c, unAttachList); // Attachment
checkAgainCard |= stateBasedAction704_attach(c, unAttachList);
checkAgainCard |= stateBasedAction_Contraption(c, noRegCreats);
checkAgainCard |= stateBasedAction704_5r(c); // annihilate +1/+1 counters with -1/-1 ones
checkAgainCard |= stateBasedAction704_5q(c); // annihilate +1/+1 counters with -1/-1 ones
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
if (c.getCounters(dreamType) > 7 && c.hasKeyword("CARDNAME can't have more than seven dream counters on it.")) {
c.subtractCounter(dreamType, c.getCounters(dreamType) - 7, null);
checkAgainCard = true;
}
checkAgainCard |= stateBasedAction704_5r(c);
if (c.hasKeyword("The number of loyalty counters on CARDNAME is equal to the number of Beebles you control.")) {
int beeble = CardLists.getValidCardCount(game.getCardsIn(ZoneType.Battlefield), "Beeble.YouCtrl", c.getController(), c, null);
@@ -1554,9 +1510,7 @@ public class GameAction {
if (!spaceSculptors.isEmpty() && !spaceSculptors.contains(p)) {
checkAgain |= stateBasedAction704_5u(p);
}
if (handleLegendRule(p, noRegCreats)) {
checkAgain = true;
}
checkAgain |= handleLegendRule(p, noRegCreats);
if ((game.getRules().hasAppliedVariant(GameType.Commander)
|| game.getRules().hasAppliedVariant(GameType.Brawl)
@@ -1569,15 +1523,21 @@ public class GameAction {
}
}
if (handlePlaneswalkerRule(p, noRegCreats)) {
// 704.5z If a player controls a permanent with start your engines! and that player has no speed, that players speed becomes 1.
if (p.getSpeed() == 0 && p.getCardsIn(ZoneType.Battlefield).anyMatch(c -> c.hasKeyword(Keyword.START_YOUR_ENGINES))) {
p.increaseSpeed();
checkAgain = true;
}
checkAgain |= handlePlaneswalkerRule(p, noRegCreats);
}
for (Player p : spaceSculptors) {
checkAgain |= stateBasedAction704_5u(p);
}
// 704.5m World rule
checkAgain |= handleWorldRule(noRegCreats);
// only check static abilities once after destroying all the creatures
// (e.g. helpful for Erebos's Titan and another creature dealing lethal damage to each other simultaneously)
setHoldCheckingStaticAbilities(true);
@@ -1609,6 +1569,7 @@ public class GameAction {
orderedSacrificeList = true;
}
sacrifice(sacrificeList, null, true, mapParams);
setHoldCheckingStaticAbilities(false);
table.triggerChangesZoneAll(game, null);
@@ -1667,7 +1628,7 @@ public class GameAction {
private boolean stateBasedAction_Saga(Card c, CardCollection sacrificeList) {
boolean checkAgain = false;
if (!c.isSaga()) {
if (!c.isSaga() || !c.hasChapter()) {
return false;
}
// needs to be effect, because otherwise it might be a cost?
@@ -1686,11 +1647,40 @@ public class GameAction {
private boolean stateBasedAction_Battle(Card c, CardCollection removeList) {
boolean checkAgain = false;
if (!c.getType().isBattle()) {
return false;
if (!c.isBattle()) {
return checkAgain;
}
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())) ||
(c.getType().hasStringType("Siege") && battleController.equals(battleProtector))) {
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
if (newProtector == null) {
removeList.add(c);
} else {
c.setProtectingPlayer(newProtector);
}
checkAgain = true;
}
if (c.getCounters(CounterEnumType.DEFENSE) > 0) {
return false;
return checkAgain;
}
// 704.5v If a battle has defense 0 and it isn't the source of an ability that has triggered but not yet left the stack,
// its put into its owners graveyard.
@@ -1766,12 +1756,12 @@ public class GameAction {
}
private boolean stateBasedAction_Contraption(Card c, CardCollection removeList) {
if(!c.isContraption())
if (!c.isContraption())
return false;
int currentSprocket = c.getSprocket();
//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);
return true;
}
@@ -1780,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,
//we assign it here. A contraption uses sprocket -1 to signify it has been assembled previously but now needs
//a new sprocket.
if(currentSprocket > 0 && currentSprocket <= 3)
if (currentSprocket > 0 && currentSprocket <= 3)
return false;
int sprocket = c.getController().getController().chooseSprocket(c);
@@ -1832,13 +1822,17 @@ public class GameAction {
return false;
}
private boolean stateBasedAction704_5r(Card c) {
private boolean stateBasedAction704_5q(Card c) {
boolean checkAgain = false;
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
int plusOneCounters = c.getCounters(p1p1);
int minusOneCounters = c.getCounters(m1m1);
if (plusOneCounters > 0 && minusOneCounters > 0) {
if (!c.canRemoveCounters(p1p1) || !c.canRemoveCounters(m1m1)) {
return checkAgain;
}
int remove = Math.min(plusOneCounters, minusOneCounters);
// If a permanent has both a +1/+1 counter and a -1/-1 counter on it,
// N +1/+1 and N -1/-1 counters are removed from it, where N is the
@@ -1850,6 +1844,26 @@ public class GameAction {
}
return checkAgain;
}
private boolean stateBasedAction704_5r(Card c) {
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
int old = c.getCounters(dreamType);
if (old <= 0) {
return false;
}
Integer max = c.getCounterMax(dreamType);
if (max == null) {
return false;
}
if (old > max) {
if (!c.canRemoveCounters(dreamType)) {
return false;
}
c.subtractCounter(dreamType, old - max, null);
return true;
}
return false;
}
// If a token is in a zone other than the battlefield, it ceases to exist.
private boolean stateBasedAction704_5d(Card c) {
@@ -1872,19 +1886,10 @@ public class GameAction {
public void checkGameOverCondition() {
// award loses as SBE
List<Player> losers = null;
FCollectionView<Player> allPlayers = game.getPlayers();
for (Player p : allPlayers) {
if (p.checkLoseCondition()) { // this will set appropriate outcomes
if (losers == null) {
losers = Lists.newArrayListWithCapacity(3);
}
losers.add(p);
}
}
GameEndReason reason = null;
List<Player> losers = null;
FCollectionView<Player> allPlayers = game.getPlayers();
// Has anyone won by spelleffect?
for (Player p : allPlayers) {
if (!p.hasWon()) {
@@ -1910,24 +1915,17 @@ public class GameAction {
break;
}
// loop through all the non-losing players that can't win
// see if all of their opponents are in that "about to lose" collection
if (losers != null) {
if (reason == null) {
for (Player p : allPlayers) {
if (losers.contains(p)) {
continue;
if (p.checkLoseCondition()) { // this will set appropriate outcomes
if (losers == null) {
losers = Lists.newArrayListWithCapacity(3);
}
losers.add(p);
}
if (p.cantWin()) {
if (losers.containsAll(p.getOpponents())) {
// what to do here?!?!?!
System.err.println(p.toString() + " is about to win, but can't!");
}
}
}
}
// need a separate loop here, otherwise ConcurrentModificationException is raised
if (losers != null) {
for (Player p : losers) {
game.onPlayerLost(p);
@@ -2040,7 +2038,7 @@ public class GameAction {
}
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) {
return false;
}
@@ -2049,7 +2047,7 @@ public class GameAction {
long ts = 0;
for (final Card crd : worlds) {
long crdTs = crd.getGameTimestamp();
long crdTs = crd.getWorldTimestamp();
if (crdTs > ts) {
ts = crdTs;
toKeep.clear();
@@ -2430,15 +2428,14 @@ public class GameAction {
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
// 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()) {
List<String> colorChoices = new ArrayList<>(MagicColor.Constant.ONLY_COLORS);
String prompt = CardTranslation.getTranslatedName(c.getName()) + ": " +
Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(2));
SpellAbility sa = new SpellAbility.EmptySa(ApiType.ChooseColor, c, takesAction);
sa.putParam("AILogic", "MostProminentInComputerDeck");
Set<String> chosenColors = new HashSet<>(takesAction.getController().chooseColors(prompt, sa, 2, 2, colorChoices));
c.setChosenColorID(chosenColors);
ColorSet chosenColors = ColorSet.fromNames(takesAction.getController().chooseColors(prompt, sa, 2, 2, MagicColor.Constant.ONLY_COLORS));
c.setMarkedColors(chosenColors);
}
}
}
@@ -2684,8 +2681,8 @@ public class GameAction {
if (isCombat) {
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {
final GameEntity ge = et.getKey();
if (ge instanceof Card) {
((Card) ge).clearAssignedDamage();
if (ge instanceof Card c) {
c.clearAssignedDamage();
}
}
}
@@ -2705,8 +2702,7 @@ public class GameAction {
continue;
}
if (e.getKey() instanceof Card && !lethalDamage.containsKey(e.getKey())) {
Card c = (Card) e.getKey();
if (e.getKey() instanceof Card c && !lethalDamage.containsKey(c)) {
lethalDamage.put(c, c.getExcessDamageValue(false));
}
@@ -2788,21 +2784,35 @@ public class GameAction {
* the source
* @return true, if successful
*/
public static boolean attachAuraOnIndirectEnterBattlefield(final Card source, Map<AbilityKey, Object> params) {
// When an Aura ETB without being cast you can choose a valid card to
// attach it to
final SpellAbility aura = source.getFirstAttachSpell();
private boolean attachAuraOnIndirectETB(final Card source, Map<AbilityKey, Object> params) {
// When an Aura ETB without being cast you can choose a valid card to attach it to
if (!source.hasKeyword(Keyword.ENCHANT)) {
return false;
}
SpellAbility aura = source.getCurrentState().getAuraSpell();
if (aura == null) {
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();
if (tgt.canTgtPlayer()) {
final FCollection<Player> players = game.getPlayers().filter(PlayerPredicates.canBeAttached(source, aura));
if (canTargetPlayer) {
final FCollection<Player> players = game.getPlayers().filter(PlayerPredicates.canBeAttached(source, null));
final Player pa = p.getController().chooseSingleEntityForEffect(players, aura,
Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", CardTranslation.getTranslatedName(source.getName())), null);
@@ -2811,9 +2821,7 @@ public class GameAction {
return true;
}
} else {
List<ZoneType> zones = Lists.newArrayList(tgt.getZone());
CardCollection list = new CardCollection();
if (params != null) {
if (zones.contains(ZoneType.Battlefield)) {
list.addAll((CardCollectionView) params.get(AbilityKey.LastStateBattlefield));
@@ -2826,7 +2834,7 @@ public class GameAction {
}
list.addAll(game.getCardsIn(zones));
list = CardLists.filter(list, CardPredicates.canBeAttached(source, aura));
list = CardLists.filter(list, CardPredicates.canBeAttached(source, null));
if (list.isEmpty()) {
return false;
}

View File

@@ -43,6 +43,7 @@ import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAlternativeCost;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.staticability.StaticAbilityMode;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
@@ -183,6 +184,34 @@ public final class GameActionUtil {
flashback.setKeyword(inst);
flashback.setIntrinsic(inst.isIntrinsic());
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")) {
// Foretell cast only from Exile
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));
for (final Card ca : costSources) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions("OptionalCost")) {
if (!stAb.checkConditions(StaticAbilityMode.OptionalCost)) {
continue;
}
@@ -582,9 +611,8 @@ public final class GameActionUtil {
" or greater>";
final Cost cost = new Cost(casualtyCost, false);
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) {
result = sa.copy();
}
@@ -630,9 +658,7 @@ public final class GameActionUtil {
final Cost cost = new Cost(k[1], false);
String str = "Pay for Offspring? " + cost.toSimpleString();
boolean v = pc.addKeywordCost(sa, cost, ki, str);
if (v) {
if (pc.addKeywordCost(sa, cost, ki, str)) {
if (result == null) {
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()) {
String kw = "As an additional cost to cast creature spells," +
" 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
// add back to where it came from, hopefully old state
// skip GameAction
oldCard.getZone().remove(oldCard);
// might have been an alternative lki host
oldCard = ability.getCardState().getCard();
oldCard.setCastSA(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)
Integer newPosition = zonePosition >= 0 ? Math.min(zonePosition, fromZone.size()) : null;
fromZone.add(oldCard, newPosition, null, true);

View File

@@ -23,15 +23,12 @@ package forge.game;
public enum GameEndReason {
/** The All opponents lost. */
AllOpponentsLost,
// Noone won
/** The Draw. */
Draw, // Having little idea how they can reach a draw, so I didn't enumerate
// possible reasons here
// Special conditions, they force one player to win and thus end the game
/** The Wins game spell effect. */
WinsGameSpellEffect, // ones that could be both hardcoded (felidar) and
// scripted ( such as Mayael's Aria )
/** Noone won */
Draw,
/** Special conditions, they force one player to win and thus end the game */
WinsGameSpellEffect,
/** Used to end multiplayer games where the all humans have lost or conceded while AIs cannot end match by themselves.*/
AllHumansLost,

View File

@@ -37,11 +37,11 @@ import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.event.GameEventCardAttachment;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityCantAttach;
import forge.game.zone.ZoneType;
@@ -267,15 +267,18 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
}
protected boolean canBeEnchantedBy(final Card aura) {
// TODO need to check for multiple Enchant Keywords
SpellAbility sa = aura.getFirstAttachSpell();
TargetRestrictions tgt = null;
if (sa != null) {
tgt = sa.getTargetRestrictions();
if (!aura.hasKeyword(Keyword.ENCHANT)) {
return false;
}
return tgt != null && isValid(tgt.getValidTgts(), aura.getController(), aura, sa);
for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) {
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() {
@@ -318,11 +321,20 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return canReceiveCounters(CounterType.get(type));
}
public final void addCounter(final CounterType counterType, final int n, final Player source, GameEntityCounterTable table) {
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
if (n <= 0 || !canReceiveCounters(counterType)) {
// As per rule 107.1b
return;
}
Integer max = getCounterMax(counterType);
if (max != null) {
n = Math.min(n, max - getCounters(counterType));
if (n <= 0) {
return;
}
}
// doesn't really add counters, but is just a helper to add them to the Table
// so the Table can handle the Replacement Effect
table.put(source, this, counterType, n);
@@ -340,6 +352,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params);
}
public Integer getCounterMax(final CounterType counterType) {
return null;
}
public List<Pair<Integer, Boolean>> getDamageReceivedThisTurn() {
return damageReceivedThisTurn;

View File

@@ -159,12 +159,17 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
}
// Add ETB flag
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Cause, cause);
if (params != null) {
runParams.putAll(params);
}
boolean firstTime = false;
if (gm.getKey() instanceof Card c) {
firstTime = game.getCounterAddedThisTurn(null, c) == 0;
}
// Apply counter after replacement effect
for (Map.Entry<Optional<Player>, Map<CounterType, Integer>> e : values.entrySet()) {
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();

View File

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

View File

@@ -273,8 +273,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
}
String controllerName;
if (defender instanceof Card) {
Card c = ((Card)defender);
if (defender instanceof Card c) {
controllerName = c.isBattle() ? c.getProtectingPlayer().getName() : c.getController().getName();
} else {
controllerName = defender.getName();
@@ -305,8 +304,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventCardForetold ev) {
String sb = TextUtil.concatWithSpace(ev.activatingPlayer.toString(), "has foretold.");
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, sb);
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
}
@Override
@@ -314,6 +312,11 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
}
@Override
public GameLogEntry visit(GameEventDoorChanged ev) {
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
}
@Subscribe
public void recieve(GameEvent ev) {
GameLogEntry le = ev.visit(this);

View File

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

View File

@@ -22,7 +22,7 @@ public enum GameType {
Winston (DeckFormat.Limited, true, true, true, "lblWinston", ""),
Gauntlet (DeckFormat.Constructed, false, true, true, "lblGauntlet", ""),
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", ""),
QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""),
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""),

View File

@@ -1,8 +1,12 @@
package forge.game;
import java.util.List;
import java.util.Set;
import com.google.common.collect.Iterables;
import com.google.common.collect.Table;
import com.google.common.collect.Table.Cell;
import forge.LobbyPlayer;
import forge.deck.Deck;
import forge.game.GameOutcome.AnteResult;
@@ -16,6 +20,8 @@ import forge.game.phase.PhaseType;
import forge.game.player.PlayerView;
import forge.game.player.RegisteredPlayer;
import forge.game.spellability.StackItemView;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.zone.MagicStack;
import forge.trackable.TrackableCollection;
import forge.trackable.TrackableObject;
@@ -200,15 +206,36 @@ public class GameView extends TrackableObject {
public TrackableCollection<CardView> getRevealedCollection() {
return get(TrackableProperty.RevealedCardsCollection);
}
public void updateRevealedCards(TrackableCollection<CardView> collection) {
set(TrackableProperty.RevealedCardsCollection, collection);
}
public String getDependencies() {
return get(TrackableProperty.Dependencies);
}
public void setDependencies(Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
if (dependencies.isEmpty()) {
return;
}
StringBuilder sb = new StringBuilder();
StaticAbilityLayer layer = null;
for (StaticAbilityLayer sal : StaticAbilityLayer.CONTINUOUS_LAYERS_WITH_DEPENDENCY) {
for (Cell<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dep : dependencies.cellSet()) {
if (dep.getValue().contains(sal)) {
if (layer != sal) {
layer = sal;
sb.append("Layer " + layer.num).append(": ");
}
sb.append(dep.getColumnKey().getHostCard().toString()).append(" <- ").append(dep.getRowKey().getHostCard().toString()).append("\n");
}
}
}
set(TrackableProperty.Dependencies, sb.toString());
}
public CombatView getCombat() {
return get(TrackableProperty.CombatView);
}
public void updateCombatView(CombatView combatView) {
set(TrackableProperty.CombatView, combatView);
}

View File

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

View File

@@ -206,7 +206,6 @@ public class StaticEffect {
if (layers.contains(StaticAbilityLayer.ABILITIES)) {
p.removeChangedKeywords(getTimestamp(), ability.getId());
}
}
// modify the affected card
@@ -219,7 +218,9 @@ public class StaticEffect {
if (layers.contains(StaticAbilityLayer.TEXT)) {
// Revert changed color words
if (hasParam("ChangeColorWordsTo")) {
affectedCard.removeChangedTextColorWord(getTimestamp(), ability.getId());
}
// remove changed name
if (hasParam("SetName") || hasParam("AddNames")) {
@@ -265,7 +266,7 @@ public class StaticEffect {
if (hasParam("AddAbility") || hasParam("GainsAbilitiesOf")
|| hasParam("GainsAbilitiesOfDefined") || hasParam("GainsTriggerAbsOf")
|| hasParam("AddTrigger") || hasParam("AddStaticAbility")
|| hasParam("AddReplacementEffects") || hasParam("RemoveAllAbilities")
|| hasParam("AddReplacementEffect") || hasParam("RemoveAllAbilities")
|| hasParam("RemoveLandTypes")) {
affectedCard.removeChangedCardTraits(getTimestamp(), ability.getId());
}
@@ -275,11 +276,14 @@ public class StaticEffect {
}
affectedCard.removeChangedSVars(getTimestamp(), ability.getId());
// need update for clean reapply
affectedCard.updateKeywordsCache(affectedCard.getCurrentState());
}
if (layers.contains(StaticAbilityLayer.SETPT)) {
if (layers.contains(StaticAbilityLayer.CHARACTERISTIC) || layers.contains(StaticAbilityLayer.SETPT)) {
if (hasParam("SetPower") || hasParam("SetToughness")) {
affectedCard.removeNewPT(getTimestamp(), ability.getId());
affectedCard.removeNewPT(getTimestamp(), ability.getId(), false);
}
}
@@ -311,8 +315,6 @@ public class StaticEffect {
affectedCard.removeCanBlockAdditional(getTimestamp());
}
}
affectedCard.updateAbilityTextForView(); // need to update keyword cache for clean reapply
}
return affectedCards;
}

View File

@@ -180,26 +180,19 @@ public final class AbilityFactory {
}
public static Cost parseAbilityCost(final CardState state, Map<String, String> mapParams, AbilityRecordType type) {
Cost abCost = null;
if (type != AbilityRecordType.SubAbility) {
String cost = mapParams.get("Cost");
if (cost == null) {
if (type == AbilityRecordType.Spell) {
SpellAbility firstAbility = state.getFirstAbility();
if (firstAbility != null && firstAbility.isSpell()) {
// TODO might remove when Enchant Keyword is refactored
System.err.println(state.getName() + " already has Spell using mana cost");
if (type == AbilityRecordType.SubAbility) {
return null;
}
String cost = mapParams.get("Cost");
if (cost != null) {
return new Cost(cost, type == AbilityRecordType.Ability);
}
if (type == AbilityRecordType.Spell) {
// for a Spell if no Cost is used, use the card states ManaCost
abCost = new Cost(state.getManaCost(), false);
return new Cost(state.getManaCost(), false);
} 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,
@@ -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) {
abCost = parseAbilityCost(state, mapParams, type);
}
@@ -510,8 +494,9 @@ public final class AbilityFactory {
AbilityRecordType leftType = AbilityRecordType.getRecordType(leftMap);
ApiType leftApi = leftType.getApiTypeOf(leftMap);
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("Secondary", "True");
CardState rightState = card.getState(CardStateName.RightSplit);
SpellAbility rightAbility = rightState.getFirstAbility();
@@ -526,8 +511,10 @@ public final class AbilityFactory {
totalCost.add(parseAbilityCost(rightState, rightMap, rightType));
final SpellAbility left = getAbility(leftType, leftApi, leftMap, totalCost, leftState, leftState);
left.setOriginalAbility(leftAbility);
left.setCardState(card.getState(CardStateName.Original));
final AbilitySub right = (AbilitySub) getAbility(AbilityRecordType.SubAbility, rightApi, rightMap, null, rightState, rightState);
right.setOriginalAbility(rightAbility);
left.appendSubAbility(right);
return left;
}

View File

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

View File

@@ -14,6 +14,7 @@ import forge.game.*;
import forge.game.ability.AbilityFactory.AbilityRecordType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.IndividualCostPaymentInstance;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana;
@@ -537,6 +538,8 @@ public class AbilityUtils {
val = handlePaid(card.getEmerged(), calcX[1], card, ability);
} else if (calcX[0].startsWith("Crewed")) {
val = handlePaid(card.getCrewedByThisTurn(), calcX[1], card, ability);
} else if (calcX[0].startsWith("ChosenCard")) {
val = handlePaid(card.getChosenCards(), calcX[1], card, ability);
} else if (calcX[0].startsWith("Remembered")) {
// Add whole Remembered list to handlePaid
final CardCollection list = new CardCollection();
@@ -1362,10 +1365,8 @@ public class AbilityUtils {
}
// do blessing there before condition checks
if (source.hasKeyword(Keyword.ASCEND)) {
if (controller.getZone(ZoneType.Battlefield).size() >= 10) {
controller.setBlessing(true);
}
if (source.hasKeyword(Keyword.ASCEND) && controller.getZone(ZoneType.Battlefield).size() >= 10) {
controller.setBlessing(true, source.getSetCode());
}
if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) {
@@ -1620,7 +1621,8 @@ public class AbilityUtils {
final String[] sq;
sq = l[0].split("\\.");
String[] paidparts = l[0].split("\\$", 2);
Iterable<Card> someCards = null;
final Game game = c.getGame();
if (ctb != null) {
@@ -1785,11 +1787,10 @@ public class AbilityUtils {
}
// Count$NumTimesChoseMode
if (sq[0].startsWith("NumTimesChoseMode")) {
SpellAbility sub = sa.getRootAbility();
int amount = 0;
while (sub != null) {
if (sub.getDirectSVars().containsKey("CharmOrder")) amount++;
sub = sub.getSubAbility();
SpellAbility tail = sa.getTailAbility();
if (tail.hasSVar("CharmOrder")) {
amount = tail.getSVarInt("CharmOrder");
}
return doXMath(amount, expr, c, ctb);
}
@@ -1808,27 +1809,25 @@ public class AbilityUtils {
}
if (sq[0].startsWith("LastStateBattlefield")) {
final String[] k = l[0].split(" ");
CardCollectionView list;
final String[] k = paidparts[0].split(" ");
// this is only for spells that were cast
if (sq[0].contains("WithFallback")) {
if (!sa.getHostCard().wasCast()) {
return doXMath(0, expr, c, ctb);
}
list = sa.getHostCard().getCastSA().getLastStateBattlefield();
someCards = sa.getHostCard().getCastSA().getLastStateBattlefield();
} else {
list = sa.getLastStateBattlefield();
someCards = sa.getLastStateBattlefield();
}
if (list == null || list.isEmpty()) {
if (someCards == null || Iterables.isEmpty(someCards)) {
// LastState is Empty
if (sq[0].contains("WithFallback")) {
list = game.getCardsIn(ZoneType.Battlefield);
someCards = game.getCardsIn(ZoneType.Battlefield);
} else {
return doXMath(0, expr, c, ctb);
}
}
list = CardLists.getValidCards(list, k[1], player, c, sa);
return doXMath(list.size(), expr, c, ctb);
someCards = CardLists.getValidCards(someCards, k[1], player, c, sa);
}
if (sq[0].startsWith("LastStateGraveyard")) {
@@ -1855,6 +1854,10 @@ public class AbilityUtils {
return doXMath(list.size(), expr, c, ctb);
}
if (sq[0].equals("ActivatedThisGame")) {
return doXMath(sa.getActivationsThisGame(), expr, c, ctb);
}
if (sq[0].equals("ResolvedThisTurn")) {
return doXMath(sa.getResolvedThisTurn(), expr, c, ctb);
}
@@ -1955,9 +1958,6 @@ public class AbilityUtils {
return doXMath(sum, expr, c, ctb);
}
String[] paidparts = l[0].split("\\$", 2);
Iterable<Card> someCards = null;
// count valid cards in any specified zone/s
if (sq[0].startsWith("Valid")) {
String[] lparts = paidparts[0].split(" ", 2);
@@ -2216,7 +2216,7 @@ public class AbilityUtils {
// Count$IfCastInOwnMainPhase.<numMain>.<numNotMain> // 7/10
if (sq[0].contains("IfCastInOwnMainPhase")) {
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);
}
@@ -2225,6 +2225,11 @@ public class AbilityUtils {
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>
if (sq[0].startsWith("AttachedTo")) {
final String[] k = l[0].split(" ");
@@ -2267,6 +2272,9 @@ public class AbilityUtils {
if (sq[0].equals("Delirium")) {
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")) {
return doXMath(calculateAmount(c, sq[player.getLife() <= 5 ? 1 : 2], ctb), expr, c, ctb);
}
@@ -2316,6 +2324,10 @@ public class AbilityUtils {
return doXMath(player.getNumDrawnLastTurn(), expr, c, ctb);
}
if (sq[0].equals("YouFlipThisTurn")) {
return doXMath(player.getNumFlipsThisTurn(), expr, c, ctb);
}
if (sq[0].equals("YouRollThisTurn")) {
return doXMath(player.getNumRollsThisTurn(), expr, c, ctb);
}
@@ -2401,6 +2413,10 @@ public class AbilityUtils {
return doXMath(player.getMaxOpponentAssignedDamage(), expr, c, ctb);
}
if (sq[0].equals("MaxCombatDamageThisTurn")) {
return doXMath(player.getMaxAssignedCombatDamage(), expr, c, ctb);
}
if (sq[0].contains("TotalDamageThisTurn")) {
String[] props = l[0].split(" ");
int sum = 0;
@@ -2468,7 +2484,6 @@ public class AbilityUtils {
// But these aren't really things you count so they'll show up in properties most likely
}
//Count$TypesSharedWith [defined]
if (sq[0].startsWith("TypesSharedWith")) {
Set<CardType.CoreType> thisTypes = Sets.newHashSet(c.getType().getCoreTypes());
@@ -2698,24 +2713,6 @@ public class AbilityUtils {
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>
if (sq[0].startsWith("Chroma")) {
final CardCollectionView cards;
@@ -2774,16 +2771,6 @@ public class AbilityUtils {
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")) {
return doXMath(game.getPhaseHandler().getTurn(), expr, c, ctb);
}
@@ -2839,7 +2826,13 @@ public class AbilityUtils {
final String[] workingCopy = paidparts[0].split("_");
final String validFilter = workingCopy[1];
// use objectXCount ?
return CardUtil.getThisTurnActivated(validFilter, c, ctb, player).size();
int activated = CardUtil.getThisTurnActivated(validFilter, c, ctb, player).size();
for (IndividualCostPaymentInstance i : game.costPaymentStack) {
if (i.getPayment().getAbility().isValid(validFilter, player, c, ctb)) {
activated++;
}
}
return activated;
}
// Count$ThisTurnEntered <ZoneDestination> [from <ZoneOrigin>] <Valid>
@@ -2923,18 +2916,6 @@ public class AbilityUtils {
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
if (sq[0].startsWith("SumPower")) {
final String[] restrictions = l[0].split("_");
@@ -3419,17 +3400,14 @@ public class AbilityUtils {
return doXMath(numTied, m, source, ctb);
}
final String[] sq;
sq = l[0].split("\\.");
// the number of players passed in
if (sq[0].equals("Amount")) {
if (l[0].equals("Amount")) {
return doXMath(players.size(), m, source, ctb);
}
if (sq[0].startsWith("HasProperty")) {
if (l[0].startsWith("HasProperty")) {
int totPlayer = 0;
String property = sq[0].substring(11);
String property = l[0].substring(11);
for (Player p : players) {
if (p.hasProperty(property, controller, source, ctb)) {
totPlayer++;
@@ -3453,7 +3431,7 @@ public class AbilityUtils {
return doXMath(totPlayer, m, source, ctb);
}
if (sq[0].contains("DamageThisTurn")) {
if (l[0].contains("DamageThisTurn")) {
int totDmg = 0;
for (Player p : players) {
totDmg += p.getAssignedDamage();
@@ -3699,6 +3677,10 @@ public class AbilityUtils {
return doXMath(amount, m, source, ctb);
}
if (value.equals("AttractionsVisitedThisTurn")) {
return doXMath(player.getAttractionsVisitedThisTurn(), m, source, ctb);
}
if (value.startsWith("PlaneswalkedToThisTurn")) {
int found = 0;
String name = value.split(" ")[1];
@@ -3777,6 +3759,10 @@ public class AbilityUtils {
return Aggregates.max(paidList, Card::getCMC);
}
if (string.equals("Colors")) {
return CardUtil.getColorsFromCards(paidList).countColors();
}
if (string.equals("DifferentColorPair")) {
final Set<ColorSet> diffPair = new HashSet<>();
for (final Card card : paidList) {
@@ -3806,10 +3792,25 @@ public class AbilityUtils {
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")) {
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;
Iterable<Card> filteredList = paidList;
final String[] filter = filteredString.split("_");

View File

@@ -59,7 +59,6 @@ public enum ApiType {
Cleanup (CleanUpEffect.class),
Cloak (CloakEffect.class),
Clone (CloneEffect.class),
CompanionChoose (ChooseCompanionEffect.class),
Connive (ConniveEffect.class),
CopyPermanent (CopyPermanentEffect.class),
CopySpellAbility (CopySpellAbilityEffect.class),
@@ -86,12 +85,14 @@ public enum ApiType {
Encode (EncodeEffect.class),
EndCombatPhase (EndCombatPhaseEffect.class),
EndTurn (EndTurnEffect.class),
Endure (EndureEffect.class),
ExchangeLife (LifeExchangeEffect.class),
ExchangeLifeVariant (LifeExchangeVariantEffect.class),
ExchangeControl (ControlExchangeEffect.class),
ExchangeControlVariant (ControlExchangeVariantEffect.class),
ExchangePower (PowerExchangeEffect.class),
ExchangeZone (ZoneExchangeEffect.class),
ExchangeTextBox (TextBoxExchangeEffect.class),
Explore (ExploreEffect.class),
Fight (FightEffect.class),
FlipACoin (FlipCoinEffect.class),
@@ -207,6 +208,7 @@ public enum ApiType {
BlankLine (BlankLineEffect.class),
DamageResolve (DamageResolveEffect.class),
ChangeZoneResolve (ChangeZoneResolveEffect.class),
CompanionChoose (CharmEffect.class),
InternalLegendaryRule (CharmEffect.class),
InternalIgnoreEffect (CharmEffect.class),
InternalRadiation (InternalRadiationEffect.class),

View File

@@ -83,8 +83,8 @@ public abstract class SpellAbilityEffect {
if ("SpellDescription".equalsIgnoreCase(stackDesc)) {
if (params.containsKey("SpellDescription")) {
String rawSDesc = params.get("SpellDescription");
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replaceAll(",,,,,,", " ");
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replaceAll(",,,", " ");
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replace(",,,,,,", " ");
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replace(",,,", " ");
String spellDesc = CardTranslation.translateSingleDescriptionText(rawSDesc, sa.getHostCard());
//trim reminder text from StackDesc
@@ -356,6 +356,7 @@ public abstract class SpellAbilityEffect {
boolean intrinsic = sa.isIntrinsic();
boolean your = location.startsWith("Your");
boolean combat = location.endsWith("Combat");
boolean upkeep = location.endsWith("Upkeep");
String desc = sa.getParamOrDefault("AtEOTDesc", "");
@@ -365,11 +366,16 @@ public abstract class SpellAbilityEffect {
if (combat) {
location = location.substring(0, location.length() - "Combat".length());
}
if (upkeep) {
location = location.substring(0, location.length() - "Upkeep".length());
}
if (desc.isEmpty()) {
StringBuilder sb = new StringBuilder();
if (location.equals("Hand")) {
sb.append("Return ");
} else if (location.equals("Library")) {
sb.append("Shuffle ");
} else if (location.equals("SacrificeCtrl")) {
sb.append("Its controller sacrifices ");
} else {
@@ -378,6 +384,8 @@ public abstract class SpellAbilityEffect {
sb.append(Lang.joinHomogenous(crds));
if (location.equals("Hand")) {
sb.append(" to your hand");
} else if (location.equals("Library")) {
sb.append(" into your library");
}
sb.append(" at the ");
if (combat) {
@@ -385,14 +393,18 @@ public abstract class SpellAbilityEffect {
} else {
sb.append("beginning of ");
sb.append(your ? "your" : "the");
if (upkeep) {
sb.append(" next upkeep.");
} else {
sb.append(" next end step.");
}
}
desc = sb.toString();
}
StringBuilder delTrig = new StringBuilder();
delTrig.append("Mode$ Phase | Phase$ ");
delTrig.append(combat ? "EndCombat " : "End Of Turn ");
delTrig.append(combat ? "EndCombat " : upkeep ? "Upkeep" : "End Of Turn ");
if (your) {
delTrig.append("| ValidPlayer$ You ");
@@ -410,6 +422,8 @@ public abstract class SpellAbilityEffect {
String trigSA = "";
if (location.equals("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")) {
trigSA = "DB$ SacrificeAll | Defined$ DelayTriggerRememberedLKI";
} else if (location.equals("Sacrifice")) {
@@ -588,11 +602,10 @@ public abstract class SpellAbilityEffect {
// create a basic template for Effect to be used somewhere els
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image) {
return createEffect(sa, controller, name, image, controller.getGame().getNextTimestamp());
return createEffect(sa, sa.getHostCard(), controller, name, image, controller.getGame().getNextTimestamp());
}
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image, final long timestamp) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
public static Card createEffect(final SpellAbility sa, final Card hostCard, final Player controller, final String name, final String image, final long timestamp) {
final Game game = controller.getGame();
final Card eff = new Card(game.nextCardId(), game);
eff.setGameTimestamp(timestamp);
@@ -608,12 +621,7 @@ public abstract class SpellAbilityEffect {
eff.setRarity(hostCard.getRarity());
}
if (sa.hasParam("Boon")) {
eff.setBoon(true);
}
eff.setOwner(controller);
eff.setSVars(sa.getSVars());
eff.setSetCode(hostCard.getSetCode());
if (image != null) {
@@ -621,7 +629,12 @@ public abstract class SpellAbilityEffect {
}
eff.setGamePieceType(GamePieceType.EFFECT);
if (sa != null) {
eff.setEffectSource(sa);
eff.setSVars(sa.getSVars());
} else {
eff.setEffectSource(hostCard);
}
return eff;
}
@@ -768,7 +781,11 @@ public abstract class SpellAbilityEffect {
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 Game game = hostCard.getGame();
hostCard.addUntilLeavesBattlefield(triggerList.allCards());
@@ -783,7 +800,7 @@ public abstract class SpellAbilityEffect {
lki = null;
}
return new GameCommand() {
GameCommand gc = new GameCommand() {
private static final long serialVersionUID = 1L;
@@ -838,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) {
@@ -898,6 +922,8 @@ public abstract class SpellAbilityEffect {
} else {
game.getUpkeep().addUntilEnd(controller, until);
}
} else if ("UntilTheEndOfYourNextUntap".equals(duration)) {
game.getUntap().addUntilEnd(controller, until);
} else if ("UntilNextEndStep".equals(duration)) {
game.getEndOfTurn().addAt(until);
} else if ("UntilYourNextEndStep".equals(duration)) {
@@ -998,8 +1024,9 @@ public abstract class SpellAbilityEffect {
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
final Player activator = sa.getActivatingPlayer();
final PlayerCollection options;
if (loser.isOpponentOf(activator)) {
options = activator.getOpponents();
@@ -1041,7 +1068,9 @@ public abstract class SpellAbilityEffect {
exilingSource = cause.getOriginalHost();
}
movedCard.setExiledWith(exilingSource);
movedCard.setExiledBy(cause.getActivatingPlayer());
Player exiler = cause.hasParam("DefinedExiler") ?
getDefinedPlayersOrTargeted(cause, "DefinedExiler").get(0) : cause.getActivatingPlayer();
movedCard.setExiledBy(exiler);
}
public static GameCommand exileEffectCommand(final Game game, final Card effect) {

View File

@@ -25,7 +25,7 @@ public class AddPhaseEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
final Player activator = sa.getActivatingPlayer();
boolean isTopsy = activator.getAmountOfKeyword("The phases of your turn are reversed.") % 2 == 1;
boolean isTopsy = activator.isPhasesReversed();
PhaseHandler phaseHandler = activator.getGame().getPhaseHandler();
PhaseType currentPhase = phaseHandler.getPhase();

View File

@@ -7,6 +7,8 @@ import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.phase.ExtraTurn;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
@@ -72,11 +74,12 @@ public class AddTurnEffect extends SpellAbilityEffect {
final Card eff = createEffect(sa, sa.getActivatingPlayer(), name, image);
String stEffect = "Mode$ CantSetSchemesInMotion | EffectZone$ Command | Description$ Schemes can't be set in Motion";
eff.addStaticAbility(stEffect);
String strRe = "Event$ SetInMotion | EffectZone$ Command | Layer$ CantHappen | Description$ Schemes can't be set in Motion";
ReplacementEffect re = ReplacementHandler.parseReplacement(strRe, eff, true);
eff.addReplacementEffect(re);
game.getAction().moveToCommand(eff, sa);
game.getEndOfTurn().addUntil(exileEffectCommand(game, eff));
}
}

View File

@@ -8,13 +8,13 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.event.GameEventCardPlotted;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.util.Lang;
import forge.util.TextUtil;
public class AlterAttributeEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
@@ -48,6 +48,8 @@ public class AlterAttributeEffect extends SpellAbilityEffect {
switch (attr.trim()) {
case "Plotted":
altered = gameCard.setPlotted(activate);
c.getGame().fireEvent(new GameEventCardPlotted(c, sa.getActivatingPlayer()));
break;
case "Solve":
case "Solved":

View File

@@ -202,7 +202,7 @@ public class AnimateEffect extends AnimateEffectBase {
if (sa.isCrew()) {
gameCard.becomesCrewed(sa);
gameCard.updatePowerToughnessForView();
gameCard.updatePTforView();
}
game.fireEvent(new GameEventCardStatsChanged(gameCard));

View File

@@ -121,6 +121,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
if (perpetual) {
Map <String, Object> params = new HashMap<>();
params.put("AddKeywords", keywords);
params.put("RemoveKeywords", removeKeywords);
params.put("RemoveAll", removeAll);
params.put("Timestamp", timestamp);
params.put("Category", "Keywords");
@@ -164,14 +165,8 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
// remove abilities
final List<SpellAbility> removedAbilities = Lists.newArrayList();
boolean clearSpells = sa.hasParam("OverwriteSpells");
if (clearSpells) {
removedAbilities.addAll(Lists.newArrayList(c.getSpells()));
}
if (sa.hasParam("RemoveThisAbility") && !removedAbilities.contains(sa)) {
removedAbilities.add(sa);
if (sa.hasParam("RemoveThisAbility")) {
removedAbilities.add(sa.getOriginalAbility());
}
// give abilities
@@ -251,9 +246,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
}
if (!"Permanent".equals(duration) && !perpetual) {
if ("UntilControllerNextUntap".equals(duration)) {
game.getUntap().addUntil(c.getController(), unanimate);
} else if ("UntilAnimatedFaceup".equals(duration)) {
if ("UntilAnimatedFaceup".equals(duration)) {
c.addFaceupCommand(unanimate);
} else {
addUntilCommand(sa, unanimate);

View File

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

View File

@@ -1,6 +1,5 @@
package forge.game.ability.effects;
import java.util.List;
import java.util.Map;

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