Compare commits

..

346 Commits

Author SHA1 Message Date
Hans Mackowiak
1efddc9a92 CardTraitChanges: as Record 2025-08-30 10:44:01 +02: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
Tim Miller
e4d58a0a88 Add other deck sections for other formats 2025-08-10 17:03:30 -04:00
998 changed files with 36154 additions and 28640 deletions

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);
new EditorMainWindow(Config.instance());
}
}

View File

@@ -563,7 +563,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 +573,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 +716,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

@@ -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);
@@ -2542,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);

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

@@ -499,19 +499,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"));
@@ -1607,7 +1594,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()) {
@@ -2171,16 +2158,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

@@ -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

@@ -154,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);
}
}
}
@@ -163,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

@@ -170,7 +170,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) {
@@ -336,7 +336,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);
@@ -608,7 +608,7 @@ 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));
// each non +1/+1 counter on the card is a 10% chance of not
// activating this ability.
@@ -623,7 +623,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);
}
}
@@ -961,8 +961,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) {
@@ -1083,8 +1083,8 @@ public class CountersPutAi extends CountersAi {
if (e instanceof Card) {
Card c = (Card) e;
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)) {
@@ -1101,12 +1101,12 @@ public class CountersPutAi extends CountersAi {
} else if (e instanceof Player) {
Player p = (Player) e;
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;
}
}

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

@@ -133,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

@@ -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

@@ -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

@@ -159,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;

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

@@ -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

@@ -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...

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;
@@ -200,7 +201,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 +242,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])) {

View File

@@ -552,26 +552,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);
@@ -659,31 +649,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));
}
}
}
@@ -850,7 +846,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");

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) {
@@ -220,7 +221,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() {
@@ -361,16 +364,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();
}
@@ -823,6 +831,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

@@ -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

@@ -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;
@@ -208,14 +210,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 +528,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 +632,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 +689,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

@@ -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());
}

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

@@ -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);
}
}

View File

@@ -199,11 +199,8 @@ 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;
@@ -222,29 +219,37 @@ 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");
}
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, encodeUtf8(cardCollectorNumber),

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>8.18.0</version>
<version>8.19.1</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>

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>

View File

