Compare commits

..

730 Commits

Author SHA1 Message Date
Chris H
35edd16a7e Limit edition range for Adventure drafts 2025-10-11 23:04:39 -04:00
kevlahnota
31c2686307 Merge pull request #8852 from kevlahnota/master4
prevent NPE
2025-10-07 09:48:10 +08:00
Anthony Calosa
04f454ae83 prevent NPE 2025-10-07 05:32:41 +08:00
tool4ever
5b2a56bd4d Update the_rise_of_sozin_fire_lord_sozin.txt 2025-10-06 17:52:18 +00:00
Eradev
5bbc86c1a1 Fix crash in Quest rewards screen 2025-10-06 07:41:52 -04:00
Eradev
a5765905d8 Fix Quest start with boosters and unsactionned pool 2025-10-06 07:41:52 -04:00
tool4ever
c4a3b5c545 Update avatar_aang_aang_master_of_elements.txt 2025-10-06 13:01:37 +02:00
tool4ever
81e1306cd7 Update garland_royal_kidnapper.txt 2025-10-05 18:31:42 +00:00
Hans Mackowiak
39f6377f33 remove leftover variable 2025-10-05 19:12:38 +02:00
Hans Mackowiak
0af4be42c4 DungeonsCompleted with handlePaid (#8842) 2025-10-05 18:31:12 +02:00
Agetian
da8b77acf7 Add AtOppEOT logic to a couple lands that the AI misuses (#8839)
* - AtOppEOT is automatic now (also tweak the FlipACoinAi template to make use of it)

* - AILogic$ Never is now generic.
2025-10-05 16:21:15 +00:00
Hans Mackowiak
4cef049a1d AbilityUtils: move SumPower to handlePaid (#8838)
* AbilityUtils: move SumPower to handlePaid

* Replace SumPower with CardPower

* Replace SumToughness with CardToughness
2025-10-05 11:27:21 +02:00
tool4ever
8cd386b654 Update ChangeZoneAi.java 2025-10-05 08:39:51 +00:00
tool4ever
c1e87334cd Some cleanup (#8833) 2025-10-04 20:31:47 +00:00
Jetz72
3ae3e1b218 Merge pull request #8828 from Eradev/FixDivZero
Fix Div by zero error for collection percentage.
2025-10-04 09:37:24 -04:00
Hans Mackowiak
7acfe6c240 Different names rework (#8823)
* turn CardChangedName into record

* Add CardLists.getDifferentNamesCount

* CostDiscard: rename to +WithDifferentNames

* CostSacrifice: use getDifferentNamesCount

* Refactor DifferentCardNames_
2025-10-04 15:36:17 +02:00
tool4ever
66a8e1f04f Fix Henzie + Gorex (#8829) 2025-10-04 11:30:21 +00:00
Eradev
091c61829b Fix Div by zero error 2025-10-04 03:56:40 -04:00
kevlahnota
a2b324c9ce prevent NPE VAvatar.java 2025-10-04 07:50:11 +08:00
Paul Hammerton
4ab8c1414c Merge pull request #8827 from paulsnoops/fix-sld
Edition updates: SLD
2025-10-03 21:29:03 +01:00
Paul Hammerton
46df4cdb0d Edition updates: SLD 2025-10-03 21:28:30 +01:00
tool4ever
183100fbdc Update soul_collector.txt 2025-10-03 20:16:57 +00:00
squee1968
50c7188e04 Update chocobo_camp.txt 2025-10-03 21:17:34 +02:00
Chris H
d46d7300b4 Reduce cost of Bird creature token spell 2025-10-02 23:40:28 -04:00
tool4ever
27a0a8f791 Update the_spot_living_portal.txt
Closes #8819
2025-10-02 19:48:54 +00:00
Paul Hammerton
5a63250735 Merge pull request #8817 from paulsnoops/edition-updates
Edition updates: ECL, FIC, FIN, SLD, SLP
2025-10-01 18:13:23 +01:00
Paul Hammerton
04fbe9fac3 Edition updates: ECL, FIC, FIN, SLD, SLP 2025-10-01 18:12:16 +01:00
tool4ever
31f8da5687 Some fixes (#8816) 2025-10-01 14:54:46 +00:00
Hans Mackowiak
c0fe93ee30 Update the_destined_thief.txt
fix DBDraw SVars
2025-10-01 11:39:31 +02:00
Simisays
a1042e0a1d Update jin_sakai_ghost_of_tsushima.txt (#8815) 2025-09-30 19:24:51 +00:00
Hans Mackowiak
cdbf10f1f7 Change type desc (#8809)
* add ChangeTypeDesc for basic land

* Search N is up to

* add some more ChangeTypeDesc

* add some more ChangeTypeDesc

* add some more ChangeTypeDesc

* Update panorama

* update landscape

* Update Clan Monuments

* Small Monument to Perfection change

* update capenna fetchers

* remove ChangeNum from basic Land searchers

* better desc for fetch lands

* remove uneeded ChangeTypeDesc
2025-09-30 20:43:42 +02:00
Simisays
bc6eed5c7d Update joel_resolute_survivor.txt (#8814) 2025-09-30 17:32:21 +00:00
Simisays
b90c14660c FIC 12 Panorama/Holiday box cards (#8803) 2025-09-30 14:22:24 +00:00
Renato Filipe Vidal Santos
eeac0272ab YEOE: 7 cards (#8476) 2025-09-30 06:42:50 +00:00
Renato Filipe Vidal Santos
dcdf2b6e7a YEOE: 4 cards, batch 3 (#8499) 2025-09-30 06:35:08 +00:00
Renato Filipe Vidal Santos
dab01c9b86 YEOE: 4 cards, batch 2 (#8451) 2025-09-30 05:48:49 +00:00
Fulgur14
74dc09b2e5 12 FIC Scene cards + Formidable Speaker (#8808) 2025-09-29 18:07:43 +00:00
Hans Mackowiak
8dc9a18c4f Card: addColor use internal Record (#8793) 2025-09-29 16:31:21 +02:00
Hans Mackowiak
a390284b8e Property cleanup (#8804)
* remove NotTriggeredCard

* remove nonManaAbility

* remove notattacking and notblocking

* remove notnamed
2025-09-28 21:55:25 +02:00
Hans Mackowiak
1d97a4ec7e Update eirdu_carrier_of_dawn_isilu_carrier_of_twilight.txt 2025-09-28 21:20:22 +02:00
Simisays
a21a61cb05 SLD 11 cards + support for new partner variants (#8797) 2025-09-28 19:07:54 +00:00
Simisays
cd84663bed [ECL] 10 cards (#8794) 2025-09-28 13:55:52 +00:00
Hans Mackowiak
4d2b634e4f cantBeEnchantedByMsg and cantAttach StaticAbility (#8772)
* cantBeEnchantedByMsg and cantAttach StaticAbility

* finish cantBeAttachedMsg
2025-09-28 15:35:19 +02:00
tool4ever
4eccfa8177 Restore fixes (#8800) 2025-09-28 07:09:47 +00:00
Paul Hammerton
90ea109a17 Merge pull request #8795 from paulsnoops/edition-updates
Edition updates: ECL, OLGC, OVNT, SLD
2025-09-27 10:42:39 +01:00
Paul Hammerton
a9c42fb340 Edition updates: ECL, OLGC, OVNT, SLD 2025-09-27 10:40:42 +01:00
kevlahnota
f1a095fb56 Merge pull request #8792 from kevlahnota/master4
add breadcrumb to GameAction moveTo
2025-09-27 08:16:02 +08:00
Anthony Calosa
4ed06fae87 add breadcrumb to GameAction moveTo 2025-09-27 08:08:53 +08:00
tool4ever
0d020070a2 Fix tapped cost parts blocking real payment (#8789) 2025-09-26 23:20:04 +00:00
Paul Hammerton
5d39efd9fc Merge pull request #8787 from paulsnoops/edition-updates
Edition updates: AA3, AA4, ECL, MAR, PMEI, SLD, TLE
2025-09-26 17:47:55 +01:00
Paul Hammerton
fd5555fdd6 Edition updates: AA3, AA4, ECL, MAR, PMEI, SLD, TLE 2025-09-26 17:46:48 +01:00
Hans Mackowiak
6dd10be044 Update wild_pack_squad.txt
Closes #8786
2025-09-26 17:00:33 +02:00
Agetian
1b419a13e8 - Fix isWebSligned in Card.
- Add puzzleds PS_SPM1 and PS_TLA1.
2025-09-26 16:07:20 +02:00
Chris H
c0f5615885 Manually fixing version touches 2025-09-26 09:08:49 -04:00
tool4ever
c10b5706f1 Some fixes (#8779) 2025-09-26 08:48:22 +02:00
kevlahnota
9599d01d2c Merge pull request #8780 from kevlahnota/master4
fix Vertical layout for VManaPool
2025-09-26 08:32:51 +08:00
Anthony Calosa
f11c2ad8a4 fix Vertical layout for VManaPool 2025-09-26 08:29:07 +08:00
kevlahnota
20479518da Merge pull request #8777 from kevlahnota/master4
fix removing item by name
2025-09-25 06:57:09 +08:00
Anthony Calosa
1ded8fe5da fix removing item by name 2025-09-25 06:51:02 +08:00
Hans Mackowiak
2d1fe88d32 FSkinProp: unify Enum to SkinProp places (#8764)
* FSkinProp: unify Enum to SkinProp places

* FSkinProp: add iconFromDeckSection
2025-09-24 19:52:58 +02:00
Paul Hammerton
7ca0c2b87b Merge pull request #8776 from paulsnoops/archived-formats-update
Format updates: AA3, AA4
2025-09-24 17:53:50 +01:00
Paul Hammerton
e4a83de6cf Format updates: AA3, AA4 2025-09-24 17:52:33 +01:00
Paul Hammerton
7321aac496 Merge pull request #8774 from paulsnoops/edition-updates
Edition updates: AA3, AA4, ECL, SLD
2025-09-24 09:24:00 +01:00
Paul Hammerton
28dcdf6ddf Update Secret Lair Drop Series.txt 2025-09-24 09:21:44 +01:00
Paul Hammerton
5999909d32 Create Arena Anthology 4.txt 2025-09-24 09:19:15 +01:00
Paul Hammerton
0036c27470 Create Arena Anthology 3.txt 2025-09-24 08:50:46 +01:00
Paul Hammerton
7325443da5 Create Lorwyn Eclipsed.txt 2025-09-24 08:29:33 +01:00
kevlahnota
c4c1388f8b Merge pull request #8773 from kevlahnota/master4
fix NoSuchMethodError on older Android
2025-09-24 13:57:46 +08:00
Anthony Calosa
f4e90e1d38 fix NoSuchMethodError on older Android
- closes #8771
- closes #8770
2025-09-24 13:47:12 +08:00
Renato Filipe Vidal Santos
ab3b00cb65 Cleanup 2025-09-23 2025-09-23 16:26:25 +00:00
kevlahnota
a6cbbeef84 Merge pull request #8767 from kevlahnota/master4
update Sentry, update oshi, update android-all, npe prevention
2025-09-23 19:17:46 +08:00
Anthony Calosa
40629a72a9 update Sentry, update oshi, update android-all, npe prevention 2025-09-23 19:09:15 +08:00
squee1968
f6bc6d6742 Fix Lady Octopus, Inspired Inventor (#8766)
* Fix Lady Octopus, Inspired Inventor
2025-09-23 10:00:33 +02:00
Hans Mackowiak
da52bc17e8 GameEventAttachment: flip oldTarget and newTarget param (#8761)
* GameEventAttachment: flip oldTarget and newTarget param

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-09-22 16:16:57 +02:00
Hans Mackowiak
50b7543933 ColorEnum: use ITranslatable interface 2025-09-22 16:14:21 +02:00
Chris H
a4378a20e5 Manually fixing version touches 2025-09-21 20:58:46 -04:00
GitHub Actions
a6395373f7 Restore POM files for preparation of next release 2025-09-21 20:58:46 -04:00
GitHub Actions
54f41a28cd [maven-release-plugin] prepare for next development iteration 2025-09-21 20:58:46 -04:00
GitHub Actions
a2c798ba86 [maven-release-plugin] prepare release forge-2.0.06 2025-09-21 20:58:46 -04:00
Chris H
8f7a77354a Update apt-get so latest libxml2-utils is available 2025-09-21 20:58:46 -04:00
kevlahnota
b50cdea5df Update drop_tower.txt 2025-09-22 06:37:26 +08:00
Renato Filipe Vidal Santos
a7568b0845 Cleanup 2025-09-21 2025-09-21 07:30:43 +00:00
kevlahnota
e158b5966e Merge pull request #8755 from kevlahnota/master4
use Supplier for FModel for thread safety
2025-09-21 09:01:18 +08:00
kevlahnota
bea5ffb0a8 Update FModel.java
unused var
2025-09-21 08:51:30 +08:00
Anthony Calosa
ac492fac35 use Supplier for FModel for thread safety 2025-09-21 08:47:28 +08:00
Renato Filipe Vidal Santos
35e6268a95 Add files via upload (#8754) 2025-09-20 19:58:28 +03:00
Hans Mackowiak
90bd0c73d0 Transform: all DFC can transform now 2025-09-20 18:11:39 +02:00
kevlahnota
ea293a46b1 Merge pull request #8750 from kevlahnota/master4
update FModel, add Icon Overlay for Horizontal tabs
2025-09-19 22:58:47 +08:00
Anthony Calosa
6ec6d64cb2 update FModel, add Icon Overlay for Horizontal tabs 2025-09-19 22:06:38 +08:00
Chris H
9bc6ab747d Update Venom, Eddie Brock's power and toughness 2025-09-19 09:22:54 -04:00
kevlahnota
a72df6ad16 updateRarityFilterOdds (#8746) 2025-09-19 07:19:50 +02:00
Renato Filipe Vidal Santos
1105b3fcbd Add files via upload 2025-09-19 07:14:55 +02:00
kevlahnota
08239def86 Merge pull request #8745 from kevlahnota/master4
fix gap and visibility on horizontal layout
2025-09-19 10:39:52 +08:00
Anthony Calosa
cb7fc3df4e fix gap and visibility on horizontal layout 2025-09-19 10:32:18 +08:00
tool4EvEr
9b8020bb30 Fix copied SA using old replacingObjects 2025-09-18 21:20:21 +02:00
Eradev
398917d010 Support named abilities (#8464)
* Support named abilities

* Check if spell was cast from a named ability
2025-09-18 20:15:07 +03:00
tool4ever
10a68c27d5 Update omarthis_ghostfire_initiate.txt 2025-09-18 16:36:25 +00:00
kevlahnota
ba4ee98c3b Merge pull request #8741 from kevlahnota/master4
remove duplicated code and minor format
2025-09-18 19:44:29 +08:00
Anthony Calosa
c2c3add52b update switch 2025-09-18 19:35:23 +08:00
Anthony Calosa
1c7bc1c0d3 remove duplicated code and minor format 2025-09-18 19:27:55 +08:00
tool4EvEr
14f146f8d0 Remove TargetsFromSingleZone 2025-09-18 08:02:15 +02:00
kevlahnota
0cab03b96c Update VManaPool.java 2025-09-18 06:37:45 +08:00
tool4ever
8f5f2059a2 Fix Self-Destruct (#8730) 2025-09-17 18:58:40 +00:00
Hans Mackowiak
34323107c9 AetherFilter: use Predicate for Caption (#8724)
* AetherFilter: use Predicate for Caption
2025-09-17 18:00:08 +02:00
kevlahnota
dbf9568866 Merge pull request #8726 from kevlahnota/master4
update setlookup files
2025-09-17 14:13:38 +08:00
Anthony Calosa
7933893dcb update setlookup files
- closes #8721
2025-09-17 14:08:54 +08:00
kevlahnota
415e2c12e1 Merge pull request #8725 from kevlahnota/master4
fix tab display area size on Horizontal layout
2025-09-17 12:28:25 +08:00
Anthony Calosa
930019db40 fix tab display area size on Horizontal layout 2025-09-17 12:24:35 +08:00
tool4ever
72139b523c Improve AI timeout detection (#8723) 2025-09-16 18:04:00 +00:00
Hans Mackowiak
6cf2f20cdc ColorSet: store EnumSet and toString as Final (#8720) 2025-09-16 16:32:16 +02:00
kevlahnota
cebddb7f4b Update damage_control_crew.txt 2025-09-16 15:38:33 +08:00
kevlahnota
4a6275d282 Merge pull request #8710 from Card-Forge/enhance-prize-visiblity
Only show event cost before you sign up for the event
2025-09-16 15:15:58 +08:00
kevlahnota
bdeaf8cef1 Merge pull request #8719 from kevlahnota/master4
update AltZoneLayout VCardDisplayArea for Landscape
2025-09-16 12:11:22 +08:00
Anthony Calosa
40f6a5b472 update 2025-09-16 12:03:18 +08:00
Anthony Calosa
f4bff30680 update AltZoneLayout VCardDisplayArea for Landscape
- Off (Default Layout)
- Vertical (Alt Layout with old Vertical Scroll Card Display Area)
- Horizontal (Alt Layout with Horizontal Scroll Card Display Area)
- fix mouse scroll to scroll horizontall while ShiftKey is pressed
2025-09-16 11:50:15 +08:00
marthinwurer
5f32c23dc5 moved stopwatch to a class field 2025-09-15 14:57:16 -04:00
marthinwurer
ae7159297b made simulation test extend ai test 2025-09-15 14:57:16 -04:00
marthinwurer
cedfa68c16 removed unused imports 2025-09-15 14:57:16 -04:00
marthinwurer
1f235c5e71 remove todos 2025-09-15 14:57:16 -04:00
marthinwurer
9f8bb8ae4d added integration test 2025-09-15 14:57:16 -04:00
marthinwurer
cd3a26e434 break out main loop step 2025-09-15 14:57:16 -04:00
marthinwurer
d2d6b7bc53 break out main game loop function 2025-09-15 14:57:16 -04:00
marthinwurer
052e72d8ea break out first turn setup 2025-09-15 14:57:16 -04:00
marthinwurer
b902d516a7 Added todos for known problems 2025-09-15 14:57:16 -04:00
tool4ever
79fd4a3f8d Fix crash after removing target when copying divided spell (#8714)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-09-15 14:27:21 +02:00
Hans Mackowiak
10d359e7d7 Update MagicColor.Color enum (#8715)
Adds ShortName and Symbol
2025-09-15 14:12:46 +02:00
kevlahnota
1cb9e78b21 Merge pull request #8712 from kevlahnota/master4
Prevent NPE
2025-09-15 10:30:31 +08:00
Anthony Calosa
d999c5e213 Prevent NPE 2025-09-15 09:48:45 +08:00
tool4ever
1f7862c970 Update superior_foes_of_spider_man.txt
Closes #8711
2025-09-14 19:26:44 +00:00
Chris H
cdc696c506 Only show event cost before you sign up for the event 2025-09-14 11:13:29 -04:00
Chris H
64897f1ab4 Simplify logic 2025-09-14 10:42:19 -04:00
Chris H
905cf52c5e Disable 4 player drafts for Quest and Adventure until the UIs can handle it 2025-09-14 10:42:19 -04:00
kevlahnota
3139d593a3 Merge pull request #8708 from kevlahnota/master4
throw RuntimeException message for BoosterGenerator makesheet
2025-09-14 08:49:04 +08:00
Anthony Calosa
8f71a5b06e throw RuntimeException message for BoosterGenerator makesheet 2025-09-14 08:44:10 +08:00
kevlahnota
e9742a2292 Merge pull request #8707 from kevlahnota/master4
use player or geneticAI deck for missing decks
2025-09-14 08:17:11 +08:00
Anthony Calosa
a6c893693d remove duplicated code 2025-09-14 08:14:28 +08:00
Anthony Calosa
327ee6853f use player or geneticAI deck for missing decks 2025-09-14 08:04:22 +08:00
kevlahnota
3e7f98db6a Merge pull request #8706 from kevlahnota/master4
Different effect card for Emrakul Castle difficulty
2025-09-13 21:19:19 +08:00
Anthony Calosa
e49021ffdd Different effect card for Emrakul Castle difficulty 2025-09-13 21:13:26 +08:00
Chris H
0c5ff17e94 Fix typo for SPM draft 2025-09-12 23:36:36 -04:00
kevlahnota
2925e94cec Merge pull request #8703 from kevlahnota/master4
fix crash Android 8-13 NoSuchMethodError, prevent crash for missing card
2025-09-13 09:14:09 +08:00
Anthony Calosa
02b7e408dc fix crash Android 8-13 NoSuchMethodError, prevent crash for missing card 2025-09-13 09:09:04 +08:00
Anthony Calosa
b61044abb5 prevent crash for missing card, fix crash Android 8-13 NoSuchMethodError 2025-09-13 08:57:29 +08:00
kevlahnota
f1114eeada Merge pull request #8700 from kevlahnota/master4
Android crash fix
2025-09-12 10:15:15 +08:00
Anthony Calosa
b131e4eb04 Android crash fix 2025-09-12 10:11:34 +08:00
kevlahnota
8ba2ae4a24 Merge pull request #8699 from kevlahnota/master4
remove duplicated code for keyword search
2025-09-12 09:55:22 +08:00
Anthony Calosa
ac93233e19 remove duplicated code for keyword search 2025-09-12 09:38:40 +08:00
kevlahnota
6b29e18a58 Merge pull request #8698 from kevlahnota/master4
update Keyword get method
2025-09-12 05:17:38 +08:00
Anthony Calosa
fd2d3c7bb9 update Keyword get method 2025-09-12 05:09:55 +08:00
Paul Hammerton
a1d67cf844 Merge pull request #8697 from paulsnoops/edition-updates
Edition updates: OMB
2025-09-11 17:44:02 +01:00
Paul Hammerton
2b64f82f98 Edition updates: OMB 2025-09-11 17:42:20 +01:00
Jetz72
43a1570601 Deck Importer support for Adventure, Quest, and Planar Conquest (#8681)
* Some cleanup.

* Expanded/fixed basic land set functions for quest and adventure.

* Get land sets from unlocked planes in conquest mode.

* Add importer for Adventure, Quest, and Conquest.

* Remove unused import

* Remove redundant override

* Deprecate hasBasicLands predicate.

* Delete getManaNameAndSymbol

---------

Co-authored-by: Jetz <Jetz722@gmail.com>
2025-09-11 19:22:17 +03:00
Agetian
db8e084332 - Add puzzle PS_EOE3. (#8696) 2025-09-11 19:21:51 +03:00
kevlahnota
c2fc877201 minor changes to Emrakul Boss Deck
replaced some cards that AI can't use properly
2025-09-11 19:45:39 +08:00
kevlahnota
50f34442f6 Merge pull request #8694 from kevlahnota/master4
fix Keyword Advanced Search
2025-09-11 19:04:51 +08:00
Anthony Calosa
1dbd358c19 fix to include main face 2025-09-11 19:01:59 +08:00
Anthony Calosa
bc3faa88c4 fix Keyword Advanced Search 2025-09-11 18:55:44 +08:00
kevlahnota
10618d4f31 Merge pull request #8693 from kevlahnota/master4
update Emrakul Deck, losing to boss fight breaks random equipment
2025-09-11 16:10:10 +08:00
Anthony Calosa
47e873b4e9 update Emrakul Deck, losing to boss fight breaks random equipment 2025-09-11 16:03:20 +08:00
Chris H
acc9ead5b2 Spm new set (#8687)
* Add draft info for Spider-man
2025-09-11 06:02:40 +02:00
Chris H
6125022c9e Migrate SPM (and some other early spoilers) 2025-09-10 19:08:22 -04:00
Hans Mackowiak
a0be5e25cb CardEdition: fix tokenCN with ☇ 2025-09-10 22:10:00 +02:00
Hans Mackowiak
f2e3cdc111 fix c_1_1_myr to c_1_1_a_myr 2025-09-10 22:02:38 +02:00
Hans Mackowiak
7384aada40 Update pictures_of_spider_man.txt 2025-09-10 06:34:11 +02:00
Eradev
1a8a4f63ae Update Recruit Instructor 2025-09-10 06:31:01 +02:00
Eradev
530d1efcd8 Fix tokens and some images ID (#8684)
* Fig hags ID

* Double sided tokens should now be supported

* Fix zombie ID
2025-09-10 06:30:15 +02:00
tool4ever
d8b1d76f42 Update silk_web_weaver.txt 2025-09-09 21:40:24 +00:00
kevlahnota
830b145e88 Merge pull request #8680 from kevlahnota/master4
update android hwinfo
2025-09-09 22:52:21 +08:00
Anthony Calosa
51ed06d60a update android hwinfo 2025-09-09 22:36:09 +08:00
Eradev
ac9b73935f Tokens cleanup (F-G sets) (#8049)
* Update FEM

* Update PIP

* Update FRF

* Update 5DN

* Update 5ED

* Update FDN

* Update 4ED

* Update F12

* Update V15

* Update V14

* Update DRB

* Update V11

* Update V16

* Update V12

* Update V10

* Update V17

* Update FUT

* Update F17

* Update GDY

* Update GNT

* Update GN2

* Update GTC

* Update GS1

* Update PGPX

* Update GPT

* Update GK1

* Update GRN

* Update GK1

* Added old numbers back for now.

* Rename Fallout set
2025-09-09 13:35:19 +02:00
Eradev
fe2f49fbcc Tokens cleanup (D-E sets) (#8046)
* Update DKA

* Update DST

* Update DIS

* Update WHO

* Update DMR

* Update DMU

* Update DOM

* Update 2X2

* Update 2XM

* Update DGM

* Update DTK

* Update DS0

* Update DDQ

* Update DDC

* Update DDO

* Update DDU

* Update DDT

* Update DDS

* Update TD2

* Update DDN

* Update DDP

* Update DPA

* Update AFR

* Update DSC

* Update P8ED

* Update 8ED

* Update EMN

* Update EMA

* Update EVE

* Update EXO

* Update E02

* Fixed emblem name

* Add u_2_1_a_myr back

* Add b_0_0_germ back

* Update NPH
2025-09-09 13:24:40 +02:00
Eradev
3d31494a1b Tokens cleanup (N-P sets) (#8056)
* Update NMS

* Update NPH

* Update 9ED

* Update OGW

* Update ODY

* Update ONS

* Update PHEL

* Update OTJ

* Update ONE

* Update PLC

* Update PC2

* Re-add planes

* Update HOP

* Update PLS

* Update PCA

* Update PWCS

* Update PCY

* Update PD3
2025-09-09 13:15:57 +02:00
Eradev
ff42e730a2 Tokens cleanup (R-S sets) (#8138)
* Update SOK

* Update SCG

* Update ALA

* Update SOM

* Update SHM

* Update SOI

* Update SCD

* Update SNC

* Update STX

* Update STH

* Update PSVC

* Update RNA

* Update RAV

* Update RTR

* Update REV

* Update ROE

* Update PRIX

* Update RIX

* Fix RNA
2025-09-09 13:00:12 +02:00
Paul Hammerton
a66fb8791c Merge pull request #8678 from paulsnoops/sld-update
Edition updates: OM1, SLD
2025-09-09 10:49:43 +01:00
Paul Hammerton
2ad5d20e83 Edition updates: OM1, SLD 2025-09-09 10:45:32 +01:00
Hans Mackowiak
4790394698 Update miles_morales_ultimate_spider_man.txt
Fix typo
2025-09-09 08:42:16 +02:00
Eradev
8b1c427809 Fix crash when equipping 2025-09-09 05:42:46 +02:00
tool4ever
c0b667c373 Some clean up (#8673) 2025-09-08 20:40:03 +00:00
kevlahnota
ca61627f5b Merge pull request #8671 from kevlahnota/master4
restore backup save for errors
2025-09-08 19:28:52 +08:00
Anthony Calosa
c3e2a5b5ea update 2025-09-08 19:23:58 +08:00
Anthony Calosa
0d26b499d3 restore backup save for errors 2025-09-08 19:17:51 +08:00
kevlahnota
bbbc5e0ee6 Merge pull request #8669 from kevlahnota/master4
return extra info for save errors
2025-09-08 15:46:43 +08:00
Anthony Calosa
714d9442f1 return extra info for save errors 2025-09-08 15:41:16 +08:00
tool4ever
d199765e6d Update coal_hill_school.txt 2025-09-08 08:47:27 +02:00
kevlahnota
e5156d8999 Merge pull request #8668 from kevlahnota/master4
revert save read
2025-09-08 14:34:46 +08:00
Anthony Calosa
c0d5397541 revert save read
- todo find a better way to inform player with save error
2025-09-08 14:34:10 +08:00
kevlahnota
444897d0f9 Update lizard_connorss_curse.txt 2025-09-08 14:20:43 +08:00
kevlahnota
f2cb7956d3 Merge pull request #8667 from kevlahnota/master4
announce savefile error
2025-09-08 12:10:10 +08:00
Anthony Calosa
e0c2a49c6b don't write data with errors 2025-09-08 12:06:23 +08:00
Anthony Calosa
d64e5ebc13 announce savefile error 2025-09-08 12:01:32 +08:00
xanxer6rB
9bea1bc717 Update the_reaver_cleaver.txt (#8661) 2025-09-07 12:43:19 +00:00
Hans Mackowiak
bdf9573467 CantTarget: fix AffectedZone not for Players 2025-09-07 11:25:35 +02:00
Hans Mackowiak
042eb4bf79 CantTarget: return the StaticAbility 2025-09-07 10:35:05 +02:00
Hans Mackowiak
4b0337a836 remove unneeded Protection flag 2025-09-07 09:44:22 +02:00
kevlahnota
9284775921 update ImageView group, piles
should fix concurrentmodification and npe prevention
2025-09-07 13:54:52 +08:00
tool4ever
1671a3298e Fix AI sometimes tapping mana sources for 0 (#8659) 2025-09-06 14:18:36 +00:00
kevlahnota
64e3193a80 Merge pull request #8660 from kevlahnota/master4
update VSubmenuSealed panel
2025-09-06 21:35:57 +08:00
Anthony Calosa
3e6cd92f14 update VSubmenuSealed panel
- closes #8658
2025-09-06 21:32:17 +08:00
Paul Hammerton
b7d4e0129e Merge pull request #8657 from paulsnoops/edition-updates
Edition updates: MAR, OMB, PSPM, SPM
2025-09-06 10:48:18 +01:00
Paul Hammerton
f76127dcea Edition updates: MAR, OMB, PSPM, SPM 2025-09-06 10:33:25 +01:00
Paul Hammerton
c7c0938936 Merge pull request #8656 from paulsnoops/spm-formats
SPM Format updates (+ MAR, OM1, OMB, SPE)
2025-09-06 10:20:18 +01:00
Paul Hammerton
9788a815a7 SPM Format updates (+ MAR, OM1, OMB, SPE) 2025-09-06 10:17:33 +01:00
Matija Huremović
cce25a37c4 Added the -c (clock) flag to determine the maximum time in seconds before the sim execution calls the match a draw. (#8202) 2025-09-06 09:04:59 +00:00
Hans Mackowiak
85812d819f Mayhem: add Reminder for Land with no Cost (#8654)
* Mayhem: add Reminder for Land with no Cost
2025-09-06 11:02:15 +02:00
Hans Mackowiak
a974352908 CountersPutAi: prevent AI from going overflow 2025-09-06 09:18:42 +02:00
kevlahnota
45e04cfef7 Update CONTRIBUTORS.txt
reference:
https://api.github.com/repos/Card-Forge/forge/contributors
[?per_page=100&&page=1] | [?per_page=100&&page=2]
2025-09-06 12:00:22 +08:00
kevlahnota
6a83cb89f7 Merge pull request #8653 from kevlahnota/master4
update mobile BugReporter savetofile
2025-09-06 10:57:34 +08:00
Anthony Calosa
494bd75572 update mobile BugReporter savetofile 2025-09-06 10:53:34 +08:00
Paul Hammerton
b065e468b0 Merge pull request #8652 from paulsnoops/fix-spm
Edition updates: SPM
2025-09-05 18:49:52 +01:00
Paul Hammerton
86aff59e40 Edition updates: SPM 2025-09-05 18:49:16 +01:00
Paul Hammerton
748c4e1dd7 Merge pull request #8651 from paulsnoops/edition-updates
Edition updates: MAR, OM1, PSPM, SLD, SPM
2025-09-05 18:34:51 +01:00
Fulgur14
dfb776f526 Final SPM batch (#8648) 2025-09-05 17:33:46 +00:00
Paul Hammerton
ca5d2bb109 Undo Add OM1 AltNames 2025-09-05 18:33:40 +01:00
Paul Hammerton
b42050a40f Add OM1 AltNames 2025-09-05 18:30:37 +01:00
Paul Hammerton
14220ae37f Edition updates: PSPM & SPM tokens 2025-09-05 18:26:56 +01:00
Paul Hammerton
46900c3f49 Edition updates: MAR, OM1, SLD, SPM 2025-09-05 18:19:16 +01:00
tool4ever
cb2bf996d0 Fix Profane Transfusion (#8650)
* Param for Mister Negative
2025-09-05 17:08:46 +00:00
tool4ever
edbf6692f2 Remove fake KW (#8649) 2025-09-05 16:04:07 +00:00
tool4ever
b5f16b83a0 Fix AI checking cost before less expensive logic (#8643) 2025-09-05 09:54:53 +02:00
Fulgur14
ee4902a5ae Spider-Girl and friends (SPM) (#8644) 2025-09-05 09:54:05 +02:00
Fulgur14
0835817d8a Morbius and friends (SPM) (#8640) 2025-09-05 09:48:21 +02:00
Hans Mackowiak
15d4e1ac20 Update spider_suit.txt 2025-09-05 09:23:07 +02:00
kevlahnota
ecdce50f12 Merge pull request #8645 from kevlahnota/master4
NPE prevention
2025-09-05 10:50:45 +08:00
Anthony Calosa
247fd7810e NPE prevention 2025-09-05 06:43:48 +08:00
tool4ever
055cd40fb1 Fix special actions going through frozen stack (#8642) 2025-09-04 19:16:46 +00:00
Renato Filipe Vidal Santos
f1d448509e YEOE: 6 cards 2025-09-04 15:50:36 +00:00
Paul Hammerton
f2581855a4 Merge pull request #8639 from paulsnoops/master
Edition updates: MAR, SPM
2025-09-04 16:03:09 +01:00
Paul Hammerton
1e4c7d4b00 Update Marvel's Spider-Man.txt 2025-09-04 16:02:16 +01:00
Paul Hammerton
2d80e44f70 Update Marvel's Spider-Man.txt 2025-09-04 16:01:06 +01:00
Paul Hammerton
a207bdbb51 Edition updates 2025-09-04 15:54:26 +01:00
Jetz72
c09ec2f410 Merge pull request #8619 from Jetz72/fixes20250902
Fix Arachnus Spinner not choosing a target for the Web
2025-09-04 08:20:21 -05:00
kevlahnota
c579df4c82 Merge pull request #8584 from SapphiCat/master
Fix crash copying adventure decks with full slots
2025-09-04 18:35:09 +08:00
Fulgur14
d39ab505df Sandman's Quicksand (SPM) 2025-09-04 11:38:12 +02:00
Fulgur14
7756dfa5be Jackal and friends (SPM) (#8634) 2025-09-04 11:13:36 +02:00
kevlahnota
c1b6f3126e Merge pull request #8633 from Eradev/FixCollectorNoRegex
Fix CollNo pattern after #7921
2025-09-04 15:47:54 +08:00
kevlahnota
11d45e0fa3 Merge pull request #8636 from kevlahnota/master4
Prevent building some card vars for view purposes only
2025-09-04 14:09:21 +08:00
Anthony Calosa
0504b41dbd Prevent building some card vars for view purposes only
buggy cards will still crash ingame when invoked
2025-09-04 14:04:56 +08:00
kevlahnota
b670cc3736 Update spider_verse.txt 2025-09-04 13:19:43 +08:00
kevlahnota
885300ba49 Merge pull request #8635 from kevlahnota/master4
prevent Concurrent Modificaton onRefresh ItemListView
2025-09-04 13:07:38 +08:00
Anthony Calosa
ff8fd51bf1 prevent NPE, update The Clone Saga trigger 2025-09-04 12:58:11 +08:00
Anthony Calosa
80475c6b62 prevent Concurrent Modificaton onRefresh ItemListView 2025-09-04 12:47:06 +08:00
kevlahnota
88ea36523b Merge pull request #8632 from Eradev/RemoveSetLookupForJ21
Delete SetLookup for J21
2025-09-04 10:17:31 +08:00
Eradev
3386d79cb1 Remove the Alchemy cards that were removed. 2025-09-03 22:07:36 -04:00
Eradev
f97429d1d4 Fix CollNo pattern after #7921 2025-09-03 21:53:49 -04:00
kevlahnota
3628d4e22a Merge pull request #8630 from kevlahnota/master4
remove unsupported cards on game matches
2025-09-04 09:38:26 +08:00
Eradev
fffc9a4282 Delete SetLookup for J21 2025-09-03 21:09:57 -04:00
Anthony Calosa
7cb32b0b25 update deck check 2025-09-04 07:33:21 +08:00
Anthony Calosa
cb13682737 remove unsupported cards on game matches
- closes #8616
2025-09-04 06:50:04 +08:00
Fulgur14
9656a9fb4c Lizard, Connors's Curse (SPM) (#8629) 2025-09-03 20:55:26 +00:00
tool4ever
2fd9d3ae14 CardDb: Remove hardcoded workaround in favor of reusing newer filter (#8628) 2025-09-03 20:49:33 +00:00
Fulgur14
1ea90f6655 Black Cat and gang (SPM) (#8626) 2025-09-03 17:53:50 +00:00
Fulgur14
37342910fa Swarm, Being of Bees and friends (SPM) (#8627) 2025-09-03 17:46:08 +00:00
autumnmyst
3a00727602 Add new duplicate cards directory to deal with duplicate test cards and implement correct Fast // Furious card in UNK (#8520)
* Added Steamflogger Boss to eternal cards in UST
2025-09-03 17:39:53 +00:00
Fulgur14
589d592997 Rhino's Rampage (SPM) (#8624) 2025-09-03 16:12:49 +02:00
Jetz
83101f7f41 Fix incorrect conformity in adventure events. 2025-09-03 09:11:09 -04:00
Fulgur14
c7d24f1c01 4 SPM cards (#8623) 2025-09-03 13:06:42 +02:00
Fulgur14
2a678aa7e9 Spider-UK et al. (SPM) (#8613) 2025-09-03 13:04:46 +02:00
tool4ever
6a758ffc0a Mayhem for Lands (#8615) 2025-09-03 13:03:52 +02:00
tool4ever
83421bac56 Optional fixes (#8622) 2025-09-03 13:00:15 +02:00
Fulgur14
5234365cc1 Heroes' Hangout (SPM) (#8621) 2025-09-03 09:39:06 +02:00
Fulgur14
76558a841c Create the_spot_living_portal.txt (#8617) 2025-09-03 09:37:47 +02:00
Chris H
1338735000 Spiderman is only pick 2 if pod size is 4 2025-09-02 23:12:32 -04:00
Chris H
44c33e2955 Apply pod size to Desktop UI draft view 2025-09-02 23:12:32 -04:00
Chris H
b8a5668db6 Add ability to draft with less than 8 players 2025-09-02 23:12:32 -04:00
Chris H
6a84a7a6f0 Update token entries in Fate Reforged.txt 2025-09-02 23:12:14 -04:00
Jetz
4811c9d726 end of file line break 2025-09-02 20:08:09 -04:00
Jetz
7cea6fce1e Fix Arachnus Spinner not choosing a target for the Web 2025-09-02 20:06:07 -04:00
tool4ever
46ba289329 Update sculpted_sunburst.txt
Closes #8618
2025-09-02 21:52:24 +00:00
Fulgur14
2c9e5ee814 Superior Spider-Man and other nonsense (SPM) (#8614) 2025-09-02 19:09:54 +00:00
Eradev
230eba687d Add unique suffix for reversible cards (#7921) 2025-09-02 18:05:19 +00:00
Fulgur14
5faf317aaf 3 SPM cards (2nd September) (#8612) 2025-09-02 16:59:23 +02:00
Fulgur14
388448c94f Cosmic Spider-Man (SPM) (#8610) 2025-09-02 16:24:11 +02:00
Fulgur14
230282b525 Parker Luck (SPM) (#8609) 2025-09-02 15:34:22 +02:00
Fulgur14
0f92bc18d4 Chameleon, Master of Disguise & Spinneret and Spiderling (#8608) 2025-09-02 13:16:27 +02:00
Paul Hammerton
dc4cee62dc Merge pull request #8607 from paulsnoops/edition-updates
Edition updates: MAR, SPM
2025-09-02 10:44:36 +01:00
Paul Hammerton
ea4c8fbc72 Edition updates: MAR, SPM 2025-09-02 10:42:42 +01:00
Fulgur14
068ea955d9 Impostor Syndrome (SPM) (#8600) 2025-09-02 09:19:18 +02:00
xanxer6rB
f898263bac Update the_soul_stone.txt
Added missing T: Add {B}
2025-09-02 06:34:47 +02:00
Hans Mackowiak
a3147f5e26 ~ fix spiders_man_heroic_horde TrigGainLife 2025-09-02 00:43:53 +02:00
tool4ever
a309eaee02 Update ultimate_green_goblin.txt 2025-09-01 19:25:29 +00:00
Fulgur14
94b058ec07 Agent Venom (SPM) (#8599) 2025-09-01 18:02:11 +00:00
Fulgur14
46bf3348be Flash Thompson and Peter Parker's Camera (SPM) (#8598) 2025-09-01 17:27:24 +00:00
Fulgur14
7c4c820e92 Ezekiel Sims and friends (SPM) (#8596) 2025-09-01 16:35:03 +00:00
Fulgur14
ed3ea99e1a Spiders-Man, Heroic Horde (SPM) (#8593) 2025-09-01 16:24:07 +02:00
tool4ever
ec21a0c789 The Soul Stone and support (#8579) 2025-09-01 13:45:01 +02:00
Fulgur14
bfe0a78765 Radioactive Spider (SPM) (#8592) 2025-09-01 13:40:06 +02:00
Fulgur14
0abc224e1b Steel Wrecking Ball (SPM) (#8588) 2025-08-31 18:06:16 +00:00
SapphiCat
fa3ce1c7f7 Merge branch 'Card-Forge:master' into master 2025-08-31 07:35:08 -07:00
tool4ever
48a5046d93 Fix Urza's Miter (#8586) 2025-08-31 10:39:59 +00:00
Fulgur14
9f9e68f34d Create pictures_of_spider_man.txt (#8583) 2025-08-31 07:29:35 +00:00
kit
d7c7aaa51e Fix crash copying adventure decks with full slots 2025-08-30 14:02:02 -07:00
tool4ever
ffe883e7d9 Behold the Sinister Six! and support (#8578) 2025-08-30 10:57:13 +00:00
Hans Mackowiak
b07a9944aa GameActionUtil: fix getFirstSpellAbility using castSA 2025-08-30 12:55:28 +02:00
Fulgur14
997d489a7e SPM cards from the debut (#8576) 2025-08-30 10:19:58 +00:00
Paul Hammerton
d181746679 Merge pull request #8581 from paulsnoops/master
Edition updates: MAR, PF25, SLD, SPM
2025-08-30 11:16:07 +01:00
Paul Hammerton
693d6cd9e3 Edition updates: MAR, PF25, SLD, SPM 2025-08-30 11:14:15 +01:00
kevlahnota
01d5bf3c21 Merge pull request #8521 from antoniomartinelli/fix/lastimportedcubeid
fix: wrong cube id saved as LAST_IMPORTED_CUBE_ID
2025-08-30 06:55:02 +08:00
kevlahnota
29a630386f Merge pull request #8410 from rappazzo/stop-tracking-changelog
fix: move CHANGES.txt generation to target directory
2025-08-30 06:31:10 +08:00
Hans Mackowiak
75c7938f1e CounterType: turn wrapper into an interface (#8572)
* CounterType: turn wrapper into an interface

* remove CounterType.get(CounterEnumType) helper
2025-08-29 20:44:08 +02:00
Fulgur14
110e885c67 Create with_great_power.txt (#8573) 2025-08-29 18:40:39 +00:00
Hans Mackowiak
548b448f0d Update Game.java
`getNonactivePlayers` not needed anymore
2025-08-29 20:38:55 +02:00
Hans Mackowiak
a2e80ac0f3 TriggerHandler: no need for APNAP (#8567)
* TriggerHandler: no need for APNAP

The order of the Trigger will be handled later in MagicStack

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-29 20:37:26 +02:00
Eradev
238d426202 Alchemy update 20250818 (#8492) 2025-08-29 17:55:18 +00:00
Hans Mackowiak
d6f585a80c Event: use Record 2025-08-29 18:05:05 +02:00
tool4ever
223aeb1bff Update displaced_dinosaurs.txt 2025-08-29 12:02:27 +02:00
Hans Mackowiak
4bf02ea61f MagicStack: remove getNonactivePlayers for SpellCastSinceBegOfYourLastTurn 2025-08-29 11:20:38 +02:00
tool4ever
ea186ae4af Update faunsbane_troll.txt 2025-08-28 19:26:09 +00:00
kevlahnota
3e4cd9e189 Update LoadDraftScreen.java
prevent IllegalStateException since the option pane show method needs to run in EDT
2025-08-28 20:13:01 +08:00
Eradev
e35c193f92 Fix content passed 2025-08-27 22:36:22 -04:00
Eradev
e5443fc394 Typo 2025-08-27 22:36:22 -04:00
Eradev
9998092c70 Create helper class to not duplicate the code 2025-08-27 22:36:22 -04:00
Eradev
9f81e0cd34 Move custom types into edition files 2025-08-27 22:36:22 -04:00
Eradev
5569f18053 Remove param 2025-08-27 22:36:22 -04:00
Eradev
40190b5442 Remove TreeSet 2025-08-27 22:36:22 -04:00
Eradev
a33905241d Revert "Ensure subtype not used"
This reverts commit 6797f0c1b0.
2025-08-27 22:36:22 -04:00
Eradev
3a40b560f6 Removed import 2025-08-27 22:36:22 -04:00
Eradev
d79fb96770 Ensure subtype not used 2025-08-27 22:36:22 -04:00
Eradev
f50fe2f89c Now read from a directory 2025-08-27 22:36:22 -04:00
Eradev
a36deab334 Allow custom types 2025-08-27 22:36:22 -04:00
kevlahnota
28feff99d7 Merge pull request #8565 from kevlahnota/master4
prevent NPE, ConcurrentModification on ImageView onRefresh
2025-08-28 10:04:57 +08:00
Anthony Calosa
bab8791012 prevent NPE, ConcurrentModification on ImageView onRefresh
use Record for ImageRecord
2025-08-28 06:28:34 +08:00
Fulgur14
db20b27c9d Update soul_seizer_ghastly_haunting.txt (#8431) 2025-08-27 15:20:24 +00:00
Renato Filipe Vidal Santos
087db3f757 YEOE: 5 cards (#8484) 2025-08-27 15:16:24 +00:00
kevlahnota
db194fab97 Merge pull request #8561 from kevlahnota/master4
Remove unsupported cards from AdventurePlayer inventory
2025-08-27 21:52:52 +08:00
Anthony Calosa
0585ece2c1 Remove unsupported cards from AdventurePlayer inventory
- closes #8545
2025-08-27 21:34:07 +08:00
Eradev
1611559909 Add new card filters (#8557)
* Add is:vanilla

* Add is:custom
2025-08-27 06:10:49 +00:00
Cees Timmerman
d6dee9575b More human-readable events (#8502) 2025-08-27 07:23:14 +02:00
Cees Timmerman
6dd9a731fa Fix event.oldEntiy typo 2025-08-27 07:23:14 +02:00
Leandro Doctors
cf5a8508d6 Remove empty test class
Dead, unmaintained code eventually leads to problems.
2025-08-27 06:56:12 +02:00
tool4ever
0fa7df090e Fix Muraganda Petroglyphs vs. Saga with no abilities (#8551)
---------

Co-authored-by: TRT <>
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-27 06:51:15 +02:00
kevlahnota
86838d94f7 Merge pull request #8544 from Eradev/RemoveUnsupportedCard
Option to remove unsupported card from collection
2025-08-27 12:21:53 +08:00
kevlahnota
83c4db07ac Merge pull request #8559 from Eradev/RemoveAdvCardsRewards
Remove custom adventure cards from rewards.
2025-08-27 12:19:58 +08:00
Eradev
b544f2dd00 Remove custom adventure cards from rewards. 2025-08-26 20:54:26 -04:00
kevlahnota
079e6f04ac update canBeOathbreaker check (#8556) 2025-08-27 05:22:00 +08:00
Eradev
d04c541578 Fix edition mapping 2025-08-26 10:34:41 -04:00
Hans Mackowiak
7cc2cf530d AnimateBase: PerpetualManaCost 2025-08-26 15:57:50 +02:00
Hans Mackowiak
61a2c7cadb Update DamageAllAi.java (#8550)
Remove `getNonactivePlayers`
2025-08-26 15:51:39 +02:00
tool4ever
efbf2e1a9c Saga ETB counters aren't intrinsic (#8549) 2025-08-26 15:47:17 +02:00
Anthony Calosa
9b8441d45b update Console Textfield navigation 2025-08-26 21:04:06 +08:00
Hans Mackowiak
f8b7a0fb9a MagicStack: check for StackEntries in addAllTriggeredAbilities 2025-08-26 14:56:24 +02:00
shenshinoman
ab49e97797 Expanding my Innistrad further. new enemies. New maps. Second biome is partially populated. Everything is contained to innistrad plane, so should have no impact on the main game outside that plane. 2025-08-26 08:40:30 -04:00
shenshinoman
c27a9d136f Expanding my Innistrad further. new enemies. New maps. Second biome is partially populated. Everything is contained to innistrad plane, so should have no impact on the main game outside that plane. 2025-08-26 08:40:30 -04:00
Leandro Doctors
91241ee53c clean up: remove obsolete GitLab templates
The project moved to GitHub almost a year ago.

Having duplicated, unmaintained templates will generate problems in the
future. If needed, they can always be retrieved from the commit history.
2025-08-26 13:32:30 +02:00
Eradev
31e537f42b Remove changes to FContainer 2025-08-26 06:33:30 -04:00
Eradev
e550c307c2 Option to remove unsupported card from collection 2025-08-26 06:29:49 -04:00
Paul Hammerton
b4f01b7ebb Merge pull request #8542 from paulsnoops/treetop-recluse
Add treetop_recluse.txt
2025-08-26 09:10:59 +01:00
Eradev
7744474f39 Fix token numbers 2025-08-26 09:58:01 +02:00
Paul Hammerton
1365b82968 Add treetop_recluse.txt 2025-08-26 08:43:01 +01:00
Hans Mackowiak
5bb532bf9d Game: only reverse NonactivePlayers if more than one
Reversing the Turn Order should have no Effect there if only 2 players
2025-08-26 07:35:39 +02:00
tool4ever
258b8c18a9 Fix Thief of Blood simultaneous ETB (#8535)
Co-authored-by: TRT <>
2025-08-26 08:26:32 +03:00
Eradev
a66349d8a1 Missing token (#8538)
* Add g_1_1_forest_dryad_squirrel

* Update curiosity
2025-08-26 08:26:25 +03:00
kevlahnota
1933cf1863 Merge pull request #8537 from kevlahnota/master3
migrate Callback to Consumer Interface
2025-08-26 12:59:53 +08:00
kevlahnota
0d342c778f fix NG+ 2025-08-26 09:11:31 +08:00
kevlahnota
f452b94cb8 update missing migration 2025-08-26 07:16:43 +08:00
kevlahnota
9325794e2f Update FFileChooser.java 2025-08-26 07:03:24 +08:00
kevlahnota
192a64bbc3 Update Forge.java 2025-08-26 06:58:09 +08:00
kevlahnota
deb8369f11 Update FDeckChooser.java 2025-08-26 06:53:59 +08:00
Anthony Calosa
b24f536190 use Java Consumer 2025-08-26 06:34:59 +08:00
Leandro Doctors
28ec24069c CI: add support for JDK 21
Depends on https://github.com/Card-Forge/forge/pull/8533
2025-08-25 17:24:19 -04:00
Anthony Calosa
f27472d9bd migrate Callback to Interface
- closes #5717
2025-08-26 05:14:20 +08:00
Leandro Doctors
780cc8ddbf Fix build on JDK 21+
This bumps `izpack-maven-plugin`.

Tested with JDK 25.

Similar fix for another project:
https://github.com/lsc-project/lsc/pull/385

Original Error message:
`[ERROR] Failed to execute goal
org.codehaus.izpack:izpack-maven-plugin:5.2.3:izpack
(standard-installer) on project forge-installer: Execution
standard-installer
of goal org.codehaus.izpack:izpack-maven-plugin:5.2.3:izpack failed:
java.lang.ArrayIndexOutOfBoundsException: Index 70131 out of bounds for
length 22674 -> [Help 1]`
2025-08-25 13:50:57 -04:00
kevlahnota
23555b9564 migrate function to method reference 2025-08-25 19:54:42 +08:00
kevlahnota
431827be35 Update Keyword.java for Double Team
- closes #8519
2025-08-25 11:49:49 +08:00
kevlahnota
b0dba74c6c Merge pull request #8528 from kevlahnota/master3
update ConsoleCommandInterpreter
2025-08-25 06:56:58 +08:00
Anthony Calosa
ad0a690764 minor typo 2025-08-25 06:56:27 +08:00
Anthony Calosa
977f2c75b9 update comment 2025-08-25 06:53:28 +08:00
Anthony Calosa
8313414f78 update ConsoleCommandInterpreter 2025-08-25 06:49:59 +08:00
kevlahnota
89955bf201 Merge pull request #8527 from kevlahnota/master3
capture adventure SaveFileData exception to sentry
2025-08-25 05:38:14 +08:00
Anthony Calosa
99d191901a capture adventure SaveFileData exception to sentry 2025-08-25 05:33:45 +08:00
kevlahnota
1a306b3da3 Merge pull request #8525 from kevlahnota/master3
prevent saving error on inventory
2025-08-25 04:55:51 +08:00
Anthony Calosa
9ff03cbd41 prevent saving error on inventory 2025-08-25 04:50:58 +08:00
tool4ever
07a1dbc099 Fix scripts (#8524) 2025-08-24 22:10:56 +02:00
Hans Mackowiak
f24b3ea3b3 Update enchantment_alteration.txt
closes #8522
2025-08-24 22:02:36 +02:00
kevlahnota
770cc72fcd Merge pull request #8523 from kevlahnota/master3
update migration message
2025-08-25 03:36:27 +08:00
Anthony Calosa
8426a74900 update migration message 2025-08-25 03:34:43 +08:00
antoniomartinelli
8d98eda18d fix: wrong cube id saved as LAST_IMPORTED_CUBE_ID 2025-08-24 18:17:54 +02:00
Paul Hammerton
6e6509eaff Merge pull request #8518 from paulsnoops/edition-updates
Edition updates: PSPM
2025-08-24 11:13:05 +01:00
Paul Hammerton
c56ddee47d Edition updates: PSPM 2025-08-24 11:11:41 +01:00
Eradev
07985ac487 Allow custom cards as reward (#8512) 2025-08-23 21:55:28 -04:00
kevlahnota
6c93491d7f Don't select sideboard first when creating new deck 2025-08-24 05:13:36 +08:00
Eradev
c9b012c88a Fix Bloomburrow lands (#8489)
* Fix Bloomburrow lands

* Proper fix

* Revert "Proper fix"

This reverts commit f3a46e40d9.

* Use collector number
2025-08-23 14:14:57 -04:00
kevlahnota
9b53144976 Merge pull request #8509 from kevlahnota/master3
remove Normalizer from clipboard and PaperCard artist.
2025-08-24 00:53:22 +08:00
Anthony Calosa
31020296d8 unused import 2025-08-24 00:46:02 +08:00
Anthony Calosa
65fb3414d8 remove Normalizer from clipboard and PaperCard artist. 2025-08-24 00:42:40 +08:00
kevlahnota
98fee0d86b Merge pull request #8497 from kevlahnota/master3
update InventoryScene, add repair cracked item for gold
2025-08-23 23:17:08 +08:00
Anthony Calosa
becdadb279 NPE prevention for rewards 2025-08-23 23:08:11 +08:00
Hans Mackowiak
26da0ab0d4 Create rw_1_1_soldier.txt
Fix missing TokenScript
2025-08-23 16:46:16 +02:00
Anthony Calosa
e1f4d755e0 minor fix for refreshing overlay 2025-08-23 21:38:52 +08:00
Anthony Calosa
efe7d67d9f add unusable indicator 2025-08-23 21:34:57 +08:00
Anthony Calosa
3a5e11504a add migration message, fix cloning of itemData 2025-08-23 20:07:28 +08:00
Fulgur14
6657602b80 J. Jonah Jameson (SPM) (#8506) 2025-08-23 11:00:45 +02:00
Eradev
eeca33855d Fix minor card render problems (#8478)
* Missing space

* Unescape line breaks in card descriptions
2025-08-23 06:56:21 +03:00
Matthew Scott Krafczyk
cdc63f35bb Add python specific entries to .gitignore (#8390) 2025-08-23 06:55:28 +03:00
Jetz72
5a9ea8d260 Merge pull request #8344 from verifiedtm/deck-editor-sections
Add other deck sections for other formats
2025-08-22 07:56:55 -05:00
Fulgur14
5a363922bd Oracle updates - EOE (#8496) 2025-08-22 12:57:01 +02:00
Anthony Calosa
1ff16ca509 prevent NPE 2025-08-22 16:43:17 +08:00
TRT
c878401197 Script cleanup 2025-08-22 10:03:27 +02:00
Eradev
8f518b7b1f Fix IsDoctorCheck 2025-08-22 06:33:29 +02:00
Anthony Calosa
fb624458f0 update InventoryScene, add repair cracked item for gold 2025-08-22 12:32:12 +08:00
Paul Hammerton
ee3220f33b Merge pull request #8487 from paulsnoops/fix-yeoe
YEOE edition and format updates for check land cycle
2025-08-21 19:31:16 +01:00
Paul Hammerton
7bc591fa78 YEOE mana-fixing lands edtion and format update 2025-08-21 19:25:47 +01:00
kevlahnota
c02d942fc3 Merge pull request #8485 from kevlahnota/master3
request access if needed to use backup and restore
2025-08-21 22:00:02 +08:00
Anthony Calosa
3b9ded8270 request access if needed to use backup and restore 2025-08-21 21:49:45 +08:00
tool4ever
fdf0c13a7f Remove outdated Briar Shield code path (#8477) 2025-08-21 07:15:21 +01:00
Hans Mackowiak
80b1cac394 lf 2025-08-21 07:05:35 +02:00
Hans Mackowiak
8c80c32113 lf 2025-08-21 07:01:46 +02:00
Agetian
a74b033c26 Revert "Ambrosia Whiteheart AI slight improvement (#8473)" (#8483)
This reverts commit ba8a30ebdd.
2025-08-21 07:48:20 +03:00
kevlahnota
16fcbc0ebf Merge pull request #8481 from kevlahnota/master3
fix osFamily name for sentry, update android-all
2025-08-21 09:54:47 +08:00
Anthony Calosa
62ffada6fe update cpu name 2025-08-21 09:46:30 +08:00
Anthony Calosa
e5a0d335af fix osFamily name for sentry, update android-all 2025-08-21 08:00:29 +08:00
Agetian
ba8a30ebdd Ambrosia Whiteheart AI slight improvement (#8473)
* - Add achievement for EOE/EOC by Marek14.

* - Slight logic improvement for Ambrosia Whiteheart
2025-08-20 18:17:35 +03:00
Agetian
e25dbe5196 Add puzzles PS_EOE1 and PS_EOE2. (#8475)
* - Add achievement for EOE/EOC by Marek14.

* - Add puzzles PS_EOE1 and PS_EOE2.
2025-08-20 18:17:11 +03:00
Renato Filipe Vidal Santos
3533ad8b74 Update darigaaz_shivan_champion.txt (#8474) 2025-08-20 18:16:58 +03:00
kevlahnota
2d20ce07e3 Merge pull request #8470 from kevlahnota/master3
move hwinfo output on create method
2025-08-20 21:59:21 +08:00
Anthony Calosa
e5e0a9240d move hwinfo output on create method 2025-08-20 21:55:22 +08:00
kevlahnota
5e83fff859 Merge pull request #8469 from kevlahnota/master3
update device info
2025-08-20 20:58:00 +08:00
Anthony Calosa
2d0acc734e indention 2025-08-20 20:56:22 +08:00
Anthony Calosa
b6bc3a6e96 update device info
should fill up sentry basic device and os info for mobile builds
2025-08-20 20:41:46 +08:00
Eradev
7ec7a65f33 Partially revert 38b6283a by restoring deleted rebalanced card files 2025-08-20 12:55:47 +02:00
Eradev
2a97b67f72 Remove unused import 2025-08-20 12:55:47 +02:00
Eradev
06be01d81c Remove WIP 2025-08-20 12:55:47 +02:00
Eradev
386f6aaac6 Try to get non-Alchemy version 2025-08-20 12:55:47 +02:00
Eradev
82d11bfb45 Update coll.no. for Zendikar Rising 2025-08-20 12:55:47 +02:00
Eradev
f39d900fbf Huge Alchemy Cleanup 2025-08-20 12:55:47 +02:00
Eradev
335edec357 Add info 2025-08-20 12:53:44 +02:00
Eradev
e58f035b19 Update JUD 2025-08-20 12:53:44 +02:00
Eradev
f85d610393 Update J13 2025-08-20 12:53:44 +02:00
Eradev
fce0095af1 Update J12 2025-08-20 12:53:44 +02:00
Eradev
82bea111df Update G11 2025-08-20 12:53:44 +02:00
Eradev
0aded8f1f4 Add borders to H17 2025-08-20 12:53:44 +02:00
Eradev
84130f0586 Update XLN 2025-08-20 12:53:44 +02:00
Eradev
ed4d1059dd Update INV 2025-08-20 12:53:44 +02:00
Eradev
c73be86211 Update ISD 2025-08-20 12:53:44 +02:00
Eradev
6cb2e7a91a Update MID 2025-08-20 12:53:44 +02:00
Eradev
58713e65ed Update VOW 2025-08-20 12:53:44 +02:00
Eradev
676c26fae7 Update IKO 2025-08-20 12:53:44 +02:00
Eradev
f2e4e67021 Update ICE 2025-08-20 12:53:44 +02:00
Eradev
18ba476fc9 Fix H17 2025-08-20 12:53:44 +02:00
Eradev
d9ed82972f Add H17 2025-08-20 12:53:44 +02:00
Eradev
c9affaa1a1 Update HOU 2025-08-20 12:53:44 +02:00
Eradev
8ef89adaef Update HML 2025-08-20 12:53:44 +02:00
Eradev
f25db898ae Remove Code2 MD1 2025-08-20 12:52:52 +02:00
Eradev
cddc41b353 Update MOR 2025-08-20 12:52:52 +02:00
Eradev
d4e918660b Update MMA 2025-08-20 12:52:52 +02:00
Eradev
3f16a3e27f Update MM3 2025-08-20 12:52:52 +02:00
Eradev
53d27f1437 Update MM2 2025-08-20 12:52:52 +02:00
Eradev
408dd310d3 Update MH1 2025-08-20 12:52:52 +02:00
Eradev
0050446ff0 Update MH3 2025-08-20 12:52:52 +02:00
Eradev
efb47d949f Update MH2 2025-08-20 12:52:52 +02:00
Eradev
a67303866e Update MD1 2025-08-20 12:52:52 +02:00
Eradev
756e80595c Update MRD 2025-08-20 12:52:52 +02:00
Eradev
089a021d02 Update PMBS 2025-08-20 12:52:52 +02:00
Eradev
d89a94dcd2 Update MBS 2025-08-20 12:52:52 +02:00
Eradev
b298f66348 Update MIR 2025-08-20 12:52:52 +02:00
Eradev
fa277c7b7c Update MMQ 2025-08-20 12:52:52 +02:00
Eradev
76ec449d33 Update ME1 2025-08-20 12:52:52 +02:00
Eradev
d481ade524 Update ME4 2025-08-20 12:52:52 +02:00
Eradev
1b468efff1 Update ME3 2025-08-20 12:52:52 +02:00
Eradev
9be9a4795b Update ME2 2025-08-20 12:52:52 +02:00
Eradev
49bcf7ded4 Update A25 2025-08-20 12:52:52 +02:00
Eradev
28989b9c49 Update P04 2025-08-20 12:52:52 +02:00
Eradev
9e40b1f6cf Update MPR 2025-08-20 12:52:52 +02:00
Eradev
a9e95caa42 Update ORI 2025-08-20 12:52:52 +02:00
Eradev
eb39c97661 Update TD0 2025-08-20 12:52:52 +02:00
Eradev
52f19272d0 Update M15 2025-08-20 12:52:52 +02:00
Eradev
4cf37379c7 Update PM15 2025-08-20 12:52:52 +02:00
Eradev
746d0476c9 Update PM10 2025-08-20 12:52:52 +02:00
Eradev
cf93d61d0c Update M14 2025-08-20 12:52:52 +02:00
Eradev
5d9197446b Update PM14 2025-08-20 12:52:52 +02:00
Eradev
753d5560d6 Update M13 2025-08-20 12:52:52 +02:00
Eradev
1b68d30ff1 Update PM10 2025-08-20 12:52:52 +02:00
Eradev
278eed7af9 Update PPC1 2025-08-20 12:52:52 +02:00
Hans Mackowiak
6eb4e32225 Update Murders at Karlov Manor.txt 2025-08-20 12:48:52 +02:00
tool4ever
e160a7d517 Tighten scripts (#8467) 2025-08-20 12:34:56 +02:00
Eradev
476cdda0c2 Use KTK goblin as r_1_1_goblin_haste since it has unique art 2025-08-20 12:30:18 +02:00
Eradev
efdabb8bf6 Update PLG25 2025-08-20 12:30:18 +02:00
Eradev
ca648df852 Update LRW 2025-08-20 12:30:18 +02:00
Eradev
04afb61351 Update LEB 2025-08-20 12:30:18 +02:00
Eradev
869632361c Update LEA 2025-08-20 12:30:18 +02:00
Eradev
d994abb559 Update LGN 2025-08-20 12:30:18 +02:00
Eradev
d082c2c250 Update LEG 2025-08-20 12:30:18 +02:00
Eradev
586a474168 Update PZ1 2025-08-20 12:30:18 +02:00
Eradev
b7268dd6fd Update NEO 2025-08-20 12:30:18 +02:00
Eradev
c3e4ea228a Update KTK 2025-08-20 12:30:18 +02:00
Eradev
237946f569 Update KHM 2025-08-20 12:30:18 +02:00
Eradev
cae32e5b2a Update KLD 2025-08-20 12:30:18 +02:00
tool4ever
715ba9803f Fix running out of memory from too many waiting triggers (#8466) 2025-08-20 11:42:04 +02:00
Renato Filipe Vidal Santos
283198f0a4 Tidying trigger-granting Slivers (#8465) 2025-08-20 11:40:54 +02:00
Fulgur14
bab889c406 Firebending cards (TLA) (#8445) 2025-08-20 11:40:22 +02:00
tool4ever
92c6f5369d Fix missing trigger (#8462) 2025-08-20 10:30:55 +02:00
Renato Filipe Vidal Santos
6042948aed Update freyalise_skyshroud_partisan.txt 2025-08-20 06:39:07 +02:00
Fulgur14
136577fec0 Avatar Aang (TLA) (#8459) 2025-08-19 21:30:13 +02:00
tool4ever
180fda53df Update krumar_initiate.txt 2025-08-19 20:03:36 +02:00
Antonio Martinelli
ca6b175fb8 Import from CubeCobra for Draft and Sealed (#8375)
* Import from CubeCobra feature for Draft and Sealed

* adjust comment for cube Import

* CubeImporter refactoring and generalisation

* Allow hyphens in Cube ID validation

* remove unused imports

* make parseFromURL private

* add LAST_IMPORTED_CUBE_ID to ForgePreferences

---------

Co-authored-by: Antonio <mart@gmail.com>
2025-08-19 11:25:50 -04:00
Antonio Martinelli
dd25dc4b06 update rankings_cubecobra.txt (#8458) 2025-08-19 18:13:08 +03:00
Hans Mackowiak
9893fc3cf9 Player: extra Flag for Element Bend 2025-08-19 16:41:31 +02:00
Michael Rappazzo
67b6c2c03f fix: move CHANGES.txt generation to target directory
- Generate CHANGES.txt in forge-gui-desktop/target/ instead of source tree
- Update installer to copy from target directory for all build profiles
- Add CHANGES.txt to .gitignore since it's generated
- Remove hardcoded fromRef to use latest tag automatically
- Remove maven-release-plugin exclusion for untracked file
2025-08-19 06:49:42 -04:00
Agetian
bb073b8682 Tweak AI logic for Krumar Initiate (#8453)
* - Add achievement for EOE/EOC by Marek14.

* - Add AI logic for Krumar Initiate

* - Tweak AI logic for Krumar Initiate

* Update EndureAi.java

only when doing X

---------

Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-08-19 13:36:11 +03:00
Agetian
9bbfad5be3 AI logic for Krumar Initiate (#8450)
* - Add achievement for EOE/EOC by Marek14.

* - Add AI logic for Krumar Initiate
2025-08-19 13:11:41 +03:00
Paul Hammerton
2c7e7d5b63 Merge pull request #8448 from paulsnoops/edition-updates
Edition updates: AA1, AA2
2025-08-19 10:21:44 +01:00
Paul Hammerton
94e7258a03 Edition updates: AA1, AA2 2025-08-19 10:18:58 +01:00
Renato Filipe Vidal Santos
95eb90eac2 Update claws_out.txt 2025-08-19 07:08:24 +02:00
Eradev
0f0a4c54e7 Fix Hanweir Garrison CollNo 2025-08-19 07:07:27 +02:00
Eradev
399923bfa4 Remove dupe + fix SIR 2025-08-19 07:07:27 +02:00
Eradev
39f0ab9eae Fix closing bracket 2025-08-19 07:07:27 +02:00
Eradev
8b1cd54417 Search split types 2025-08-19 07:07:27 +02:00
Eradev
ff2192fa7a Fix Meld key 2025-08-19 07:07:27 +02:00
Eradev
08443a307c Fix Domri Rade's emblem 2025-08-19 06:35:36 +02:00
Eradev
bd90e1bccc Check for emblems in other instead of tokens 2025-08-19 06:35:36 +02:00
Hans Mackowiak
9cb0bd301e TLA: Firebending 2025-08-19 06:13:00 +02:00
kevlahnota
0923960215 Merge pull request #8442 from Jetz72/fixes20250818
Fix auto-sell cards reappearing in collection
2025-08-19 08:36:26 +08:00
Jetz
0f68dc1ab6 Make give item command case-insensitive 2025-08-18 19:06:52 -04:00
Jetz
09b88b8575 Fix auto-sell not being removed from adventure collection 2025-08-18 19:05:02 -04:00
kevlahnota
fae2f25b69 Merge pull request #8441 from kevlahnota/master3
update Itemmanager portrait layout, update Sentry scope
2025-08-19 06:29:03 +08:00
Anthony Calosa
d3e0696ecc update Itemmanager portrait layout, update Sentry scope 2025-08-19 06:19:44 +08:00
Paul Hammerton
6a7723eba9 Merge pull request #8440 from paulsnoops/fix-scripts
Fix Earth Rumble and Roku's Mastery
2025-08-18 23:05:34 +01:00
Paul Hammerton
10d6fd157e Fix Earth Rumble and Roku's Mastery 2025-08-18 23:01:10 +01:00
Paul Hammerton
048133df30 Merge pull request #8439 from paulsnoops/aa2-edition-fixes
AA2 edition spelling
2025-08-18 22:56:04 +01:00
Paul Hammerton
2fe7fec14f AA2 edition spelling 2025-08-18 22:53:36 +01:00
Paul Hammerton
73e7b27c09 Merge pull request #8438 from paulsnoops/yeoe-formats
Format updates: YEOE
2025-08-18 22:35:33 +01:00
Paul Hammerton
2fdbd5a85c Add YEOE to formats 2025-08-18 22:32:06 +01:00
Paul Hammerton
78e27e0073 Merge pull request #8437 from paulsnoops/edition-updates
Edition updates: AA1, AA2, SLD, TLA, TLE, YEOE
2025-08-18 22:25:38 +01:00
Paul Hammerton
bad585c8c9 Edition updates: AA1, AA2, SLD, TLA, TLE, YEOE 2025-08-18 22:23:11 +01:00
Eradev
11d10a8129 Fix Meld cards fetch (#7981)
* BOM

* ANB Update

* J25 Update

* SIR update

* Mend

* Find the right back for meld cards

* ANB Update

* J25 Update

* SIR update

* Mend

* Find the right back for meld cards

* Update PW25

* Update SPG

* Typo

* Update ACR

* Update CMM

* Add PFDN and Invasion alternate arts

* Update MKM

* Update NPH

* J25 -> J22 for non-existing cards

* Update WOC

* Update HBG

* Use printsheet

* Use helper functions
2025-08-18 14:31:33 +02:00
Agetian
0b89e9d137 - Add achievement for EOE/EOC by Marek14. (#8433) 2025-08-18 15:21:09 +03:00
kevlahnota
70df1ff0aa Merge pull request #8428 from Jetz72/fixes20250817
Handful of Deck Editor updates
2025-08-18 16:52:30 +08:00
Jetz
24d3169592 Enable variant replacement in Quest and Planar Conquest 2025-08-17 21:37:36 -04:00
Jetz
b8db0fea5e Disable custom cards conformity check in Adventure mode. 2025-08-17 21:11:51 -04:00
Jetz
514519b45b Merge branch 'master' into fixes20250817
# Conflicts:
#	forge-gui-mobile/src/forge/itemmanager/ItemManager.java
2025-08-17 21:01:42 -04:00
Jetz
70f6bcb63c Fix missing or crushed group and pile-by buttons in narrow layouts 2025-08-17 20:52:21 -04:00
Jetz
5a3e55f704 Fix crushed filter buttons in portrait mode 2025-08-17 20:51:20 -04:00
Jetz
4452e07443 Remove unused format filter visibility controls 2025-08-17 19:58:20 -04:00
Jetz
a125e6c0ea Support command line args width= and height= for easier testing of portrait layouts. 2025-08-17 19:57:21 -04:00
kevlahnota
fa4688e113 Merge pull request #8426 from kevlahnota/master3
fix Itemmanager portrait layout
2025-08-18 07:46:12 +08:00
Anthony Calosa
0236609558 fix Itemmanager portrait layout 2025-08-18 07:40:52 +08:00
kevlahnota
3f15fb1b98 Merge pull request #8424 from shenshinoman/Crystal_Expansions
Fixing bugs that were preventing proper world gen in Crystal Kingdoms…
2025-08-18 06:01:41 +08:00
kevlahnota
e22718c72a Merge pull request #8425 from kevlahnota/master3
fix RandomDeckGenerator AI Deck Selection by preference
2025-08-18 05:59:47 +08:00
Anthony Calosa
e94985576c refactor check 2025-08-18 05:56:54 +08:00
Anthony Calosa
71e132a9e0 fix RandomDeckGenerator AI Deck Selection by preference 2025-08-18 05:52:24 +08:00
Jetz
8d5d56bed8 Add dev cheat to add cards to inventory and draft packs 2025-08-17 17:18:17 -04:00
shenshinoman
386c9799c2 Fixing bugs that were preventing proper world gen in Crystal Kingdoms. Very basic fix, and a slight notice in the quests to inform the player that they have not been udpated yet. 2025-08-17 16:08:03 -04:00
Fulgur14
a1be657460 Airbending cards (#8422) 2025-08-17 19:36:15 +01:00
Fulgur14
d4994e8fa0 Fire Lord Ozai (TLE) (#8361)
* Support CombatMana
2025-08-17 17:03:26 +01:00
Renato Filipe Vidal Santos
fb054d9f64 Tidying DBCleanup (#8420) 2025-08-17 13:34:23 +01:00
kevlahnota
8790843b3b Merge pull request #8421 from kevlahnota/master3
revert fileprovider, add sentryscope
2025-08-17 20:32:51 +08:00
Anthony Calosa
38383dff02 revert fileprovider, add sentryscope 2025-08-17 20:20:33 +08:00
Hans Mackowiak
a0f9923c21 TLA: Airbend (#8419) 2025-08-17 13:24:11 +02:00
kevlahnota
812c0ac5b1 Merge pull request #8418 from kevlahnota/master3
update SettingsScene, Settings page indent
2025-08-17 11:12:43 +08:00
Anthony Calosa
2e09146f44 remove unused options 2025-08-17 11:02:48 +08:00
Anthony Calosa
25080411d5 update SettingsScene, Settings page indent
limit hasprintedpt to vehicles and spacecraft to fix modal card renders with non creature backside
- closes #8413
2025-08-17 10:52:17 +08:00
kevlahnota
55e20e96f9 Merge pull request #8414 from Jetz72/fixes20250816
Attempt to fix another NPE in deserializing decks
2025-08-17 06:14:31 +08:00
kevlahnota
981ab1c6ed prevent NPE 2025-08-17 06:13:51 +08:00
Hans Mackowiak
a1089f8073 Update the_cabbage_merchant.txt 2025-08-16 23:56:03 +02:00
tool4ever
fe747a3908 Update the_cabbage_merchant.txt 2025-08-16 21:50:39 +02:00
Paul Hammerton
a16c9480e7 Merge pull request #8412 from Eradev/FixEOE
Fix EOE def
2025-08-16 16:21:41 +01:00
Jetz72
b40e9fc817 Merge branch 'Card-Forge:master' into fixes20250816 2025-08-16 10:15:33 -05:00
Jetz
6eec0e8988 Fix another potential NPE deserializing old decks... 2025-08-16 11:13:42 -04:00
Hans Mackowiak
d6320caadf CardEdition: getPrintSheetsBySection use collectorNumber instead of index 2025-08-16 16:53:43 +02:00
Eradev
c4f125525a Fix EOE def 2025-08-16 10:38:43 -04:00
kevlahnota
56209261d0 Merge pull request #8411 from Jetz72/fixes20250816
Fix NPE deserializing old decks
2025-08-16 22:37:44 +08:00
Jetz
5df4d64345 Fix a potential NPE deserializing old decks.
Fix `Deck.cloneFieldsTo` to copy tags in the right direction.
2025-08-16 10:05:41 -04:00
kevlahnota
d6d6065104 Merge pull request #8402 from Jetz72/fixes20250815c
A few auto-sell section fixes
2025-08-16 14:32:04 +08:00
kevlahnota
333aea0641 Merge pull request #8405 from kevlahnota/master3
fix booster draft menu not showing on first try
2025-08-16 14:29:57 +08:00
Anthony Calosa
c7bb3d49a6 fix booster draft menu not showing on first try 2025-08-16 14:14:33 +08:00
Jetz
7c982498dd Avoid trying to generate a move option for cards that don't have a valid destination 2025-08-15 23:51:19 -04:00
Jetz
0e2c95afed Fix ClassCastException 2025-08-15 23:48:50 -04:00
Jetz
df6eb23341 Always show quantity of auto-sell section. 2025-08-15 23:48:31 -04:00
kevlahnota
1204de13c3 Merge pull request #8400 from Jetz72/fixes20250815b
Fix negative gold payouts from selling cards.
2025-08-16 09:55:23 +08:00
Jetz
9cb21de11d Fix negative gold payouts from selling cards. 2025-08-15 21:37:58 -04:00
kevlahnota
2f16b9e110 Merge pull request #8399 from Jetz72/fixes20250815
Fix some crashes from the new editor
2025-08-16 07:22:57 +08:00
Jetz
cf28139b02 Fix a second crash for the same reason... 2025-08-15 19:19:28 -04:00
Jetz
87e8e7d5e5 Fix crash entering a game without extra sections 2025-08-15 19:12:40 -04:00
Jetz
dcc3a681d9 Whoops committed the wrong file.
This reverts commit cc7f30da88.
2025-08-15 19:12:39 -04:00
Jetz
cc7f30da88 Fix crash entering a game without extra sections 2025-08-15 19:05:59 -04:00
Jetz
ed9eabed38 Fix Jumpstart deck preview 2025-08-15 18:31:45 -04:00
Paul Hammerton
99ac95bfca Merge pull request #8398 from paulsnoops/edition-updates
Edition updates: PMEI
2025-08-15 21:24:05 +01:00
Paul Hammerton
eb178fc9d1 Edition updates: PMEI 2025-08-15 21:15:07 +01:00
Paul Hammerton
7567e29cf1 Merge pull request #8397 from paulsnoops/edition-updates
Edition updates: TLA, TLE, YEOE
2025-08-15 19:18:48 +01:00
Paul Hammerton
b2a456140c YEOE 2025-08-15 19:11:49 +01:00
Paul Hammerton
9c3ff6b570 Edition updates: TLA, TLE 2025-08-15 18:40:44 +01:00
Hans Mackowiak
e4d238ba6c ~lf 2025-08-15 19:27:29 +02:00
Hans Mackowiak
42363204dd add PrintSheet & ItemPool helper 2025-08-15 19:27:09 +02:00
Jetz72
de89d557dd Mobile Deck Editor Reorganization and Feature Update (#7519)
Mobile Deck Editor Reorg
2025-08-15 11:48:25 -04:00
kevlahnota
c5a53b21e7 Merge pull request #8396 from kevlahnota/master3
fix flip cards redownloading same image with different name
2025-08-15 20:43:07 +08:00
Anthony Calosa
02f43b1ef0 update comment 2025-08-15 20:37:58 +08:00
Anthony Calosa
d7cb0b7ac1 fix flip cards redownloading same image with different name
also fix sync for sentry native ndk since this is declared as system scope
2025-08-15 20:34:26 +08:00
kevlahnota
2efeb573e6 Merge pull request #8395 from Eradev/DisplayFlipCards
Display flip cards correctly
2025-08-15 20:10:24 +08:00
kevlahnota
370ff638ab Merge pull request #8394 from dracontes/rv-fix_odin
Update summon_primal_odin.txt
2025-08-15 20:10:10 +08:00
kevlahnota
108564efe3 Merge pull request #8392 from Eradev/UpdatePLST
Update PLST
2025-08-15 20:09:34 +08:00
Eradev
43f96657e0 Rename vars 2025-08-15 06:57:06 -04:00
Eradev
cb004bfba2 Fix imports 2025-08-15 06:53:17 -04:00
Eradev
f276597a82 Flip in Reward screen 2025-08-15 06:49:18 -04:00
Renato Filipe Vidal Santos
4fd9855daa Update summon_primal_odin.txt 2025-08-15 11:27:31 +01:00
Eradev
ebdce74f92 Fix display in inventory 2025-08-15 06:20:20 -04:00
Eradev
98e9b8f28b Display flipped cards correctly 2025-08-15 05:54:31 -04:00
Hans Mackowiak
a31c0358a4 Update TriggerType.java
Add the other Bending trigger
2025-08-15 11:21:22 +02:00
Fulgur14
8640522fff TLA/TLE cards (14th August), Batch 4 (#8387) 2025-08-15 09:59:17 +02:00
Fulgur14
a5fa350e7e TLA/TLE cards (14th August), Batch 3 (#8385) 2025-08-15 09:57:34 +02:00
Fulgur14
c4a0cff1ca TLA/TLE cards (14th August), Batch 2 (#8383) 2025-08-15 09:56:22 +02:00
Fulgur14
30d02ebbb6 TLA/TLE cards (14th August), Batch 1 (#8382) 2025-08-15 09:48:11 +02:00
Hans Mackowiak
9a90359283 Update EarthbendEffect.java
Fix apostrophe
2025-08-15 09:44:55 +02:00
Fulgur14
edbcab544e TLA/TLE Earthbending cards (#8393) 2025-08-15 09:42:48 +02:00
Hans Mackowiak
f55bf4691d TLA: Earthbend (#8386) 2025-08-15 09:24:45 +02:00
Eradev
16e44309a6 Update PLST 2025-08-15 02:43:27 -04:00
tool4ever
c4d58e3dba Update wormfang_drake.txt 2025-08-15 08:33:17 +02:00
Ellios77
16155d3670 Punctuation fixes for card scripts (#8389) 2025-08-15 08:21:49 +02:00
kevlahnota
bd929bcc72 Merge pull request #8388 from kevlahnota/master3
add Preload Custom Drafts
2025-08-15 08:52:49 +08:00
Anthony Calosa
e2cc52fd02 add Preload Custom Drafts
Should fix longer startup time on mobile version instead of forcing to load custom drafts file at startup.
2025-08-15 08:45:21 +08:00
Paul Hammerton
be5c7cfd04 Merge pull request #8381 from paulsnoops/unf-update
Edition updates: UNF
2025-08-14 18:27:57 +01:00
Paul Hammerton
9d14949138 Edition updates: UNF 2025-08-14 18:25:28 +01:00
tool4ever
6fe5dbad3c Update InputAttack.java 2025-08-14 16:53:18 +00:00
Matthew Scott Krafczyk
f4f8ee9cb6 android-dev-build (#8352)
* improve resource finding procedure and add android-dev-build

Added the `android-dev-build` profile to trigger building a developer
apk.

Developer apk is at application id forge.app.dev, and has name
'Forge (dev)'. It is installable in parallel with the official release
app allowing developers to test the android build on their phones
without interfering with their existing installation.

Updated how android resources are found to accomodate changes in
application id at runtime. This method doesn't rely on the 'R' package.

* Use all arguments of getIdentifier

Use all arguments of getIdentifier instead of using a fully qualified
resource name
2025-08-14 19:06:54 +03:00
Antonio Martinelli
0767eb03e0 Add LSVCube and Name-Sticker Goblin (card) (#8348)
* Add LSVCube and draft

* Add "Name Sticker" Goblin card (MTGO Version)

---------

Co-authored-by: Antonio <mart@gmail.com>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-08-14 19:06:46 +03:00
kevlahnota
feb6062f2f Update MakeCardEffect.java
- closes #8373
2025-08-14 22:44:23 +08:00
Eradev
113dc4d5e8 Don't try to attack itself. (#8376) 2025-08-14 16:11:00 +02:00
Hans Mackowiak
ca20d98a2c Update Tarkir Dragonstorm.txt 2025-08-14 14:15:00 +02:00
Hans Mackowiak
599a629068 Update Dungeons & Dragons Adventures in the Forgotten Realms.txt
Also add Dungeons to "other" section
2025-08-14 09:54:59 +02:00
Hans Mackowiak
4c05ad655d Update Dungeons & Dragons Adventures in the Forgotten Realms.txt
Fix Dungeon CN
2025-08-14 09:22:13 +02:00
Ellios77
203233f0d2 Fixed typo in script (#8372) 2025-08-14 09:18:31 +02:00
Hans Mackowiak
ac8f7357db Update corrupted_shapeshifter.txt 2025-08-14 06:29:33 +02:00
kevlahnota
4c396065f5 Merge pull request #8370 from kevlahnota/master3
update Sentry
2025-08-14 06:07:54 +08:00
Anthony Calosa
cc4dac38fe update Sentry 2025-08-14 05:59:47 +08:00
Eradev
394818f533 Filter by foil status (#8358) 2025-08-13 19:45:53 +03:00
Eradev
bfd4a68e23 Try to attack mustAttackEntities first. (#8355) 2025-08-13 19:45:42 +03:00
Paul Hammerton
9d54e82214 Merge pull request #8368 from paulsnoops/edition-updates
Edition updates: TLA, TLE
2025-08-13 17:41:30 +01:00
Paul Hammerton
18c2278066 Edition updates: TLA, TLE 2025-08-13 17:40:00 +01:00
tool4ever
b3d549a712 Update mirror_shield_hoplite.txt 2025-08-13 16:22:58 +00:00
tool4ever
0887e78e90 Update agrus_kos_eternal_soldier.txt 2025-08-13 16:22:26 +00:00
Hans Mackowiak
df3f86dd73 CardState: Fix Card copies itself though ReplacementEffect (#8356)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-13 17:51:14 +02:00
Hans Mackowiak
89af1d0cf1 Update Card.java 2025-08-13 17:07:32 +02:00
Eradev
10a68d63a1 Fix exact search (#8357) 2025-08-13 08:46:47 +00:00
Hans Mackowiak
8ccebbc13c Update The List.txt
Closes #8363
2025-08-13 10:37:22 +02:00
Fulgur14
4c663cd943 TLA/TLE cards that don't require bending (#8359) 2025-08-13 08:36:41 +00:00
Hans Mackowiak
ca16bf74f5 Update Card.hasNoAbilities
For Adventure and Omen
2025-08-13 10:09:01 +02:00
tool4ever
a1968ef9fd Update depthshaker_titan.txt 2025-08-13 07:58:25 +02:00
kevlahnota
de1a999611 Merge pull request #8353 from kevlahnota/master3
exclude unused xcf files in releases
2025-08-12 20:56:41 +08:00
Anthony Calosa
d0e8bc5de0 exclude unused xcf files in releases
- closes #8322
2025-08-12 20:49:49 +08:00
Eradev
38fb647cd7 Migration to PLST (#7972)
* Fallback to PLST

* Fix audit for Funny cards.

* Remove FMB1, update PLST

* Fallback to PLST

* Convert cards to PLST

* BOM

* Minor fix

* LF

* Remove conversion. Already handled.
2025-08-12 08:17:11 +03:00
kevlahnota
90bb83c5d4 Merge pull request #8351 from kevlahnota/master3
add backup/restore classic mode data
2025-08-11 22:00:07 +08:00
Anthony Calosa
467fff3651 add backup/restore classic mode data 2025-08-11 21:53:19 +08:00
Hans Mackowiak
3ab50a5fb5 Update samis_curiosity.txt
Add StackDesc for Lander Token
2025-08-11 09:35:03 +02:00
Tim Miller
e4d58a0a88 Add other deck sections for other formats 2025-08-10 17:03:30 -04:00
kevlahnota
10e592c741 Merge pull request #8343 from kevlahnota/master3
request external storage access
2025-08-10 20:04:33 +08:00
Anthony Calosa
b3241c85eb request external storage access
- closes #7405
2025-08-10 19:56:29 +08:00
Cees Timmerman
abd1702c96 Fix NullPointerException in forge.game.zone.Zone.toString (#8326) (#8327) 2025-08-10 08:03:56 +02:00
Hans Mackowiak
d91e86c608 Update GameEventManaPool.java (#8340)
Closes #8328, #8329
2025-08-09 16:13:49 +02:00
Hans Mackowiak
f0ebac019d Perpetual: use Record for different types (#8337)
* Perpetual: use Record for different types
2025-08-09 13:04:14 +02:00
tool4ever
c18995915a Fix scripts (#8339) 2025-08-09 10:50:27 +00:00
Paul Hammerton
55d2c23aa0 Merge pull request #8338 from paulsnoops/edition-updates
Edition updates: PSPM, PW25, SLD, SPM
2025-08-09 10:57:58 +01:00
Paul Hammerton
28c9f719c5 Edition updates: PSPM, PW25, SLD, SPM 2025-08-09 10:56:08 +01:00
Hans Mackowiak
bb5a276324 Update kessig.txt
Fix ActiveZones
2025-08-09 08:51:13 +02:00
tool4ever
e99df5b267 Update wellgabber_apothecary.txt
Closes #8334
2025-08-08 18:07:11 +00:00
kevlahnota
54e67b3d57 Merge pull request #8333 from kevlahnota/master3
update progressbar, update boosterdraft readfile
2025-08-08 22:01:42 +08:00
Anthony Calosa
b1d30ab9dd remove unused borderless list 2025-08-08 21:31:32 +08:00
Anthony Calosa
f2d65b2515 update check 2025-08-08 21:01:58 +08:00
Anthony Calosa
286fcd3fbe update progressbar, update boosterdraft readfile
also removed unneccesary settings since forge uses shaders for round corners for rendering borders
2025-08-08 20:46:12 +08:00
Hans Mackowiak
00332ff3fe Fix Phantom Train not updating PT (#8332)
* Fix Phantom Train not updating PT
2025-08-08 12:52:29 +02:00
tool4ever
000fa14c17 Fix voting crash (#8325) 2025-08-08 10:15:27 +02:00
tool4ever
c5b4be6c97 Update edgar_master_machinist.txt 2025-08-07 16:13:52 +00:00
kevlahnota
d9b2a8f4e0 Merge pull request #8321 from kevlahnota/master3
update sentry
2025-08-07 21:50:08 +08:00
Anthony Calosa
2898b00300 update sentry 2025-08-07 21:42:20 +08:00
Hans Mackowiak
a4671b62d4 CardStateName: combine Modal with Transformed (#8305)
* CardStateName: combine Modal with Transformed

* Make new TMDFC transformable

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-07 11:28:24 +02:00
kevlahnota
0c87ed7381 Merge pull request #8319 from kevlahnota/master3
fix VPrompt
2025-08-07 05:29:34 +08:00
Anthony Calosa
4104a0b2a8 fix VPrompt 2025-08-07 05:28:13 +08:00
tool4ever
4d62f436d5 ExiledWith check for keyworded (#8317) 2025-08-06 19:41:39 +00:00
kevlahnota
506f88bd6f Update friendly_neighborhood.txt
fix no API
2025-08-06 20:47:32 +08:00
Hans Mackowiak
a1ad355e25 Update CardEdition.java
Closes #8314 8314
2025-08-06 14:17:13 +02:00
kevlahnota
91d6571cff Merge pull request #8315 from kevlahnota/master3
Reversed prompt button toggle
2025-08-06 18:57:55 +08:00
Anthony Calosa
e43c5534b3 Reversed prompt button toggle
- closes #7366
2025-08-06 18:51:49 +08:00
Jetz
77e3453d35 Fix vote out card type 2025-08-05 21:50:39 -04:00
Jetz
2568df1b91 Reorder ReplaceMoved.canReplace to do some common easy checks first. 2025-08-05 21:50:39 -04:00
Jetz
dc830a8432 Include weird zones in forEachCardInGame 2025-08-05 21:50:39 -04:00
Chris H
a9cffbd313 Collect all of slots paper cards before adding it to the final result 2025-08-05 21:05:49 -04:00
tool4ever
65775f5c20 Update moonlit_meditation.txt
Closes #8310
2025-08-05 20:00:03 +00:00
Fulgur14
c6f9056e2c Gwen and Miles (Transformation to be added later) (#8306)
* Gwen and Miles (Transformation to be added later)

* Create miles_morales_ultimate_spider_man.txt

* Update miles_morales_ultimate_spider_man.txt

* Update miles_morales_ultimate_spider_man.txt
2025-08-05 19:06:01 +03:00
Hans Mackowiak
967807d441 Merge pull request #8307
Fixes Vote Protection
2025-08-05 11:17:15 +02:00
Hans Mackowiak
cc7b3505d7 Update ComputerUtilCard.java 2025-08-05 09:54:43 +02:00
Hans Mackowiak
3cd965a24f Update ComputerUtilCard.java 2025-08-05 09:49:11 +02:00
Hans Mackowiak
3502bf6de2 Update ComputerUtil.java
Fix
2025-08-05 09:45:31 +02:00
Hans Mackowiak
458e05635c Update CardFactoryUtil.java
Allow Iterable for getMostProminentColorsFromList
2025-08-05 09:42:47 +02:00
Hans Mackowiak
f4cd9e0f4f Update ComputerUtil.java
Fix Vote "Protection"
2025-08-05 09:40:12 +02:00
tool4EvEr
1f3871d5dc Thread.stop fallback 2025-08-04 22:15:23 +02:00
tool4ever
f4d304b2c1 Update abyssal_hunter.txt 2025-08-04 13:00:15 +00:00
tool4ever
24937fb32d Reduce potential for ConcurrentModificationException (#8302) 2025-08-04 14:30:10 +02:00
Hans Mackowiak
5d222577be Update AddTurnAi.java
Fix AddTurnAi
2025-08-04 14:15:46 +02:00
Hans Mackowiak
0e010cece2 Update ComputerUtil.java
Fix vote "StrengthOrNumbers"
2025-08-04 14:15:02 +02:00
Hans Mackowiak
21080d65f5 Create b_0_0_germ.txt
Closes #8301
2025-08-04 13:20:47 +02:00
Fulgur14
83475b9063 Carnage, Crimson Chaos (SPM) (#8299) 2025-08-04 07:40:39 +02:00
tool4ever
15a6494d9c Update space_time_anomaly.txt
Closes #8297
2025-08-03 16:26:40 +00:00
Hans Mackowiak
58ad1f3103 Keyword: Web-slinging 2025-08-03 16:24:52 +02:00
Hans Mackowiak
b2bff7762f Mayhem: add new Cast-from-Graveyard variant 2025-08-03 16:24:24 +02:00
kevlahnota
cd4a673862 Merge pull request #8295 from kevlahnota/master3
fix NPE and FChoiceList render display
2025-08-03 20:54:13 +08:00
Anthony Calosa
1f61c6bd00 fix NPE and FChoiceList render display 2025-08-03 20:51:02 +08:00
Cees Timmerman
cc2f46795f Fix infinite loop OOM crash (#8254) 2025-08-02 15:15:49 +02:00
Chris H
db5ea3ec9c Fix undo restore restoring to bottom of library 2025-08-02 15:03:27 +02:00
Adam Jones
21f2f6e2c3 Update forge.sh
Allow parameters to be passed in to the shell script
2025-08-02 15:03:11 +02:00
Hans Mackowiak
65b5f4fac3 VoteEffect: use SpellAbility Choices (#8286)
* VoteEffect: use SpellAbility Choices

* fixes

* fix VotePlayer

* subAbs only for Choices
2025-08-02 16:01:12 +03:00
tool4ever
2291cf9d0c Fix scripts (#8288) 2025-08-02 11:29:01 +00:00
Paul Hammerton
3f8b3fa2a7 Merge pull request #8287 from paulsnoops/edition-updates
Edition updates: PW25, SLD, SPM
2025-08-02 10:24:59 +01:00
Paul Hammerton
e6fa1268d9 Edition updates: PW25, SLD, SPM 2025-08-02 10:23:36 +01:00
Renato Filipe Vidal Santos
eda8205acd Fixing Ultima, Origin of Oblivion (#8278)
* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt
2025-08-02 08:20:50 +03:00
Renato Filipe Vidal Santos
019131447a Update icetill_explorer.txt 2025-08-02 06:59:59 +02:00
kevlahnota
41cd90bc2a Fix dual foil renders 2025-08-02 00:38:03 +08:00
kevlahnota
0fe1677ced Update Foil rotation check 2025-08-02 00:27:00 +08:00
tool4ever
b1a2a93f75 Update PumpAllAi.java 2025-08-01 12:43:56 +02:00
tool4ever
50a2c283f1 Update lightstall_inquisitor.txt 2025-08-01 08:34:25 +02:00
kevlahnota
83188daba4 Merge pull request #8275 from Card-Forge/kevlahnota-rotatefix
Extra check for rotation
2025-08-01 08:31:50 +08:00
kevlahnota
3312e409fd Extra check for rotation 2025-08-01 08:24:38 +08:00
tool4ever
5420a90f05 Update iron_spider_stark_upgrade.txt
Closes #8272
2025-07-31 20:16:28 +00:00
tool4ever
4d340d7bf3 Update wrathful_raptors.txt 2025-07-31 20:15:06 +00:00
tool4ever
5342175a97 Update wrathful_red_dragon.txt 2025-07-31 20:14:39 +00:00
tool4ever
e5a1c86b1f Align invalid Player property result with Card (for Lozhan) (#8264) 2025-07-31 18:56:55 +00:00
tool4ever
1aca32005b Fix Brave the Wilds (#8266)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-31 21:32:12 +03:00
tool4ever
6d7853428f Fix bad refactor (for real this time) (#8263)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-31 20:05:05 +02:00
kevlahnota
683d92399d FDeckEditor Foil Menu (#8260) 2025-07-31 21:58:05 +08:00
kevlahnota
9b9bf6cf53 update holofoil texture (#8259)
a bit modern but has more blended effect with most card images along with hue changes effect
2025-07-31 20:24:55 +08:00
kevlahnota
c9b826d9b4 Merge pull request #8237 from kevlahnota/master2
Refactor Foil Effect for Mobile
2025-07-31 05:48:36 +08:00
kevlahnota
81ff72f1f2 Merge pull request #8189 from kevlahnota/nyxDesktop
add nyx effect to desktop card renders
2025-07-31 05:45:46 +08:00
tool4ever
dfda04f7ba Fix bad refactor (#8252) 2025-07-30 20:43:20 +00:00
tool4ever
3bc0e0c386 More AI cleanup (#8251)
* Fix infinite loop
2025-07-30 19:59:20 +00:00
Hans Mackowiak
5829192446 Update valgavoths_onslaught.txt
fix SpellDescription
2025-07-30 21:50:57 +02:00
Chris H
cbae45be87 Update shaun_father_of_synths.txt (#8250) 2025-07-30 19:23:54 +00:00
Agetian
2b4ec73318 - Tweak marked damage on PS_FIN4. (#8249) 2025-07-30 18:21:59 +03:00
Agetian
7ff426e413 - Add puzzle PS_FIN4. (#8248) 2025-07-30 18:19:26 +03:00
shenshinoman
25e3ad8432 Resolving the land shop issue in Shandalar Old World. (#8231) 2025-07-30 17:52:25 +03:00
tool4ever
051edff18e Consolidate logic (#8247) 2025-07-30 09:50:59 +00:00
Anthony Calosa
100ae7b164 update dispose 2025-07-30 08:51:32 +08:00
Anthony Calosa
3b4c417549 Refactor Foil Effect for Mobile
Should fix rendering for Full, Crop and Art renders
2025-07-29 19:42:01 +08:00
kevlahnota
3fcb9e7a3c add stars.png 2025-07-24 19:57:11 +08:00
Anthony Calosa
f69ff10b7c add nyx effect to desktop card renders 2025-07-24 19:31:26 +08:00
2383 changed files with 47758 additions and 34823 deletions

View File

@@ -129,7 +129,9 @@ jobs:
makeLatest: true
- name: 🔧 Install XML tools
run: sudo apt-get install -y libxml2-utils
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: 🔼 Bump versionCode in root POM
id: bump_version

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17' ]
java: ['17', '21']
name: Test with Java ${{ matrix.Java }}
steps:
- uses: actions/checkout@v3

7
.gitignore vendored
View File

@@ -66,6 +66,9 @@ forge-gui-mobile-dev/testAssets
forge-gui/res/cardsfolder/*.bat
# Generated changelog file
forge-gui/release-files/CHANGES.txt
forge-gui/res/PerSetTrackingResults
forge-gui/res/decks
forge-gui/res/layouts
@@ -87,3 +90,7 @@ forge-gui/tools/PerSetTrackingResults
*.tiled-session
/forge-gui/res/adventure/*.tiled-project
/forge-gui/res/adventure/*.tiled-session
# Ignore python temporaries
__pycache__
*.pyc

View File

@@ -1,33 +0,0 @@
Summary
(Summarize the bug encountered concisely)
Steps to reproduce
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
What is the current bug behavior?
(What actually happens)
What is the expected correct behavior?
(What you should see instead)
Relevant logs and/or screenshots
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
Possible fixes
(If you can, link to the line of code that might be responsible for the problem)
/label ~needs-investigation

View File

@@ -1,15 +0,0 @@
Summary
(Summarize the feature you wish concisely)
Example screenshots
(If this is a UI change, please provide an example screenshot of how this feature might work)
Feature type
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
/label ~feature request

View File

@@ -15,7 +15,7 @@ public class Main {
public static void main(String[] args) {
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
GuiBase.setDeviceInfo("", "", 0, 0);
GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/");
new EditorMainWindow(Config.instance());
}
}

View File

@@ -59,7 +59,6 @@ public class AiCardMemory {
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
BOUNCED_THIS_TURN, // These cards were bounced this turn
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part

View File

@@ -66,13 +66,11 @@ import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import java.util.*;
import java.util.concurrent.FutureTask;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
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;
@@ -99,6 +97,7 @@ public class AiController {
private int lastAttackAggression;
private boolean useLivingEnd;
private List<SpellAbility> skipped;
private boolean timeoutReached;
public AiController(final Player computerPlayer, final Game game0) {
player = computerPlayer;
@@ -483,7 +482,7 @@ public class AiController {
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
// don't play MDFC land if other side is spell and enough lands are available
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
return false;
}
@@ -888,27 +887,8 @@ public class AiController {
private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
final Card host = sa.getHostCard();
// Check a predefined condition
if (sa.hasParam("AICheckSVar")) {
final String svarToCheck = sa.getParam("AICheckSVar");
String comparator = "GE";
int compareTo = 1;
if (sa.hasParam("AISVarCompare")) {
final String fullCmp = sa.getParam("AISVarCompare");
comparator = fullCmp.substring(0, 2);
final String strCmpTo = fullCmp.substring(2);
try {
compareTo = Integer.parseInt(strCmpTo);
} catch (final Exception ignored) {
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
}
}
int left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
if (!Expressions.compare(left, comparator, compareTo)) {
return AiPlayDecision.AnotherTime;
}
if (sa.hasParam("AICheckSVar") && !aiShouldRun(sa, sa, host, null)) {
return AiPlayDecision.AnotherTime;
}
// this is the "heaviest" check, which also sets up targets, defines X, etc.
@@ -926,7 +906,7 @@ public class AiController {
// check if enough left (pass memory indirectly because we don't want to include those)
Set<Card> tappedForMana = AiCardMemory.getMemorySet(player, MemorySet.PAYS_TAP_COST);
if (tappedForMana != null && tappedForMana.isEmpty() &&
if (tappedForMana != null && !tappedForMana.isEmpty() &&
!ComputerUtilCost.checkTapTypeCost(player, sa.getPayCosts(), host, sa, new CardCollection(tappedForMana))) {
return AiPlayDecision.CantAfford;
}
@@ -1666,8 +1646,10 @@ public class AiController {
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
final ExecutorService executor = Executors.newSingleThreadExecutor();
Future<SpellAbility> future = executor.submit(() -> {
// in case of infinite loop reset below would not be reached
timeoutReached = false;
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
//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)) {
@@ -1676,6 +1658,11 @@ public class AiController {
continue;
}
if (timeoutReached) {
timeoutReached = false;
break;
}
if (sa.getHostCard().hasKeyword(Keyword.STORM)
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
&& player.getZone(ZoneType.Hand).contains(
@@ -1745,11 +1732,21 @@ public class AiController {
return null;
});
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
Thread t = new Thread(future);
t.start();
try {
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
future.cancel(true);
try {
t.stop();
} catch (UnsupportedOperationException ex) {
// Android and Java 20 dropped support to stop so sadly thread will keep running
timeoutReached = true;
future.cancel(true);
// TODO wait a few more seconds to try and exit at a safe point before letting the engine continue
// TODO mark some as skipped to increase chance to find something playable next priority
}
return null;
}
}
@@ -1801,14 +1798,9 @@ public class AiController {
* @param sa the sa
* @return true, if successful
*/
public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) {
Card hostCard = effect.getHostCard();
if (hostCard.hasAlternateState()) {
hostCard = game.getCardState(hostCard);
}
public final boolean aiShouldRun(final CardTraitBase effect, final SpellAbility sa, final Card host, final GameEntity affected) {
if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) {
final Player controller = hostCard.getController();
final Player controller = host.getController();
if (affected instanceof Player) {
return !((Player) affected).isOpponentOf(controller);
}
@@ -1817,7 +1809,6 @@ public class AiController {
}
}
if (effect.hasParam("AICheckSVar")) {
System.out.println("aiShouldRun?" + sa);
final String svarToCheck = effect.getParam("AICheckSVar");
String comparator = "GE";
int compareTo = 1;
@@ -1830,9 +1821,9 @@ public class AiController {
compareTo = Integer.parseInt(strCmpTo);
} catch (final Exception ignored) {
if (sa == null) {
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect);
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), effect);
} else {
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa);
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
}
}
}
@@ -1840,13 +1831,12 @@ public class AiController {
int left = 0;
if (sa == null) {
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect);
left = AbilityUtils.calculateAmount(host, svarToCheck, effect);
} else {
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
}
System.out.println("aiShouldRun?" + left + comparator + compareTo);
return Expressions.compare(left, comparator, compareTo);
} else if (effect.hasParam("AICheckDredge")) {
} else if (effect.isKeyword(Keyword.DREDGE)) {
return player.getCardsIn(ZoneType.Library).size() > 8 || player.isCardInPlay("Laboratory Maniac");
} else return sa != null && doTrigger(sa, false);
}

View File

@@ -29,12 +29,15 @@ public class AiCostDecision extends CostDecisionMakerBase {
private final CardCollection tapped;
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) {
this(ai0, sa, effect, false);
}
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect, final boolean payMana) {
super(ai0, effect, sa, sa.getHostCard());
discarded = new CardCollection();
tapped = new CardCollection();
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
if (tappedForMana != null) {
if (!payMana && tappedForMana != null) {
tapped.addAll(tappedForMana);
}
}
@@ -110,7 +113,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability);
}
return PaymentDecision.card(randomSubset);
} else if (type.equals("DifferentNames")) {
} else if (type.contains("+WithDifferentNames")) {
CardCollection differentNames = new CardCollection();
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
while (c > 0) {
@@ -563,7 +566,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) {
removed += thisRemove;
table.put(null, prefCard, CounterType.get(cType), thisRemove);
table.put(null, prefCard, cType, thisRemove);
}
}
}
@@ -573,7 +576,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostRemoveAnyCounter cost) {
final int c = cost.getAbilityAmount(ability);
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source);
if (c <= 0) {
return null;
@@ -716,7 +719,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) {
toRemove += over;
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
table.put(null, crd, CounterEnumType.QUEST, over);
}
}
}

View File

@@ -19,7 +19,7 @@ public enum AiPlayDecision {
StackNotEmpty,
AnotherTime,
// Don't play decision reasons,
// Don't play decision reasons
CantPlaySa,
CantPlayAi,
CantAfford,

View File

@@ -767,7 +767,7 @@ public class ComputerUtil {
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
if (untap) {
typeList.remove(activate);
@@ -1074,6 +1074,80 @@ public class ComputerUtil {
return prevented;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
final Card card = sa.getHostCard();
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
@@ -1245,80 +1319,6 @@ public class ComputerUtil {
return false;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final SpellAbility sub = sa.getSubAbility();
@@ -1327,7 +1327,6 @@ public class ComputerUtil {
return true;
}
// Cipher spells
if (sub != null) {
final ApiType api = sub.getApi();
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
@@ -1622,7 +1621,6 @@ public class ComputerUtil {
damage = dmg;
}
// Triggered abilities
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
for (final Trigger t : c.getTriggers()) {
if (TriggerType.Attacks.equals(t.getMode())) {
@@ -2544,7 +2542,7 @@ public class ComputerUtil {
boolean opponent = controller.isOpponentOf(ai);
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
final CounterType p1p1Type = CounterEnumType.P1P1;
if (!sa.hasParam("AILogic")) {
return Aggregates.random(options);
@@ -2553,7 +2551,7 @@ public class ComputerUtil {
String logic = sa.getParam("AILogic");
switch (logic) {
case "Torture":
return "Torture";
return options.get(1);
case "GraceOrCondemnation":
List<ZoneType> graceZones = new ArrayList<ZoneType>();
graceZones.add(ZoneType.Battlefield);
@@ -2561,12 +2559,12 @@ public class ComputerUtil {
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
return aiGrace > humanGrace ? "Grace" : "Condemnation";
return options.get(aiGrace > humanGrace ? 0 : 1);
case "CarnageOrHomage":
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
CardCollection computerlist = ai.getCreaturesInPlay();
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
case "Judgment":
if (votes.isEmpty()) {
CardCollection list = new CardCollection();
@@ -2580,68 +2578,71 @@ public class ComputerUtil {
return Iterables.getFirst(votes.keySet(), null);
case "Protection":
if (votes.isEmpty()) {
List<String> restrictedToColors = Lists.newArrayList();
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
for (Object o : options) {
if (o instanceof String) {
restrictedToColors.add((String) o);
}
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
restrictedToColors.put(sp.getOriginalDescription(), sp);
}
}
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
}
return Iterables.getFirst(votes.keySet(), null);
case "FeatherOrQuill":
SpellAbility feather = (SpellAbility)options.get(0);
SpellAbility quill = (SpellAbility)options.get(1);
// try to mill opponent with Quill vote
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
int numQuill = votes.get("Quill").size();
int numQuill = votes.get(quill).size();
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Feather" : "Quill";
return opponent ? feather : quill;
}
// if source is not on the battlefield anymore, choose +1/+1 ones
if (!game.getCardState(source).isInPlay()) {
return opponent ? "Feather" : "Quill";
return opponent ? feather : quill;
}
// if no hand cards, try to mill opponent
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
return opponent ? "Quill" : "Feather";
return opponent ? quill : feather;
}
// AI has something to discard
if (ai.equals(controller)) {
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
return "Quill";
return quill;
}
}
// default card draw and discard are better than +1/+1 counter
return opponent ? "Feather" : "Quill";
return opponent ? feather : quill;
case "StrengthOrNumbers":
SpellAbility strength = (SpellAbility)options.get(0);
SpellAbility numbers = (SpellAbility)options.get(1);
// similar to fabricate choose +1/+1 or Token
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
int numStrength = votes.get("Strength").size();
int numNumbers = votes.get("Numbers").size();
int numStrength = votes.get(strength).size();
int numNumbers = votes.get(numbers).size();
Card token = TokenAi.spawnToken(controller, saToken);
Card token = TokenAi.spawnToken(controller, numbers);
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Strength" : "Numbers";
return opponent ? strength : numbers;
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? "Strength" : "Numbers";
return opponent ? strength : numbers;
}
// token would not survive
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
return opponent ? "Numbers" : "Strength";
return opponent ? numbers : strength;
}
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
@@ -2662,35 +2663,40 @@ public class ComputerUtil {
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
case "SproutOrHarvest":
SpellAbility sprout = (SpellAbility)options.get(0);
SpellAbility harvest = (SpellAbility)options.get(1);
// lifegain would hurt or has no effect
if (opponent) {
if (lifegainNegative(controller, source)) {
return "Harvest";
return harvest;
}
} else {
if (lifegainNegative(controller, source)) {
return "Sprout";
return sprout;
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Sprout" : "Harvest";
return opponent ? sprout : harvest;
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? "Sprout" : "Harvest";
return opponent ? sprout : harvest;
}
// TODO add Lifegain to +1/+1 counters trigger
// for now +1/+1 counters are better
return opponent ? "Harvest" : "Sprout";
return opponent ? harvest : sprout;
case "DeathOrTaxes":
int numDeath = votes.get("Death").size();
int numTaxes = votes.get("Taxes").size();
SpellAbility death = (SpellAbility)options.get(0);
SpellAbility taxes = (SpellAbility)options.get(1);
int numDeath = votes.get(death).size();
int numTaxes = votes.get(taxes).size();
if (opponent) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
@@ -2698,29 +2704,29 @@ public class ComputerUtil {
// would need to sacrifice more creatures than AI has
// sacrifice even more
if (aiCreatures.size() <= numDeath) {
return "Death";
return death;
}
// would need to discard more cards than it has
if (aiCardsInHand.size() <= numTaxes) {
return "Taxes";
return taxes;
}
// has cards with SacMe or Token
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
return "Death";
return death;
}
// has cards with DiscardMe
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
return "Taxes";
return taxes;
}
// discard is probably less worse than sacrifice
return "Taxes";
return taxes;
} else {
// ai is first voter or ally of controller
// both are not affected, but if opponents control creatures, sacrifice is worse
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
}
default:
return Iterables.getFirst(options, null);
@@ -2737,7 +2743,7 @@ public class ComputerUtil {
return safeCards;
}
public static Card getKilledByTargeting(final SpellAbility sa, Iterable<Card> validCards) {
public static Card getKilledByTargeting(final SpellAbility sa, CardCollectionView validCards) {
CardCollection killables = CardLists.filter(validCards, c -> c.getController() != sa.getActivatingPlayer() && c.getSVar("Targeting").equals("Dies"));
return ComputerUtilCard.getBestCreatureAI(killables);
}
@@ -3098,41 +3104,38 @@ public class ComputerUtil {
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
final Card source = sa.getHostCard();
if (source == null) { return srcList; }
if (sa.hasParam("AITgts")) {
CardCollection list;
String aiTgts = sa.getParam("AITgts");
if (aiTgts.startsWith("BetterThan")) {
int value = 0;
if (aiTgts.endsWith("Source")) {
value = ComputerUtilCard.evaluateCreature(source);
if (source.isEnchanted()) {
for (Card enc : source.getEnchantedBy()) {
if (enc.getController().equals(ai)) {
value += 100; // is 100 per AI's own aura enough?
}
}
}
} else if (aiTgts.contains("EvalRating.")) {
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
} else {
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
value = ComputerUtilCard.evaluateCreature(source);
}
final int totalValue = value;
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
} else {
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
}
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
return list;
} else {
return srcList;
}
if (source == null || !sa.hasParam("AITgts")) {
return srcList;
}
CardCollection list;
String aiTgts = sa.getParam("AITgts");
if (aiTgts.startsWith("BetterThan")) {
int value = 0;
if (aiTgts.endsWith("Source")) {
value = ComputerUtilCard.evaluateCreature(source);
if (source.isEnchanted()) {
for (Card enc : source.getEnchantedBy()) {
if (enc.getController().equals(ai)) {
value += 100; // is 100 per AI's own aura enough?
}
}
}
} else if (aiTgts.contains("EvalRating.")) {
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
} else {
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
value = ComputerUtilCard.evaluateCreature(source);
}
final int totalValue = value;
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
} else {
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
}
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
return list;
}
return srcList;
}

View File

@@ -919,14 +919,14 @@ public class ComputerUtilCard {
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
}
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
for (byte c : MagicColor.WUBRG) {
if ((colors & c) != 0) {
return MagicColor.toLongString(c);
}
}
return restrictedToColors.get(0); // no difference, there was no prominent color
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
}
public static List<String> getColorByProminence(final List<Card> list) {

View File

@@ -974,17 +974,13 @@ public class ComputerUtilCombat {
continue;
}
int pBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumAtt")) {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (pBonus > 0) {
power += pBonus;
}
}
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
@@ -998,12 +994,11 @@ public class ComputerUtilCombat {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (pBonus > 0) {
power += pBonus;
}
}
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
power += pBonus;
}
}
@@ -1107,17 +1102,13 @@ public class ComputerUtilCombat {
continue;
}
int tBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumDef")) {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
@@ -1131,12 +1122,11 @@ public class ComputerUtilCombat {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
toughness += tBonus;
}
}
return toughness;
@@ -1305,6 +1295,7 @@ public class ComputerUtilCombat {
continue;
}
int pBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumAtt")) {
continue;
@@ -1314,11 +1305,8 @@ public class ComputerUtilCombat {
continue;
}
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (pBonus > 0) {
power += pBonus;
}
if (!ability.getPayCosts().hasTapCost()) {
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
}
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
@@ -1333,13 +1321,14 @@ public class ComputerUtilCombat {
continue;
}
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (pBonus > 0) {
power += pBonus;
}
if (!ability.getPayCosts().hasTapCost()) {
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
}
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
power += pBonus;
}
}
return power;
}
@@ -1530,16 +1519,14 @@ public class ComputerUtilCombat {
if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) {
continue;
}
if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
continue;
}
int tBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumDef")) {
continue;
}
toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
@@ -1553,10 +1540,11 @@ public class ComputerUtilCombat {
continue;
}
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
toughness += tBonus;
}
}
return toughness;

View File

@@ -287,7 +287,9 @@ public class ComputerUtilMana {
continue;
}
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1;
if (amount <= 0) {
// wrong gamestate for variable amount
continue;
}
@@ -351,9 +353,14 @@ public class ComputerUtilMana {
continue;
}
// these should come last since they reserve the paying cards
// (this means if a mana ability has both parts it doesn't currently undo reservations if the second part fails)
if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma, ma.isTrigger())) {
continue;
}
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
continue;
}
return paymentChoice;
}
@@ -443,7 +450,6 @@ public class ComputerUtilMana {
manaProduced = manaProduced.replace(s, color);
}
} else if (saMana.hasParam("ReplaceColor")) {
// replace color
String color = saMana.getParam("ReplaceColor");
if ("Chosen".equals(color)) {
if (card.hasChosenColor()) {
@@ -735,7 +741,8 @@ public class ComputerUtilMana {
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) {
saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
// not a good idea to sac a card that you're targeting with the SA you're paying for
saExcludeList.add(saPayment);
continue;
}
}
@@ -809,11 +816,11 @@ public class ComputerUtilMana {
String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
payMultipleMana(cost, manaProduced, ai);
// remove from available lists
// remove to prevent re-usage since resources don't get consumed
sourcesForShards.values().removeIf(CardTraitPredicates.isHostCard(saPayment.getHostCard()));
} else {
final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment);
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect))) {
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect, true))) {
saList.remove(saPayment);
continue;
}
@@ -822,8 +829,10 @@ public class ComputerUtilMana {
// subtract mana from mana pool
manapool.payManaFromAbility(sa, cost, saPayment);
// no need to remove abilities from resource map,
// once their costs are paid and consume resources, they can not be used again
// need to consider if another use is now prevented
if (!cost.isPaid() && saPayment.isActivatedAbility() && !saPayment.getRestrictions().canPlay(saPayment.getHostCard(), saPayment)) {
sourcesForShards.values().removeIf(s -> s == saPayment);
}
if (hasConverge) {
// hack to prevent converge re-using sources
@@ -1496,7 +1505,7 @@ public class ComputerUtilMana {
}
if (!cost.isReusuableResource()) {
for(CostPart part : cost.getCostParts()) {
for (CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice && !part.payCostFromSource()) {
unpreferredCost = true;
}
@@ -1587,10 +1596,8 @@ public class ComputerUtilMana {
// don't use abilities with dangerous drawbacks
AbilitySub sub = m.getSubAbility();
if (sub != null) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
}
if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
}
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list
@@ -1658,7 +1665,6 @@ public class ComputerUtilMana {
if (replaced.contains("C")) {
manaMap.put(ManaAtom.COLORLESS, m);
}
}
}
}

View File

@@ -264,12 +264,14 @@ public abstract class GameState {
}
if (c.hasMergedCard()) {
String suffix = c.getTopMergedCard().hasPaperFoil() ? "+" : "";
// we have to go by the current top card name here
newText.append(c.getTopMergedCard().getPaperCard().getName()).append("|Set:")
newText.append(c.getTopMergedCard().getPaperCard().getName()).append(suffix).append("|Set:")
.append(c.getTopMergedCard().getPaperCard().getEdition()).append("|Art:")
.append(c.getTopMergedCard().getPaperCard().getArtIndex());
} else {
newText.append(c.getPaperCard().getName()).append("|Set:").append(c.getPaperCard().getEdition())
String suffix = c.hasPaperFoil() ? "+" : "";
newText.append(c.getPaperCard().getName()).append(suffix).append("|Set:").append(c.getPaperCard().getEdition())
.append("|Art:").append(c.getPaperCard().getArtIndex());
}
}
@@ -319,18 +321,21 @@ public abstract class GameState {
newText.append(":Cloaked");
}
}
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
newText.append("|Transformed");
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
if (c.getMeldedWith() != null) {
String suffix = c.getMeldedWith().hasPaperFoil() ? "+" : "";
newText.append(":");
newText.append(c.getMeldedWith().getName());
newText.append(c.getMeldedWith().getName()).append(suffix);
}
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
if (c.isModal()) {
newText.append("|Modal");
} else {
newText.append("|Transformed");
}
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal");
}
if (c.getPlayerAttachedTo() != null) {
@@ -1311,8 +1316,8 @@ public abstract class GameState {
if (info.endsWith("Cloaked")) {
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
}
} else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true);
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
c.setState(CardStateName.Backside, true);
c.setBackSide(true);
} else if (info.startsWith("Flipped")) {
c.setState(CardStateName.Flipped, true);
@@ -1330,9 +1335,6 @@ public abstract class GameState {
}
c.setState(CardStateName.Meld, true);
c.setBackSide(true);
} else if (info.startsWith("Modal")) {
c.setState(CardStateName.Modal, true);
c.setBackSide(true);
}
else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";

View File

@@ -460,7 +460,11 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
return brains.aiShouldRun(replacementEffect, effectSA, affected);
Card host = replacementEffect.getHostCard();
if (host.hasAlternateState()) {
host = host.getGame().getCardState(host);
}
return brains.aiShouldRun(replacementEffect, effectSA, host, affected);
}
@Override
@@ -1347,6 +1351,11 @@ public class PlayerControllerAi extends PlayerController {
// Ai won't understand that anyway
}
@Override
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
// Ai won't understand that anyway
}
@Override
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
// TODO check if profile detection set to Auto

View File

@@ -171,7 +171,7 @@ public class SpecialAiLogic {
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
}
@@ -277,7 +277,7 @@ public class SpecialAiLogic {
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
}

View File

@@ -144,7 +144,7 @@ public abstract class SpellAbilityAi {
return false;
}
if (!"Once".equals(aiLogic)) {
return !AiCardMemory.isRememberedCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai);
}
return true;
}
@@ -153,7 +153,7 @@ public abstract class SpellAbilityAi {
* The rest of the logic not covered by the canPlayAI template is defined here
*/
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (MyRandom.getRandom().nextFloat() < .8f) {
if (sa.getActivationsThisTurn() == 0 || MyRandom.getRandom().nextFloat() < .8f) {
// 80% chance to play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}

View File

@@ -77,7 +77,6 @@ public enum SpellApiToAi {
.put(ApiType.DelayedTrigger, DelayedTriggerAi.class)
.put(ApiType.Destroy, DestroyAi.class)
.put(ApiType.DestroyAll, DestroyAllAi.class)
.put(ApiType.Detain, DetainAi.class)
.put(ApiType.Dig, DigAi.class)
.put(ApiType.DigMultiple, DigMultipleAi.class)
.put(ApiType.DigUntil, DigUntilAi.class)

View File

@@ -83,7 +83,7 @@ public class AddTurnAi extends SpellAbilityAi {
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return doTriggerNoCost(aiPlayer, sa, false);
}
}

View File

@@ -159,81 +159,78 @@ public class AnimateAi extends SpellAbilityAi {
}
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger());
sa.setXManaCostPaid(xPay);
}
if (!sa.usesTargeting()) {
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
for (final Card c : defined) {
bFlag |= !c.isCreature() && !c.isTapped()
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
&& !c.isEquipping();
if (sa.usesTargeting()) {
sa.resetTargets();
return animateTgtAI(sa);
}
// for creatures that could be improved (like Figure of Destiny)
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
}
int toughness = -5;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
}
if (sa.hasParam("Keywords")) {
for (String keyword : sa.getParam("Keywords").split(" & ")) {
if (!c.hasKeyword(keyword)) {
bFlag = true;
}
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
for (final Card c : defined) {
bFlag |= !c.isCreature() && !c.isTapped()
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
&& !c.isEquipping();
// for creatures that could be improved (like Figure of Destiny)
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
}
int toughness = -5;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
}
if (sa.hasParam("Keywords")) {
for (String keyword : sa.getParam("Keywords").split(" & ")) {
if (!c.hasKeyword(keyword)) {
bFlag = true;
}
}
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
}
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.isCrew() && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Card animatedCopy = becomeAnimated(c, sa);
if (ph.isPlayerTurn(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
// also check if maybe there are static effects applied to the animated copy that would matter
// (e.g. Myth Realized)
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, source)) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.isCrew() && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Card animatedCopy = becomeAnimated(c, sa);
if (ph.isPlayerTurn(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
// also check if maybe there are static effects applied to the animated copy that would matter
// (e.g. Myth Realized)
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
}
}
if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else {
sa.resetTargets();
return animateTgtAI(sa);
}
if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, source);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
@@ -255,8 +252,7 @@ public class AnimateAi extends SpellAbilityAi {
return decision;
} else if (!mandatory) {
return decision;
}
else {
} else {
// fallback if animate is mandatory
sa.resetTargets();
List<Card> list = CardUtil.getValidCardsToTarget(sa);
@@ -277,8 +273,13 @@ public class AnimateAi extends SpellAbilityAi {
}
private AiAbilityDecision animateTgtAI(final SpellAbility sa) {
if (sa.getMaxTargets() == 0) {
// this happens if an optional cost is skipped, e.g. Brave the Wilds
return new AiAbilityDecision(80, AiPlayDecision.WillPlay);
}
final Player ai = sa.getActivatingPlayer();
final PhaseHandler ph = ai.getGame().getPhaseHandler();
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
final String logic = sa.getParamOrDefault("AILogic", "");
final boolean alwaysActivatePWAbility = sa.isPwAbility()
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
@@ -290,10 +291,8 @@ public class AnimateAi extends SpellAbilityAi {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
}
final Game game = ai.getGame();
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false);
// list is empty, no possible targets
@@ -398,7 +397,8 @@ public class AnimateAi extends SpellAbilityAi {
}
if (logic.equals("SetPT")) {
// TODO: 1. Teach the AI to use this to save the creature from direct damage; 2. Determine the best target in a smarter way?
// TODO: 1. Teach the AI to use this to save the creature from direct damage;
// 2. Determine the best target in a smarter way?
Card worst = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
Card buffed = becomeAnimated(worst, sa);
@@ -435,11 +435,9 @@ public class AnimateAi extends SpellAbilityAi {
if (sa.hasParam("AITgts") && !list.isEmpty()) {
//No logic, but we do have preferences. Pick the best among those?
Card best = ComputerUtilCard.getBestAI(list);
if(best != null) {
sa.getTargets().add(best);
rememberAnimatedThisTurn(ai, best);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
sa.getTargets().add(best);
rememberAnimatedThisTurn(ai, best);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or

View File

@@ -988,6 +988,7 @@ public class AttachAi extends SpellAbilityAi {
}
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
// Living Weapon or similar
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);

View File

@@ -101,11 +101,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.getHostCard().removeSVar("AIPreferenceOverride");
}
if (aiLogic.equals("BeforeCombat")) {
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
return false;
}
} else if (aiLogic.equals("SurpriseBlock")) {
if (aiLogic.equals("SurpriseBlock")) {
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
}
@@ -289,9 +285,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return a boolean.
*/
private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
final Cost abCost = sa.getPayCosts();
// Fetching should occur fairly often as it helps cast more spells, and have access to more mana
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -300,13 +294,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) {
try {
origin = ZoneType.listValueOf(sa.getParam("Origin"));
} catch (IllegalArgumentException ex) {
// This happens when Origin is something like
// "Graveyard,Library" (Doomsday)
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
origin = ZoneType.listValueOf(sa.getParam("Origin"));
}
final String destination = sa.getParam("Destination");
@@ -455,12 +443,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
}
final AbilitySub subAb = sa.getSubAbility();
if (subAb == null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/**
@@ -506,19 +489,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
if (sa.hasParam("AILogic")) {
if (sa.getParam("AILogic").equals("Never")) {
/*
* Hack to stop AI from using Aviary Mechanic's "may bounce" trigger.
* Ideally it should look for a good bounce target like "Pacifism"-victims
* but there is no simple way to check that. It is preferable for the AI
* to make sub-optimal choices (waste bounce) than to make obvious mistakes
* (bounce useful permanent).
*/
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
List<ZoneType> origin = new ArrayList<>();
if (sa.hasParam("Origin")) {
origin = ZoneType.listValueOf(sa.getParam("Origin"));
@@ -773,12 +743,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
final AbilitySub subAb = sa.getSubAbility();
if (subAb == null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/*
@@ -796,6 +761,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
} else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) {
return true;
} else if (aiLogic.equals("BeforeCombat")) {
return !ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN);
}
if (sa.isHidden()) {
@@ -922,9 +889,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
list = ComputerUtil.filterAITgts(sa, ai, list, true);
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
}
if (source.isInZone(ZoneType.Hand)) {
list = CardLists.filter(list, CardPredicates.nameNotEquals(source.getName())); // Don't get the same card back.
@@ -933,8 +897,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
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)) {
@@ -1277,53 +1239,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
// if max CMC exceeded, do not choose this card (but keep looking for other options)
if (sa.hasParam("MaxTotalTargetCMC")) {
if (choice.getCMC() > sa.getTargetRestrictions().getMaxTotalCMC(choice, sa) - sa.getTargets().getTotalTargetedCMC()) {
list.remove(choice);
continue;
}
}
// if max power exceeded, do not choose this card (but keep looking for other options)
if (sa.hasParam("MaxTotalTargetPower")) {
if (choice.getNetPower() > sa.getTargetRestrictions().getMaxTotalPower(choice, sa) -sa.getTargets().getTotalTargetedPower()) {
list.remove(choice);
continue;
}
}
// honor the Same Creature Type restriction
if (sa.getTargetRestrictions().isWithSameCreatureType()) {
Card firstTarget = sa.getTargetCard();
if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) {
list.remove(choice);
continue;
}
}
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
// vs. two cards in the second player's graveyard, which cards are more relevant to be targeted, etc.). Consider improving.
if (sa.getTargetRestrictions().isSingleZone()) {
Card firstTgt = sa.getTargetCard();
CardCollection toRemove = new CardCollection();
if (firstTgt != null) {
for (Card t : sa.getTargets().getTargetCards()) {
if (!t.getController().equals(firstTgt.getController())) {
toRemove.add(t);
}
}
sa.getTargets().removeAll(toRemove);
}
}
return true;
}
@@ -1619,7 +1540,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (logic.startsWith("ExilePreference")) {
return doExilePreferenceLogic(decider, sa, fetchList);
} else if (logic.equals("BounceOwnTrigger")) {
return doBounceOwnTriggerLogic(decider, fetchList);
return doBounceOwnTriggerLogic(decider, sa, fetchList);
}
}
if (fetchList.isEmpty()) {
@@ -2183,16 +2104,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
}
private static Card doBounceOwnTriggerLogic(Player ai, CardCollection choices) {
private static Card doBounceOwnTriggerLogic(Player ai, SpellAbility sa, CardCollection choices) {
CardCollection unprefChoices = CardLists.filter(choices, c -> !c.isToken() && c.getOwner().equals(ai));
// TODO check for threatened cards
CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false));
if (!prefChoices.isEmpty()) {
return ComputerUtilCard.getBestAI(prefChoices);
} else if (!unprefChoices.isEmpty()) {
return ComputerUtilCard.getWorstAI(unprefChoices);
} else {
return null;
}
if (!unprefChoices.isEmpty() && sa.getSubAbility() != null) {
// some extra benefit like First Responder
return ComputerUtilCard.getWorstAI(unprefChoices);
}
return null;
}
@Override

View File

@@ -286,8 +286,6 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, final SpellAbility sa, boolean mandatory) {
// Change Zone All, can be any type moving from one zone to another
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);

View File

@@ -9,7 +9,6 @@ import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import java.util.Collections;
@@ -89,12 +88,7 @@ public class CharmAi extends SpellAbilityAi {
CharmEffect.chainAbilities(sa, chosenList);
}
// prevent run-away activations - first time will always return true
if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return super.checkApiLogic(ai, sa);
}
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {

View File

@@ -78,10 +78,8 @@ public class ChooseCardNameAi extends SpellAbilityAi {
}
if (mandatory) {
// If mandatory, then we will play it.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If not mandatory, then we won't play it.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -63,8 +63,7 @@ public class ChooseNumberAi extends SpellAbilityAi {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} {
return canPlay(ai, sa);
}
return canPlay(ai, sa);
}
}

View File

@@ -20,7 +20,7 @@ import java.util.Map;
public class CloneAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
@@ -38,10 +38,6 @@ public class CloneAi extends SpellAbilityAi {
// TODO - add some kind of check for during human turn to answer
// "Can I use this to block something?"
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) {
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
}
PhaseHandler phase = game.getPhaseHandler();
if (!sa.usesTargeting()) {
@@ -77,7 +73,7 @@ public class CloneAi extends SpellAbilityAi {
return useAbility ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} // end cloneCanPlayAI()
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
@@ -100,6 +96,10 @@ public class CloneAi extends SpellAbilityAi {
if (sa.usesTargeting()) {
chance = cloneTgtAI(sa);
} else {
if (sa.isReplacementAbility() && host.isCloned()) {
// prevent StackOverflow from infinite loop copying another ETB RE
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
}
if (sa.hasParam("Choices")) {
CardCollectionView choices = CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield),
sa.getParam("Choices"), host.getController(), host, sa);
@@ -192,7 +192,7 @@ public class CloneAi extends SpellAbilityAi {
final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
: "Permanent.YouDontCtrl+notnamed" + name + ",Permanent.nonLegendary+notnamed" + name;
: "Permanent.YouDontCtrl+!named" + name + ",Permanent.nonLegendary+!named" + name;
// TODO: rewrite this block so that this is done somehow more elegantly
if (canCloneLegendary) {

View File

@@ -119,7 +119,7 @@ public class ConniveAi extends SpellAbilityAi {
}
}
return new AiAbilityDecision(
sa.isTargetNumberValid() && !sa.getTargets().isEmpty() ? 100 : 0,
sa.isTargetNumberValid() ? 100 : 0,
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed
);
}

View File

@@ -53,17 +53,15 @@ public class ControlExchangeAi extends SpellAbilityAi {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
if (mandatory) {
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return decision;
} else {
return canPlay(aiPlayer, sa);
} else if (mandatory) {
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return decision;
} else {
return canPlay(aiPlayer, sa);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}

View File

@@ -44,7 +44,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
// Not at EOT phase
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
}
} if ("DuplicatePerms".equals(aiLogic)) {
} else if ("DuplicatePerms".equals(aiLogic)) {
final List<Card> valid = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if (valid.size() < 2) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
@@ -212,7 +212,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
}
}

View File

@@ -42,7 +42,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (!MyRandom.percentTrue(chance)
&& !"AlwaysIfViable".equals(logic)
&& !"Always".equals(logic)
&& !"AlwaysCopyActivatedAbilities".equals(logic)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@@ -93,7 +93,6 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (decision == AiPlayDecision.WillPlay) {
sa.getTargets().add(top);
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, decision);
@@ -115,7 +114,6 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (logic.contains("Always")) {
// If the logic is "Always" or "AlwaysIfViable", we will always play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@@ -126,10 +124,11 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
}
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
}
AiAbilityDecision decision = canPlay(aiPlayer, sa);
if (!decision.willingToPlay()) {
if (sa.isMandatory()) {

View File

@@ -102,7 +102,7 @@ public abstract class CountersAi extends SpellAbilityAi {
} else if (type.equals("DIVINITY")) {
final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0);
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon);
} else if (CounterType.get(type).isKeywordCounter()) {
} else if (CounterType.getType(type).isKeywordCounter()) {
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
} else {
// The AI really should put counters on cards that can use it.

View File

@@ -20,41 +20,40 @@ public class CountersMultiplyAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (sa.usesTargeting()) {
return setTargets(ai, sa);
}
final CounterType counterType = getCounterType(sa);
// defined are mostly Self or Creatures you control
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
if (!sa.usesTargeting()) {
// defined are mostly Self or Creatures you control
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
list = CardLists.filter(list, c -> {
if (!c.hasCounters()) {
return false;
}
list = CardLists.filter(list, c -> {
if (!c.hasCounters()) {
if (counterType != null) {
if (c.getCounters(counterType) <= 0) {
return false;
}
if (counterType != null) {
if (c.getCounters(counterType) <= 0) {
if (!c.canReceiveCounters(counterType)) {
return false;
}
} else {
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
// has negative counter it would double
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
return false;
}
if (!c.canReceiveCounters(counterType)) {
return false;
}
} else {
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
// has negative counter it would double
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
return false;
}
}
}
return true;
});
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
} else {
return setTargets(ai, sa);
return true;
});
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return super.checkApiLogic(ai, sa);
@@ -155,7 +154,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
}
if (counterType == null || counterType.is(type)) {
addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
addTargetsByCounterType(ai, sa, aiList, type);
}
}
}
@@ -164,7 +163,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (!oppList.isEmpty()) {
// not enough targets
if (sa.canAddMoreTarget()) {
final CounterType type = CounterType.get(CounterEnumType.M1M1);
final CounterType type = CounterEnumType.M1M1;
if (counterType == null || counterType == type) {
addTargetsByCounterType(ai, sa, oppList, type);
}

View File

@@ -110,7 +110,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Proliferate is always optional for all, no need to select best
final CounterType poison = CounterType.get(CounterEnumType.POISON);
final CounterType poison = CounterEnumType.POISON;
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
// because countertype can't be chosen anymore, only look for poison counters

View File

@@ -92,9 +92,8 @@ public class CountersPutAi extends CountersAi {
return false;
}
return chance > MyRandom.getRandom().nextFloat();
} else {
return false;
}
return false;
}
if (sa.isKeyword(Keyword.LEVEL_UP)) {
@@ -124,7 +123,6 @@ public class CountersPutAi extends CountersAi {
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
CardCollection list;
Card choice = null;
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.isDividedAsYouChoose();
@@ -170,7 +168,7 @@ public class CountersPutAi extends CountersAi {
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)));
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterEnumType.M1M1));
Card best = ComputerUtilCard.getBestAI(oppCreatM1);
if (best != null) {
@@ -247,10 +245,9 @@ public class CountersPutAi extends CountersAi {
} else if (sa.getSubAbility() != null
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
&& !source.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
// Bristling Hydra: save from death using a ping activation
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
@@ -293,10 +290,8 @@ public class CountersPutAi extends CountersAi {
if (willActivate) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("ChargeToBestCMC")) {
return doChargeToCMCLogic(ai, sa);
} else if (logic.equals("ChargeToBestOppControlledCMC")) {
@@ -337,7 +332,7 @@ public class CountersPutAi extends CountersAi {
Game game = ai.getGame();
Combat combat = game.getCombat();
if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return doCombatAdaptLogic(source, amount, combat);
@@ -349,7 +344,7 @@ public class CountersPutAi extends CountersAi {
if (type.equals("P1P1")) {
nPump = amount;
}
return FightAi.canFightAi(ai, sa, nPump, nPump);
return FightAi.canFight(ai, sa, nPump, nPump);
}
if (amountStr.equals("X")) {
@@ -443,17 +438,16 @@ public class CountersPutAi extends CountersAi {
}
sa.addDividedAllocation(c, amount);
return decision;
} else {
if (!hasSacCost) {
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision;
}
} else if (!hasSacCost) {
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision;
}
}
}
sa.resetTargets();
CardCollection list;
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
@@ -609,7 +603,21 @@ public class CountersPutAi extends CountersAi {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
final int currCounters = cards.get(0).getCounters(CounterType.getType(type));
// adding counters would cause counter amount to overflow
if (Integer.MAX_VALUE - currCounters <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (type.equals("P1P1")) {
if (Integer.MAX_VALUE - cards.get(0).getNetPower() <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (Integer.MAX_VALUE - cards.get(0).getNetToughness() <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// each non +1/+1 counter on the card is a 10% chance of not
// activating this ability.
@@ -624,7 +632,7 @@ public class CountersPutAi extends CountersAi {
}
// Useless since the card already has the keyword (or for another reason)
if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -671,14 +679,12 @@ public class CountersPutAi extends CountersAi {
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
if (sa.usesTargeting()) {
CardCollection list = null;
CardCollection list;
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
}
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty() && isMandatoryTrigger) {
@@ -694,9 +700,8 @@ public class CountersPutAi extends CountersAi {
|| sa.getTargets().isEmpty()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
break;
}
break;
}
if (sa.isCurse()) {
@@ -738,10 +743,7 @@ public class CountersPutAi extends CountersAi {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final SpellAbility root = sa.getRootAbility();
final Card source = sa.getHostCard();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// boolean chance = true;
boolean preferred = true;
CardCollection list;
final String aiLogic = sa.getParam("AILogic");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.isDividedAsYouChoose();
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
@@ -760,28 +762,10 @@ public class CountersPutAi extends CountersAi {
}
if ("ChargeToBestCMC".equals(aiLogic)) {
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
if (decision.willingToPlay()) {
// If the AI logic is to charge to best CMC, we can return true
// if the logic was successfully applied or if it's mandatory.
return decision;
} else if (mandatory) {
// If the logic was not applied and it's mandatory, we return false.
if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else {
// If the logic was not applied and it's not mandatory, we return false.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
AiAbilityDecision decision = doChargeToOppCtrlCMCLogic(ai, sa);
if (decision.willingToPlay()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (mandatory) {
// If the logic was not applied and it's mandatory, we return false.
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return doChargeToCMCLogic(ai, sa);
}
if (!sa.usesTargeting()) {
@@ -805,7 +789,6 @@ public class CountersPutAi extends CountersAi {
// things like Powder Keg, which are way too complex for the AI
}
} else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) {
// can only target opponent
PlayerCollection playerList = new PlayerCollection(IterableUtil.filter(
sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class));
@@ -820,34 +803,32 @@ public class CountersPutAi extends CountersAi {
sa.getTargets().add(choice);
}
} else {
String logic = sa.getParam("AILogic");
if ("Fight".equals(logic) || "PowerDmg".equals(logic)) {
if ("Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic)) {
int nPump = 0;
if (type.equals("P1P1")) {
nPump = amount;
}
AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump);
AiAbilityDecision decision = FightAi.canFight(ai, sa, nPump, nPump);
if (decision.willingToPlay()) {
return decision;
}
}
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
}
list = CardLists.getTargetableCards(list, sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false);
int totalTargets = list.size();
sa.resetTargets();
Iterable<Card> filteredField;
if (sa.isCurse()) {
filteredField = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
filteredField = ai.getCardsIn(ZoneType.Battlefield);
}
CardCollection list = CardLists.getTargetableCards(filteredField, sa);
list = ComputerUtil.filterAITgts(sa, ai, list, false);
int totalTargets = list.size();
boolean preferred = true;
while (sa.canAddMoreTarget()) {
if (mandatory) {
// When things are mandatory, gotta handle a little differently
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
@@ -869,12 +850,10 @@ public class CountersPutAi extends CountersAi {
if (list.isEmpty()) {
// Not mandatory, or the the list was regenerated and is still empty,
// so return whether or not we found enough targets
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
}
Card choice = null;
Card choice;
// Choose targets here:
if (sa.isCurse()) {
@@ -883,33 +862,27 @@ public class CountersPutAi extends CountersAi {
if (choice == null && mandatory) {
choice = Aggregates.random(list);
}
} else if (type.equals("M1M1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
if (type.equals("M1M1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = Aggregates.random(list);
}
choice = Aggregates.random(list);
}
} else if (preferred) {
list = ComputerUtil.getSafeTargets(ai, sa, list);
choice = chooseBoonTarget(list, type);
if (choice == null && mandatory) {
choice = Aggregates.random(list);
}
} else if (type.equals("P1P1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
if (preferred) {
list = ComputerUtil.getSafeTargets(ai, sa, list);
choice = chooseBoonTarget(list, type);
if (choice == null && mandatory) {
choice = Aggregates.random(list);
}
} else {
if (type.equals("P1P1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = Aggregates.random(list);
}
}
choice = Aggregates.random(list);
}
if (choice != null && divided) {
int alloc = Math.max(amount / totalTargets, 1);
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
sa.addDividedAllocation(choice, left);
} else {
int alloc = Math.max(amount / totalTargets, 1);
sa.addDividedAllocation(choice, alloc);
left -= alloc;
}
@@ -979,8 +952,8 @@ public class CountersPutAi extends CountersAi {
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Bolster does use this
// TODO need more or less logic there?
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
final CounterType m1m1 = CounterEnumType.M1M1;
final CounterType p1p1 = CounterEnumType.P1P1;
// no logic if there is no options or no to choice
if (!isOptional && Iterables.size(options) <= 1) {
@@ -999,9 +972,7 @@ public class CountersPutAi extends CountersAi {
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
final boolean isCurse = sa.isCurse();
if (isCurse) {
if (sa.isCurse()) {
final CardCollection opponents = CardLists.filterControlledBy(options, ai.getOpponents());
if (!opponents.isEmpty()) {
@@ -1098,11 +1069,10 @@ public class CountersPutAi extends CountersAi {
Player ai = sa.getActivatingPlayer();
GameEntity e = (GameEntity) params.get("Target");
// for Card try to select not useless counter
if (e instanceof Card) {
Card c = (Card) e;
if (e instanceof Card c) {
if (c.getController().isOpponentOf(ai)) {
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterType.get(CounterEnumType.M1M1);
if (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterEnumType.M1M1;
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, c)) {
@@ -1116,15 +1086,14 @@ public class CountersPutAi extends CountersAi {
}
}
}
} else if (e instanceof Player) {
Player p = (Player) e;
} else if (e instanceof Player p) {
if (p.isOpponentOf(ai)) {
if (options.contains(CounterType.get(CounterEnumType.POISON))) {
return CounterType.get(CounterEnumType.POISON);
if (options.contains(CounterEnumType.POISON)) {
return CounterEnumType.POISON;
}
} else {
if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
return CounterType.get(CounterEnumType.EXPERIENCE);
if (options.contains(CounterEnumType.EXPERIENCE)) {
return CounterEnumType.EXPERIENCE;
}
}
@@ -1228,12 +1197,9 @@ public class CountersPutAi extends CountersAi {
}
}
if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
@@ -1253,9 +1219,8 @@ public class CountersPutAi extends CountersAi {
if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -218,18 +218,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
Card tgt = (Card) params.get("Target");
// planeswalker has high priority for loyalty counters
if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) {
return CounterType.get(CounterEnumType.LOYALTY);
if (tgt.isPlaneswalker() && options.contains(CounterEnumType.LOYALTY)) {
return CounterEnumType.LOYALTY;
}
if (tgt.getController().isOpponentOf(ai)) {
// creatures with BaseToughness below or equal zero might be
// killed if their counters are removed
if (tgt.isCreature() && tgt.getBaseToughness() <= 0) {
if (options.contains(CounterType.get(CounterEnumType.P1P1))) {
return CounterType.get(CounterEnumType.P1P1);
} else if (options.contains(CounterType.get(CounterEnumType.M1M1))) {
return CounterType.get(CounterEnumType.M1M1);
if (options.contains(CounterEnumType.P1P1)) {
return CounterEnumType.P1P1;
} else if (options.contains(CounterEnumType.M1M1)) {
return CounterEnumType.M1M1;
}
}
@@ -241,17 +241,17 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
} else {
// this counters are treat first to be removed
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) {
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterEnumType.ICE)) {
CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
if (maritEmpty) {
return CounterType.get(CounterEnumType.ICE);
return CounterEnumType.ICE;
}
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) {
return CounterType.get(CounterEnumType.P1P1);
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) {
return CounterType.get(CounterEnumType.M1M1);
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterEnumType.P1P1)) {
return CounterEnumType.P1P1;
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterEnumType.M1M1)) {
return CounterEnumType.M1M1;
}
// fallback logic, select positive counter to add more

View File

@@ -384,7 +384,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (targetCard.getController().isOpponentOf(ai)) {
// if its a Planeswalker try to remove Loyality first
if (targetCard.isPlaneswalker()) {
return CounterType.get(CounterEnumType.LOYALTY);
return CounterEnumType.LOYALTY;
}
for (CounterType type : options) {
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
@@ -392,10 +392,10 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
}
} else {
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) {
return CounterType.get(CounterEnumType.M1M1);
} else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) {
return CounterType.get(CounterEnumType.P1P1);
if (options.contains(CounterEnumType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) {
return CounterEnumType.M1M1;
} else if (options.contains(CounterEnumType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) {
return CounterEnumType.P1P1;
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, targetCard)) {

View File

@@ -5,7 +5,6 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -21,14 +20,6 @@ public class DamageAllAi extends SpellAbilityAi {
// based on what the expected targets could be
final Card source = sa.getHostCard();
// abCost stuff that should probably be centralized...
final Cost abCost = sa.getPayCosts();
if (abCost != null) {
// AI currently disabled for some costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
}
// wait until stack is empty (prevents duplicate kills)
if (!ai.getGame().getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
@@ -142,9 +133,9 @@ public class DamageAllAi extends SpellAbilityAi {
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
// When using Pestilence to hurt players, do it at
// the end of the opponent's turn only
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))
|| (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)))
// Need further improvement : if able to kill immediately with repeated activations, do not wait
// for phases! Will also need to implement considering repeated activations for killed creatures!
// || (ai.sa.getPayCosts(). ??? )

View File

@@ -110,15 +110,7 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection list;
// Targeting
if (sa.usesTargeting()) {
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
// (e.g. Heliod's Intervention)
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
sa.getRootAbility().setXManaCostPaid(xPay);
}
// Assume there where already enough targets chosen by AI Logic Above
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
@@ -128,7 +120,10 @@ public class DestroyAi extends SpellAbilityAi {
sa.resetTargets();
int maxTargets;
if (sa.getRootAbility().costHasManaX()) {
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
// (e.g. Heliod's Intervention)
if (sa.getRootAbility().costHasManaX() ||
("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid"))) {
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
// need to set XPaid to get the right number for

View File

@@ -1,119 +0,0 @@
package forge.ai.ability;
import java.util.List;
import java.util.function.Predicate;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.combat.CombatUtil;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class DetainAi extends SpellAbilityAi {
Predicate<Card> CREATURE_OR_TAP_ABILITY = c -> {
if (c.isCreature()) {
return true;
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
};
protected boolean prefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
final Game game = ai.getGame();
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.filter(list, CREATURE_OR_TAP_ABILITY);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true);
if (list.isEmpty()) {
return false;
}
while (sa.canAddMoreTarget()) {
Card choice = null;
if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (!mandatory) {
sa.resetTargets();
}
return false;
}
}
PhaseHandler phase = game.getPhaseHandler();
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, list);
if (primeTarget != null) {
choice = primeTarget;
} else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// Tap creatures possible blockers before combat during AI's turn.
List<Card> attackers;
if (phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
//Combat has already started
attackers = game.getCombat().getAttackers();
} else {
attackers = CardLists.filter(ai.getCreaturesInPlay(), c -> CombatUtil.canAttack(c, opp));
attackers.remove(source);
}
List<Card> creatureList = CardLists.filter(list, CardPredicates.possibleBlockerForAtLeastOne(attackers));
// TODO check if own creature would be forced to attack and we want to keep it alive
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
} else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
}
} else if (phase.isPlayerTurn(opp)
&& phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
// Tap creatures possible blockers before combat during AI's turn.
if (list.anyMatch(CardPredicates.CREATURES)) {
List<Card> creatureList = CardLists.filter(list, c -> c.isCreature() && CombatUtil.canAttack(c, opp));
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
} else { // no creatures available
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
}
} else {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
}
if (choice == null) { // can't find anything left
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (!mandatory) {
sa.resetTargets();
}
return false;
} else {
if (!ComputerUtil.shouldCastLessThanMax(ai, source)) {
return false;
}
break;
}
}
list.remove(choice);
sa.getTargets().add(choice);
}
return true;
}
}

View File

@@ -26,7 +26,6 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -370,7 +369,7 @@ public class DrawAi extends SpellAbilityAi {
// try to make opponent lose to poison
// currently only Caress of Phyrexia
if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (getPoison != null && oppA.canReceiveCounters(CounterEnumType.POISON)) {
if (oppA.getPoisonCounters() + numCards > 9) {
sa.getTargets().add(oppA);
return true;
@@ -414,7 +413,7 @@ public class DrawAi extends SpellAbilityAi {
}
}
if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (getPoison != null && ai.canReceiveCounters(CounterEnumType.POISON)) {
if (numCards + ai.getPoisonCounters() >= 8) {
aiTarget = false;
}
@@ -472,7 +471,7 @@ public class DrawAi extends SpellAbilityAi {
}
// ally would lose because of poison
if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON)) && ally.getPoisonCounters() + numCards > 9) {
if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards > 9) {
continue;
}

View File

@@ -23,10 +23,8 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMode;
import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
import forge.util.FileSection;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
@@ -39,22 +37,17 @@ public class EffectAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
final PhaseHandler phase = game.getPhaseHandler();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667;
String logic = "";
if (sa.hasParam("AILogic")) {
logic = sa.getParam("AILogic");
final PhaseHandler phase = game.getPhaseHandler();
if (logic.equals("BeginningOfOppTurn")) {
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isAfter(PhaseType.DRAW)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
randomReturn = true;
} else if (logic.equals("EndOfOppTurn")) {
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isBefore(PhaseType.END_OF_TURN)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
randomReturn = true;
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
for (Player opp : ai.getOpponents()) {
boolean worthHolding = false;
@@ -277,7 +270,7 @@ public class EffectAi extends SpellAbilityAi {
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("Fight")) {
return FightAi.canFightAi(ai, sa, 0,0);
return FightAi.canFight(ai, sa, 0,0);
} else if (logic.equals("Pump")) {
sa.resetTargets();
List<Card> options = CardUtil.getValidCardsToTarget(sa);
@@ -296,6 +289,7 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("YawgmothsWill")) {
return SpecialCardAi.YawgmothsWill.consider(ai, sa) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.startsWith("NeedCreatures")) {
// TODO convert to AiCheckSVar
if (ai.getCreaturesInPlay().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@@ -330,8 +324,8 @@ public class EffectAi extends SpellAbilityAi {
Combat combat = game.getCombat();
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
&& phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something
&& !host.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
// ideally needs once per combat or something
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
@@ -385,118 +379,6 @@ public class EffectAi extends SpellAbilityAi {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else if (sa.hasParam("RememberObjects")) { //generic
boolean cantAttack = false;
boolean cantBlock = false;
boolean cantActivate = false;
String duraction = sa.getParam("Duration");
String matchStr = "Card.IsRemembered";
for (String st : sa.getParam("StaticAbilities").split(",")) {
Map<String, String> params = FileSection.parseToMap(sa.getSVar(st), FileSection.DOLLAR_SIGN_KV_SEPARATOR);
List<StaticAbilityMode> modes = StaticAbilityMode.listValueOf(params.get("Mode"));
if (modes.contains(StaticAbilityMode.CantAttack) && matchStr.equals(params.get("ValidCard"))) {
cantAttack = true;
}
if (modes.contains(StaticAbilityMode.CantBlock) && matchStr.equals(params.get("ValidCard"))) {
cantBlock = true;
}
if (modes.contains(StaticAbilityMode.CantBlockBy) && matchStr.equals(params.get("ValidBlocker"))) {
cantBlock = true;
}
if (modes.contains(StaticAbilityMode.CantBeActivated) && matchStr.equals(params.get("ValidCard"))) {
cantActivate = true;
}
}
// TODO add more cases later
if (!cantAttack && !cantBlock && !cantActivate) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (cantBlock && duraction == null && phase.isPlayerTurn(ai) && !phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.usesTargeting()) {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa));
list = ComputerUtil.filterAITgts(sa, ai, list, true);
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
List<Card> oppCreatures = CardLists.filterAsList(list, c -> {
return c.isCreature() && c.getController().isOpponentOf(ai);
});
List<Card> oppWithAbilities = CardLists.filterAsList(list, c -> {
return !c.isCreature() && c.getController().isOpponentOf(ai) && c.getSpellAbilities().anyMatch(SpellAbility::isActivatedAbility);
});
if (cantAttack || cantBlock) {
if (oppCreatures.isEmpty()) {
if (!cantActivate || oppWithAbilities.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
while (sa.canAddMoreTarget()) {
Card choice = null;
if (cantAttack && cantBlock && !oppCreatures.isEmpty()) {
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, oppCreatures);
if (primeTarget != null) {
choice = primeTarget;
} else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// Tap creatures possible blockers before combat during AI's turn.
List<Card> attackers;
if (phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
//Combat has already started
attackers = game.getCombat().getAttackers();
} else {
attackers = CardLists.filter(ai.getCreaturesInPlay(), c -> CombatUtil.canAttack(c, opp));
}
List<Card> creatureList = CardLists.filter(list, CardPredicates.possibleBlockerForAtLeastOne(attackers));
// TODO check if own creature would be forced to attack and we want to keep it alive
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
} else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
}
}
} // TODO add logic to tap non creatures with activated abilities if cantActivate is true
if (choice == null) { // can't find anything left
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else {
if (!ComputerUtil.shouldCastLessThanMax(ai, sa.getHostCard())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
break;
}
}
list.remove(choice);
oppCreatures.remove(choice);
sa.getTargets().add(choice);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else { //no AILogic
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}

View File

@@ -1,17 +1,15 @@
package forge.ai.ability;
import com.google.common.collect.Sets;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayLife;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
@@ -36,6 +34,26 @@ public class EndureAi extends SpellAbilityAi {
sa.getTargets().add(bestCreature);
}
// Card-specific logic
final String num = sa.getParamOrDefault("Num", "1");
if ("X".equals(num) && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
if (!aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
int curLife = aiPlayer.getLife();
int dangerLife = (((PlayerControllerAi) aiPlayer.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
if (curLife <= dangerLife) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
int availableMana = ComputerUtilMana.getAvailableManaEstimate(aiPlayer) - 1;
int maxEndureX = Math.min(availableMana, curLife - dangerLife);
if (maxEndureX > 0) {
sa.setXManaCostPaid(maxEndureX);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}

View File

@@ -177,7 +177,7 @@ public class FightAi extends SpellAbilityAi {
* @param power bonus to power
* @return true if fight effect should be played, false otherwise
*/
public static AiAbilityDecision canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
public static AiAbilityDecision canFight(final Player ai, final SpellAbility sa, int power, int toughness) {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
AbilitySub tgtFight = sa.getSubAbility();

View File

@@ -12,15 +12,13 @@ import forge.game.spellability.SpellAbility;
public class FlipACoinAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
* @see forge.card.abilityfactory.SpellAiLogic#checkApiLogic(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (sa.hasParam("AILogic")) {
String ailogic = sa.getParam("AILogic");
if (ailogic.equals("Never")) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (ailogic.equals("PhaseOut")) {
if (ailogic.equals("PhaseOut")) {
if (!ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(sa.getHostCard())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}

View File

@@ -1,6 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;

View File

@@ -76,7 +76,8 @@ public class ManaAi extends SpellAbilityAi {
return ph.is(PhaseType.MAIN2, ai) || ph.is(PhaseType.MAIN1, ai);
}
if ("AtOppEOT".equals(logic)) {
return (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai
&& (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife());
}
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
@@ -158,7 +159,7 @@ public class ManaAi extends SpellAbilityAi {
int numCounters = 0;
int manaSurplus = 0;
if ("Count$xPaid".equals(host.getSVar("X")) && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
CounterType ctrType = CounterType.get(CounterEnumType.KI); // Petalmane Baku
CounterType ctrType = CounterEnumType.KI; // Petalmane Baku
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
ctrType = ((CostRemoveCounter)part).counter;
@@ -277,10 +278,4 @@ public class ManaAi extends SpellAbilityAi {
}
return !lose;
}
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
final String logic = sa.getParamOrDefault("AILogic", "");
return checkApiLogic(ai, sa);
}
}

View File

@@ -23,10 +23,8 @@ public class MeldAi extends SpellAbilityAi {
boolean hasPrimaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(primaryMeld).and(CardPredicates.isOwner(aiPlayer)));
boolean hasSecondaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(secondaryMeld).and(CardPredicates.isOwner(aiPlayer)));
if (hasPrimaryMeld && hasSecondaryMeld && sa.getHostCard().getName().equals(primaryMeld)) {
// If the primary meld card is on the battlefield and both meld cards are owned by the AI, play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the secondary meld card is on the battlefield and it is the one being activated, play the ability
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -24,14 +24,8 @@ public class MillAi extends SpellAbilityAi {
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
if (aiLogic.equals("Main1")) {
return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases")
|| ComputerUtil.castSpellInMain1(ai, sa);
} else if (aiLogic.equals("EndOfOppTurn")) {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
} else if (aiLogic.equals("LilianaMill")) {
if (aiLogic.equals("LilianaMill")) {
// TODO convert to AICheckSVar
// Only mill if a "Raise Dead" target is available, in case of control decks with few creatures
return CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES).size() >= 1;
}
@@ -56,9 +50,10 @@ public class MillAi extends SpellAbilityAi {
// because they are also potentially useful for combat
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
}
return true;
return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases")
|| ComputerUtil.castSpellInMain1(ai, sa);
}
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
/*

View File

@@ -6,10 +6,10 @@ import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardUtil;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -27,7 +27,8 @@ public class MustBlockAi extends SpellAbilityAi {
if (combat == null || !combat.isAttacking(source)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (AiCardMemory.isRememberedCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
}
if (source.getAbilityActivatedThisTurn().getActivators(sa).contains(aiPlayer)) {
// The AI can meaningfully do it only to one creature per card yet, trying to do it to multiple cards
// may result in overextending and losing the attacker
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
@@ -37,11 +38,7 @@ public class MustBlockAi extends SpellAbilityAi {
if (!list.isEmpty()) {
final Card blocker = ComputerUtilCard.getBestCreatureAI(list);
if (blocker == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.getTargets().add(blocker);
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@@ -63,11 +60,6 @@ public class MustBlockAi extends SpellAbilityAi {
protected AiAbilityDecision doTriggerNoCost(final Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
// only use on creatures that can attack
if (!ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Card attacker = source;
if (sa.hasParam("DefinedAttacker")) {
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedAttacker"), sa);
@@ -81,13 +73,9 @@ public class MustBlockAi extends SpellAbilityAi {
boolean chance = false;
if (sa.usesTargeting()) {
final List<Card> list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true);
if (list.isEmpty()) {
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
List<Card> list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true);
if (list.isEmpty() && mandatory) {
list = CardUtil.getValidCardsToTarget(sa);
}
final Card blocker = ComputerUtilCard.getBestCreatureAI(list);
if (blocker == null) {

View File

@@ -3,7 +3,6 @@ package forge.ai.ability;
import forge.ai.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
@@ -29,11 +28,6 @@ public class PeekAndRevealAi extends SpellAbilityAi {
if (aiPlayer.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("EndOfOppTurn".equals(logic)) {
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
if (!(ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// So far this only appears on Triggers, but will expand
// once things get converted from Dig + NoMove

View File

@@ -78,7 +78,7 @@ public class PermanentCreatureAi extends PermanentAi {
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
&& ai.getManaPool().totalMana() <= 0
&& (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS))
&& (!card.hasETBTrigger(true) && !card.hasSVar("AmbushAI"))
&& !card.hasETBTrigger(true) && !card.hasSVar("AmbushAI")
&& game.getStack().isEmpty()
&& !ComputerUtil.castPermanentInMain1(ai, sa)) {
// AiPlayDecision.AnotherTime;

View File

@@ -33,10 +33,8 @@ public class PhasesAi extends SpellAbilityAi {
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(source);
if (isThreatened) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);

View File

@@ -153,8 +153,8 @@ public class PlayAi extends SpellAbilityAi {
final boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
final CardStateName state;
if (sa.hasParam("CastTransformed")) {
state = CardStateName.Transformed;
options.forEach(c -> c.changeToState(CardStateName.Transformed));
state = CardStateName.Backside;
options.forEach(c -> c.changeToState(CardStateName.Backside));
} else {
state = CardStateName.Original;
}

View File

@@ -6,7 +6,6 @@ import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.GameLossReason;
@@ -65,7 +64,7 @@ public class PoisonAi extends SpellAbilityAi {
boolean result;
if (sa.usesTargeting()) {
result = tgtPlayer(ai, sa, mandatory);
} else if (mandatory || !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
} else if (mandatory || !ai.canReceiveCounters(CounterEnumType.POISON)) {
// mandatory or ai is uneffected
result = true;
} else {
@@ -90,7 +89,7 @@ public class PoisonAi extends SpellAbilityAi {
PlayerCollection betterTgts = tgts.filter(input -> {
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
return false;
} else if (!input.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
} else if (!input.canReceiveCounters(CounterEnumType.POISON)) {
return false;
}
return true;
@@ -109,7 +108,7 @@ public class PoisonAi extends SpellAbilityAi {
if (tgts.isEmpty()) {
if (mandatory) {
// AI is uneffected
if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterEnumType.POISON)) {
sa.getTargets().add(ai);
return true;
}
@@ -121,7 +120,7 @@ public class PoisonAi extends SpellAbilityAi {
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
return true;
}
return !input.canReceiveCounters(CounterType.get(CounterEnumType.POISON));
return !input.canReceiveCounters(CounterEnumType.POISON);
});
if (!betterAllies.isEmpty()) {
allies = betterAllies;

View File

@@ -7,8 +7,6 @@ import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostTapType;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -23,13 +21,6 @@ import java.util.*;
public class PumpAi extends PumpAiBase {
private static boolean hasTapCost(final Cost cost, final Card source) {
if (cost == null) {
return true;
}
return cost.hasSpecificCostType(CostTapType.class);
}
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("MoveCounter".equals(aiLogic)) {
@@ -96,7 +87,7 @@ public class PumpAi extends PumpAiBase {
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final Game game = ai.getGame();
boolean main1Preferred = "Main1IfAble".equals(sa.getParam("AILogic")) && ph.is(PhaseType.MAIN1, ai);
if (game.getStack().isEmpty() && hasTapCost(sa.getPayCosts(), sa.getHostCard())) {
if (game.getStack().isEmpty() && sa.getPayCosts().hasTapCost()) {
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) {
return false;
}
@@ -347,13 +338,13 @@ public class PumpAi extends PumpAiBase {
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
//Targeted
if (!pumpTgtAI(ai, sa, defense, attack, false, false)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} // pumpPlayAI()
}
private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory,
boolean immediately) {
@@ -462,7 +453,7 @@ public class PumpAi extends PumpAiBase {
}
if (isFight) {
return FightAi.canFightAi(ai, sa, attack, defense).willingToPlay();
return FightAi.canFight(ai, sa, attack, defense).willingToPlay();
}
}
@@ -487,18 +478,28 @@ public class PumpAi extends PumpAiBase {
}
}
if (game.getStack().isEmpty()) {
// If the cost is tapping, don't activate before declare attack/block
if (sa.getPayCosts().hasTapCost()) {
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& game.getPhaseHandler().isPlayerTurn(ai)) {
list.remove(sa.getHostCard());
}
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
list.remove(sa.getHostCard());
}
if (game.getStack().isEmpty() && sa.getPayCosts().hasTapCost()) {
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& game.getPhaseHandler().isPlayerTurn(ai)) {
list.remove(source);
}
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
list.remove(source);
}
}
// Detain target nonland permanent: don't target noncreature permanents that don't have
// any activated abilities.
if ("DetainNonLand".equals(sa.getParam("AILogic"))) {
list = CardLists.filter(list, CardPredicates.CREATURES.or(card -> {
for (SpellAbility sa1 : card.getSpellAbilities()) {
if (sa1.isActivatedAbility()) {
return true;
}
}
return false;
}));
}
// Filter AI-specific targets if provided

View File

@@ -147,7 +147,7 @@ public class PumpAllAi extends PumpAiBase {
// important to call canPlay first so targets are added if needed
AiAbilityDecision decision = canPlay(ai, sa);
if (mandatory && decision.decision().willingToPlay()) {
if (mandatory && !decision.decision().willingToPlay()) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return decision;

View File

@@ -37,13 +37,12 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
}
// Do it once per turn, generally (may be improved later)
if (AiCardMemory.isRememberedCardByName(aiPlayer, source.getName(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
if (source.getAbilityActivatedThisTurn().getActivators(sa).contains(aiPlayer)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (sa.usesTargeting()) {
// ability is targeted
sa.resetTargets();
PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
@@ -62,14 +61,6 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // could not find a valid target
}
if (!canTgtHuman || !canTgtAI) {
// can't target another player anyway, remember for no second activation this turn
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
}
} else {
// if it's just defined, no big deal
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
@@ -80,8 +71,6 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
*/
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
// Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList
AiAbilityDecision decision = canPlay(ai, sa);
if (decision.willingToPlay()) {
return decision;

View File

@@ -88,9 +88,9 @@ public class ScryAi extends SpellAbilityAi {
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
// even if there's no mana cost.
if (logic.equals("AtOppEOT") || (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
&& !isSorcerySpeed(sa, ai))) {
if (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || sa.getHostCard().isCreature())
&& !isSorcerySpeed(sa, ai)) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}

View File

@@ -21,10 +21,6 @@ public class SurveilAi extends SpellAbilityAi {
*/
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { // TODO: It doesn't appear that Surveil ever targets, is this necessary?
sa.resetTargets();
sa.getTargets().add(ai);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@@ -92,7 +88,6 @@ public class SurveilAi extends SpellAbilityAi {
}
if (randomReturn) {
AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}

View File

@@ -20,19 +20,6 @@ import java.util.function.Predicate;
public abstract class TapAiBase extends SpellAbilityAi {
Predicate<Card> CREATURE_OR_TAP_ABILITY = c -> {
if (c.isCreature()) {
return true;
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
};
/**
* <p>
* tapTargetList.
@@ -115,12 +102,34 @@ public abstract class TapAiBase extends SpellAbilityAi {
final Game game = ai.getGame();
CardCollection tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
tapList = CardLists.filter(tapList, CardPredicates.CAN_TAP);
tapList = CardLists.filter(tapList, CREATURE_OR_TAP_ABILITY);
tapList = CardLists.filter(tapList, c -> {
if (c.isCreature()) {
return true;
}
for (final SpellAbility sa1 : c.getSpellAbilities()) {
if (sa1.isAbility() && sa1.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
});
//use broader approach when the cost is a positive thing
if (tapList.isEmpty() && ComputerUtil.activateForCost(sa, ai)) {
tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
tapList = CardLists.filter(tapList, CREATURE_OR_TAP_ABILITY);
tapList = CardLists.filter(tapList, c -> {
if (c.isCreature()) {
return true;
}
for (final SpellAbility sa12 : c.getSpellAbilities()) {
if (sa12.isAbility() && sa12.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
});
}
//try to exclude things that will already be tapped due to something on stack or because something is

View File

@@ -8,7 +8,6 @@ import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController;
@@ -40,7 +39,7 @@ public class TimeTravelAi extends SpellAbilityAi {
// so removing them is good; stuff on the battlefield is usually stuff like Vanishing or As Foretold, which favors adding Time
// counters for better effect, but exceptions should be added here).
Card target = (Card)params.get("Target");
return !ComputerUtil.isNegativeCounter(CounterType.get(CounterEnumType.TIME), target);
return !ComputerUtil.isNegativeCounter(CounterEnumType.TIME, target);
}
@Override

View File

@@ -26,7 +26,6 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
@@ -67,17 +66,15 @@ public class TokenAi extends SpellAbilityAi {
}
}
}
String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
Card actualToken = spawnToken(ai, sa);
if (actualToken == null || (actualToken.isCreature() && actualToken.getNetToughness() < 1)) {
final AbilitySub sub = sa.getSubAbility();
// useful
// no token created
return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub).chkDrawback(sub, ai).willingToPlay()); // planeswalker plus ability or sub-ability is
// planeswalker plus ability or sub-ability is useful
return pwPlus || sa.getSubAbility() != null;
}
String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString());
String tokenToughness = sa.getParamOrDefault("TokenToughness", actualToken.getBaseToughnessString());
@@ -134,9 +131,6 @@ public class TokenAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
/*
* readParameters() is called in checkPhaseRestrictions
*/
final Game game = ai.getGame();
final Player opp = ai.getWeakestOpponent();

View File

@@ -31,6 +31,8 @@ public final class ImageKeys {
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 SPEED_IMAGE = "speed";
public static final String MAX_SPEED_IMAGE = "max_speed";
public static final String BACKFACE_POSTFIX = "$alt";
public static final String SPECFACE_W = "$wspec";

View File

@@ -18,9 +18,9 @@ import java.util.*;
import java.util.function.Predicate;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* The class holding game invariants, such as cards, editions, game formats. All that data, which is not supposed to be changed by player
*
@@ -29,8 +29,6 @@ import java.util.stream.Collectors;
public class StaticData {
private final CardStorageReader cardReader;
private final CardStorageReader tokenReader;
private final CardStorageReader customCardReader;
private final String blockDataFolder;
private final CardDb commonCards;
private final CardDb variantCards;
@@ -79,7 +77,6 @@ public class StaticData {
this.tokenReader = tokenReader;
this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder)));
this.blockDataFolder = blockDataFolder;
this.customCardReader = customCardReader;
this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance;
this.enableSmartCardArtSelection = enableSmartCardArtSelection;
this.loadNonLegalCards = loadNonLegalCards;
@@ -784,6 +781,7 @@ public class StaticData {
Queue<String> TOKEN_Q = new ConcurrentLinkedQueue<>();
boolean nifHeader = false;
boolean cniHeader = false;
final Pattern funnyCardCollectorNumberPattern = Pattern.compile("^F\\d+");
for (CardEdition e : editions) {
if (CardEdition.Type.FUNNY.equals(e.getType()))
continue;
@@ -791,11 +789,13 @@ public class StaticData {
Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>();
List<CompletableFuture<?>> futures = new ArrayList<>();
for (CardEdition.EditionEntry c : e.getObtainableCards()) {
int amount = 1;
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));
amount = cardCount.get(c.name()).getRight() + 1;
}
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && funnyCardCollectorNumberPattern.matcher(c.collectorNumber()).matches(), amount));
}
// loop through the cards in this edition, considering art variations...
@@ -878,7 +878,7 @@ public class StaticData {
}
}
}
// stream().toList() causes crash on Android, use Collectors.toList()
// stream().toList() causes crash on Android 8-13, use Collectors.toList()
List<String> NIF = new ArrayList<>(NIF_Q).stream().sorted().collect(Collectors.toList());
List<String> CNI = new ArrayList<>(CNI_Q).stream().sorted().collect(Collectors.toList());
List<String> TOK = new ArrayList<>(TOKEN_Q).stream().sorted().collect(Collectors.toList());

View File

@@ -21,6 +21,7 @@ import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import forge.ImageKeys;
import forge.StaticData;
import forge.card.CardEdition.EditionEntry;
import forge.card.CardEdition.Type;
@@ -44,8 +45,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public final static char NameSetSeparator = '|';
public final static String FlagPrefix = "#";
public static final String FlagSeparator = "\t";
private final String exlcudedCardName = "Concentrate";
private final String exlcudedCardSet = "DS0";
// need this to obtain cardReference by name+set+artindex
private final ListMultimap<String, PaperCard> allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
@@ -200,7 +199,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
private static boolean isArtIndex(String s) {
return StringUtils.isNumeric(s) && s.length() <= 2 ; // only artIndex between 1-99
return StringUtils.isNumeric(s) && s.length() <= 2; // only artIndex between 1-99
}
private static boolean isSetCode(String s) {
@@ -241,8 +240,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
setCode = info[index];
index++;
}
if(info.length > index && isArtIndex(info[index])) {
artIndex = Integer.parseInt(info[index]);
if(info.length > index && isArtIndex(info[index].replace(ImageKeys.BACKFACE_POSTFIX, ""))) {
artIndex = Integer.parseInt(info[index].replace(ImageKeys.BACKFACE_POSTFIX, ""));
index++;
}
if(info.length > index && isCollectorNumber(info[index])) {
@@ -302,7 +301,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// create faces list from rules
for (final CardRules rule : rules.values()) {
if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName()))
if (filteredCards.contains(rule.getName()))
continue;
for (ICardFace face : rule.getAllFaces()) {
addFaceToDbNames(face);
@@ -500,8 +499,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
public void addCard(PaperCard paperCard) {
if (excludeCard(paperCard.getName(), paperCard.getEdition()))
if (filtered.contains(paperCard.getName())) {
return;
}
allCardsByName.put(paperCard.getName(), paperCard);
@@ -522,17 +522,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
}
private boolean excludeCard(String cardName, String cardEdition) {
if (filtered.isEmpty())
return false;
if (filtered.contains(cardName)) {
if (exlcudedCardSet.equalsIgnoreCase(cardEdition) && exlcudedCardName.equalsIgnoreCase(cardName))
return true;
else return !exlcudedCardName.equalsIgnoreCase(cardName);
}
return false;
}
private void reIndex() {
uniqueCardsByName.clear();
for (Entry<String, Collection<PaperCard>> kv : allCardsByName.asMap().entrySet()) {

View File

@@ -52,6 +52,14 @@ import java.util.stream.Collectors;
*/
public final class CardEdition implements Comparable<CardEdition> {
public DraftOptions getDraftOptions() {
return draftOptions;
}
public void setDraftOptions(DraftOptions draftOptions) {
this.draftOptions = draftOptions;
}
// immutable
public enum Type {
UNKNOWN,
@@ -275,18 +283,22 @@ public final class CardEdition implements Comparable<CardEdition> {
// Booster/draft info
private List<BoosterSlot> boosterSlots = null;
private boolean smallSetOverride = false;
private boolean foilAlwaysInCommonSlot = false;
private String additionalUnlockSet = "";
private FoilType foilType = FoilType.NOT_SUPPORTED;
// Replace all of these things with booster slots
private boolean foilAlwaysInCommonSlot = false;
private double foilChanceInBooster = 0;
private double chanceReplaceCommonWith = 0;
private String slotReplaceCommonWith = "Common";
private String additionalSheetForFoils = "";
private String additionalUnlockSet = "";
private String boosterMustContain = "";
private String boosterReplaceSlotFromPrintSheet = "";
private String sheetReplaceCardFromSheet = "";
private String sheetReplaceCardFromSheet2 = "";
private String doublePickDuringDraft = "";
// Draft options
private DraftOptions draftOptions = null;
private String[] chaosDraftThemes = new String[0];
private final ListMultimap<String, EditionEntry> cardMap;
@@ -373,7 +385,6 @@ public final class CardEdition implements Comparable<CardEdition> {
public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; }
public String getAdditionalSheetForFoils() { return additionalSheetForFoils; }
public String getAdditionalUnlockSet() { return additionalUnlockSet; }
public String getDoublePickDuringDraft() { return doublePickDuringDraft; }
public String getBoosterMustContain() { return boosterMustContain; }
public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; }
public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; }
@@ -552,26 +563,16 @@ public final class CardEdition implements Comparable<CardEdition> {
public List<PrintSheet> getPrintSheetsBySection() {
final CardDb cardDb = StaticData.instance().getCommonCards();
Map<String, Integer> cardToIndex = new HashMap<>();
List<PrintSheet> sheets = Lists.newArrayList();
for (String sectionName : cardMap.keySet()) {
if (sectionName.equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) {
for (Map.Entry<String, java.util.Collection<EditionEntry>> section : cardMap.asMap().entrySet()) {
if (section.getKey().equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) {
continue;
}
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName));
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), section.getKey()));
List<EditionEntry> cards = cardMap.get(sectionName);
for (EditionEntry card : cards) {
int index = 1;
if (cardToIndex.containsKey(card.name)) {
index = cardToIndex.get(card.name) + 1;
}
cardToIndex.put(card.name, index);
PaperCard pCard = cardDb.getCard(card.name, this.getCode(), index);
sheet.add(pCard);
for (EditionEntry card : section.getValue()) {
sheet.add(cardDb.getCard(card.name, this.getCode(), card.collectorNumber));
}
sheets.add(sheet);
@@ -629,7 +630,7 @@ 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)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
);
final Pattern tokenPattern = Pattern.compile(
@@ -638,7 +639,7 @@ public final class CardEdition implements Comparable<CardEdition> {
* name - grouping #3
* artist name - grouping #5
*/
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
"(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
);
ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create();
@@ -659,31 +660,37 @@ public final class CardEdition implements Comparable<CardEdition> {
continue;
}
// parse sections of the format "<collector number> <rarity> <name>"
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
for(String line : contents.get(sectionName)) {
Matcher matcher = pattern.matcher(line);
if (!matcher.matches()) {
continue;
}
String collectorNumber = matcher.group(2);
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
String cardName = matcher.group(5);
String artistName = matcher.group(7);
String functionalVariantName = matcher.group(9);
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
cardMap.put(sectionName, cis);
}
} else if (boosterSlotsToParse.contains(sectionName)) {
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
if (sectionName.endsWith("Types")) {
CardType.Helper.parseTypes(sectionName, contents.get(sectionName));
} else {
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
// Parse cards
// parse sections of the format "<collector number> <rarity> <name>"
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
for(String line : contents.get(sectionName)) {
Matcher matcher = pattern.matcher(line);
if (!matcher.matches()) {
continue;
}
String collectorNumber = matcher.group(2);
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
String cardName = matcher.group(5);
String artistName = matcher.group(7);
String functionalVariantName = matcher.group(9);
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
cardMap.put(sectionName, cis);
}
} else if (boosterSlotsToParse.contains(sectionName)) {
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
} else {
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
}
}
}
@@ -812,7 +819,6 @@ public final class CardEdition implements Comparable<CardEdition> {
res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral
res.smallSetOverride = metadata.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon)
res.doublePickDuringDraft = metadata.get("DoublePick", ""); // "FirstPick" or "Always"
res.boosterMustContain = metadata.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature
res.boosterReplaceSlotFromPrintSheet = metadata.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card
@@ -820,6 +826,23 @@ public final class CardEdition implements Comparable<CardEdition> {
res.sheetReplaceCardFromSheet2 = metadata.get("SheetReplaceCardFromSheet2", "");
res.chaosDraftThemes = metadata.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names
// Draft options
String doublePick = metadata.get("DoublePick", "Never");
int maxPodSize = metadata.getInt("MaxPodSize", 8);
int recommendedPodSize = metadata.getInt("RecommendedPodSize", 8);
int maxMatchPlayers = metadata.getInt("MaxMatchPlayers", 2);
String deckType = metadata.get("DeckType", "Normal");
String freeCommander = metadata.get("FreeCommander", "");
res.draftOptions = new DraftOptions(
doublePick,
maxPodSize,
recommendedPodSize,
maxMatchPlayers,
deckType,
freeCommander
);
return res;
}
@@ -850,7 +873,7 @@ public final class CardEdition implements Comparable<CardEdition> {
@Override
public void add(CardEdition item) { //Even though we want it to be read only, make an exception for custom content.
if(lock) throw new UnsupportedOperationException("This is a read-only storage");
else map.put(item.getName(), item);
else map.put(item.getCode(), item);
}
public void append(CardEdition.Collection C){ //Append custom editions
if (lock) throw new UnsupportedOperationException("This is a read-only storage");
@@ -995,16 +1018,13 @@ public final class CardEdition implements Comparable<CardEdition> {
public static final Predicate<CardEdition> HAS_BOOSTER_BOX = edition -> edition.getBoosterBoxCount() > 0;
@Deprecated //Use CardEdition::hasBasicLands and a nonnull test.
public static final Predicate<CardEdition> hasBasicLands = ed -> {
if (ed == null) {
// Happens for new sets with "???" code
return false;
}
for(String landName : MagicColor.Constant.BASIC_LANDS) {
if (null == StaticData.instance().getCommonCards().getCard(landName, ed.getCode(), 0))
return false;
}
return true;
return ed.hasBasicLands();
};
}
@@ -1025,7 +1045,7 @@ public final class CardEdition implements Comparable<CardEdition> {
public boolean hasBasicLands() {
for(String landName : MagicColor.Constant.BASIC_LANDS) {
if (null == StaticData.instance().getCommonCards().getCard(landName, this.getCode(), 0))
if (this.getCardInSet(landName).isEmpty())
return false;
}
return true;

View File

@@ -53,6 +53,7 @@ public final class CardRules implements ICardCharacteristics {
private boolean addsWildCardColor;
private int setColorID;
private boolean custom;
private boolean unsupported;
private String path;
public CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
@@ -166,6 +167,10 @@ public final class CardRules implements ICardCharacteristics {
return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values());
}
public boolean isTransformable() {
return CardSplitType.Transform == getSplitType() || CardSplitType.Modal == getSplitType();
}
public ICardFace getWSpecialize() {
return specializedParts.get(CardStateName.SpecializeW);
}
@@ -202,7 +207,9 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean isCustom() { return custom; }
public void setCustom() { custom = true; }
public void setCustom() { custom = true; }
public boolean isUnsupported() { return unsupported; }
@Override
public CardType getType() {
@@ -317,6 +324,12 @@ public final class CardRules implements ICardCharacteristics {
}
if (hasKeyword("Friends forever") && b.hasKeyword("Friends forever")) {
legal = true; // Stranger Things Secret Lair gimmick partner commander
}
if (hasKeyword("Partner - Survivors") && b.hasKeyword("Partner - Survivors")) {
legal = true; // The Last of Us Secret Lair gimmick partner commander
}
if (hasKeyword("Partner - Father & Son") && b.hasKeyword("Partner - Father & Son")) {
legal = true; // God of War Secret Lair gimmick partner commander
}
if (hasKeyword("Choose a Background") && b.canBeBackground()
|| b.hasKeyword("Choose a Background") && canBeBackground()) {
@@ -335,6 +348,7 @@ public final class CardRules implements ICardCharacteristics {
}
return canBeCommander() && (hasKeyword("Partner") || !this.partnerWith.isEmpty() ||
hasKeyword("Friends forever") || hasKeyword("Choose a Background") ||
hasKeyword("Partner - Father & Son") || hasKeyword("Partner - Survivors") ||
hasKeyword("Doctor's companion") || isDoctor());
}
@@ -343,16 +357,21 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean isDoctor() {
Set<String> subtypes = new HashSet<>();
for (String type : mainPart.getType().getSubtypes()) {
if (!type.equals("Time Lord") && !type.equals("Doctor")) {
return false;
}
subtypes.add(type);
}
return true;
return subtypes.size() == 2 &&
subtypes.contains("Time Lord") &&
subtypes.contains("Doctor");
}
public boolean canBeOathbreaker() {
CardType type = mainPart.getType();
if (mainPart.getOracleText().contains("can be your commander")) {
return true;
}
return type.isPlaneswalker();
}
@@ -805,6 +824,8 @@ public final class CardRules implements ICardCharacteristics {
faces[0].assignMissingFields();
final CardRules result = new CardRules(faces, CardSplitType.None, cah);
result.unsupported = true;
return result;
}

View File

@@ -189,6 +189,38 @@ public final class CardRulesPredicates {
return card -> card.getType().hasSupertype(type);
}
/**
* @return a Predicate that matches cards that are of the split type.
*/
public static Predicate<CardRules> isSplitType(final CardSplitType type) {
return card -> card.getSplitType().equals(type);
}
/**
* @return a Predicate that matches cards that are vanilla.
*/
public static Predicate<CardRules> isVanilla() {
return card -> {
if (!(card.getType().isCreature() || card.getType().isLand()) ||
card.getSplitType() != CardSplitType.None ||
card.hasFunctionalVariants()) {
return false;
}
ICardFace mainPart = card.getMainPart();
boolean hasAny =
mainPart.getKeywords().iterator().hasNext() ||
mainPart.getAbilities().iterator().hasNext() ||
mainPart.getStaticAbilities().iterator().hasNext() ||
mainPart.getTriggers().iterator().hasNext() ||
(mainPart.getDraftActions() != null && mainPart.getDraftActions().iterator().hasNext()) ||
mainPart.getReplacements().iterator().hasNext();
return !hasAny;
};
}
/**
* Checks for color.
*

View File

@@ -7,13 +7,13 @@ import java.util.EnumSet;
public enum CardSplitType
{
None(FaceSelectionMethod.USE_PRIMARY_FACE, null),
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Transformed),
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
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.Secondary),
Omen(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal),
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);
public static final EnumSet<CardSplitType> DUAL_FACED_CARDS = EnumSet.of(

View File

@@ -5,12 +5,11 @@ public enum CardStateName {
Original,
FaceDown,
Flipped,
Transformed,
Backside,
Meld,
LeftSplit,
RightSplit,
Secondary,
Modal,
EmptyRoom,
SpecializeW,
SpecializeU,
@@ -42,7 +41,7 @@ public enum CardStateName {
return CardStateName.Flipped;
}
if ("DoubleFaced".equalsIgnoreCase(value)) {
return CardStateName.Transformed;
return CardStateName.Backside;
}
throw new IllegalArgumentException("No element named " + value + " in enum CardCharactersticName");

View File

@@ -1066,4 +1066,74 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return type;
}
public static class Helper {
public static final void parseTypes(String sectionName, List<String> content) {
Set<String> addToSection = null;
switch (sectionName) {
case "BasicTypes":
addToSection = CardType.Constant.BASIC_TYPES;
break;
case "LandTypes":
addToSection = CardType.Constant.LAND_TYPES;
break;
case "CreatureTypes":
addToSection = CardType.Constant.CREATURE_TYPES;
break;
case "SpellTypes":
addToSection = CardType.Constant.SPELL_TYPES;
break;
case "EnchantmentTypes":
addToSection = CardType.Constant.ENCHANTMENT_TYPES;
break;
case "ArtifactTypes":
addToSection = CardType.Constant.ARTIFACT_TYPES;
break;
case "WalkerTypes":
addToSection = CardType.Constant.WALKER_TYPES;
break;
case "DungeonTypes":
addToSection = CardType.Constant.DUNGEON_TYPES;
break;
case "BattleTypes":
addToSection = CardType.Constant.BATTLE_TYPES;
break;
case "PlanarTypes":
addToSection = CardType.Constant.PLANAR_TYPES;
break;
}
if (addToSection == null) {
return;
}
for(String line : content) {
if (line.length() == 0) continue;
if (line.contains(":")) {
String[] k = line.split(":");
if (addToSection.contains(k[0])) {
continue;
}
addToSection.add(k[0]);
CardType.Constant.pluralTypes.put(k[0], k[1]);
if (k[0].contains(" ")) {
CardType.Constant.MultiwordTypes.add(k[0]);
}
} else {
if (addToSection.contains(line)) {
continue;
}
addToSection.add(line);
if (line.contains(" ")) {
CardType.Constant.MultiwordTypes.add(line);
}
}
}
}
}
}

View File

@@ -20,7 +20,6 @@ package forge.card;
import com.google.common.collect.UnmodifiableIterator;
import forge.card.MagicColor.Color;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostShard;
import forge.util.BinaryUtil;
import java.io.Serializable;
@@ -41,27 +40,97 @@ import java.util.stream.Stream;
public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Serializable {
private static final long serialVersionUID = 794691267379929080L;
// needs to be before other static
private static final ColorSet[] cache = new ColorSet[MagicColor.ALL_COLORS + 1];
static {
byte COLORLESS = MagicColor.COLORLESS;
byte WHITE = MagicColor.WHITE;
byte BLUE = MagicColor.BLUE;
byte BLACK = MagicColor.BLACK;
byte RED = MagicColor.RED;
byte GREEN = MagicColor.GREEN;
Color C = Color.COLORLESS;
Color W = Color.WHITE;
Color U = Color.BLUE;
Color B = Color.BLACK;
Color R = Color.RED;
Color G = Color.GREEN;
//colorless
cache[COLORLESS] = new ColorSet(C);
//mono-color
cache[WHITE] = new ColorSet(W);
cache[BLUE] = new ColorSet(U);
cache[BLACK] = new ColorSet(B);
cache[RED] = new ColorSet(R);
cache[GREEN] = new ColorSet(G);
//two-color
cache[WHITE | BLUE] = new ColorSet(W, U);
cache[WHITE | BLACK] = new ColorSet(W, B);
cache[BLUE | BLACK] = new ColorSet(U, B);
cache[BLUE | RED] = new ColorSet(U, R);
cache[BLACK | RED] = new ColorSet(B, R);
cache[BLACK | GREEN] = new ColorSet(B, G);
cache[RED | GREEN] = new ColorSet(R, G);
cache[RED | WHITE] = new ColorSet(R, W);
cache[GREEN | WHITE] = new ColorSet(G, W);
cache[GREEN | BLUE] = new ColorSet(G, U);
//three-color
cache[WHITE | BLUE | BLACK] = new ColorSet(W, U, B);
cache[WHITE | BLACK | GREEN] = new ColorSet(W, B, G);
cache[BLUE | BLACK | RED] = new ColorSet(U, B, R);
cache[BLUE | RED | WHITE] = new ColorSet(U, R, W);
cache[BLACK | RED | GREEN] = new ColorSet(B, R, G);
cache[BLACK | GREEN | BLUE] = new ColorSet(B, G, U);
cache[RED | GREEN | WHITE] = new ColorSet(R, G, W);
cache[RED | WHITE | BLACK] = new ColorSet(R, W, B);
cache[GREEN | WHITE | BLUE] = new ColorSet(G, W, U);
cache[GREEN | BLUE | RED] = new ColorSet(G, U, R);
//four-color
cache[WHITE | BLUE | BLACK | RED] = new ColorSet(W, U, B, R);
cache[BLUE | BLACK | RED | GREEN] = new ColorSet(U, B, R, G);
cache[BLACK | RED | GREEN | WHITE] = new ColorSet(B, R, G, W);
cache[RED | GREEN | WHITE | BLUE] = new ColorSet(R, G, W, U);
cache[GREEN | WHITE | BLUE | BLACK] = new ColorSet(G, W, U, B);
//five-color
cache[WHITE | BLUE | BLACK | RED | GREEN] = new ColorSet(W, U, B, R, G);
}
private final Collection<Color> orderedShards;
private final byte myColor;
private final float orderWeight;
private static final ColorSet[] cache = new ColorSet[32];
private final Set<Color> enumSet;
private final String desc;
public static final ColorSet ALL_COLORS = fromMask(MagicColor.ALL_COLORS);
private static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
public static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
private ColorSet(final byte mask) {
this.myColor = mask;
private ColorSet(final Color... ordered) {
this.orderedShards = Arrays.asList(ordered);
this.myColor = orderedShards.stream().map(Color::getColorMask).reduce((byte)0, (a, b) -> (byte)(a | b));
this.orderWeight = this.getOrderWeight();
this.enumSet = EnumSet.copyOf(orderedShards);
this.desc = orderedShards.stream().map(Color::getShortName).collect(Collectors.joining());
}
public static ColorSet fromMask(final int mask) {
final int mask32 = mask & MagicColor.ALL_COLORS;
if (cache[mask32] == null) {
cache[mask32] = new ColorSet((byte) mask32);
}
return cache[mask32];
}
public static ColorSet fromEnums(final Color... colors) {
byte mask = 0;
for (Color e : colors) {
mask |= e.getColorMask();
}
return fromMask(mask);
}
public static ColorSet fromNames(final String... colors) {
byte mask = 0;
for (final String s : colors) {
@@ -293,17 +362,7 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
*/
@Override
public String toString() {
final ManaCostShard[] orderedShards = getOrderedShards();
return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
}
/**
* Gets the null color.
*
* @return the nullColor
*/
public static ColorSet getNullColor() {
return NO_COLORS;
return desc;
}
/**
@@ -325,16 +384,7 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
}
public Set<Color> toEnumSet() {
if (isColorless()) {
return EnumSet.of(Color.COLORLESS);
}
List<Color> list = new ArrayList<>();
for (Color c : Color.values()) {
if (hasAnyColor(c.getColormask())) {
list.add(c);
}
}
return EnumSet.copyOf(list);
return EnumSet.copyOf(enumSet);
}
@Override
@@ -372,72 +422,12 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
}
}
public Stream<MagicColor.Color> stream() {
public Stream<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];
}
private static final ManaCostShard[][] shardOrderLookup = new ManaCostShard[MagicColor.ALL_COLORS + 1][];
static {
byte COLORLESS = MagicColor.COLORLESS;
byte WHITE = MagicColor.WHITE;
byte BLUE = MagicColor.BLUE;
byte BLACK = MagicColor.BLACK;
byte RED = MagicColor.RED;
byte GREEN = MagicColor.GREEN;
ManaCostShard C = ManaCostShard.COLORLESS;
ManaCostShard W = ManaCostShard.WHITE;
ManaCostShard U = ManaCostShard.BLUE;
ManaCostShard B = ManaCostShard.BLACK;
ManaCostShard R = ManaCostShard.RED;
ManaCostShard G = ManaCostShard.GREEN;
//colorless
shardOrderLookup[COLORLESS] = new ManaCostShard[] { C };
//mono-color
shardOrderLookup[WHITE] = new ManaCostShard[] { W };
shardOrderLookup[BLUE] = new ManaCostShard[] { U };
shardOrderLookup[BLACK] = new ManaCostShard[] { B };
shardOrderLookup[RED] = new ManaCostShard[] { R };
shardOrderLookup[GREEN] = new ManaCostShard[] { G };
//two-color
shardOrderLookup[WHITE | BLUE] = new ManaCostShard[] { W, U };
shardOrderLookup[WHITE | BLACK] = new ManaCostShard[] { W, B };
shardOrderLookup[BLUE | BLACK] = new ManaCostShard[] { U, B };
shardOrderLookup[BLUE | RED] = new ManaCostShard[] { U, R };
shardOrderLookup[BLACK | RED] = new ManaCostShard[] { B, R };
shardOrderLookup[BLACK | GREEN] = new ManaCostShard[] { B, G };
shardOrderLookup[RED | GREEN] = new ManaCostShard[] { R, G };
shardOrderLookup[RED | WHITE] = new ManaCostShard[] { R, W };
shardOrderLookup[GREEN | WHITE] = new ManaCostShard[] { G, W };
shardOrderLookup[GREEN | BLUE] = new ManaCostShard[] { G, U };
//three-color
shardOrderLookup[WHITE | BLUE | BLACK] = new ManaCostShard[] { W, U, B };
shardOrderLookup[WHITE | BLACK | GREEN] = new ManaCostShard[] { W, B, G };
shardOrderLookup[BLUE | BLACK | RED] = new ManaCostShard[] { U, B, R };
shardOrderLookup[BLUE | RED | WHITE] = new ManaCostShard[] { U, R, W };
shardOrderLookup[BLACK | RED | GREEN] = new ManaCostShard[] { B, R, G };
shardOrderLookup[BLACK | GREEN | BLUE] = new ManaCostShard[] { B, G, U };
shardOrderLookup[RED | GREEN | WHITE] = new ManaCostShard[] { R, G, W };
shardOrderLookup[RED | WHITE | BLACK] = new ManaCostShard[] { R, W, B };
shardOrderLookup[GREEN | WHITE | BLUE] = new ManaCostShard[] { G, W, U };
shardOrderLookup[GREEN | BLUE | RED] = new ManaCostShard[] { G, U, R };
//four-color
shardOrderLookup[WHITE | BLUE | BLACK | RED] = new ManaCostShard[] { W, U, B, R };
shardOrderLookup[BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { U, B, R, G };
shardOrderLookup[BLACK | RED | GREEN | WHITE] = new ManaCostShard[] { B, R, G, W };
shardOrderLookup[RED | GREEN | WHITE | BLUE] = new ManaCostShard[] { R, G, W, U };
shardOrderLookup[GREEN | WHITE | BLUE | BLACK] = new ManaCostShard[] { G, W, U, B };
//five-color
shardOrderLookup[WHITE | BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { W, U, B, R, G };
public Collection<Color> getOrderedColors() {
return orderedShards;
}
}

View File

@@ -0,0 +1,75 @@
package forge.card;
public class DraftOptions {
public enum DoublePick {
NEVER,
FIRST_PICK, // only first pick each pack
WHEN_POD_SIZE_IS_4, // only when pod size is 4, so you can pick two cards each time
ALWAYS // each time you receive a pack, you can pick two cards
};
public enum DeckType {
Normal, // Standard deck, usually 40 cards
Commander // Special deck type for Commander format. Important for selection/construction
}
private DoublePick doublePick = DoublePick.NEVER;
private final int maxPodSize; // Usually 8, but could be smaller for cubes. I guess it could be larger too
private final int recommendedPodSize; // Usually 8, but is 4 for new double pick
private final int maxMatchPlayers; // Usually 2, but 4 for things like Commander or Conspiracy
private final DeckType deckType; // Normal or Commander
private final String freeCommander;
public DraftOptions(String doublePickOption, int maxPodSize, int recommendedPodSize, int maxMatchPlayers, String deckType, String freeCommander) {
this.maxPodSize = maxPodSize;
this.recommendedPodSize = recommendedPodSize;
this.maxMatchPlayers = maxMatchPlayers;
this.deckType = DeckType.valueOf(deckType);
this.freeCommander = freeCommander;
if (doublePickOption != null) {
switch (doublePickOption.toLowerCase()) {
case "firstpick":
doublePick = DoublePick.FIRST_PICK;
break;
case "always":
doublePick = DoublePick.ALWAYS;
break;
case "whenpodsizeis4":
doublePick = DoublePick.WHEN_POD_SIZE_IS_4;
break;
}
}
}
public int getMaxPodSize() {
return maxPodSize;
}
public int getRecommendedPodSize() {
return recommendedPodSize;
}
public DoublePick getDoublePick() {
return doublePick;
}
public DoublePick isDoublePick(int podSize) {
if (doublePick == DoublePick.WHEN_POD_SIZE_IS_4) {
if (podSize != 4) {
return DoublePick.NEVER;
}
// only when pod size is 4, so you can pick two cards each time
return DoublePick.ALWAYS;
}
return doublePick;
}
public int getMaxMatchPlayers() {
return maxMatchPlayers;
}
public DeckType getDeckType() {
return deckType;
}
public String getFreeCommander() {
return freeCommander;
}
}

View File

@@ -1,7 +1,9 @@
package forge.card;
import com.google.common.collect.ImmutableList;
import forge.deck.DeckRecognizer;
import forge.util.ITranslatable;
import forge.util.Localizer;
/**
* Holds byte values for each color magic has.
@@ -157,21 +159,24 @@ public final class MagicColor {
}
}
public enum Color {
WHITE(Constant.WHITE, MagicColor.WHITE, "{W}"),
BLUE(Constant.BLUE, MagicColor.BLUE, "{U}"),
BLACK(Constant.BLACK, MagicColor.BLACK, "{B}"),
RED(Constant.RED, MagicColor.RED, "{R}"),
GREEN(Constant.GREEN, MagicColor.GREEN, "{G}"),
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "{C}");
public enum Color implements ITranslatable {
WHITE(Constant.WHITE, MagicColor.WHITE, "W", "lblWhite"),
BLUE(Constant.BLUE, MagicColor.BLUE, "U", "lblBlue"),
BLACK(Constant.BLACK, MagicColor.BLACK, "B", "lblBlack"),
RED(Constant.RED, MagicColor.RED, "R", "lblRed"),
GREEN(Constant.GREEN, MagicColor.GREEN, "G", "lblGreen"),
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "C", "lblColorless");
private final String name, symbol;
private final String name, shortName, symbol;
private final String label;
private final byte colormask;
Color(String name0, byte colormask0, String symbol0) {
Color(String name0, byte colormask0, String shortName, String label) {
name = name0;
colormask = colormask0;
symbol = symbol0;
this.shortName = shortName;
symbol = "{" + shortName + "}";
this.label = label;
}
public static Color fromByte(final byte color) {
@@ -185,25 +190,25 @@ public final class MagicColor {
}
}
@Override
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 String getShortName() {
return shortName;
}
public byte getColormask() {
@Override
public String getTranslatedName() {
return Localizer.getInstance().getMessage(label);
}
public byte getColorMask() {
return colormask;
}
public String getSymbol() {
return symbol;
}
@Override
public String toString() {
return name;
}
}
}

View File

@@ -13,6 +13,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.Predicate;
/**
* TODO: Write javadoc for this type.
@@ -68,6 +69,13 @@ public class PrintSheet {
cardsWithWeights.remove(card);
}
public boolean contains(PaperCard pc) {
return cardsWithWeights.contains(pc);
}
public PaperCard find(Predicate<PaperCard> filter) {
return cardsWithWeights.find(filter);
}
private PaperCard fetchRoulette(int start, int roulette, Collection<PaperCard> toSkip) {
int sum = start;
boolean isSecondRun = start > 0;
@@ -85,15 +93,6 @@ public class PrintSheet {
return fetchRoulette(sum + 1, roulette, toSkip); // start over from beginning, in case last cards were to skip
}
public List<PaperCard> all() {
List<PaperCard> result = new ArrayList<>();
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
for (int i = 0; i < kv.getValue(); i++) {
result.add(kv.getKey());
}
}
return result;
}
public boolean containsCardNamed(String name,int atLeast) {
int count=0;
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
@@ -144,7 +143,7 @@ public class PrintSheet {
return cardsWithWeights.isEmpty();
}
public Iterable<PaperCard> toFlatList() {
public List<PaperCard> toFlatList() {
return cardsWithWeights.toFlatList();
}

View File

@@ -37,7 +37,6 @@ import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CardPool extends ItemPool<PaperCard> {
private static final long serialVersionUID = -5379091255613968393L;
@@ -78,12 +77,20 @@ public class CardPool extends ItemPool<PaperCard> {
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;
}
}
// Try to get non-Alchemy version if it cannot find it.
if (cardName.startsWith("A-")) {
System.out.println("Alchemy card not found for '" + cardName + "'. Trying to get its non-Alchemy equivalent.");
cardName = cardName.replaceFirst("A-", "");
}
//Failed to find it. Fall back accordingly?
this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny, flags);
}
@@ -419,6 +426,12 @@ public class CardPool extends ItemPool<PaperCard> {
return pool;
}
public static CardPool fromSingleCardRequest(String cardRequest) {
if(StringUtils.isBlank(cardRequest))
return new CardPool();
return fromCardList(List.of(cardRequest));
}
public static List<Pair<String, Integer>> processCardList(final Iterable<String> lines) {
List<Pair<String, Integer>> cardRequests = new ArrayList<>();
if (lines == null)
@@ -468,6 +481,7 @@ public class CardPool extends ItemPool<PaperCard> {
* @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
*/
@Override
public CardPool getFilteredPool(Predicate<PaperCard> predicate) {
CardPool filteredPool = new CardPool();
for (PaperCard c : this.items.keySet()) {

View File

@@ -28,6 +28,8 @@ import forge.item.PaperCard;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.io.ObjectStreamException;
import java.io.Serial;
import java.util.*;
import java.util.Map.Entry;
@@ -113,6 +115,20 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return parts.get(DeckSection.Main);
}
public Pair<Deck, List<PaperCard>> getValid() {
List<PaperCard> unsupported = new ArrayList<>();
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
CardPool pool = kv.getValue();
for (Entry<PaperCard, Integer> pc : pool) {
if (pc.getKey().getRules() != null && pc.getKey().getRules().isUnsupported()) {
unsupported.add(pc.getKey());
pool.remove(pc.getKey());
}
}
}
return Pair.of(this, unsupported);
}
public List<PaperCard> getCommanders() {
List<PaperCard> result = Lists.newArrayList();
final CardPool cp = get(DeckSection.Commander);
@@ -208,14 +224,19 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
super.cloneFieldsTo(clone);
final Deck result = (Deck) clone;
loadDeferredSections();
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
CardPool cp = new CardPool();
result.parts.put(kv.getKey(), cp);
cp.addAll(kv.getValue());
// parts shouldn't be null
if (parts != null) {
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
CardPool cp = new CardPool();
result.parts.put(kv.getKey(), cp);
cp.addAll(kv.getValue());
}
}
result.setAiHints(StringUtils.join(aiHints, " | "));
result.setDraftNotes(draftNotes);
tags.addAll(result.getTags());
//noinspection ConstantValue
if(tags != null) //Can happen deserializing old Decks.
result.tags.addAll(this.tags);
}
/*
@@ -521,6 +542,17 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return sum;
}
/**
* Counts the number of copies of this exact card print across all deck sections.
*/
public int count(PaperCard card) {
int sum = 0;
for (Entry<DeckSection, CardPool> section : this) {
sum += section.getValue().count(card);
}
return sum;
}
public void setAiHints(String aiHintsInfo) {
if (aiHintsInfo == null || aiHintsInfo.trim().isEmpty()) {
return;
@@ -614,6 +646,14 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return this;
}
@Serial
private Object readResolve() throws ObjectStreamException {
//If we deserialized an old deck that doesn't have tags, fix it here.
if(this.tags == null)
return new Deck(this, this.getName() == null ? "" : this.getName());
return this;
}
/** {@inheritDoc} */
@Override
public boolean equals(final Object o) {
@@ -663,4 +703,4 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
}
return totalCount == 0 ? 0 : Math.round(totalCMC / totalCount);
}
}
}

View File

@@ -32,11 +32,8 @@ import forge.util.TextUtil;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.*;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Predicate;
/**
@@ -60,6 +57,13 @@ public enum DeckFormat {
//Limited contraption decks have no restrictions.
return null;
}
@Override
public int getExtraSectionMaxCopies(DeckSection section) {
if(section == DeckSection.Attractions || section == DeckSection.Contraptions)
return Integer.MAX_VALUE;
return super.getExtraSectionMaxCopies(section);
}
},
Commander ( Range.is(99), Range.of(0, 10), 1, null,
card -> StaticData.instance().getCommanderPredicate().test(card)
@@ -108,7 +112,13 @@ public enum DeckFormat {
}
},
PlanarConquest ( Range.of(40, Integer.MAX_VALUE), Range.is(0), 1),
Adventure ( Range.of(40, Integer.MAX_VALUE), Range.of(0, 15), 4),
Adventure ( Range.of(40, Integer.MAX_VALUE), Range.of(0, Integer.MAX_VALUE), 4) {
@Override
public boolean allowCustomCards() {
//If the player has them, may as well allow them.
return true;
}
},
Vanguard ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
Planechase ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
Archenemy ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
@@ -191,12 +201,57 @@ public enum DeckFormat {
}
/**
* @return the maxCardCopies
* @return the default maximum copies of a card in this format.
*/
public int getMaxCardCopies() {
return maxCardCopies;
}
/**
* @return the maximum copies of the specified card allowed in this format. This does not include ban or restricted lists.
*/
public int getMaxCardCopies(PaperCard card) {
if(canHaveSpecificNumberInDeck(card) != null)
return canHaveSpecificNumberInDeck(card);
else if (canHaveAnyNumberOf(card))
return Integer.MAX_VALUE;
else if (card.getRules().isVariant()) {
DeckSection section = DeckSection.matchingSection(card);
if(section == DeckSection.Planes && card.getRules().getType().isPhenomenon())
return 2; //These are two-of.
return getExtraSectionMaxCopies(section);
}
else
return this.getMaxCardCopies();
}
public int getExtraSectionMaxCopies(DeckSection section) {
return switch (section) {
case Avatar, Commander, Planes, Dungeon, Attractions, Contraptions -> 1;
case Schemes -> 2;
case Conspiracy -> Integer.MAX_VALUE;
default -> maxCardCopies;
};
}
/**
* @return the deck sections used by most decks in this format.
*/
public EnumSet<DeckSection> getPrimaryDeckSections() {
if(this == Planechase)
return EnumSet.of(DeckSection.Planes);
if(this == Archenemy)
return EnumSet.of(DeckSection.Schemes);
if(this == Vanguard)
return EnumSet.of(DeckSection.Avatar);
EnumSet<DeckSection> out = EnumSet.of(DeckSection.Main);
if(sideRange == null || sideRange.getMaximum() > 0)
out.add(DeckSection.Sideboard);
if(hasCommander())
out.add(DeckSection.Commander);
return out;
}
public String getDeckConformanceProblem(Deck deck) {
if (deck == null) {
return "is not selected";
@@ -353,7 +408,7 @@ public enum DeckFormat {
// Should group all cards by name, so that different editions of same card are really counted as the same card
for (final Entry<String, Integer> cp : Aggregates.groupSumBy(allCards, pc -> StaticData.instance().getCommonCards().getName(pc.getName(), true))) {
IPaperCard simpleCard = StaticData.instance().getCommonCards().getCard(cp.getKey());
if (simpleCard != null && simpleCard.getRules().isCustom() && !StaticData.instance().allowCustomCardsInDecksConformance())
if (simpleCard != null && simpleCard.getRules().isCustom() && !allowCustomCards())
return TextUtil.concatWithSpace("contains a Custom Card:", cp.getKey(), "\nPlease Enable Custom Cards in Forge Preferences to use this deck.");
// Might cause issues since it ignores "Special" Cards
if (simpleCard == null) {
@@ -484,6 +539,10 @@ public enum DeckFormat {
// Not needed by default
}
public boolean allowCustomCards() {
return StaticData.instance().allowCustomCardsInDecksConformance();
}
public boolean isLegalCard(PaperCard pc) {
if (cardPoolFilter == null) {
if (paperCardPoolFilter == null) {
@@ -498,13 +557,13 @@ public enum DeckFormat {
if (cardPoolFilter != null && !cardPoolFilter.test(rules)) {
return false;
}
if (this.equals(DeckFormat.Oathbreaker)) {
if (this == DeckFormat.Oathbreaker) {
return rules.canBeOathbreaker();
}
if (this.equals(DeckFormat.Brawl)) {
if (this == DeckFormat.Brawl) {
return rules.canBeBrawlCommander();
}
if (this.equals(DeckFormat.TinyLeaders)) {
if (this == DeckFormat.TinyLeaders) {
return rules.canBeTinyLeadersCommander();
}
return rules.canBeCommander();
@@ -553,6 +612,8 @@ public enum DeckFormat {
for (final PaperCard p : commanders) {
cmdCI |= p.getRules().getColorIdentity().getColor();
}
if(cmdCI == MagicColor.ALL_COLORS)
return x -> true;
Predicate<CardRules> predicate = CardRulesPredicates.hasColorIdentity(cmdCI);
if (commanders.size() == 1 && commanders.get(0).getRules().canBePartnerCommander()) {
// Also show available partners a commander can have a partner.

View File

@@ -49,6 +49,16 @@ public class DeckRecognizer {
LIMITED_CARD,
CARD_FROM_NOT_ALLOWED_SET,
CARD_FROM_INVALID_SET,
/**
* Valid card request, but can't be imported because the player does not have enough copies.
* Should be replaced with a different printing if possible.
*/
CARD_NOT_IN_INVENTORY,
/**
* Valid card request for a card that isn't in the player's inventory, but new copies can be acquired freely.
* Usually used for basic lands. Should be supplied to the import controller by the editor.
*/
FREE_CARD_NOT_IN_INVENTORY,
// Warning messages
WARNING_MESSAGE,
UNKNOWN_CARD,
@@ -63,10 +73,14 @@ public class DeckRecognizer {
CARD_TYPE,
CARD_RARITY,
CARD_CMC,
MANA_COLOUR
MANA_COLOUR;
public static final EnumSet<TokenType> CARD_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY);
public static final EnumSet<TokenType> IN_DECK_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, DECK_NAME, FREE_CARD_NOT_IN_INVENTORY);
public static final EnumSet<TokenType> CARD_PLACEHOLDER_TOKEN_TYPES = EnumSet.of(CARD_TYPE, CARD_RARITY, CARD_CMC, MANA_COLOUR);
}
public enum LimitedCardType{
public enum LimitedCardType {
BANNED,
RESTRICTED,
}
@@ -108,6 +122,10 @@ public class DeckRecognizer {
return new Token(TokenType.CARD_FROM_INVALID_SET, count, card, cardRequestHasSetCode);
}
public static Token NotInInventoryFree(final PaperCard card, final int count, final DeckSection section) {
return new Token(TokenType.FREE_CARD_NOT_IN_INVENTORY, count, card, section, true);
}
// WARNING MESSAGES
// ================
public static Token UnknownCard(final String cardName, final String setCode, final int count) {
@@ -126,6 +144,10 @@ public class DeckRecognizer {
return new Token(TokenType.WARNING_MESSAGE, msg);
}
public static Token NotInInventory(final PaperCard card, final int count, final DeckSection section) {
return new Token(TokenType.CARD_NOT_IN_INVENTORY, count, card, section, false);
}
/* =================================
* DECK SECTIONS
* ================================= */
@@ -239,14 +261,11 @@ public class DeckRecognizer {
/**
* Filters all token types that have a PaperCard instance set (not null)
* @return true for tokens of type:
* LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET.
* LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY.
* False otherwise.
*/
public boolean isCardToken() {
return (this.type == TokenType.LEGAL_CARD ||
this.type == TokenType.LIMITED_CARD ||
this.type == TokenType.CARD_FROM_NOT_ALLOWED_SET ||
this.type == TokenType.CARD_FROM_INVALID_SET);
return TokenType.CARD_TOKEN_TYPES.contains(this.type);
}
/**
@@ -255,9 +274,7 @@ public class DeckRecognizer {
* LEGAL_CARD, LIMITED_CARD, DECK_NAME; false otherwise.
*/
public boolean isTokenForDeck() {
return (this.type == TokenType.LEGAL_CARD ||
this.type == TokenType.LIMITED_CARD ||
this.type == TokenType.DECK_NAME);
return TokenType.IN_DECK_TOKEN_TYPES.contains(this.type);
}
/**
@@ -266,7 +283,7 @@ public class DeckRecognizer {
* False otherwise.
*/
public boolean isCardTokenForDeck() {
return (this.type == TokenType.LEGAL_CARD || this.type == TokenType.LIMITED_CARD);
return isCardToken() && isTokenForDeck();
}
/**
@@ -276,10 +293,7 @@ public class DeckRecognizer {
* CARD_RARITY, CARD_CMC, CARD_TYPE, MANA_COLOUR
*/
public boolean isCardPlaceholder(){
return (this.type == TokenType.CARD_RARITY ||
this.type == TokenType.CARD_CMC ||
this.type == TokenType.MANA_COLOUR ||
this.type == TokenType.CARD_TYPE);
return TokenType.CARD_PLACEHOLDER_TOKEN_TYPES.contains(this.type);
}
/** Determines if current token is a Deck Section token
@@ -536,7 +550,7 @@ public class DeckRecognizer {
PaperCard tokenCard = token.getCard();
if (isAllowed(tokenSection)) {
if (!tokenSection.equals(referenceDeckSectionInParsing)) {
if (tokenSection != referenceDeckSectionInParsing) {
Token sectionToken = Token.DeckSection(tokenSection.name(), this.allowedDeckSections);
// just check that last token is stack is a card placeholder.
// In that case, add the new section token before the placeholder
@@ -575,7 +589,7 @@ public class DeckRecognizer {
refLine = purgeAllLinks(refLine);
String line;
if (StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER))
if (refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER))
line = refLine.replaceAll(LINE_COMMENT_DELIMITER_OR_MD_HEADER, "");
else
line = refLine.trim(); // Remove any trailing formatting
@@ -584,7 +598,7 @@ public class DeckRecognizer {
// Final fantasy cards like Summon: Choco/Mog should be ommited to be recognized. TODO: fix maybe for future cards
if (!line.contains("Summon:"))
line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // ");
if (StringUtils.startsWith(line, ASTERISK)) // markdown lists (tappedout md export)
if (line.startsWith(ASTERISK)) // Markdown lists (tappedout md export)
line = line.substring(2);
// == Patches to Corner Cases
@@ -600,8 +614,8 @@ public class DeckRecognizer {
Token result = recogniseCardToken(line, referenceSection);
if (result == null)
result = recogniseNonCardToken(line);
return result != null ? result : StringUtils.startsWith(refLine, DOUBLE_SLASH) ||
StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER) ?
return result != null ? result : refLine.startsWith(DOUBLE_SLASH) ||
refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER) ?
new Token(TokenType.COMMENT, 0, refLine) : new Token(TokenType.UNKNOWN_TEXT, 0, refLine);
}
@@ -613,7 +627,7 @@ public class DeckRecognizer {
while (m.find()) {
line = line.replaceAll(m.group(), "").trim();
}
if (StringUtils.endsWith(line, "()"))
if (line.endsWith("()"))
return line.substring(0, line.length()-2);
return line;
}
@@ -741,21 +755,12 @@ public class DeckRecognizer {
// This would save tons of time in parsing Input + would also allow to return UnsupportedCardTokens beforehand
private DeckSection getTokenSection(String deckSec, DeckSection currentDeckSection, PaperCard card){
if (deckSec != null) {
DeckSection cardSection;
switch (deckSec.toUpperCase().trim()) {
case "MB":
cardSection = DeckSection.Main;
break;
case "SB":
cardSection = DeckSection.Sideboard;
break;
case "CM":
cardSection = DeckSection.Commander;
break;
default:
cardSection = DeckSection.matchingSection(card);
break;
}
DeckSection cardSection = switch (deckSec.toUpperCase().trim()) {
case "MB" -> DeckSection.Main;
case "SB" -> DeckSection.Sideboard;
case "CM" -> DeckSection.Commander;
default -> DeckSection.matchingSection(card);
};
if (cardSection.validate(card))
return cardSection;
}
@@ -989,7 +994,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", magicColor.getLocalizedName(), magicColor.getSymbol());
return String.format("%s %s", magicColor.getTranslatedName(), magicColor.getSymbol());
}
private static final HashMap<Integer, String> manaSymbolsMap = new HashMap<Integer, String>() {{
@@ -1008,60 +1013,30 @@ 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 = magicColor1.getLocalizedName();
String localisedName2 = magicColor2.getLocalizedName();
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
String localisedName1 = magicColor1.getTranslatedName();
String localisedName2 = magicColor2.getTranslatedName();
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColorMask() | magicColor2.getColorMask());
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
}
private static MagicColor.Color getMagicColor(String colorName){
if (colorName.toLowerCase().startsWith("multi") || colorName.equalsIgnoreCase("m"))
return null; // will be handled separately
byte color = MagicColor.fromName(colorName.toLowerCase());
switch (color) {
case MagicColor.WHITE:
return MagicColor.Color.WHITE;
case MagicColor.BLUE:
return MagicColor.Color.BLUE;
case MagicColor.BLACK:
return MagicColor.Color.BLACK;
case MagicColor.RED:
return MagicColor.Color.RED;
case MagicColor.GREEN:
return MagicColor.Color.GREEN;
default:
return MagicColor.Color.COLORLESS;
}
return MagicColor.Color.fromByte(MagicColor.fromName(colorName.toLowerCase()));
}
public static String getLocalisedMagicColorName(String colorName){
Localizer localizer = Localizer.getInstance();
switch(colorName.toLowerCase()){
case MagicColor.Constant.WHITE:
return localizer.getMessage("lblWhite");
case MagicColor.Constant.BLUE:
return localizer.getMessage("lblBlue");
case MagicColor.Constant.BLACK:
return localizer.getMessage("lblBlack");
case MagicColor.Constant.RED:
return localizer.getMessage("lblRed");
case MagicColor.Constant.GREEN:
return localizer.getMessage("lblGreen");
case MagicColor.Constant.COLORLESS:
return localizer.getMessage("lblColorless");
case "multicolour":
case "multicolor":
return localizer.getMessage("lblMulticolor");
default:
return "";
}
return switch (colorName.toLowerCase()) {
case MagicColor.Constant.WHITE -> localizer.getMessage("lblWhite");
case MagicColor.Constant.BLUE -> localizer.getMessage("lblBlue");
case MagicColor.Constant.BLACK -> localizer.getMessage("lblBlack");
case MagicColor.Constant.RED -> localizer.getMessage("lblRed");
case MagicColor.Constant.GREEN -> localizer.getMessage("lblGreen");
case MagicColor.Constant.COLORLESS -> localizer.getMessage("lblColorless");
case "multicolour", "multicolor" -> localizer.getMessage("lblMulticolor");
default -> "";
};
}
/**
@@ -1080,37 +1055,6 @@ public class DeckRecognizer {
return "";
}
private static Pair<String, String> getManaNameAndSymbol(String matchedMana) {
if (matchedMana == null)
return null;
Localizer localizer = Localizer.getInstance();
switch (matchedMana.toLowerCase()) {
case MagicColor.Constant.WHITE:
case "w":
return Pair.of(localizer.getMessage("lblWhite"), MagicColor.Color.WHITE.getSymbol());
case MagicColor.Constant.BLUE:
case "u":
return Pair.of(localizer.getMessage("lblBlue"), MagicColor.Color.BLUE.getSymbol());
case MagicColor.Constant.BLACK:
case "b":
return Pair.of(localizer.getMessage("lblBlack"), MagicColor.Color.BLACK.getSymbol());
case MagicColor.Constant.RED:
case "r":
return Pair.of(localizer.getMessage("lblRed"), MagicColor.Color.RED.getSymbol());
case MagicColor.Constant.GREEN:
case "g":
return Pair.of(localizer.getMessage("lblGreen"), MagicColor.Color.GREEN.getSymbol());
case MagicColor.Constant.COLORLESS:
case "c":
return Pair.of(localizer.getMessage("lblColorless"), MagicColor.Color.COLORLESS.getSymbol());
default: // Multicolour
return Pair.of(localizer.getMessage("lblMulticolor"), "");
}
}
public static boolean isDeckName(final String lineAsIs) {
if (lineAsIs == null)
return false;

View File

@@ -52,9 +52,4 @@ public interface IPaperCard extends InventoryItem, Serializable {
default String getUntranslatedType() {
return getRules().getType().toString();
}
@Override
default String getUntranslatedOracle() {
return getRules().getOracleText();
}
}

View File

@@ -46,7 +46,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
// These fields are kinda PK for PrintedCard
private final String name;
private final String edition;
private String edition;
/* [NEW] Attribute to store reference to CollectorNumber of each PaperCard.
By default the attribute is marked as "unset" so that it could be retrieved and set.
(see getCollectorNumber())
@@ -154,6 +154,31 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return this.noSellVersion;
}
public PaperCard getMeldBaseCard() {
if (getRules().getSplitType() != CardSplitType.Meld) {
return null;
}
// This is the base part of the meld duo
if (getRules().getOtherPart() == null) {
return this;
}
String meldWith = getRules().getMeldWith();
if (meldWith == null) {
return null;
}
List<PrintSheet> sheets = StaticData.instance().getCardEdition(this.edition).getPrintSheetsBySection();
for (PrintSheet sheet : sheets) {
if (sheet.contains(this)) {
return sheet.find(PaperCardPredicates.name(meldWith));
}
}
return null;
}
public PaperCard copyWithoutFlags() {
if(this.flaglessVersion == null) {
if(this.flags == PaperCardFlags.IDENTITY_FLAGS)
@@ -225,7 +250,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
this.foil = foil;
this.rarity = rarity;
this.artist = TextUtil.normalizeText(artist);
this.artist = 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
@@ -350,7 +375,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
System.out.println("PaperCard: " + name + " not found with set and index " + edition + ", " + artIndex);
pc = readObjectAlternate(name, edition);
if (pc == null) {
throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
pc = StaticData.instance().getCommonCards().createUnsupportedCard(name);
//throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
}
System.out.println("Alternate object found: " + pc.getName() + ", " + pc.getEdition() + ", " + pc.getArtIndex());
}
@@ -567,7 +593,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public PaperCardFlags withMarkedColors(ColorSet markedColors) {
if(markedColors == null)
markedColors = ColorSet.getNullColor();
markedColors = ColorSet.NO_COLORS;
return new PaperCardFlags(this, markedColors, null);
}

View File

@@ -50,6 +50,13 @@ public abstract class PaperCardPredicates {
return new PredicateNames(what);
}
/**
* Filters on a card foil status
*/
public static Predicate<PaperCard> isFoil(final boolean isFoil) {
return new PredicateFoil(isFoil);
}
private static final class PredicatePrintedWithRarity implements Predicate<PaperCard> {
private final CardRarity matchingRarity;
@@ -93,6 +100,17 @@ public abstract class PaperCardPredicates {
}
}
private static final class PredicateFoil implements Predicate<PaperCard> {
private final boolean operand;
@Override
public boolean test(final PaperCard card) { return card.isFoil() == operand; }
private PredicateFoil(final boolean isFoil) {
this.operand = isFoil;
}
}
private static final class PredicateRarity implements Predicate<PaperCard> {
private final CardRarity operand;

View File

@@ -156,7 +156,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
return false;
CardSplitType cst = this.cardRules.getSplitType();
//expand this on future for other tokens that has other backsides besides transform..
return cst == CardSplitType.Transform;
return cst == CardSplitType.Transform || cst == CardSplitType.Modal;
}
@Override

View File

@@ -19,6 +19,11 @@ public class SealedTemplate {
Pair.of(BoosterSlots.RARE_MYTHIC, 1), Pair.of(BoosterSlots.BASIC_LAND, 1)
));
// This is a generic cube booster. 15 cards, no rarity slots.
public final static SealedTemplate genericNoSlotBooster = new SealedTemplate(null, Lists.newArrayList(
Pair.of(BoosterSlots.ANY, 15)
));
protected final List<Pair<String, Integer>> slots;
protected final String name;

View File

@@ -254,7 +254,7 @@ public class BoosterGenerator {
if (sheetKey.startsWith("wholeSheet")) {
PrintSheet ps = getPrintSheet(sheetKey);
result.addAll(ps.all());
result.addAll(ps.toFlatList());
continue;
}
@@ -384,7 +384,7 @@ public class BoosterGenerator {
PrintSheet replaceThis = tryGetStaticSheet(split[0]);
List<PaperCard> candidates = Lists.newArrayList();
for (PaperCard p : result) {
if (replaceThis.all().contains(p)) {
if (replaceThis.contains(p)) {
candidates.add(candidates.size(), p);
}
}
@@ -398,7 +398,7 @@ public class BoosterGenerator {
PrintSheet replaceThis = tryGetStaticSheet(split[0]);
List<PaperCard> candidates = Lists.newArrayList();
for (PaperCard p : result) {
if (replaceThis.all().contains(p)) {
if (replaceThis.contains(p)) {
candidates.add(candidates.size(), p);
}
}
@@ -462,9 +462,8 @@ public class BoosterGenerator {
} else {
paperCards.addAll(ps.random(numCardsToGenerate, true));
}
result.addAll(paperCards);
}
result.addAll(paperCards);
}
return result;
@@ -634,7 +633,10 @@ public class BoosterGenerator {
System.out.println("Parsing from main code: " + mainCode);
String sheetName = StringUtils.strip(mainCode.substring(10), "()\" ");
System.out.println("Attempting to lookup: " + sheetName);
src = tryGetStaticSheet(sheetName).toFlatList();
PrintSheet fromSheet = tryGetStaticSheet(sheetName);
if (fromSheet == null)
throw new RuntimeException("PrintSheet Error: " + ps.getName() + " didn't find " + sheetName + " from " + mainCode);
src = fromSheet.toFlatList();
setPred = x -> true;
} else if (mainCode.startsWith("promo") || mainCode.startsWith("name")) { // get exactly the named cards, that's a tiny inlined print sheet

View File

@@ -10,13 +10,11 @@ public interface ITranslatable extends IHasName {
default String getUntranslatedName() {
return getName();
}
default String getTranslatedName() {
return getName();
}
default String getUntranslatedType() {
return "";
}
default String getUntranslatedOracle() {
return "";
}
}

View File

@@ -199,19 +199,14 @@ public class ImageUtil {
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(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){
String editionCode;
if (setCode != null && !setCode.isEmpty())
editionCode = setCode;
else
editionCode = cp.getEdition().toLowerCase();
String cardCollectorNumber = cp.getCollectorNumber();
// Hack to account for variations in Arabian Nights
cardCollectorNumber = cardCollectorNumber.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";
@@ -222,29 +217,42 @@ public class ImageUtil {
} else if (cardCollectorNumber.startsWith("OPC2")) {
editionCode = "opc2";
cardCollectorNumber = cardCollectorNumber.substring("OPC2".length());
} else if (hyphenateAlchemy) {
if (!cardCollectorNumber.startsWith("A")) {
return null;
}
cardCollectorNumber = cardCollectorNumber.replace("A", "A-");
}
String versionParam = useArtCrop ? "art_crop" : "normal";
String faceParam = "";
if (cp.getRules().getOtherPart() != null) {
faceParam = (face.equals("back") ? "&face=back" : "&face=front");
} else if (cp.getRules().getSplitType() == CardSplitType.Meld
&& !cardCollectorNumber.endsWith("a")
&& !cardCollectorNumber.endsWith("b")) {
// Only the bottom half of a meld card shares a collector number.
// Hanweir Garrison EMN already has a appended.
// Exception: The front facing card doesn't use a in FIN
if (face.equals("back")) {
cardCollectorNumber += "b";
} else if (!editionCode.equals("fin")) {
cardCollectorNumber += "a";
if (cp.getRules().getSplitType() == CardSplitType.Meld) {
if (face.equals("back")) {
PaperCard meldBasePc = cp.getMeldBaseCard();
cardCollectorNumber = meldBasePc.getCollectorNumber();
String collectorNumberSuffix = "";
if (cardCollectorNumber.endsWith("a")) {
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1);
} else if (cardCollectorNumber.endsWith("as")) {
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 2);
collectorNumberSuffix = "s";
} else if (cardCollectorNumber.endsWith("ap")) {
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 2);
collectorNumberSuffix = "p";
} else if (cp.getCollectorNumber().endsWith("a")) {
// SIR
cardCollectorNumber = cp.getCollectorNumber().substring(0, cp.getCollectorNumber().length() - 1);
}
cardCollectorNumber += "b" + collectorNumberSuffix;
}
faceParam = "&face=front";
} else if (cp.getRules().getOtherPart() != null) {
faceParam = (face.equals("back") && cp.getRules().getSplitType() != CardSplitType.Flip
? "&face=back"
: "&face=front");
}
if (cardCollectorNumber.endsWith("")) {
faceParam = "&face=back";
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1);
}
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, encodeUtf8(cardCollectorNumber),
@@ -256,6 +264,10 @@ public class ImageUtil {
if (!faceParam.isEmpty()) {
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
}
if (collectorNumber.endsWith("")) {
faceParam = "&face=back";
collectorNumber = collectorNumber.substring(0, collectorNumber.length() - 1);
}
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, encodeUtf8(collectorNumber),
langCode, versionParam, faceParam);
}
@@ -276,8 +288,7 @@ public class ImageUtil {
char c;
for (int i = 0; i < in.length(); i++) {
c = in.charAt(i);
if ((c == '"') || (c == '/') || (c == ':') || (c == '?')) {
} else {
if ((c != '"') && (c != '/') && (c != ':') && (c != '?')) {
out.append(c);
}
}

View File

@@ -269,11 +269,16 @@ 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 removeIf(Predicate<T> filter) {
items.keySet().removeIf(filter);
}
public void retainIf(Predicate<T> filter) {
items.keySet().removeIf(filter.negate());
}
public T find(Predicate<T> filter) {
return items.keySet().stream().filter(filter).findFirst().orElse(null);
}
public void clear() {
@@ -285,4 +290,19 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
return (obj instanceof ItemPool ip) &&
(this.items.equals(ip.items));
}
/**
* Applies a predicate to this ItemPool's entries.
*
* @param predicate the Predicate to apply to this ItemPool
* @return a new ItemPool made from this ItemPool with only the items that agree with the provided Predicate
*/
public ItemPool<T> getFilteredPool(Predicate<T> predicate) {
ItemPool<T> filteredPool = new ItemPool<>(myClass);
for (T c : this.items.keySet()) {
if (predicate.test(c))
filteredPool.add(c, this.items.get(c));
}
return filteredPool;
}
}

View File

@@ -32,7 +32,7 @@
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-logback</artifactId>
<version>7.15.0</version>
<version>8.21.1</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>

View File

@@ -62,7 +62,9 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
/** Keys of descriptive (text) parameters. */
private static final ImmutableList<String> descriptiveKeys = ImmutableList.<String>builder()
.add("Description", "SpellDescription", "StackDescription", "TriggerDescription").build();
.add("Description", "SpellDescription", "StackDescription", "TriggerDescription")
.add("ChangeTypeDesc")
.build();
/**
* Keys that should not changed

View File

@@ -35,7 +35,7 @@ public class ForgeScript {
boolean withSource = property.endsWith("Source");
final ColorSet colors;
if (withSource && StaticAbilityColorlessDamageSource.colorlessDamageSource(cardState)) {
colors = ColorSet.getNullColor();
colors = ColorSet.NO_COLORS;
} else {
colors = cardState.getCard().getColor(cardState);
}
@@ -166,8 +166,6 @@ public class ForgeScript {
Card source, CardTraitBase spellAbility) {
if (property.equals("ManaAbility")) {
return sa.isManaAbility();
} else if (property.equals("nonManaAbility")) {
return !sa.isManaAbility();
} else if (property.equals("withoutXCost")) {
return !sa.costHasManaX();
} else if (property.startsWith("XCost")) {
@@ -195,7 +193,7 @@ public class ForgeScript {
return sa.isKeyword(Keyword.SADDLE);
} else if (property.equals("Station")) {
return sa.isKeyword(Keyword.STATION);
}else if (property.equals("Cycling")) {
} else if (property.equals("Cycling")) {
return sa.isCycling();
} else if (property.equals("Dash")) {
return sa.isDash();
@@ -237,6 +235,8 @@ public class ForgeScript {
return sa.isBoast();
} else if (property.equals("Exhaust")) {
return sa.isExhaust();
} else if (property.equals("Mayhem")) {
return sa.isMayhem();
} else if (property.equals("Mutate")) {
return sa.isMutate();
} else if (property.equals("Ninjutsu")) {
@@ -410,6 +410,8 @@ public class ForgeScript {
return !sa.isPwAbility() && !sa.getRestrictions().isSorcerySpeed();
}
return true;
} else if(property.startsWith("NamedAbility")) {
return sa.getName().equals(property.substring(12));
} else if (sa.getHostCard() != null) {
return sa.getHostCard().hasProperty(property, sourceController, source, spellAbility);
}

View File

@@ -414,19 +414,6 @@ public class Game {
return players;
}
/**
* Gets the nonactive players who are still fighting to win, in turn order.
*/
public final PlayerCollection getNonactivePlayers() {
// Don't use getPlayersInTurnOrder to prevent copying the player collection twice
final PlayerCollection players = new PlayerCollection(ingamePlayers);
players.remove(phaseHandler.getPlayerTurn());
if (!getTurnOrder().isDefaultDirection()) {
Collections.reverse(players);
}
return players;
}
/**
* Gets the players who participated in match (regardless of outcome).
* <i>Use this in UI and after match calculations</i>
@@ -744,7 +731,7 @@ public class Game {
if (!visitor.visitAll(player.getZone(ZoneType.Exile).getCards())) {
return;
}
if (!visitor.visitAll(player.getZone(ZoneType.Command).getCards())) {
if (!visitor.visitAll(player.getCardsIn(ZoneType.PART_OF_COMMAND_ZONE))) {
return;
}
if (withSideboard && !visitor.visitAll(player.getZone(ZoneType.Sideboard).getCards())) {
@@ -858,6 +845,8 @@ public class Game {
p.revealFaceDownCards();
}
// TODO free any mindslaves
for (Card c : cards) {
// CR 800.4d if card is controlled by opponent, LTB should trigger
if (c.getOwner().equals(p) && c.getController().equals(p)) {
@@ -893,8 +882,6 @@ public class Game {
}
triggerList.put(c.getZone().getZoneType(), null, c);
getAction().ceaseToExist(c, false);
// CR 603.2f owner of trigger source lost game
getTriggerHandler().clearDelayedTrigger(c);
}
} else {
// return stolen permanents

View File

@@ -57,6 +57,8 @@ import forge.item.PaperCard;
import forge.util.*;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.jgrapht.alg.cycle.SzwarcfiterLauerSimpleCycles;
import org.jgrapht.graph.DefaultDirectedGraph;
@@ -220,10 +222,6 @@ public class GameAction {
//copied.setGamePieceType(GamePieceType.COPIED_SPELL);
}
if (c.isTransformed()) {
copied.incrementTransformedTimestamp();
}
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) {
copied.setCastSA(cause);
copied.setSplitStateToPlayAbility(cause);
@@ -753,26 +751,29 @@ public class GameAction {
public final Card moveTo(final ZoneType name, final Card c, final int libPosition, SpellAbility cause, Map<AbilityKey, Object> params) {
// Call specific functions to set PlayerZone, then move onto moveTo
switch(name) {
case Hand: return moveToHand(c, cause, params);
case Library: return moveToLibrary(c, libPosition, cause, params);
case Battlefield: return moveToPlay(c, c.getController(), cause, params);
case Graveyard: return moveToGraveyard(c, cause, params);
case Exile:
if (!c.canExiledBy(cause, true)) {
return null;
}
return exile(c, cause, params);
case Stack: return moveToStack(c, cause, params);
case PlanarDeck:
case SchemeDeck:
case AttractionDeck:
case ContraptionDeck:
return moveToVariantDeck(c, name, libPosition, cause, params);
case Junkyard:
return moveToJunkyard(c, cause, params);
default: // sideboard will also get there
return moveTo(c.getOwner().getZone(name), c, cause);
try {
return switch (name) {
case Hand -> moveToHand(c, cause, params);
case Library -> moveToLibrary(c, libPosition, cause, params);
case Battlefield -> moveToPlay(c, c.getController(), cause, params);
case Graveyard -> moveToGraveyard(c, cause, params);
case Exile -> !c.canExiledBy(cause, true) ? null : exile(c, cause, params);
case Stack -> moveToStack(c, cause, params);
case PlanarDeck, SchemeDeck, AttractionDeck, ContraptionDeck -> moveToVariantDeck(c, name, libPosition, cause, params);
case Junkyard -> moveToJunkyard(c, cause, params);
default -> moveTo(c.getOwner().getZone(name), c, cause); // sideboard will also get there
};
} catch (Exception e) {
String msg = "GameAction:moveTo: Exception occured";
Breadcrumb bread = new Breadcrumb(msg);
bread.setData("Card", c.getName());
bread.setData("SA", cause.toString());
bread.setData("ZoneType", name.name());
bread.setData("Player", c.getOwner());
Sentry.addBreadcrumb(bread);
throw new RuntimeException("Error in GameAction moveTo " + c.getName() + " to Player Zone " + name.name(), e);
}
}
@@ -974,6 +975,7 @@ public class GameAction {
// in some corner cases there's no zone yet (copied spell that failed targeting)
if (z != null) {
z.remove(c);
c.setZone(c.getOwner().getZone(ZoneType.None));
if (z.is(ZoneType.Battlefield)) {
c.runLeavesPlayCommands();
}
@@ -1600,9 +1602,7 @@ public class GameAction {
}
// recheck the game over condition at this point to make sure no other win conditions apply now.
if (!game.isGameOver()) {
checkGameOverCondition();
}
checkGameOverCondition();
if (game.getAge() != GameStage.Play) {
return false;
@@ -1822,8 +1822,8 @@ public class GameAction {
private boolean stateBasedAction704_5q(Card c) {
boolean checkAgain = false;
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
final CounterType p1p1 = CounterEnumType.P1P1;
final CounterType m1m1 = CounterEnumType.M1M1;
int plusOneCounters = c.getCounters(p1p1);
int minusOneCounters = c.getCounters(m1m1);
if (plusOneCounters > 0 && minusOneCounters > 0) {
@@ -1843,7 +1843,7 @@ public class GameAction {
return checkAgain;
}
private boolean stateBasedAction704_5r(Card c) {
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
final CounterType dreamType = CounterEnumType.DREAM;
int old = c.getCounters(dreamType);
if (old <= 0) {
@@ -1883,6 +1883,10 @@ public class GameAction {
}
public void checkGameOverCondition() {
if (game.isGameOver()) {
return;
}
// award loses as SBE
GameEndReason reason = null;
List<Player> losers = null;
@@ -2222,6 +2226,13 @@ public class GameAction {
}
}
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
// Notify players
for (Player p : game.getPlayers()) {
p.getController().revealUnsupported(unsupported);
}
}
/** Delivers a message to all players. (use reveal to show Cards) */
public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) {
if (saSource != null) {

View File

@@ -125,10 +125,22 @@ public final class GameActionUtil {
// need to be done there before static abilities does reset the card
// These Keywords depend on the Mana Cost of for Split Cards
if (sa.isBasicSpell() && !sa.isLandAbility()) {
if (sa.isBasicSpell()) {
for (final KeywordInterface inst : source.getKeywords()) {
final String keyword = inst.getOriginal();
if (keyword.startsWith("Mayhem")) {
if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) {
continue;
}
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem));
}
if (sa.isLandAbility()) {
continue;
}
if (keyword.startsWith("Escape")) {
if (!source.isInZone(ZoneType.Graveyard)) {
continue;
@@ -165,26 +177,7 @@ public final class GameActionUtil {
continue;
}
SpellAbility flashback = null;
// there is a flashback cost (and not the cards cost)
if (keyword.contains(":")) { // K:Flashback:Cost:ExtraParams:ExtraDescription
final String[] k = keyword.split(":");
flashback = 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()) {
flashback.putParam(param.getKey(), param.getValue());
}
}
} else { // same cost as original (e.g. Otaria plane)
flashback = sa.copy(activator);
}
flashback.setAlternativeCost(AlternativeCost.Flashback);
flashback.getRestrictions().setZone(ZoneType.Graveyard);
flashback.setKeyword(inst);
flashback.setIntrinsic(inst.isIntrinsic());
alternatives.add(flashback);
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Flashback));
} else if (keyword.startsWith("Harmonize")) {
if (!source.isInZone(ZoneType.Graveyard)) {
continue;
@@ -194,25 +187,7 @@ public final class GameActionUtil {
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);
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Harmonize));
} else if (keyword.startsWith("Foretell")) {
// Foretell cast only from Exile
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() ||
@@ -267,6 +242,7 @@ public final class GameActionUtil {
}
stackCopy.setLastKnownZone(game.getStackZone());
stackCopy.setCastFrom(oldZone);
stackCopy.setCastSA(sa);
lkicheck = true;
stackCopy.clearStaticChangedCardKeywords(false);
@@ -317,6 +293,30 @@ public final class GameActionUtil {
return alternatives;
}
public static SpellAbility getGraveyardSpellByKeyword(KeywordInterface inst, SpellAbility sa, Player activator, AlternativeCost altCost) {
String keyword = inst.getOriginal();
SpellAbility newSA = null;
// there is a flashback cost (and not the cards cost)
if (keyword.contains(":")) { // K:Flashback:Cost:ExtraParams:ExtraDescription
final String[] k = keyword.split(":");
newSA = 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()) {
newSA.putParam(param.getKey(), param.getValue());
}
}
} else { // same cost as original (e.g. Otaria plane)
newSA = sa.copy(activator);
}
newSA.setAlternativeCost(altCost);
newSA.getRestrictions().setZone(ZoneType.Graveyard);
newSA.setKeyword(inst);
newSA.setIntrinsic(inst.isIntrinsic());
return newSA;
}
public static List<SpellAbility> getMayPlaySpellOptions(final SpellAbility sa, final Card source, final Player activator, boolean altCostOnly) {
final List<SpellAbility> alternatives = Lists.newArrayList();
@@ -993,9 +993,6 @@ public final class GameActionUtil {
oldCard.setBackSide(false);
oldCard.setState(oldCard.getFaceupCardStateName(), true);
oldCard.unanimateBestow();
if (ability.isDisturb() || ability.hasParam("CastTransformed")) {
oldCard.undoIncrementTransformedTimestamp();
}
if (ability.hasParam("Prototype")) {
oldCard.removeCloneState(oldCard.getPrototypeTimestamp());

View File

@@ -33,17 +33,18 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
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.keyword.KeywordWithType;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttach;
import forge.game.zone.ZoneType;
import forge.util.Lang;
public abstract class GameEntity extends GameObject implements IIdentifiable {
protected int id;
@@ -197,14 +198,12 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
public final void addAttachedCard(final Card c) {
if (attachedCards.add(c)) {
updateAttachedCards();
getGame().fireEvent(new GameEventCardAttachment(c, null, this));
}
}
public final void removeAttachedCard(final Card c) {
if (attachedCards.remove(c)) {
updateAttachedCards();
getGame().fireEvent(new GameEventCardAttachment(c, this, null));
}
}
@@ -222,63 +221,83 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return canBeAttached(attach, sa, false);
}
public boolean canBeAttached(final Card attach, SpellAbility sa, boolean checkSBA) {
// master mode
if (!attach.isAttachment() || (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE))
|| equals(attach)) {
return false;
return cantBeAttachedMsg(attach, sa, checkSBA) == null;
}
public String cantBeAttachedMsg(final Card attach, SpellAbility sa) {
return cantBeAttachedMsg(attach, sa, false);
}
public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) {
if (!attach.isAttachment()) {
return attach.getName() + " is not an attachment";
}
if (equals(attach)) {
return attach.getName() + " can't attach to itself";
}
if (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) {
return attach.getName() + " is a creature without reconfigure";
}
if (attach.isPhasedOut()) {
return false;
return attach.getName() + " is phased out";
}
// check for rules
if (attach.isAura() && !canBeEnchantedBy(attach)) {
return false;
if (attach.isAura()) {
String msg = cantBeEnchantedByMsg(attach);
if (msg != null) {
return msg;
}
}
if (attach.isEquipment() && !canBeEquippedBy(attach, sa)) {
return false;
if (attach.isEquipment()) {
String msg = cantBeEquippedByMsg(attach, sa);
if (msg != null) {
return msg;
}
}
if (attach.isFortification() && !canBeFortifiedBy(attach)) {
return false;
if (attach.isFortification()) {
String msg = cantBeFortifiedByMsg(attach);
if (msg != null) {
return msg;
}
}
// check for can't attach static
if (StaticAbilityCantAttach.cantAttach(this, attach, checkSBA)) {
return false;
StaticAbility stAb = StaticAbilityCantAttach.cantAttach(this, attach, checkSBA);
if (stAb != null) {
return stAb.toString();
}
// true for all
return true;
return null;
}
protected boolean canBeEquippedBy(final Card aura, SpellAbility sa) {
/**
* Equip only to Creatures which are cards
*/
return false;
}
protected boolean canBeFortifiedBy(final Card aura) {
protected String cantBeEquippedByMsg(final Card aura, SpellAbility sa) {
/**
* Equip only to Lands which are cards
*/
return false;
return getName() + " is not a Creature";
}
protected boolean canBeEnchantedBy(final Card aura) {
protected String cantBeFortifiedByMsg(final Card fort) {
/**
* Equip only to Lands which are cards
*/
return getName() + " is not a Land";
}
protected String cantBeEnchantedByMsg(final Card aura) {
if (!aura.hasKeyword(Keyword.ENCHANT)) {
return false;
return "No Enchant Keyword";
}
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;
if (ki instanceof KeywordWithType kwt) {
String v = kwt.getValidType();
String desc = kwt.getTypeDescription();
if (!isValid(v.split(","), aura.getController(), aura, null)) {
return getName() + " is not " + Lang.nounWithAmount(1, desc);
}
}
}
return true;
return null;
}
public boolean hasCounters() {
@@ -305,9 +324,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
Integer value = counters.get(counterName);
return value == null ? 0 : value;
}
public final int getCounters(final CounterEnumType counterType) {
return getCounters(CounterType.get(counterType));
}
public void setCounters(final CounterType counterType, final Integer num) {
if (num <= 0) {
@@ -316,9 +332,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
counters.put(counterType, num);
}
}
public void setCounters(final CounterEnumType counterType, final Integer num) {
setCounters(CounterType.get(counterType), num);
}
abstract public void setCounters(final Map<CounterType, Integer> allCounters);
@@ -328,10 +341,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
abstract public int subtractCounter(final CounterType counterName, final int n, final Player remover);
abstract public void clearCounters();
public boolean canReceiveCounters(final CounterEnumType type) {
return canReceiveCounters(CounterType.get(type));
}
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
if (n <= 0 || !canReceiveCounters(counterType)) {
// As per rule 107.1b
@@ -351,18 +360,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
table.put(source, this, counterType, n);
}
public final void addCounter(final CounterEnumType counterType, final int n, final Player source, GameEntityCounterTable table) {
addCounter(CounterType.get(counterType), n, source, table);
}
public int subtractCounter(final CounterEnumType counterName, final int n, final Player remover) {
return subtractCounter(CounterType.get(counterName), n, remover);
}
abstract public void addCounterInternal(final CounterType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params);
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;
}

View File

@@ -6,6 +6,7 @@ public enum GameLogEntryType {
TURN("Turn"),
MULLIGAN("Mulligan"),
ANTE("Ante"),
DRAFT("Draft"),
ZONE_CHANGE("Zone Change"),
PLAYER_CONTROL("Player control"),
COMBAT("Combat"),

View File

@@ -29,25 +29,25 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventGameOutcome ev) {
// Turn number counted from the starting player
int lastTurn = (int)Math.ceil((float)ev.result.getLastTurnNumber() / 2.0);
int lastTurn = (int)Math.ceil((float)ev.result().getLastTurnNumber() / 2.0);
log.add(GameLogEntryType.GAME_OUTCOME, localizer.getMessage("lblTurn") + " " + lastTurn);
for (String outcome : ev.result.getOutcomeStrings()) {
for (String outcome : ev.result().getOutcomeStrings()) {
log.add(GameLogEntryType.GAME_OUTCOME, outcome);
}
return generateSummary(ev.history);
return generateSummary(ev.history());
}
@Override
public GameLogEntry visit(GameEventScry ev) {
String scryOutcome = "";
if (ev.toTop > 0 && ev.toBottom > 0) {
scryOutcome = localizer.getMessage("lblLogScryTopBottomLibrary").replace("%s", ev.player.toString()).replace("%top", String.valueOf(ev.toTop)).replace("%bottom", String.valueOf(ev.toBottom));
} else if (ev.toBottom == 0) {
scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player.toString()).replace("%top", String.valueOf(ev.toTop));
if (ev.toTop() > 0 && ev.toBottom() > 0) {
scryOutcome = localizer.getMessage("lblLogScryTopBottomLibrary").replace("%s", ev.player().toString()).replace("%top", String.valueOf(ev.toTop())).replace("%bottom", String.valueOf(ev.toBottom()));
} else if (ev.toBottom() == 0) {
scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player().toString()).replace("%top", String.valueOf(ev.toTop()));
} else {
scryOutcome = localizer.getMessage("lblLogScryBottomLibrary").replace("%s", ev.player.toString()).replace("%bottom", String.valueOf(ev.toBottom));
scryOutcome = localizer.getMessage("lblLogScryBottomLibrary").replace("%s", ev.player().toString()).replace("%bottom", String.valueOf(ev.toBottom()));
}
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, scryOutcome);
@@ -57,12 +57,12 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
public GameLogEntry visit(GameEventSurveil ev) {
String surveilOutcome = "";
if (ev.toLibrary > 0 && ev.toGraveyard > 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player.toString(), String.valueOf(ev.toLibrary), String.valueOf(ev.toGraveyard));
} else if (ev.toGraveyard == 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player.toString(), String.valueOf(ev.toLibrary));
if (ev.toLibrary() > 0 && ev.toGraveyard() > 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player().toString(), String.valueOf(ev.toLibrary()), String.valueOf(ev.toGraveyard()));
} else if (ev.toGraveyard() == 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player().toString(), String.valueOf(ev.toLibrary()));
} else {
surveilOutcome = localizer.getMessage("lblLogSurveiledToGraveyard", ev.player.toString(), String.valueOf(ev.toGraveyard));
surveilOutcome = localizer.getMessage("lblLogSurveiledToGraveyard", ev.player().toString(), String.valueOf(ev.toGraveyard()));
}
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, surveilOutcome);
@@ -70,26 +70,26 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventSpellResolved ev) {
String messageForLog = ev.hasFizzled ? localizer.getMessage("lblLogCardAbilityFizzles", ev.spell.getHostCard().toString()) : ev.spell.getStackDescription();
String messageForLog = ev.hasFizzled() ? localizer.getMessage("lblLogCardAbilityFizzles", ev.spell().getHostCard().toString()) : ev.spell().getStackDescription();
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, messageForLog);
}
@Override
public GameLogEntry visit(GameEventSpellAbilityCast event) {
String player = event.sa.getActivatingPlayer().getName();
String action = event.sa.isSpell() ? localizer.getMessage("lblCast")
: event.sa.isTrigger() ? localizer.getMessage("lblTriggered")
String player = event.sa().getActivatingPlayer().getName();
String action = event.sa().isSpell() ? localizer.getMessage("lblCast")
: event.sa().isTrigger() ? localizer.getMessage("lblTriggered")
: localizer.getMessage("lblActivated");
String object = event.si.getStackDescription().startsWith("Morph ")
String object = event.si().getStackDescription().startsWith("Morph ")
? localizer.getMessage("lblMorph")
: event.sa.getHostCard().toString();
: event.sa().getHostCard().toString();
String messageForLog = "";
if (event.sa.getTargetRestrictions() != null) {
if (event.sa().getTargetRestrictions() != null) {
StringBuilder sb = new StringBuilder();
for (TargetChoices ch : event.sa.getAllTargetChoices()) {
for (TargetChoices ch : event.sa().getAllTargetChoices()) {
if (null != ch) {
sb.append(ch);
}
@@ -104,18 +104,18 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventCardModeChosen ev) {
if (!ev.log) {
if (!ev.log()) {
return null;
}
String modeChoiceOutcome;
if (ev.random) {
modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName, ev.mode);
if (ev.random()) {
modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName(), ev.mode());
} else {
modeChoiceOutcome = localizer.getMessage("lblLogPlayerChosenModeForCard",
ev.player.toString(), ev.mode, ev.cardName);
ev.player().toString(), ev.mode(), ev.cardName());
}
String name = CardTranslation.getTranslatedName(ev.cardName);
String name = CardTranslation.getTranslatedName(ev.cardName());
modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "CARDNAME", name);
modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "NICKNAME",
Lang.getInstance().getNickName(name));
@@ -124,7 +124,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventRandomLog ev) {
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.message);
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.message());
}
private static GameLogEntry generateSummary(final Collection<GameOutcome> gamesPlayed) {
@@ -152,8 +152,8 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(final GameEventPlayerControl event) {
final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer;
final Player p = event.player;
final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer();
final Player p = event.player();
final String message;
if (newLobbyPlayer == null) {
@@ -166,23 +166,23 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventTurnPhase ev) {
Player p = ev.playerTurn;
return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc + Lang.getInstance().getPossessedObject(p.getName(), ev.phase.nameForUi));
Player p = ev.playerTurn();
return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc() + Lang.getInstance().getPossessedObject(p.getName(), ev.phase().nameForUi));
}
@Override
public GameLogEntry visit(GameEventCardDamaged event) {
String additionalLog = "";
if (event.type == DamageType.Deathtouch) {
if (event.type() == DamageType.Deathtouch) {
additionalLog = localizer.getMessage("lblDeathtouch");
}
if (event.type == DamageType.M1M1Counters) {
if (event.type() == DamageType.M1M1Counters) {
additionalLog = localizer.getMessage("lblAsM1M1Counters");
}
if (event.type == DamageType.LoyaltyLoss) {
additionalLog = localizer.getMessage("lblRemovingNLoyaltyCounter", String.valueOf(event.amount));
if (event.type() == DamageType.LoyaltyLoss) {
additionalLog = localizer.getMessage("lblRemovingNLoyaltyCounter", String.valueOf(event.amount()));
}
String message = localizer.getMessage("lblSourceDealsNDamageToDest", event.source.toString(), String.valueOf(event.amount), additionalLog, event.card.toString());
String message = localizer.getMessage("lblSourceDealsNDamageToDest", event.source().toString(), String.valueOf(event.amount()), additionalLog, event.card().toString());
return new GameLogEntry(GameLogEntryType.DAMAGE, message);
}
@@ -191,43 +191,43 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
*/
@Override
public GameLogEntry visit(GameEventLandPlayed ev) {
String message = localizer.getMessage("lblLogPlayerPlayedLand", ev.player.toString(), ev.land.toString());
String message = localizer.getMessage("lblLogPlayerPlayedLand", ev.player().toString(), ev.land().toString());
return new GameLogEntry(GameLogEntryType.LAND, message);
}
@Override
public GameLogEntry visit(GameEventTurnBegan event) {
String message = localizer.getMessage("lblLogTurnNOwnerByPlayer", String.valueOf(event.turnNumber), event.turnOwner.toString());
String message = localizer.getMessage("lblLogTurnNOwnerByPlayer", String.valueOf(event.turnNumber()), event.turnOwner().toString());
return new GameLogEntry(GameLogEntryType.TURN, message);
}
@Override
public GameLogEntry visit(GameEventPlayerDamaged ev) {
String extra = ev.infect ? localizer.getMessage("lblLogAsPoisonCounters") : "";
String damageType = ev.combat ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat");
String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source.toString(),
String.valueOf(ev.amount), damageType, ev.target.toString(), extra);
String extra = ev.infect() ? localizer.getMessage("lblLogAsPoisonCounters") : "";
String damageType = ev.combat() ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat");
String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source().toString(),
String.valueOf(ev.amount()), damageType, ev.target().toString(), extra);
return new GameLogEntry(GameLogEntryType.DAMAGE, message);
}
@Override
public GameLogEntry visit(GameEventPlayerPoisoned ev) {
String message = localizer.getMessage("lblLogPlayerReceivesNPosionCounterFrom",
ev.receiver.toString(), String.valueOf(ev.amount), ev.source.toString());
ev.receiver().toString(), String.valueOf(ev.amount()), ev.source().toString());
return new GameLogEntry(GameLogEntryType.DAMAGE, message);
}
@Override
public GameLogEntry visit(GameEventPlayerRadiation ev) {
String message;
final int change = ev.change;
final int change = ev.change();
String radCtr = CounterEnumType.RAD.getName().toLowerCase() + " " +
Localizer.getInstance().getMessage("lblCounter").toLowerCase();
if (change >= 0) message = localizer.getMessage("lblLogPlayerRadiation",
ev.receiver.toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr),
ev.source.toString());
ev.receiver().toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr),
ev.source().toString());
else message = localizer.getMessage("lblLogPlayerRadRemove",
ev.receiver.toString(), Lang.nounWithNumeralExceptOne(String.valueOf(Math.abs(change)), radCtr));
ev.receiver().toString(), Lang.nounWithNumeralExceptOne(String.valueOf(Math.abs(change)), radCtr));
return new GameLogEntry(GameLogEntryType.DAMAGE, message);
}
@@ -239,16 +239,16 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
// Append Defending Player/Planeswalker
// Not a big fan of the triple nested loop here
for (GameEntity k : ev.attackersMap.keySet()) {
Collection<Card> attackers = ev.attackersMap.get(k);
for (GameEntity k : ev.attackersMap().keySet()) {
Collection<Card> attackers = ev.attackersMap().get(k);
if (attackers == null || attackers.isEmpty()) {
continue;
}
if (sb.length() > 0) sb.append("\n");
sb.append(localizer.getMessage("lblLogPlayerAssignedAttackerToAttackTarget", ev.player, Lang.joinHomogenous(attackers), k));
sb.append(localizer.getMessage("lblLogPlayerAssignedAttackerToAttackTarget", ev.player(), Lang.joinHomogenous(attackers), k));
}
if (sb.length() == 0) {
sb.append(localizer.getMessage("lblPlayerDidntAttackThisTurn").replace("%s", ev.player.toString()));
sb.append(localizer.getMessage("lblPlayerDidntAttackThisTurn").replace("%s", ev.player().toString()));
}
return new GameLogEntry(GameLogEntryType.COMBAT, sb.toString());
}
@@ -262,7 +262,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
Collection<Card> blockers = null;
for (Entry<GameEntity, MapOfLists<Card, Card>> kv : ev.blockers.entrySet()) {
for (Entry<GameEntity, MapOfLists<Card, Card>> kv : ev.blockers().entrySet()) {
GameEntity defender = kv.getKey();
MapOfLists<Card, Card> attackers = kv.getValue();
if (attackers == null || attackers.isEmpty()) {
@@ -298,7 +298,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventMulligan ev) {
String message = localizer.getMessage("lblPlayerHasMulliganedDownToNCards").replace("%d", String.valueOf(ev.player.getZone(ZoneType.Hand).size())).replace("%s", ev.player.toString());
String message = localizer.getMessage("lblPlayerHasMulliganedDownToNCards").replace("%d", String.valueOf(ev.player().getZone(ZoneType.Hand).size())).replace("%s", ev.player().toString());
return new GameLogEntry(GameLogEntryType.MULLIGAN, message);
}

View File

@@ -15,6 +15,7 @@ public class GameRules {
private boolean AISideboardingEnabled = false;
private boolean sideboardForAI = false;
private final Set<GameType> appliedVariants = EnumSet.noneOf(GameType.class);
private int simTimeout = 120;
// it's a preference, not rule... but I could hardly find a better place for it
private boolean useGrayText;
@@ -124,4 +125,12 @@ public class GameRules {
public void setWarnAboutAICards(final boolean warnAboutAICards) {
this.warnAboutAICards = warnAboutAICards;
}
public int getSimTimeout() {
return this.simTimeout;
}
public void setSimTimeout(final int duration) {
this.simTimeout = duration;
}
}

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