@@ -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) {

View File

@@ -33,7 +33,6 @@ 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;
@@ -305,9 +304,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 +312,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 +321,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 +340,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

@@ -24,6 +24,7 @@ public enum GameType {
Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""),
CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommanderGauntlet", "lblCommanderDesc"),
Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""),
QuestCommander (DeckFormat.Commander, true, true, false, "lblQuestCommander", ""),
QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""),
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""),
Adventure (DeckFormat.Adventure, true, false, false, "lblAdventure", ""),
@@ -71,6 +72,8 @@ public enum GameType {
return deck;
});
private static final EnumSet<GameType> DRAFT_FORMATS = EnumSet.of(Draft, QuestDraft, AdventureEvent);
private final DeckFormat deckFormat;
private final boolean isCardPoolLimited, canSideboard, addWonCardsMidGame;
private final String name, englishName, description;
@@ -87,7 +90,7 @@ public enum GameType {
addWonCardsMidGame = addWonCardsMidgame0;
name = localizer.getMessage(name0);
englishName = localizer.getEnglishMessage(name0);
if (description0.length()>0) {
if (!description0.isEmpty()) {
description0 = localizer.getMessage(description0);
}
description = description0;
@@ -127,19 +130,8 @@ public enum GameType {
return addWonCardsMidGame;
}
public boolean isCommandZoneNeeded() {
return true; //TODO: Figure out way to move command zone into field so it can be hidden when empty
/*switch (this) {
case Archenemy:
case Commander:
case Oathbreaker:
case TinyLeaders:
case Planechase:
case Vanguard:
return true;
default:
return false;
}*/
public boolean isDraft() {
return DRAFT_FORMATS.contains(this);
}
public String toString() {
@@ -153,6 +145,27 @@ public enum GameType {
return description;
}
/**
* @return the deck sections used by most decks in this game type.
*/
public EnumSet<DeckSection> getPrimaryDeckSections() {
return deckFormat.getPrimaryDeckSections();
}
/**
* @return the set of variant card sections that decks for this game type can include.
*/
public EnumSet<DeckSection> getSupplimentalDeckSections() {
if(!deckFormat.getPrimaryDeckSections().contains(DeckSection.Main))
return EnumSet.noneOf(DeckSection.class); //Already an extra deck, like a dedicated Scheme or Planar deck.
if(deckFormat == DeckFormat.Limited)
return EnumSet.of(DeckSection.Conspiracy, DeckSection.Contraptions, DeckSection.Attractions);
if(this == Constructed || this == Commander)
return EnumSet.of(DeckSection.Avatar, DeckSection.Schemes, DeckSection.Planes, DeckSection.Conspiracy,
DeckSection.Attractions, DeckSection.Contraptions);
return EnumSet.of(DeckSection.Attractions, DeckSection.Contraptions);
}
public static GameType smartValueOf(String name) {
return Enums.getIfPresent(GameType.class, name).orNull();
}

View File

@@ -215,6 +215,7 @@ public class GameView extends TrackableObject {
}
public void setDependencies(Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
if (dependencies.isEmpty()) {
set(TrackableProperty.Dependencies, "");
return;
}
StringBuilder sb = new StringBuilder();

View File

@@ -3,7 +3,6 @@ package forge.game.ability;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.spellability.AbilityActivated;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.TargetRestrictions;
import java.util.Map;
@@ -18,14 +17,7 @@ public class AbilityApiBased extends AbilityActivated {
api = api0;
effect = api.getSpellEffect();
if (api.equals(ApiType.Mana) || api.equals(ApiType.ManaReflected)) {
this.setManaPart(new AbilityManaPart(this, mapParams));
this.setUndoable(true); // will try at least
}
if (api.equals(ApiType.ChangeZone) || api.equals(ApiType.ChangeZoneAll)) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
effect.buildSpellAbility(this);
}
@Override

View File

@@ -202,15 +202,6 @@ public final class AbilityFactory {
final Card hostCard = state.getCard();
TargetRestrictions abTgt = mapParams.containsKey("ValidTgts") ? readTarget(mapParams) : null;
if (api == ApiType.CopySpellAbility || api == ApiType.Counter || api == ApiType.ChangeTargets || api == ApiType.ControlSpell) {
// Since all "CopySpell" ABs copy things on the Stack no need for it to be everywhere
// Since all "Counter" or "ChangeTargets" abilities only target the Stack Zone
// No need to have each of those scripts have that info
if (abTgt != null) {
abTgt.setZone(ZoneType.Stack);
}
}
if (abCost == null) {
abCost = parseAbilityCost(state, mapParams, type);
}

View File

@@ -113,13 +113,6 @@ public class AbilityUtils {
}
} else if (defined.equals("Enchanted")) {
c = hostCard.getEnchantingCard();
if (c == null && sa instanceof SpellAbility) {
SpellAbility root = ((SpellAbility)sa).getRootAbility();
CardCollection sacrificed = root.getPaidList("Sacrificed", true);
if (sacrificed != null && !sacrificed.isEmpty()) {
c = sacrificed.getFirst().getEnchantingCard();
}
}
} else if (defined.equals("TopOfGraveyard")) {
final CardCollectionView grave = player.getCardsIn(ZoneType.Graveyard);
@@ -2345,6 +2338,9 @@ public class AbilityUtils {
if (sq[0].equals("YourSpeed")) {
return doXMath(player.getSpeed(), expr, c, ctb);
}
if (sq[0].equals("AllFourBend")) {
return doXMath(calculateAmount(c, sq[player.hasAllElementBend() ? 1 : 2], ctb), expr, c, ctb);
}
if (sq[0].equals("Night")) {
return doXMath(calculateAmount(c, sq[game.isNight() ? 1 : 2], ctb), expr, c, ctb);

View File

@@ -19,6 +19,7 @@ public enum ApiType {
AddPhase (AddPhaseEffect.class),
AddTurn (AddTurnEffect.class),
AdvanceCrank (AdvanceCrankEffect.class),
Airbend (AirbendEffect.class),
AlterAttribute (AlterAttributeEffect.class),
Amass (AmassEffect.class),
Animate (AnimateEffect.class),
@@ -81,6 +82,7 @@ public enum ApiType {
Draft (DraftEffect.class),
Draw (DrawEffect.class),
EachDamage (DamageEachEffect.class),
Earthbend (EarthbendEffect.class),
Effect (EffectEffect.class),
Encode (EncodeEffect.class),
EndCombatPhase (EndCombatPhaseEffect.class),

View File

@@ -49,6 +49,8 @@ public abstract class SpellAbilityEffect {
return sa.getDescription();
}
public void buildSpellAbility(final SpellAbility sa) {}
/**
* Returns this effect description with needed prelude and epilogue.
* @param params

View File

@@ -4,7 +4,6 @@ import java.util.Map;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.Spell;
import forge.game.spellability.TargetRestrictions;
@@ -24,13 +23,7 @@ public class SpellApiBased extends Spell {
// A spell is always intrinsic
this.setIntrinsic(true);
if (api.equals(ApiType.Mana) || api.equals(ApiType.ManaReflected)) {
this.setManaPart(new AbilityManaPart(this, mapParams));
}
if (api.equals(ApiType.ChangeZone) || api.equals(ApiType.ChangeZoneAll)) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
effect.buildSpellAbility(this);
}
@Override

View File

@@ -2,8 +2,6 @@ package forge.game.ability;
import java.util.Map;
import forge.game.ability.effects.ChangeZoneAllEffect;
import forge.game.ability.effects.ChangeZoneEffect;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.spellability.AbilityStatic;
@@ -20,9 +18,7 @@ public class StaticAbilityApiBased extends AbilityStatic {
api = api0;
effect = api.getSpellEffect();
if (effect instanceof ChangeZoneEffect || effect instanceof ChangeZoneAllEffect) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
effect.buildSpellAbility(this);
}
@Override

View File

@@ -0,0 +1,93 @@
package forge.game.ability.effects;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardZoneTable;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Lang;
public class AirbendEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder("Airbend ");
Iterable<Card> tgts;
if (sa.usesTargeting()) {
tgts = getCardsfromTargets(sa);
} else { // otherwise add self to list and go from there
tgts = sa.knownDetermineDefined(sa.getParam("Defined"));
}
sb.append(sa.getParamOrDefault("DefinedDesc", Lang.joinHomogenous(tgts)));
sb.append(".");
if (Iterables.size(tgts) > 1) {
sb.append(" (Exile them. While each one is exiled, its owner may cast it for {2} rather than its mana cost.)");
} else {
sb.append(" (Exile it. While its exiled, its owner may cast it for {2} rather than its mana cost.)");
}
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
final Player pl = sa.getActivatingPlayer();
final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa);
for (Card c : getTargetCards(sa)) {
final Card gameCard = game.getCardState(c, null);
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
// this should check Self too
if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) {
continue;
}
if (!gameCard.canExiledBy(sa, true)) {
continue;
}
handleExiledWith(gameCard, sa);
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
AbilityKey.addCardZoneTableParams(moveParams, triggerList);
Card movedCard = game.getAction().exile(gameCard, sa, moveParams);
if (movedCard == null || !movedCard.isInZone(ZoneType.Exile)) {
continue;
}
// Effect to cast for 2 from exile
Card eff = createEffect(sa, movedCard.getOwner(), "Airbend" + movedCard, hostCard.getImageKey());
eff.addRemembered(movedCard);
StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | MayPlayAltManaCost$ 2 | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
eff.addStaticAbility(sbPlay.toString());
addForgetOnMovedTrigger(eff, "Exile");
addForgetOnCastTrigger(eff, "Card.IsRemembered");
game.getAction().moveToCommand(eff, sa);
}
triggerList.triggerChangesZoneAll(game, sa);
handleExiledWith(triggerList.allCards(), sa);
pl.triggerElementalBend(TriggerType.Airbend);
}
}

View File

@@ -17,7 +17,6 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardZoneTable;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.card.token.TokenInfo;
import forge.game.event.GameEventCombatChanged;
import forge.game.event.GameEventTokenCreated;
@@ -86,7 +85,7 @@ public class AmassEffect extends TokenEffectBase {
}
Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", CounterType.get(CounterEnumType.P1P1));
params.put("CounterType", CounterEnumType.P1P1);
params.put("Amount", amount);
Card tgt = activator.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblChooseAnArmy"), false, params);

View File

@@ -90,6 +90,16 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
c.addPerpetual(p);
p.applyEffect(c);
}
if (sa.hasParam("ManaCost")) {
final ManaCost manaCost = new ManaCost(new ManaCostParser(sa.getParam("ManaCost")));
if (perpetual) {
PerpetualManaCost p = new PerpetualManaCost(timestamp, manaCost);
c.addPerpetual(p);
p.applyEffect(c);
} else {
c.addChangedManaCost(manaCost, timestamp, (long) 0);
}
}
if (!addType.isEmpty() || !removeType.isEmpty() || addAllCreatureTypes || !remove.isEmpty()) {
if (perpetual) {

View File

@@ -18,6 +18,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetChoices;
import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Localizer;
@@ -27,6 +28,13 @@ import forge.util.Localizer;
*/
public class ChangeTargetsEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityEffect#resolve(forge.card.spellability.SpellAbility)
*/

View File

@@ -7,6 +7,7 @@ import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
@@ -21,6 +22,12 @@ import forge.util.Localizer;
import forge.util.TextUtil;
public class ChangeZoneAllEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
AbilityFactory.adjustChangeZoneTarget(sa.getMapParams(), sa);
}
@Override
protected String getStackDescription(SpellAbility sa) {
// TODO build Stack Description will need expansion as more cards are added

View File

@@ -6,6 +6,7 @@ import com.google.common.collect.Maps;
import forge.card.CardStateName;
import forge.card.CardType;
import forge.game.*;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
@@ -18,7 +19,6 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
@@ -34,6 +34,11 @@ import java.util.Map;
public class ChangeZoneEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
AbilityFactory.adjustChangeZoneTarget(sa.getMapParams(), sa);
}
@Override
protected String getStackDescription(SpellAbility sa) {
if (sa.isHidden()) {
@@ -759,7 +764,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand+!ThisTurnEntered");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
final StaticAbility st = eff.addStaticAbility(sbPlay.toString());
eff.addStaticAbility(sbPlay.toString());
eff.addRemembered(movedCard);
addForgetOnMovedTrigger(eff, "Exile");
addForgetOnCastTrigger(eff, "Card.IsRemembered");

View File

@@ -10,8 +10,17 @@ import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
public class ControlSpellEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility)
*/

View File

@@ -23,6 +23,12 @@ import java.util.Map;
public class CopySpellAbilityEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
@Override
protected String getStackDescription(SpellAbility sa) {

View File

@@ -21,6 +21,13 @@ import java.util.List;
import java.util.Map;
public class CounterEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
if (sa.usesTargeting()) {
sa.getTargetRestrictions().setZone(ZoneType.Stack);
}
}
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();

View File

@@ -102,24 +102,29 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
int totalRemoved = 0;
CardCollectionView srcCards;
if (sa.hasParam("Choices")) {
ZoneType choiceZone = sa.hasParam("ChoiceZone") ? ZoneType.smartValueOf(sa.getParam("ChoiceZone"))
: ZoneType.Battlefield;
CardCollection choices = CardLists.getValidCards(game.getCardsIn(choiceZone), sa.getParam("Choices"),
srcCards = CardLists.getValidCards(game.getCardsIn(choiceZone), sa.getParam("Choices"),
activator, source, sa);
} else {
srcCards = getTargetCards(sa);
}
if (sa.isReplacementAbility()) {
srcCards = new CardCollection(srcCards).filter(c -> !c.isInPlay() || sa.getLastStateBattlefield().contains(c));
}
if (sa.hasParam("Choices")) {
int min = 1;
int max = 1;
if (sa.hasParam("ChoiceOptional")) {
min = 0;
max = choices.size();
max = srcCards.size();
}
if (sa.hasParam("ChoiceNum")) {
min = max = AbilityUtils.calculateAmount(source, sa.getParam("ChoiceNum"), sa);
}
if (choices.size() < min) {
if (srcCards.size() < min) {
return;
}
@@ -128,13 +133,12 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
title = title.replace(" ", " ");
Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", counterType);
srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, params);
srcCards = pc.chooseCardsForEffect(srcCards, sa, title, min, max, min == 0, params);
} else {
for (final Player tgtPlayer : getTargetPlayers(sa)) {
if (!tgtPlayer.isInGame()) {
continue;
}
// Removing energy
if (type.equals("All")) {
for (Map.Entry<CounterType, Integer> e : Lists.newArrayList(tgtPlayer.getCounters().entrySet())) {
totalRemoved += tgtPlayer.subtractCounter(e.getKey(), e.getValue(), activator);
@@ -150,8 +154,6 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
}
}
srcCards = getTargetCards(sa);
}
for (final Card tgtCard : srcCards) {

View File

@@ -0,0 +1,88 @@
package forge.game.ability.effects;
import java.util.Arrays;
import java.util.EnumSet;
import forge.card.RemoveType;
import forge.game.Game;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCopyService;
import forge.game.card.CounterEnumType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
import forge.util.Lang;
public class EarthbendEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder("Earthbend ");
final Card card = sa.getHostCard();
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa);
sb.append(amount).append(". (Target land you control becomes a 0/0 creature with haste that's still a land. Put ");
sb.append(Lang.nounWithNumeral(amount, "+1/+1 counter"));
sb.append(" on it. When it dies or is exiled, return it to the battlefield tapped.)");
return sb.toString();
}
@Override
public void buildSpellAbility(final SpellAbility sa) {
TargetRestrictions abTgt = new TargetRestrictions("Select target land you control", "Land.YouCtrl".split(","), "1", "1");
sa.setTargetRestrictions(abTgt);
}
@Override
public void resolve(SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
final Player pl = sa.getActivatingPlayer();
int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("Num", "1"), sa);
long ts = game.getNextTimestamp();
String desc = "When it dies or is exiled, return it to the battlefield tapped.";
String sbTrigA = "Mode$ ChangesZone | ValidCard$ Card.IsTriggerRemembered | Origin$ Battlefield | Destination$ Graveyard | TriggerDescription$ " + desc;
String sbTrigB = "Mode$ Exiled | Origin$ Battlefield | ValidCard$ Card.IsTriggerRemembered | TriggerZones$ Battlefield | TriggerDescription$ " + desc;
// Earthbend should only target one land
for (Card c : getTargetCards(sa)) {
c.addNewPT(0, 0, ts, 0);
c.addChangedCardTypes(Arrays.asList("Creature"), null, false, EnumSet.noneOf(RemoveType.class), ts, 0, true, false);
c.addChangedCardKeywords(Arrays.asList("Haste"), null, false, ts, null);
GameEntityCounterTable table = new GameEntityCounterTable();
c.addCounter(CounterEnumType.P1P1, num, pl, table);
table.replaceCounterEffect(game, sa, true);
buildTrigger(sa, c, sbTrigA, "Graveyard");
buildTrigger(sa, c, sbTrigB, "Exile");
}
pl.triggerElementalBend(TriggerType.Earthbend);
}
protected void buildTrigger(SpellAbility sa, Card c, String sbTrig, String zone) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
String trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRemembered | Origin$ " + zone + " | Destination$ Battlefield | Tapped$ True";
final Trigger trig = TriggerHandler.parseTrigger(sbTrig, CardCopyService.getLKICopy(source), sa.isIntrinsic());
final SpellAbility newSa = AbilityFactory.getAbility(trigSA, sa.getHostCard());
newSa.setIntrinsic(sa.isIntrinsic());
trig.addRemembered(c);
trig.setOverridingAbility(newSa);
trig.setSpawningAbility(sa.copy(sa.getHostCard(), true));
trig.setKeyword(trig.getSpawningAbility().getKeyword());
game.getTriggerHandler().registerDelayedTrigger(trig);
}
}

View File

@@ -8,6 +8,7 @@ import java.util.Map;
import com.google.common.collect.Lists;
import forge.ImageKeys;
import forge.StaticData;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
@@ -142,15 +143,20 @@ public class EffectEffect extends SpellAbilityEffect {
}
String image;
if (sa.hasParam("Image")) {
image = ImageKeys.getTokenKey(sa.getParam("Image"));
} else if (name.startsWith("Emblem")) { // try to get the image from name
image = ImageKeys.getTokenKey(
TextUtil.fastReplace(
TextUtil.fastReplace(
TextUtil.fastReplace(name.toLowerCase(), "", "_"),
if (name.startsWith("Emblem")) {
if (sa.hasParam("Image")) {
image = StaticData.instance().getOtherImageKey(sa.getParam("Image"), hostCard.getSetCode());
} else {
// try to get the image from name
String imageKey = TextUtil.fastReplace(
TextUtil.fastReplace(
TextUtil.fastReplace(name.toLowerCase(), "", "_"),
",", ""),
" ", "_").toLowerCase());
" ", "_");
image = StaticData.instance().getOtherImageKey(imageKey, hostCard.getSetCode());
}
} else if (sa.hasParam("Image")) {
image = ImageKeys.getTokenKey(sa.getParam("Image"));
} else { // use host image
image = hostCard.getImageKey();
}

View File

@@ -51,7 +51,7 @@ public class MakeCardEffect extends SpellAbilityEffect {
if (source.hasNamedCard()) {
names.addAll(source.getNamedCards());
} else {
System.err.println("Malformed MakeCard entry! - " + source.toString());
System.err.println("Malformed MakeCard entry! - " + source);
}
} else {
names.add(n);
@@ -72,7 +72,8 @@ public class MakeCardEffect extends SpellAbilityEffect {
cards = AbilityUtils.getDefinedCards(source, def, sa);
}
for (final Card c : cards) {
names.add(c.getName());
//get the original papercard name
names.add(c.getPaperCard().getName());
}
} else if (sa.hasParam("Spellbook")) {
faces.addAll(parseFaces(sa, "Spellbook"));

View File

@@ -19,9 +19,11 @@ import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Localizer;
import io.sentry.Breadcrumb;
@@ -29,6 +31,14 @@ import io.sentry.Sentry;
public class ManaEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
sa.setManaPart(new AbilityManaPart(sa, sa.getMapParams()));
if (sa.getParent() == null) {
sa.setUndoable(true); // will try at least
}
}
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
@@ -265,6 +275,9 @@ public class ManaEffect extends SpellAbilityEffect {
// Only clear express choice after mana has been produced
abMana.clearExpressChoice();
if (sa.isKeyword(Keyword.FIREBENDING)) {
activator.triggerElementalBend(TriggerType.Firebend);
}
}
/**

View File

@@ -17,6 +17,14 @@ import forge.util.Localizer;
public class ManaReflectedEffect extends SpellAbilityEffect {
@Override
public void buildSpellAbility(SpellAbility sa) {
sa.setManaPart(new AbilityManaPart(sa, sa.getMapParams()));
if (sa.getParent() == null) {
sa.setUndoable(true); // will try at least
}
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellEffect#resolve(java.util.Map, forge.card.spellability.SpellAbility)
*/

View File

@@ -369,8 +369,8 @@ public class RollDiceEffect extends SpellAbilityEffect {
List<Card> canIncrementDice = new ArrayList<>();
for (Card c : xenosquirrels) {
// Xenosquirrels must have a P1P1 counter on it to remove in order to modify
Integer P1P1Counters = c.getCounters().get(CounterType.get(CounterEnumType.P1P1));
if (P1P1Counters != null && P1P1Counters > 0 && c.canRemoveCounters(CounterType.get(CounterEnumType.P1P1))) {
Integer P1P1Counters = c.getCounters().get(CounterEnumType.P1P1);
if (P1P1Counters != null && P1P1Counters > 0 && c.canRemoveCounters(CounterEnumType.P1P1)) {
canIncrementDice.add(c);
}
}
@@ -399,6 +399,7 @@ public class RollDiceEffect extends SpellAbilityEffect {
* @param repParams replacement effect parameters
* @return list of final roll results after applying ignores and replacements, sorted in ascending order
*/
@SuppressWarnings("unchecked")
private static List<Integer> rollAction(int amount, int sides, int ignore, List<Integer> rollsResult, List<Integer> ignored, Map<Player, Integer> ignoreChosenMap, Set<Card> dicePTExchanges, Player player, Map<AbilityKey, Object> repParams) {
repParams.put(AbilityKey.Sides, sides);
@@ -416,6 +417,8 @@ public class RollDiceEffect extends SpellAbilityEffect {
ignoreChosenMap = (Map<Player, Integer>) repParams.get(AbilityKey.IgnoreChosen);
break;
}
default:
break;
}
List<Integer> naturalRolls = (rollsResult == null ? new ArrayList<>() : rollsResult);

View File

@@ -41,6 +41,7 @@ public class TapOrUntapEffect extends SpellAbilityEffect {
tapper = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Tapper"), sa).getFirst();
}
PlayerController pc = tapper.getController();
boolean toggle = sa.hasParam("Toggle");
CardCollection tapped = new CardCollection();
final Map<Player, CardCollection> untapMap = Maps.newHashMap();
@@ -61,8 +62,12 @@ public class TapOrUntapEffect extends SpellAbilityEffect {
continue;
}
// If the effected card is controlled by the same controller of the SA, default to untap.
boolean tap = pc.chooseBinary(sa, Localizer.getInstance().getMessage("lblTapOrUntapTarget", CardTranslation.getTranslatedName(gameCard.getName())), PlayerController.BinaryChoiceType.TapOrUntap,
boolean tap;
if(!toggle)
tap = pc.chooseBinary(sa, Localizer.getInstance().getMessage("lblTapOrUntapTarget", CardTranslation.getTranslatedName(gameCard.getName())), PlayerController.BinaryChoiceType.TapOrUntap,
!gameCard.getController().equals(tapper));
else
tap = !gameCard.isTapped();
if (tap) {
if (gameCard.tap(true, sa, tapper)) tapped.add(gameCard);
} else if (gameCard.untap()) {

View File

@@ -11,6 +11,7 @@ import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.card.CounterEnumType;
import forge.game.player.Player;
import forge.game.player.PlayerController;
@@ -37,7 +38,7 @@ public class TimeTravelEffect extends SpellAbilityEffect {
PlayerController pc = activator.getController();
final CounterEnumType counterType = CounterEnumType.TIME;
final CounterType counterType = CounterEnumType.TIME;
for (int i = 0; i < num; i++) {
FCollection<Card> list = new FCollection<>();

View File

@@ -1051,7 +1051,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
// it was always created), adjust threshold based on its existence.
int threshold = states.containsKey(CardStateName.FaceDown) ? 2 : 1;
int numStates = states.keySet().size();
int numStates = states.size();
return numStates > threshold;
}
@@ -2762,7 +2762,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")
|| keyword.startsWith("Encore") || keyword.startsWith("Mutate") || keyword.startsWith("Dungeon")
|| keyword.startsWith("Class") || keyword.startsWith("Blitz") || keyword.startsWith("Web-slinging")
|| keyword.startsWith("Specialize") || keyword.equals("Ravenous")
|| keyword.startsWith("Specialize") || keyword.equals("Ravenous") || keyword.startsWith("Firebending")
|| keyword.equals("For Mirrodin") || keyword.equals("Job select") || keyword.startsWith("Craft")
|| keyword.startsWith("Landwalk") || keyword.startsWith("Visit") || keyword.startsWith("Mobilize")
|| keyword.startsWith("Station") || keyword.startsWith("Warp") || keyword.startsWith("Devour")) {
@@ -3219,6 +3219,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
desc = TextUtil.fastReplace(desc, "EFFECTSOURCE", getEffectSource().getName());
}
// Ensure no more escaped linebreak are present
desc = desc.replace("\\r", "\r")
.replace("\\n", "\n");
return desc.trim();
}
@@ -3557,14 +3561,23 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (!getStaticAbilities().isEmpty()) {
return false;
}
if (!getReplacementEffects().isEmpty()) {
if (!getReplacementEffects().isEmpty()
&& (getReplacementEffects().size() > 1 || !isSaga() || hasKeyword(Keyword.READ_AHEAD))) {
return false;
}
if (!getTriggers().isEmpty()) {
return false;
}
for (SpellAbility sa : getSpellAbilities()) {
if (!(sa instanceof SpellPermanent && sa.isBasicSpell()) && !sa.isMorphUp() && !sa.isDisguiseUp()) {
// morph up and disguise up are not part of the card
if (sa.isMorphUp() || sa.isDisguiseUp()) {
continue;
}
// while Adventure and Omen are part of Secondary
if ((sa.isAdventure() || sa.isOmen()) && !getCurrentStateName().equals(sa.getCardState())) {
continue;
}
if (!(sa instanceof SpellPermanent && sa.isBasicSpell())) {
return false;
}
}
@@ -3601,13 +3614,11 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
}
}
} else {
} else if (hasState(CardStateName.Secondary) && state.getStateName() == CardStateName.Original) {
// Adventure and Omen may only be cast not from Battlefield
if (hasState(CardStateName.Secondary) && state.getStateName() == CardStateName.Original) {
for (SpellAbility sa : getState(CardStateName.Secondary).getSpellAbilities()) {
if (mana == null || mana == sa.isManaAbility()) {
list.add(sa);
}
for (SpellAbility sa : getState(CardStateName.Secondary).getSpellAbilities()) {
if (mana == null || mana == sa.isManaAbility()) {
list.add(sa);
}
}
}
@@ -6431,10 +6442,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
DamageType damageType = DamageType.Normal;
if (isPlaneswalker()) { // 120.3c
subtractCounter(CounterType.get(CounterEnumType.LOYALTY), damageIn, null, true);
subtractCounter(CounterEnumType.LOYALTY, damageIn, null, true);
}
if (isBattle()) {
subtractCounter(CounterType.get(CounterEnumType.DEFENSE), damageIn, null, true);
subtractCounter(CounterEnumType.DEFENSE, damageIn, null, true);
}
if (isCreature()) {
if (source.isWitherDamage()) { // 120.3d

View File

@@ -198,7 +198,9 @@ public class CardFactory {
if (c.hasAlternateState()) {
if (c.isFlipCard()) {
c.setState(CardStateName.Flipped, false);
c.setImageKey(cp.getImageKey(true));
// set the imagekey altstate to false since the rotated image is handled by graphics renderer
// setting this to true will download the original image with different name.
c.setImageKey(cp.getImageKey(false));
}
else if (c.isDoubleFaced() && cardRules != null) {
c.setState(cardRules.getSplitType().getChangedStateName(), false);

View File

@@ -1170,6 +1170,29 @@ public class CardFactoryUtil {
removeCounterSA.setIntrinsic(intrinsic);
trigger.setOverridingAbility(removeCounterSA);
inst.addTrigger(trigger);
} else if (keyword.startsWith("Firebending")) {
final String[] k = keyword.split(":");
final String n = k[1];
StringBuilder desc = new StringBuilder("Firebending ");
desc.append(n);
if (k.length > 2) {
desc.append(" ").append(k[2]);
}
desc.append(" (").append(inst.getReminderText()).append(")");
final String trigStr = "Mode$ Attacks | ValidCard$ Card.Self | TriggerDescription$ " + desc.toString();
final String manaStr = "DB$ Mana | Defined$ You | CombatMana$ True | Produced$ R | Amount$ " + n;
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
SpellAbility manaSA = AbilityFactory.getAbility(manaStr, card);
manaSA.setIntrinsic(intrinsic);
trigger.setOverridingAbility(manaSA);
inst.addTrigger(trigger);
} else if (keyword.equals("Flanking")) {
final StringBuilder trigFlanking = new StringBuilder(
@@ -2545,7 +2568,7 @@ public class CardFactoryUtil {
} else if (keyword.equals("Sunburst")) {
// Rule 702.43a If this object is entering the battlefield as a creature,
// ignoring any type-changing effects that would affect it
CounterType t = CounterType.get(host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE);
CounterType t = host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE;
StringBuilder sb = new StringBuilder("etbCounter:");
sb.append(t).append(":Sunburst:no Condition:");

View File

@@ -213,16 +213,10 @@ public final class CardPredicates {
public static Predicate<Card> hasCounter(final CounterType type) {
return hasCounter(type, 1);
}
public static Predicate<Card> hasCounter(final CounterEnumType type) {
return hasCounter(type, 1);
}
public static Predicate<Card> hasCounter(final CounterType type, final int n) {
return c -> c.getCounters(type) >= n;
}
public static Predicate<Card> hasCounter(final CounterEnumType type, final int n) {
return hasCounter(CounterType.get(type), n);
}
public static Predicate<Card> hasLessCounter(final CounterType type, final int n) {
return c -> {
@@ -230,16 +224,10 @@ public final class CardPredicates {
return x > 0 && x <= n;
};
}
public static Predicate<Card> hasLessCounter(final CounterEnumType type, final int n) {
return hasLessCounter(CounterType.get(type), n);
}
public static Predicate<Card> canReceiveCounters(final CounterType counter) {
return c -> c.canReceiveCounters(counter);
}
public static Predicate<Card> canReceiveCounters(final CounterEnumType counter) {
return canReceiveCounters(CounterType.get(counter));
}
public static Predicate<Card> hasGreaterPowerThan(final int minPower) {
return c -> c.getNetPower() > minPower;
@@ -248,9 +236,6 @@ public final class CardPredicates {
public static Comparator<Card> compareByCounterType(final CounterType type) {
return Comparator.comparingInt(arg0 -> arg0.getCounters(type));
}
public static Comparator<Card> compareByCounterType(final CounterEnumType type) {
return compareByCounterType(CounterType.get(type));
}
public static Predicate<Card> hasSVar(final String name) {
return c -> c.hasSVar(name);

View File

@@ -605,18 +605,18 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
result.add(loyaltyRep);
}
if (type.isBattle()) {
// TODO This is currently breaking for Battle/Defense
// Going to script the cards to work but ideally it would happen here
if (defenseRep == null) {
defenseRep = CardFactoryUtil.makeEtbCounter("etbCounter:DEFENSE:" + this.baseDefense, this, true);
}
result.add(defenseRep);
// TODO add Siege "Choose a player to protect it"
}
card.updateReplacementEffects(result, this);
// below are global rules
if (type.hasSubtype("Saga") && !hasKeyword(Keyword.READ_AHEAD)) {
if (sagaRep == null) {
sagaRep = CardFactoryUtil.makeEtbCounter("etbCounter:LORE:1", this, true);
sagaRep = CardFactoryUtil.makeEtbCounter("etbCounter:LORE:1", this, false);
}
result.add(sagaRep);
}
@@ -633,7 +633,6 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
result.add(omenRep);
}
card.updateReplacementEffects(result, this);
return result;
}
public boolean addReplacementEffect(final ReplacementEffect replacementEffect) {
@@ -750,11 +749,21 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
triggers.add(tr.copy(card, lki));
}
}
ReplacementEffect runRE = null;
if (ctb instanceof SpellAbility sp && sp.isReplacementAbility()
&& source.getCard().equals(ctb.getHostCard())) {
runRE = sp.getReplacementEffect();
}
replacementEffects.clear();
for (ReplacementEffect re : source.replacementEffects) {
if (re.isIntrinsic()) {
replacementEffects.add(re.copy(card, lki));
ReplacementEffect reCopy = re.copy(card, lki);
if (re.equals(runRE) && runRE.hasRun()) {
// CR 208.2b prevent loop from card copying itself
reCopy.setHasRun(true);
}
replacementEffects.add(reCopy);
}
}

View File

@@ -1,6 +1,5 @@
package forge.game.card;
import com.google.common.collect.Lists;
import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
@@ -8,74 +7,45 @@ import forge.game.trigger.Trigger;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class CardTraitChanges implements Cloneable {
private List<Trigger> triggers = Lists.newArrayList();
private List<ReplacementEffect> replacements = Lists.newArrayList();
private List<SpellAbility> abilities = Lists.newArrayList();
private List<StaticAbility> staticAbilities = Lists.newArrayList();
private List<SpellAbility> removedAbilities = Lists.newArrayList();
private boolean removeAll = false;
private boolean removeNonMana = false;
public CardTraitChanges(Collection<SpellAbility> spells, Collection<SpellAbility> removedAbilities,
Collection<Trigger> trigger, Collection<ReplacementEffect> res, Collection<StaticAbility> st,
boolean removeAll, boolean removeNonMana) {
if (spells != null) {
this.abilities.addAll(spells);
}
if (removedAbilities != null) {
this.removedAbilities.addAll(removedAbilities);
}
if (trigger != null) {
this.triggers.addAll(trigger);
}
if (res != null) {
this.replacements.addAll(res);
}
if (st != null) {
this.staticAbilities.addAll(st);
}
this.removeAll |= removeAll;
this.removeNonMana |= removeNonMana;
}
public record CardTraitChanges(Collection<SpellAbility> abilities, Collection<SpellAbility> removedAbilities,
Collection<Trigger> triggers, Collection<ReplacementEffect> replacements, Collection<StaticAbility> staticAbilities,
boolean removeAll, boolean removeNonMana) {
/**
* @return the triggers
*/
public Collection<Trigger> getTriggers() {
return triggers;
return Objects.requireNonNullElse(triggers, List.of());
}
/**
* @return the replacements
*/
public Collection<ReplacementEffect> getReplacements() {
return replacements;
return Objects.requireNonNullElse(replacements, List.of());
}
/**
* @return the abilities
*/
public Collection<SpellAbility> getAbilities() {
return abilities;
return Objects.requireNonNullElse(abilities, List.of());
}
/**
* @return the abilities
*/
public Collection<SpellAbility> getRemovedAbilities() {
return removedAbilities;
return Objects.requireNonNullElse(removedAbilities, List.of());
}
/**
* @return the staticAbilities
*/
public Collection<StaticAbility> getStaticAbilities() {
return staticAbilities;
return Objects.requireNonNullElse(staticAbilities, List.of());
}
public boolean isRemoveAll() {
@@ -87,53 +57,30 @@ public class CardTraitChanges implements Cloneable {
}
public CardTraitChanges copy(Card host, boolean lki) {
try {
CardTraitChanges result = (CardTraitChanges) super.clone();
result.abilities = Lists.newArrayList();
for (SpellAbility sa : this.abilities) {
result.abilities.add(sa.copy(host, lki));
}
result.removedAbilities = Lists.newArrayList();
for (SpellAbility sa : this.removedAbilities) {
result.removedAbilities.add(sa.copy(host, lki));
}
result.triggers = Lists.newArrayList();
for (Trigger tr : this.triggers) {
result.triggers.add(tr.copy(host, lki));
}
result.replacements = Lists.newArrayList();
for (ReplacementEffect re : this.replacements) {
result.replacements.add(re.copy(host, lki));
}
result.staticAbilities = Lists.newArrayList();
for (StaticAbility sa : this.staticAbilities) {
result.staticAbilities.add(sa.copy(host, lki));
}
return result;
} catch (final Exception ex) {
throw new RuntimeException("CardTraitChanges : clone() error", ex);
}
return new CardTraitChanges(
this.getAbilities().stream().map(sa -> sa.copy(host, lki)).collect(Collectors.toList()),
this.getRemovedAbilities().stream().map(sa -> sa.copy(host, lki)).collect(Collectors.toList()),
this.getTriggers().stream().map(tr -> tr.copy(host, lki)).collect(Collectors.toList()),
this.getReplacements().stream().map(tr -> tr.copy(host, lki)).collect(Collectors.toList()),
this.getStaticAbilities().stream().map(st -> st.copy(host, lki)).collect(Collectors.toList()),
removeAll, removeNonMana
);
}
public void changeText() {
for (SpellAbility sa : this.abilities) {
for (SpellAbility sa : this.getAbilities()) {
sa.changeText();
}
for (Trigger tr : this.triggers) {
for (Trigger tr : this.getTriggers()) {
tr.changeText();
}
for (ReplacementEffect re : this.replacements) {
for (ReplacementEffect re : this.getReplacements()) {
re.changeText();
}
for (StaticAbility sa : this.staticAbilities) {
for (StaticAbility sa : this.getStaticAbilities()) {
sa.changeText();
}
}

View File

@@ -142,9 +142,8 @@ public class CardView extends GameEntityView {
}
public boolean isFlipped() {
return get(TrackableProperty.Flipped); // getCurrentState().getState() == CardStateName.Flipped;
return get(TrackableProperty.Flipped);
}
public boolean isSplitCard() {
return get(TrackableProperty.SplitCard);
}
@@ -939,7 +938,9 @@ public class CardView extends GameEntityView {
sb.append("\r\n\r\nMerged Cards: ").append(mergedCards);
}
return sb.toString().trim();
return sb.toString().trim()
.replace("\\r", "\r")
.replace("\\n", "\n");
}
public CardStateView getCurrentState() {
@@ -1027,6 +1028,7 @@ public class CardView extends GameEntityView {
set(TrackableProperty.Cloned, c.isCloned());
set(TrackableProperty.SplitCard, isSplitCard);
set(TrackableProperty.FlipCard, c.isFlipCard());
set(TrackableProperty.Flipped, c.getCurrentStateName() == CardStateName.Flipped);
set(TrackableProperty.Facedown, c.isFaceDown());
set(TrackableProperty.Foretold, c.isForetold());
set(TrackableProperty.Secondary, c.hasState(CardStateName.Secondary));
@@ -1104,7 +1106,7 @@ public class CardView extends GameEntityView {
currentState.getView().setOriginalColors(c); //set original Colors
currentStateView.updateAttractionLights(currentState);
currentStateView.updateHasPrintedPT(c.getRules() != null && c.getRules().hasPrintedPT());
currentStateView.updateHasPrintedPT((currentStateView.isVehicle() || currentStateView.isSpaceCraft()) && c.getRules() != null && c.getRules().hasPrintedPT());
CardState alternateState = isSplitCard && isFaceDown() ? c.getState(CardStateName.RightSplit) : c.getAlternateState();

View File

@@ -28,7 +28,7 @@ import java.util.Locale;
* @author Clemens Koza
* @version V0.0 17.02.2010
*/
public enum CounterEnumType {
public enum CounterEnumType implements CounterType {
M1M1("-1/-1", "-1/-1", 255, 110, 106),
P1P1("+1/+1", "+1/+1", 96, 226, 23),
@@ -169,6 +169,8 @@ public enum CounterEnumType {
FINALITY("FINAL", 255, 255, 255),
FIRE("FIRE", 240, 30, 35),
FLAME("FLAME", 255, 143, 43),
FLAVOR("FLAVOR", 208, 152, 97), ///adventure only
@@ -553,4 +555,14 @@ public enum CounterEnumType {
public static final ImmutableList<CounterEnumType> values = ImmutableList.copyOf(values());
@Override
public boolean is(CounterEnumType eType) {
return this == eType;
}
@Override
public boolean isKeywordCounter() {
return false;
}
}

View File

@@ -0,0 +1,74 @@
package forge.game.card;
import java.util.Map;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
public record CounterKeywordType(String keyword) implements CounterType {
// Rule 122.1b
static ImmutableList<String> keywordCounter = ImmutableList.of(
"Flying", "First Strike", "Double Strike", "Deathtouch", "Decayed", "Exalted", "Haste", "Hexproof",
"Indestructible", "Lifelink", "Menace", "Reach", "Shadow", "Trample", "Vigilance");
private static Map<String, CounterKeywordType> sMap = Maps.newHashMap();
public static CounterKeywordType get(String s) {
if (!sMap.containsKey(s)) {
sMap.put(s, new CounterKeywordType(s));
}
return sMap.get(s);
}
@Override
public String toString() {
return keyword;
}
public String getName() {
return getKeywordDescription();
}
public String getCounterOnCardDisplayName() {
return getKeywordDescription();
}
private String getKeywordDescription() {
if (keyword.startsWith("Hexproof:")) {
final String[] k = keyword.split(":");
return "Hexproof from " + k[2];
}
if (keyword.startsWith("Trample:")) {
return "Trample over Planeswalkers";
}
return keyword;
}
public boolean is(CounterEnumType eType) {
return false;
}
public boolean isKeywordCounter() {
if (keyword.startsWith("Hexproof:")) {
return true;
}
if (keyword.startsWith("Trample:")) {
return true;
}
return keywordCounter.contains(keyword);
}
public int getRed() {
return 255;
}
public int getGreen() {
return 255;
}
public int getBlue() {
return 255;
}
}

View File

@@ -1,141 +1,31 @@
package forge.game.card;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import org.apache.commons.lang3.builder.EqualsBuilder;
import java.io.Serializable;
import java.util.Map;
import java.util.Objects;
public class CounterType implements Comparable<CounterType>, Serializable {
private static final long serialVersionUID = -7575835723159144478L;
private CounterEnumType eVal = null;
private String sVal = null;
// Rule 122.1b
static ImmutableList<String> keywordCounter = ImmutableList.of(
"Flying", "First Strike", "Double Strike", "Deathtouch", "Decayed", "Exalted", "Haste", "Hexproof",
"Indestructible", "Lifelink", "Menace", "Reach", "Shadow", "Trample", "Vigilance");
private static Map<CounterEnumType, CounterType> eMap = Maps.newEnumMap(CounterEnumType.class);
private static Map<String, CounterType> sMap = Maps.newHashMap();
private CounterType(CounterEnumType e, String s) {
this.eVal = e;
this.sVal = s;
}
public static CounterType get(CounterEnumType e) {
if (!eMap.containsKey(e)) {
eMap.put(e, new CounterType(e, null));
}
return eMap.get(e);
}
public static CounterType get(String s) {
if (!sMap.containsKey(s)) {
sMap.put(s, new CounterType(null, s));
}
return sMap.get(s);
}
public interface CounterType extends Serializable {
public static CounterType getType(String name) {
if ("Any".equalsIgnoreCase(name)) {
return null;
}
try {
return get(CounterEnumType.getType(name));
return CounterEnumType.getType(name);
} catch (final IllegalArgumentException ex) {
return get(name);
return CounterKeywordType.get(name);
}
}
public String getName();
@Override
public int hashCode() {
return Objects.hash(eVal, sVal);
}
public String getCounterOnCardDisplayName();
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
CounterType rhs = (CounterType) obj;
return new EqualsBuilder()
.append(eVal, rhs.eVal)
.append(sVal, rhs.sVal)
.isEquals();
}
public boolean is(CounterEnumType eType);
@Override
public String toString() {
return eVal != null ? eVal.toString() : sVal;
}
public boolean isKeywordCounter();
public String getName() {
return eVal != null ? eVal.getName() : getKeywordDescription();
}
public int getRed();
public String getCounterOnCardDisplayName() {
return eVal != null ? eVal.getCounterOnCardDisplayName() : getKeywordDescription();
}
public int getGreen();
private String getKeywordDescription() {
if (sVal.startsWith("Hexproof:")) {
final String[] k = sVal.split(":");
return "Hexproof from " + k[2];
}
if (sVal.startsWith("Trample:")) {
return "Trample over Planeswalkers";
}
return sVal;
}
@Override
public int compareTo(CounterType o) {
return ComparisonChain.start()
.compare(eVal, o.eVal, Ordering.natural().nullsLast())
.compare(sVal, o.sVal, Ordering.natural().nullsLast())
.result();
}
public boolean is(CounterEnumType eType) {
return eVal == eType;
}
public boolean isKeywordCounter() {
if (eVal != null) {
return false;
}
if (sVal.startsWith("Hexproof:")) {
return true;
}
if (sVal.startsWith("Trample:")) {
return true;
}
return keywordCounter.contains(sVal);
}
public int getRed() {
return eVal != null ? eVal.getRed() : 255;
}
public int getGreen() {
return eVal != null ? eVal.getGreen() : 255;
}
public int getBlue() {
return eVal != null ? eVal.getBlue() : 255;
}
public int getBlue();
}

View File

@@ -0,0 +1,26 @@
package forge.game.card.perpetual;
import forge.card.mana.ManaCost;
import forge.game.card.Card;
import forge.game.cost.Cost;
public record PerpetualManaCost(long timestamp, ManaCost manaCost) implements PerpetualInterface {
@Override
public long getTimestamp() {
return timestamp;
}
@Override
public void applyEffect(Card c) {
c.addChangedManaCost(manaCost, timestamp, (long) 0);
c.updateManaCostForView();
if (c.getFirstSpellAbility() != null) {
Cost cost = c.getFirstSpellAbility().getPayCosts().copyWithDefinedMana(manaCost);
c.getFirstSpellAbility().setPayCosts(cost);
}
}
}

View File

@@ -621,8 +621,11 @@ public class Cost implements Serializable {
}
public final Cost copyWithDefinedMana(String manaCost) {
return copyWithDefinedMana(new ManaCost(new ManaCostParser(manaCost)));
}
public final Cost copyWithDefinedMana(ManaCost manaCost) {
Cost toRet = copyWithNoMana();
toRet.costParts.add(new CostPartMana(new ManaCost(new ManaCostParser(manaCost)), null));
toRet.costParts.add(new CostPartMana(manaCost, null));
toRet.cacheTapCost();
return toRet;
}
@@ -994,9 +997,9 @@ public class Cost implements Serializable {
Integer counters = otherAmount - part.convertAmount();
// the cost can turn positive if multiple Carth raise it
if (counters < 0) {
costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription()));
costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterEnumType.LOYALTY, part.getType(), part.getTypeDescription()));
} else {
costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription(), Lists.newArrayList(ZoneType.Battlefield) , false));
costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterEnumType.LOYALTY, part.getType(), part.getTypeDescription(), Lists.newArrayList(ZoneType.Battlefield) , false));
}
} else {
continue;

View File

@@ -67,7 +67,7 @@ public class CostExileFromStack extends CostPart {
final String desc = this.getTypeDescription() == null ? this.getType() : this.getTypeDescription();
sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), desc));
sb.append("from stack");
sb.append(" from stack");
return sb.toString();
}

View File

@@ -22,7 +22,6 @@ import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
@@ -81,7 +80,7 @@ public class CostUntap extends CostPart {
public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) {
final Card source = ability.getHostCard();
return source.isTapped() && !source.isAbilitySick() &&
(source.getCounters(CounterEnumType.STUN) == 0 || source.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
(source.getCounters(CounterEnumType.STUN) == 0 || source.canRemoveCounters(CounterEnumType.STUN));
}
@Override

View File

@@ -86,7 +86,7 @@ public class CostUntapType extends CostPartWithList {
if (!canUntapSource) {
typeList.remove(source);
}
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));
final int amount = this.getAbilityAmount(ability);
return (typeList.size() != 0) && (typeList.size() >= amount);

View File

@@ -1,5 +1,5 @@
package forge.game.event;
public abstract class Event {
public interface Event {
}

View File

@@ -1,9 +1,5 @@
package forge.game.event;
/**
* TODO: Write javadoc for this type.
*
*/
public enum EventValueChangeType {
Added,
Removed,

View File

@@ -1,6 +1,6 @@
package forge.game.event;
public abstract class GameEvent extends Event {
public interface GameEvent extends Event {
public abstract <T> T visit(IGameEventVisitor<T> visitor);
}

View File

@@ -5,11 +5,7 @@ import com.google.common.collect.Multimap;
import forge.game.card.Card;
import forge.game.player.Player;
public class GameEventAnteCardsSelected extends GameEvent {
public final Multimap<Player, Card> cards;
public GameEventAnteCardsSelected(Multimap<Player, Card> list) {
cards = list;
}
public record GameEventAnteCardsSelected(Multimap<Player, Card> cards) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {

View File

@@ -6,27 +6,21 @@ import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.player.Player;
/**
* TODO: Write javadoc for this type.
*
*/
public class GameEventAttackersDeclared extends GameEvent {
public final Player player;
public final Multimap<GameEntity, Card> attackersMap;
public GameEventAttackersDeclared(Player playerTurn, Multimap<GameEntity, Card> attackersMap) {
this.player = playerTurn;
this.attackersMap = attackersMap;
}
public record GameEventAttackersDeclared(Player player, Multimap<GameEntity, Card> attackersMap) implements GameEvent {
/* (non-Javadoc)
* @see forge.game.event.GameEvent#visit(forge.game.event.IGameEventVisitor)
*/
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
// TODO Auto-generated method stub
return visitor.visit(this);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "" + player + " declared attackers: " + attackersMap;
}
}

View File

@@ -12,23 +12,10 @@ import forge.util.Lang;
import forge.util.TextUtil;
import forge.util.maps.MapOfLists;
/**
* TODO: Write javadoc for this type.
*
*/
public class GameEventBlockersDeclared extends GameEvent {
public final Map<GameEntity, MapOfLists<Card, Card>> blockers;
public final Player defendingPlayer;
public GameEventBlockersDeclared(Player who, Map<GameEntity, MapOfLists<Card, Card>> blockers) {
this.blockers = blockers;
defendingPlayer = who;
}
public record GameEventBlockersDeclared(Player defendingPlayer, Map<GameEntity, MapOfLists<Card, Card>> blockers) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
// TODO Auto-generated method stub
return visitor.visit(this);
}

View File

@@ -3,20 +3,18 @@ package forge.game.event;
import forge.game.GameEntity;
import forge.game.card.Card;
public class GameEventCardAttachment extends GameEvent {
public final Card equipment;
public final GameEntity newTarget; // can enchant player, I'm ssaving a class to enchants - it could be incorrect.
public final GameEntity oldEntiy;
public GameEventCardAttachment(Card attachment, GameEntity formerEntity, GameEntity newEntity) {
this.equipment = attachment;
this.newTarget = newEntity;
this.oldEntiy = formerEntity;
}
public record GameEventCardAttachment(Card equipment, GameEntity newTarget, GameEntity oldEntity) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return newTarget == null ? "Detached " + equipment + " from " + oldEntity : "Attached " + equipment + (oldEntity == null ? "" : " from " + oldEntity) + " to " + newTarget;
}
}

View File

@@ -4,17 +4,7 @@ import forge.game.card.Card;
import forge.game.zone.Zone;
import forge.util.TextUtil;
public class GameEventCardChangeZone extends GameEvent {
public final Card card;
public final Zone from;
public final Zone to;
public GameEventCardChangeZone(Card c, Zone zoneFrom, Zone zoneTo) {
card = c;
from = zoneFrom;
to = zoneTo;
}
public record GameEventCardChangeZone(Card card, Zone from, Zone to) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {

View File

@@ -3,21 +3,17 @@ package forge.game.event;
import forge.game.card.Card;
import forge.game.card.CounterType;
public class GameEventCardCounters extends GameEvent {
public final Card card;
public final CounterType type;
public final int oldValue;
public final int newValue;
public GameEventCardCounters(Card card, CounterType counterType, int old, int newValue) {
this.card = card;
type = counterType;
this.oldValue = old;
this.newValue = newValue;
}
public record GameEventCardCounters(Card card, CounterType type, int oldValue, int newValue) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "" + card + " " + type + " counters: " + oldValue + " -> " + newValue;
}
}

View File

@@ -2,7 +2,7 @@ package forge.game.event;
import forge.game.card.Card;
public class GameEventCardDamaged extends GameEvent {
public record GameEventCardDamaged(Card card, Card source, int amount, DamageType type) implements GameEvent {
public enum DamageType {
Normal,
@@ -11,21 +11,16 @@ public class GameEventCardDamaged extends GameEvent {
LoyaltyLoss
}
public final Card card;
public final Card source;
public final int amount;
public final DamageType type;
public GameEventCardDamaged(Card card, Card src, int damageToAdd, DamageType damageType) {
this.card = card;
source = src;
amount = damageToAdd;
type = damageType;
}
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "" + source + " dealt " + amount + " " + type + " damage to " + card;
}
}

View File

@@ -1,10 +1,17 @@
package forge.game.event;
public class GameEventCardDestroyed extends GameEvent {
public record GameEventCardDestroyed() implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Card destroyed";
}
}

View File

@@ -2,12 +2,7 @@ package forge.game.event;
import forge.game.player.Player;
public class GameEventCardForetold extends GameEvent {
public final Player activatingPlayer;
public GameEventCardForetold(Player player) {
activatingPlayer = player;
}
public record GameEventCardForetold(Player activatingPlayer) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {

View File

@@ -2,21 +2,7 @@ package forge.game.event;
import forge.game.player.Player;
public class GameEventCardModeChosen extends GameEvent {
public final Player player;
public final String cardName;
public final String mode;
public final boolean log;
public final boolean random;
public GameEventCardModeChosen(Player player, String cardName, String mode, boolean log, boolean random) {
this.player = player;
this.cardName = cardName;
this.mode = mode;
this.log = log;
this.random = random;
}
public record GameEventCardModeChosen(Player player, String cardName, String mode, boolean log, boolean random) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {

View File

@@ -2,19 +2,7 @@ package forge.game.event;
import forge.game.card.Card;
/**
* TODO: Write javadoc for this type.
*
*/
public class GameEventCardPhased extends GameEvent {
public final Card card;
public final boolean phaseState;
public GameEventCardPhased(Card card, boolean state) {
this.card = card;
phaseState = state;
}
public record GameEventCardPhased(Card card, boolean phaseState) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {

View File

@@ -3,16 +3,7 @@ package forge.game.event;
import forge.game.card.Card;
import forge.game.player.Player;
public class GameEventCardPlotted extends GameEvent {
public final Card card;
public final Player activatingPlayer;
public GameEventCardPlotted(Card card, Player player) {
this.card = card;
activatingPlayer = player;
}
public record GameEventCardPlotted(Card card, Player activatingPlayer) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {

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