Compare commits

..

466 Commits

Author SHA1 Message Date
GitHub Actions
72f32e5772 [maven-release-plugin] prepare release forge-2.0.03 2025-04-06 15:44:09 +00:00
Chris H
43ce790ebe Update maven-publish for releases without deploying to FTP 2025-04-06 11:35:15 -04:00
Chris H
b1a3ad1b39 Temporarily remove flatten plugin for release 2025-04-06 08:50:40 -04:00
Paul Hammerton
9109b26484 TDM: Humbling Elder (#7355) 2025-04-06 07:45:55 +00:00
Hans Mackowiak
98e5eb9652 Adventure and Omen as ReplacementEffect in CardState (#7347) 2025-04-05 17:58:20 +00:00
tool4ever
4e93f95dff Update furious_forebear.txt 2025-04-05 17:48:51 +00:00
Renato Filipe Vidal Santos
44e1332fb4 Cleanup: April 2025, pass 1 2025-04-05 17:02:21 +00:00
Hans Mackowiak
e2972acad0 Put ForColor for Call of the Spirit Dragons (#7348)
* Support for Call the Spirit Dragons

* ~ better ChoiceTitle

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

* Use OptionalCost

* Keyworded don't need type

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

* Trickery extrinsic fix

* Split fix

* Annoying checks keep failing

* Fix logic

* Clean up

---------

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

* Support rendering

---------

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

* ~ fix style

* Add files via upload

* Update dusyut_earthcarver.txt

* add better Endure Message

* Update krumar_initiate.txt

* Fix message

* - Add basic EndureAi

* - Fix imports

* Update EndureAi.java

Apply static check for the token

* Update EndureAi.java

fix import

* Add files via upload

---------

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

This reverts commit fc901f1ebb.

* Revert "Fixed WASD movement"

This reverts commit c365f5a3d1.

* Revert "Update KeyBinding.java"

This reverts commit 49697c863c.

* Revert "Adventure Keybinds"

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

* ~ no new hidden keywords

* ~ more CantBeActivated

* Update Card.java

* Refactor scripts

---------

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

* Refactor AI checking for Replacement while inactive

* Update stasis.txt

* Update sands_of_time.txt

---------

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

* Update CHANGES.txt

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

* Removed unused imports

---------

Co-authored-by: Heitor Bittencourt <heitorbite@outlook.com>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-03-14 20:03:13 +03:00
Chris H
9091cfe3b0 Fix double mastesr 2022 basic lands 2025-03-14 11:36:57 -04:00
Chris H
e2614187ac Update brotherhood_vertibird.txt 2025-03-14 10:32:09 -04:00
Ayora29
5c69bf0470 Fix : Puzzle (Possibility Storm - Foundations #01). Opponent library is empty. (#7149) 2025-03-13 11:11:33 +01:00
tool4ever
383dc85166 Some cleanup (#7147) 2025-03-13 10:13:44 +01:00
Hans Mackowiak
7e47208888 Masako the humorless as Static 2025-03-13 08:37:47 +01:00
Hans Mackowiak
137d87c3df use StaticAbility for Topsy Turvy (#7143) 2025-03-12 09:24:53 +01:00
Paul Hammerton
fbff1fe10a Merge pull request #7144 from paulsnoops/edition-updates
Edition updates: CC2, FIC, PMEI, SLD, TDM
2025-03-11 08:31:42 +00:00
Paul Hammerton
3acd4490c5 Edition updates: CC2, FIC, PMEI, SLD, TDM 2025-03-11 08:28:25 +00:00
Hans Mackowiak
2952ed79f8 ~ lf 2025-03-11 07:07:08 +01:00
Fulgur14
cb23eda5a6 Stormplain Detainment [TDM] 2025-03-10 17:10:07 +00:00
Renato Filipe Vidal Santos
ced87c8aea FIC: Celes, Rune Knight 2025-03-10 15:49:40 +00:00
Jetz72
daf87e26ad Fix importing "Ice Tunnel"; Max Speed being flipped instead of transformed (#7138) 2025-03-10 14:31:35 +00:00
Chris H
0c30c4e32c Restrict conspiracy drafts in Adventure mode for now 2025-03-08 18:55:00 -05:00
Paul Hammerton
bf5f9f69ca Merge pull request #7133 from paulsnoops/edition-updates
Edition updates: PWCS
2025-03-08 20:50:08 +00:00
Paul Hammerton
b4828d3b4d Edition updates: PWCS 2025-03-08 20:35:44 +00:00
Renato Filipe Vidal Santos
617df8e07c Fixing Chimeric Mass and Svogthos (#7130) 2025-03-08 08:15:03 +00:00
Simisays
deaba89f46 jace update 2025-03-07 21:23:23 -05:00
Hans Mackowiak
d5b13d56cc Update jackdaw.txt
Closes #7127
2025-03-07 09:26:32 +01:00
Chris H
f893c7ddf8 Balance some item costs 2025-03-06 12:14:00 -05:00
Paul Hammerton
c6cf450ac4 Merge pull request #7121 from paulsnoops/edition-updates
Edition updates: TDM
2025-03-04 18:40:10 +00:00
Paul Hammerton
fa123023c7 Edition updates: TDM 2025-03-04 18:24:38 +00:00
Fulgur14
0b285fa045 Rally the Monastery (#7120) 2025-03-04 17:03:31 +00:00
Paul Hammerton
fe6c4243ee Merge pull request #7119 from paulsnoops/ydft-formats
Add YDFT to formats
2025-03-04 14:06:36 +00:00
Paul Hammerton
1a2c18d25e Add YDFT to formats 2025-03-04 14:03:04 +00:00
Renato Filipe Vidal Santos
12e6de4697 Highway Reaver fix (#7118) 2025-03-04 14:58:02 +01:00
Paul Hammerton
f2b72d4234 Merge pull request #7090 from dracontes/rb-ydft-3
YDFT: 12 cards
2025-03-04 11:22:35 +00:00
Renato Filipe Vidal Santos
25a84e9aee Update venom_deadly_devourer.txt (#7117) 2025-03-04 11:47:54 +01:00
Hans Mackowiak
488171b02b Dream counters rework (#7116)
* Dream Counters: moved to extra getCounterMax function

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

---------

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

----

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

* Fix NPE

---------

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

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

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

* Remove enum counter type for speed.

* Make Start Your Engines an SBA.

* LifeLost -> LifeLostAll per speed rules.

* Use same game event for all speed changes.

* Fix keyword not appearing in detail text

* Cleanup extra createSpeedEffect.

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

* Text styling.

* Remove extra SBA check.

* Remove speed from PlayerView; localization support.

---------

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

* ~ fix trigger desc

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

* Update mimeoplasm_revered_one.txt

* Update radiant_lotus.txt

* Add files via upload

* Update radiant_lotus.txt

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

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

View File

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

View File

@@ -8,6 +8,9 @@ on:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 18 * * *'
jobs:
build:
@@ -109,16 +112,21 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
- name: Upload snapshot to GitHub Prerelease
uses: ncipollo/release-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
server: ftp.cardforge.org
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: izpack/
server-dir: downloads/dailysnapshots/
state-name: .ftp-deploy-both-sync-state.json
exclude: |
*.pom
*.repositories
*.xml
name: Daily Snapshot
tag: daily-snapshots
prerelease: true
artifacts: izpack/*
allowUpdates: true
removeArtifacts: true
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.03</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.03</version>
</parent>
<artifactId>forge-ai</artifactId>

View File

@@ -115,8 +115,8 @@ public class AiAttackController {
} // overloaded constructor to evaluate single specified attacker
private void refreshCombatants(GameEntity defender) {
if (defender instanceof Card && ((Card) defender).isBattle()) {
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
if (defender instanceof Card card && card.isBattle()) {
this.oppList = getOpponentCreatures(card.getProtectingPlayer());
} else {
this.oppList = getOpponentCreatures(defendingOpponent);
}
@@ -312,7 +312,8 @@ public class AiAttackController {
}
}
// Poison opponent if unblocked
if (defender instanceof Player && ComputerUtilCombat.poisonIfUnblocked(attacker, (Player) defender) > 0) {
if (defender instanceof Player player
&& ComputerUtilCombat.poisonIfUnblocked(attacker, player) > 0) {
return true;
}
@@ -849,10 +850,9 @@ public class AiAttackController {
// decided to attack another defender so related lists need to be updated
// (though usually rather try to avoid this situation for performance reasons)
if (defender != defendingOpponent) {
if (defender instanceof Player) {
defendingOpponent = (Player) defender;
} else if (defender instanceof Card) {
Card defCard = (Card) defender;
if (defender instanceof Player p) {
defendingOpponent = p;
} else if (defender instanceof Card defCard) {
if (defCard.isBattle()) {
defendingOpponent = defCard.getProtectingPlayer();
} else {
@@ -946,8 +946,8 @@ public class AiAttackController {
return 1;
}
// or weakest player
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) {
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife();
if (r1.getKey() instanceof Player p1 && r2.getKey() instanceof Player p2) {
return p1.getLife() - p2.getLife();
}
}
return r2.getValue() - r1.getValue();
@@ -1314,7 +1314,7 @@ public class AiAttackController {
attackersAssigned.add(attacker);
// check if attackers are enough to finish the attacked planeswalker
if (i < left.size() - 1 && defender instanceof Card) {
if (i < left.size() - 1 && defender instanceof Card card) {
final int blockNum = this.blockers.size();
int attackNum = 0;
int damage = 0;
@@ -1328,7 +1328,7 @@ public class AiAttackController {
}
}
// if enough damage: switch to next planeswalker
if (damage >= ComputerUtilCombat.getDamageToKill((Card) defender, true)) {
if (damage >= ComputerUtilCombat.getDamageToKill(card, true)) {
break;
}
}
@@ -1754,10 +1754,12 @@ public class AiAttackController {
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final Queue<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
// TODO: detect Revenge of Ravens by the trigger instead of by name
boolean revengeOfRavens = false;
if (defender instanceof Player) {
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card) {
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
if (defender instanceof Player player) {
revengeOfRavens = !CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card card) {
revengeOfRavens = !CardLists.filter(card.getController().getCardsIn(ZoneType.Battlefield),
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
}
if (!revengeOfRavens) {

View File

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

View File

@@ -68,8 +68,10 @@ import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -292,7 +294,7 @@ public class AiController {
}
// can't fetch partner isn't problematic
if (tr.getKeyword() != null && tr.getKeyword().getOriginal().startsWith("Partner")) {
if (tr.isKeyword(Keyword.PARTNER)) {
continue;
}
@@ -689,7 +691,6 @@ public class AiController {
}
}
// TODO handle fetchlands and what they can fetch for
// determine new color pips
int[] card_counts = new int[6]; // in WUBRGC order
@@ -1708,7 +1709,8 @@ public class AiController {
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
CompletableFuture<SpellAbility> future = CompletableFuture.supplyAsync(() -> {
final ExecutorService executor = Executors.newSingleThreadExecutor();
Future<SpellAbility> future = executor.submit(() -> {
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
@@ -1788,11 +1790,9 @@ public class AiController {
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
try {
if (game.AI_CAN_USE_TIMEOUT)
return future.completeOnTimeout(null, game.getAITimeout(), TimeUnit.SECONDS).get();
else
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
future.cancel(true);
return null;
}
}

View File

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

View File

@@ -1099,6 +1099,11 @@ public class ComputerUtil {
}
}
// if AI has no speed, play start your engines on Main1
if (ai.noSpeed() && cardState.hasKeyword(Keyword.START_YOUR_ENGINES)) {
return true;
}
// cast Blitz in main 1 if the creature attacks
if (sa.isBlitz() && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, card)) {
return true;
@@ -1408,9 +1413,7 @@ public class ComputerUtil {
}
}
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part;
if (part instanceof CostSacrifice sac) {
final String type = sac.getType();
if (type.equals("CARDNAME")) {
@@ -1776,9 +1779,7 @@ public class ComputerUtil {
noRegen = true;
}
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// indestructible
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
@@ -1842,9 +1843,7 @@ public class ComputerUtil {
if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c, false)) {
threatened.add(c);
}
} else if (o instanceof Player) {
final Player p = (Player) o;
} else if (o instanceof Player p) {
if (source.hasKeyword(Keyword.INFECT)) {
if (p.canReceiveCounters(CounterEnumType.POISON) && ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= 10 - p.getPoisonCounters()) {
threatened.add(p);
@@ -1862,8 +1861,7 @@ public class ComputerUtil {
|| saviourApi == null)) {
final int dmg = -AbilityUtils.calculateAmount(source, topStack.getParam("NumDef"), topStack);
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
final boolean canRemove = (c.getNetToughness() <= dmg)
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && dmg >= ComputerUtilCombat.getDamageToKill(c, false));
if (!canRemove) {
@@ -1909,9 +1907,7 @@ public class ComputerUtil {
|| saviourApi == ApiType.Protection || saviourApi == null
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
// indestructible
if (o instanceof Card c) {
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
}
@@ -1960,8 +1956,7 @@ public class ComputerUtil {
&& topStack.hasParam("Destination")
&& topStack.getParam("Destination").equals("Exile")) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -1988,8 +1983,7 @@ public class ComputerUtil {
&& (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|| saviourApi == ApiType.Protection || saviourApi == null)) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -2011,8 +2005,7 @@ public class ComputerUtil {
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
if (enableCurseAuraRemoval) {
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -2898,7 +2891,7 @@ public class ComputerUtil {
// Iceberg does use Ice as Storage
|| (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName()))
// some lands does use Depletion as Storage Counter
|| (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step."))
|| (type.is(CounterEnumType.DEPLETION) && c.getReplacementEffects().anyMatch(r -> r.getMode().equals(ReplacementType.Untap) && r.getLayer().equals(ReplacementLayer.CantHappen)))
// treat Time Counters on suspended Cards as Bad,
// and also on Chronozoa
|| (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName())))

View File

@@ -1862,7 +1862,7 @@ public class ComputerUtilCard {
if (!c.isCreature()) {
return false;
}
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.hasKeyword("CARDNAME doesn't untap during your untap step.") && c.isTapped()) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.isTapped() && !c.canUntap(ai, true)) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
return true;
}
return false;

View File

@@ -31,7 +31,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword;
import forge.game.phase.Untap;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
@@ -101,7 +101,7 @@ public class ComputerUtilCombat {
return false;
}
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), "BeginCombat")) {
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), PhaseType.COMBAT_BEGIN)) {
return false;
}
@@ -118,7 +118,7 @@ public class ComputerUtilCombat {
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
// || attacker.hasSVar("EndOfTurnLeavePlay"));
// The creature won't untap next turn
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && Untap.canUntap(attacker));
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && attacker.canUntap(attacker.getController(), true));
}
/**
@@ -176,7 +176,7 @@ public class ComputerUtilCombat {
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
int damage = attacker.getNetCombatDamage();
int sum = 0;
if (attacked instanceof Player && !((Player) attacked).canLoseLife()) {
if (attacked instanceof Player player && !player.canLoseLife()) {
return 0;
}
@@ -214,7 +214,7 @@ public class ComputerUtilCombat {
int damage = attacker.getNetCombatDamage();
int poison = 0;
damage += predictPowerBonusOfAttacker(attacker, null, null, false);
if (attacker.hasKeyword(Keyword.INFECT)) {
if (attacker.isInfectDamage(attacked)) {
int pd = predictDamageTo(attacked, damage, attacker, true);
// opponent can always order it so that he gets 0
if (pd == 1 && attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
@@ -357,7 +357,7 @@ public class ComputerUtilCombat {
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
if (trampleDamage > 0) {
if (attacker.hasKeyword(Keyword.INFECT)) {
if (attacker.isInfectDamage(ai)) {
poison += trampleDamage;
}
poison += predictExtraPoisonWithDamage(attacker, ai, trampleDamage);
@@ -2539,20 +2539,20 @@ public class ComputerUtilCombat {
if (combat != null) {
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
// 1. If the card that spawned the attacker was sent at a card, attack the same. Consider improving.
if (def instanceof Card && Iterables.contains(defenders, def)) {
if (((Card) def).isPlaneswalker()) {
if (def instanceof Card card && Iterables.contains(defenders, def)) {
if (card.isPlaneswalker()) {
return def;
}
if (((Card) def).isBattle()) {
if (card.isBattle()) {
return def;
}
}
// 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably.
for (GameEntity p : defenders) {
if (p instanceof Player && !ComputerUtilCard.canBeBlockedProfitably((Player)p, attacker, true)) {
if (p instanceof Player p1 && !ComputerUtilCard.canBeBlockedProfitably(p1, attacker, true)) {
return p;
}
if (p instanceof Card && !ComputerUtilCard.canBeBlockedProfitably(((Card)p).getController(), attacker, true)) {
if (p instanceof Card card && !ComputerUtilCard.canBeBlockedProfitably(card.getController(), attacker, true)) {
return p;
}
}

View File

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

View File

@@ -642,24 +642,28 @@ public class ComputerUtilMana {
List<SpellAbility> paymentList = Lists.newArrayList();
final ManaPool manapool = ai.getManaPool();
// Apply the color/type conversion matrix if necessary
manapool.restoreColorReplacements();
CardPlayOption mayPlay = sa.getMayPlayOption();
if (!effect) {
if (sa.isSpell() && mayPlay != null) {
mayPlay.applyManaConvert(manapool);
} else if (sa.isActivatedAbility() && sa.getGrantorStatic() != null && sa.getGrantorStatic().hasParam("ManaConversion")) {
AbilityUtils.applyManaColorConversion(manapool, sa.getGrantorStatic().getParam("ManaConversion"));
// Apply color/type conversion matrix if necessary (already done via autopay)
if (ai.getControllingPlayer() == null) {
manapool.restoreColorReplacements();
CardPlayOption mayPlay = sa.getMayPlayOption();
if (!effect) {
if (sa.isSpell() && mayPlay != null) {
mayPlay.applyManaConvert(manapool);
} else if (sa.isActivatedAbility() && sa.getGrantorStatic() != null && sa.getGrantorStatic().hasParam("ManaConversion")) {
AbilityUtils.applyManaColorConversion(manapool, sa.getGrantorStatic().getParam("ManaConversion"));
}
}
if (sa.hasParam("ManaConversion")) {
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
}
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
}
if (sa.hasParam("ManaConversion")) {
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
}
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
// not worth checking if it makes sense to not spend floating first
if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) {
CostPayment.handleOfferings(sa, test, cost.isPaid());
return true; // paid all from floating mana
// paid all from floating mana
return true;
}
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
@@ -1326,7 +1330,9 @@ public class ComputerUtilMana {
}
}
CostAdjustment.adjust(manaCost, sa, null, test);
if (!effect) {
CostAdjustment.adjust(manaCost, sa, null, test);
}
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
ManaCost mkCost = sa.getPayCosts().getTotalMana();

View File

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

View File

@@ -1265,8 +1265,7 @@ public class PlayerControllerAi extends PlayerController {
public boolean playSaFromPlayEffect(SpellAbility tgtSA) {
boolean optional = !tgtSA.getPayCosts().isMandatory();
boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
Spell spell = (Spell) tgtSA;
if (tgtSA instanceof Spell spell) { // Isn't it ALWAYS a spell?
// TODO if mandatory AI is only forced to use mana when it's already in the pool
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
return ComputerUtil.playStack(tgtSA, player, getGame());
@@ -1390,11 +1389,11 @@ public class PlayerControllerAi extends PlayerController {
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa);
}
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
if (source != null && source.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
// If any Conspiracies are present, try not to choose the same name twice
// (otherwise the AI will spam the same name)
for (Card consp : player.getCardsIn(ZoneType.Command)) {
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
if (consp.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
String chosenName = consp.getNamedCard();
if (!chosenName.isEmpty()) {
aiLibrary = CardLists.filter(aiLibrary, CardPredicates.nameNotEquals(chosenName));

View File

@@ -258,7 +258,7 @@ public abstract class SpellAbilityAi {
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
}
@@ -342,9 +342,9 @@ public abstract class SpellAbilityAi {
for (T ent : options) {
if (ent instanceof Player) {
hasPlayer = true;
} else if (ent instanceof Card) {
} else if (ent instanceof Card card) {
hasCard = true;
if (((Card)ent).isPlaneswalker() || ((Card)ent).isBattle()) {
if (card.isPlaneswalker() || card.isBattle()) {
hasAttackableCard = true;
}
}

View File

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

View File

@@ -1551,8 +1551,6 @@ public class AttachAi extends SpellAbilityAi {
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
}
return true;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -216,6 +216,8 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection originalList = new CardCollection(list);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ public class PermanentCreatureAi extends PermanentAi {
if (sa.isDash()) {
//only checks that the dashed creature will attack
if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat"))
if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))
return false;
if (ComputerUtilCost.canPayCost(sa.getHostCard().getSpellPermanent(), ai, false)) {
//do not dash if creature can be played normally
@@ -70,7 +70,7 @@ public class PermanentCreatureAi extends PermanentAi {
// after attacking
if (card.hasSVar("EndOfTurnLeavePlay")
&& (!ph.isPlayerTurn(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat"))) {
|| game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))) {
// AiPlayDecision.AnotherTime
return false;
}

View File

@@ -542,6 +542,8 @@ public class PumpAi extends PumpAiBase {
Card t = null;
// boolean goodt = false;
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (mandatory || ComputerUtil.activateForCost(sa, ai)) {

View File

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

View File

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

View File

@@ -206,7 +206,8 @@ public class TokenAi extends SpellAbilityAi {
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
&& game.getCombat() != null
&& !game.getCombat().getAttackers().isEmpty()
&& alwaysOnOppAttack) {
&& alwaysOnOppAttack
&& actualToken.isCreature()) {
for (Card attacker : game.getCombat().getAttackers()) {
if (CombatUtil.canBlock(attacker, actualToken)) {
return true;

View File

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

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.03</version>
</parent>
<artifactId>forge-core</artifactId>

View File

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

View File

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

View File

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

View File

@@ -645,9 +645,8 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
/** {@inheritDoc} */
@Override
public boolean equals(final Object o) {
if (o instanceof Deck) {
final DeckBase dbase = (DeckBase) o;
boolean deckBaseEquals = super.equals(dbase);
if (o instanceof DeckBase deckBase) {
boolean deckBaseEquals = super.equals(deckBase);
if (!deckBaseEquals)
return false;
// ok so far we made sure they do have the same name. Now onto comparing parts

View File

@@ -472,7 +472,8 @@ public class DeckRecognizer {
"side", "sideboard", "sb",
"main", "card", "mainboard",
"avatar", "commander", "schemes",
"conspiracy", "planes", "deck", "dungeon"};
"conspiracy", "planes", "deck", "dungeon",
"attractions", "contraptions"};
private static CharSequence[] allCardTypes(){
List<String> cardTypesList = new ArrayList<>();
@@ -671,7 +672,8 @@ public class DeckRecognizer {
return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine,
currentDeckSection, true);
// UNKNOWN card as in the Counterspell|FEM case
return Token.UnknownCard(cardName, setCode, cardCount);
unknownCardToken = Token.UnknownCard(cardName, setCode, cardCount);
continue;
}
// ok so we can simply ignore everything but card name - as set code does not exist
// At this stage, we know the card name exists in the DB so a Card MUST be found

View File

@@ -356,8 +356,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
@Override
public String getImageKey(boolean altState) {
String noramlizedName = StringUtils.stripAccents(name);
String imageKey = ImageKeys.CARD_PREFIX + noramlizedName + CardDb.NameSetSeparator
String normalizedName = StringUtils.stripAccents(name);
String imageKey = ImageKeys.CARD_PREFIX + normalizedName + CardDb.NameSetSeparator
+ edition + CardDb.NameSetSeparator + artIndex;
if (altState) {
imageKey += ImageKeys.BACKFACE_POSTFIX;

View File

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

View File

@@ -275,7 +275,7 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
@Override
public boolean equals(final Object obj) {
return (obj instanceof ItemPool) &&
(this.items.equals(((ItemPool)obj).items));
return (obj instanceof ItemPool ip) &&
(this.items.equals(ip.items));
}
}

View File

@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.03</version>
</parent>
<artifactId>forge-game</artifactId>

View File

@@ -337,9 +337,6 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
if (params.containsKey("Blessing")) {
if ("True".equalsIgnoreCase(params.get("Blessing")) != hostController.hasBlessing()) return false;
}
if (params.containsKey("MaxSpeed")) {
if ("True".equalsIgnoreCase(params.get("MaxSpeed")) != hostController.maxSpeed()) return false;
}
if (params.containsKey("DayTime")) {
if ("Day".equalsIgnoreCase(params.get("DayTime"))) {

View File

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

View File

@@ -22,6 +22,7 @@ import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.eventbus.EventBus;
import forge.GameCommand;
@@ -261,7 +262,6 @@ public class Game {
return null;
}
public void addPlayer(int id, Player player) {
playerCache.put(id, player);
}
@@ -523,7 +523,7 @@ public class Game {
* The Direction in which the turn order of this Game currently proceeds.
*/
public final Direction getTurnOrder() {
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().getAmountOfKeyword("The turn order is reversed.") % 2 == 1) {
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().isTurnOrderReversed()) {
return turnOrder.getOtherDirection();
}
return turnOrder;
@@ -1185,6 +1185,12 @@ public class Game {
for (Player player : getRegisteredPlayers()) {
player.onCleanupPhase();
}
for (final Card c : getCardsIncludePhasingIn(ZoneType.Battlefield)) {
c.onCleanupPhase(getPhaseHandler().getPlayerTurn());
}
for (final Card card : getCardsInGame()) {
card.resetActivationsPerTurn();
}
}
public void addCounterAddedThisTurn(Player putter, CounterType cType, Card card, Integer value) {
@@ -1217,13 +1223,20 @@ public class Game {
}
public int getCounterAddedThisTurn(CounterType cType, Card card) {
int result = 0;
if (!countersAddedThisTurn.containsRow(cType)) {
Set<CounterType> types = null;
if (cType == null) {
types = countersAddedThisTurn.rowKeySet();
} else if (!countersAddedThisTurn.containsRow(cType)) {
return result;
} else {
types = Sets.newHashSet(cType);
}
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(cType).values()) {
for (Pair<Card, Integer> p : l) {
if (p.getKey().equalsWithGameTimestamp(card)) {
result += p.getValue();
for (CounterType type : types) {
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(type).values()) {
for (Pair<Card, Integer> p : l) {
if (p.getKey().equalsWithGameTimestamp(card)) {
result += p.getValue();
}
}
}
}

View File

@@ -82,12 +82,6 @@ public class GameAction {
game = game0;
}
public final void resetActivationsPerTurn() {
for (final Card card : game.getCardsInGame()) {
card.resetActivationsPerTurn();
}
}
public Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer position, SpellAbility cause) {
return changeZone(zoneFrom, zoneTo, c, position, cause, null);
}
@@ -107,6 +101,8 @@ public class GameAction {
}
return c;
}
// dev mode
if (zoneFrom == null && !c.isToken()) {
zoneTo.add(c, position, CardCopyService.getLKICopy(c));
checkStaticAbilities();
@@ -314,37 +310,34 @@ public class GameAction {
c.getOwner().setCommanderReplacementSuppressed(true);
}
// in addition to actual tokens, cards "made" by digital-only mechanics
// are also added to inbound tokens so their etb replacements will work
if (zoneFrom == null || zoneFrom.is(ZoneType.None)) {
copied.getOwner().addInboundToken(copied);
}
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(copied);
repParams.put(AbilityKey.CardLKI, lastKnownInfo);
repParams.put(AbilityKey.Cause, cause);
repParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType() : null);
repParams.put(AbilityKey.Destination, zoneTo.getZoneType());
if (toBattlefield) {
repParams.put(AbilityKey.EffectOnly, true);
repParams.put(AbilityKey.CounterTable, table);
repParams.put(AbilityKey.CounterMap, table.column(copied));
}
if (params != null) {
repParams.putAll(params);
}
// in addition to actual tokens, cards "made" by digital-only mechanics
// are also added to inbound tokens so their etb replacements will work
if (zoneFrom == null || zoneFrom.is(ZoneType.None)) {
copied.getOwner().addInboundToken(copied);
}
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams);
copied.getOwner().removeInboundToken(copied);
if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) {
// reset failed manifested Cards back to original
if ((c.isManifested() || c.isCloaked()) && !c.isInPlay()) {
c.forceTurnFaceUp();
}
copied.getOwner().removeInboundToken(copied);
if (repres == ReplacementResult.Prevented) {
c.clearControllers();
cleanStaticEffect(staticEff, copied);
@@ -359,10 +352,6 @@ public class GameAction {
if (c.isInZone(ZoneType.Stack) && !zoneTo.is(ZoneType.Graveyard)) {
return moveToGraveyard(c, cause, params);
}
copied.clearDevoured();
copied.clearDelved();
copied.clearExploited();
} else if (toBattlefield && !c.isInPlay()) {
// was replaced with another Zone Change
if (c.removeChangedState()) {
@@ -379,8 +368,6 @@ public class GameAction {
copied.setGameTimestamp(game.getNextTimestamp());
}
copied.getOwner().removeInboundToken(copied);
// Aura entering as Copy from stack
// without targets it is sent to graveyard
if (copied.isAura() && !copied.isAttachedToEntity() && toBattlefield) {
@@ -432,10 +419,6 @@ public class GameAction {
}
}
if (suppress) {
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
}
if (zoneFrom != null) {
if (fromBattlefield && game.getCombat() != null) {
if (!toBattlefield) {
@@ -549,29 +532,25 @@ public class GameAction {
// order here is important so it doesn't unattach cards that might have returned from UntilHostLeavesPlay
unattachCardLeavingBattlefield(copied, c);
c.runLeavesPlayCommands();
if (copied.isTapped()) {
copied.setTapped(false); //untap card after it leaves the battlefield if needed
game.fireEvent(new GameEventCardTapped(c, false));
}
}
if (fromGraveyard) {
game.addLeftGraveyardThisTurn(lastKnownInfo);
}
// do ETB counters after zone add
if (!suppress && toBattlefield && !table.isEmpty()) {
game.getTriggerHandler().registerActiveTrigger(copied, false);
}
if (c.hasChosenColorSpire()) {
copied.setChosenColorID(ImmutableSet.copyOf(c.getChosenColorID()));
}
copied.updateStateForView();
if (fromBattlefield) {
copied.setDamage(0); //clear damage after a card leaves the battlefield
copied.setHasBeenDealtDeathtouchDamage(false);
if (copied.isTapped()) {
copied.setTapped(false); //untap card after it leaves the battlefield if needed
game.fireEvent(new GameEventCardTapped(c, false));
}
// needed for counters + ascend
if (!suppress && toBattlefield) {
game.getTriggerHandler().registerActiveTrigger(copied, false);
}
if (!table.isEmpty()) {
@@ -579,12 +558,12 @@ public class GameAction {
game.getTriggerHandler().suppressMode(TriggerType.Always);
// Need to apply any static effects to produce correct triggers
checkStaticAbilities();
// do ETB counters after zone add
table.replaceCounterEffect(game, null, true, true, params);
game.getTriggerHandler().clearSuppression(TriggerType.Always);
}
table.replaceCounterEffect(game, null, true, true, params);
// update static abilities after etb counters have been placed
game.getTriggerHandler().clearSuppression(TriggerType.Always);
checkStaticAbilities();
// 400.7g try adding keyword back into card if it doesn't already have it
@@ -607,25 +586,26 @@ public class GameAction {
c.cleanupExiledWith();
}
game.getTriggerHandler().clearActiveTriggers(copied, null);
game.getTriggerHandler().registerActiveTrigger(copied, false);
// play the change zone sound
game.fireEvent(new GameEventCardChangeZone(c, zoneFrom, zoneTo));
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(copied);
runParams.put(AbilityKey.CardLKI, lastKnownInfo);
runParams.put(AbilityKey.Cause, cause);
runParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType().name() : null);
runParams.put(AbilityKey.Destination, zoneTo.getZoneType().name());
runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek());
runParams.put(AbilityKey.MergedCards, mergedCards);
game.getTriggerHandler().clearActiveTriggers(copied, null);
game.getTriggerHandler().registerActiveTrigger(copied, false);
if (params != null) {
runParams.putAll(params);
if (!suppress) {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(copied);
runParams.put(AbilityKey.CardLKI, lastKnownInfo);
runParams.put(AbilityKey.Cause, cause);
runParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType().name() : null);
runParams.put(AbilityKey.Destination, zoneTo.getZoneType().name());
runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek());
runParams.put(AbilityKey.MergedCards, mergedCards);
if (params != null) {
runParams.putAll(params);
}
game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, true);
}
game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, true);
if (fromBattlefield && !zoneFrom.getPlayer().equals(zoneTo.getPlayer())) {
final Map<AbilityKey, Object> runParams2 = AbilityKey.mapFromCard(lastKnownInfo);
runParams2.put(AbilityKey.OriginalController, zoneFrom.getPlayer());
@@ -635,31 +615,18 @@ public class GameAction {
game.getTriggerHandler().runTrigger(TriggerType.ChangesController, runParams2, false);
}
if (suppress) {
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
}
if (zoneFrom == null) {
return copied;
}
if (!c.isRealToken() && !toBattlefield) {
copied.clearDevoured();
copied.clearDelved();
copied.clearExploited();
}
// rule 504.6: reveal a face-down card leaving the stack
if (zoneFrom != null && zoneTo != null && zoneFrom.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield) && wasFacedown) {
// CR 708.9 reveal face-down card leaving
if (wasFacedown && (fromBattlefield || (zoneFrom.is(ZoneType.Stack) && !toBattlefield))) {
Card revealLKI = CardCopyService.getLKICopy(c);
revealLKI.forceTurnFaceUp();
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card moves from the stack: ");
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card leaves the " + zoneFrom.toString() + ": ");
}
if (fromBattlefield) {
if (!c.isRealToken() && !c.isSpecialized()) {
copied.setState(CardStateName.Original, true);
}
// Soulbond unpairing
if (c.isPaired()) {
c.getPairedWith().setPairedWith(null);
@@ -680,27 +647,12 @@ public class GameAction {
}
changeZone(null, zoneTo, unmeld, position, cause, params);
}
// Reveal if face-down
if (wasFacedown) {
Card revealLKI = CardCopyService.getLKICopy(c);
revealLKI.forceTurnFaceUp();
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card leaves the battlefield: ");
copied.setState(CardStateName.Original, true);
}
} else if (toBattlefield) {
for (Player p : game.getPlayers()) {
copied.getDamageHistory().setNotAttackedSinceLastUpkeepOf(p);
copied.getDamageHistory().setNotBlockedSinceLastUpkeepOf(p);
copied.getDamageHistory().setNotBeenBlockedSinceLastUpkeepOf(p);
}
} else if (zoneTo.is(ZoneType.Graveyard)
|| zoneTo.is(ZoneType.Hand)
|| zoneTo.is(ZoneType.Library)
|| zoneTo.is(ZoneType.Exile)) {
if (copied.isFaceDown()) {
copied.setState(CardStateName.Original, true);
}
}
// Cards not on the battlefield / stack should not have controller
@@ -748,14 +700,14 @@ public class GameAction {
eff.setLayerTimestamp(timestamp);
} else {
// otherwise create effect first
eff = SpellAbilityEffect.createEffect(cause, cause.getActivatingPlayer(), name, source.getImageKey(), timestamp);
eff = SpellAbilityEffect.createEffect(cause, cause.getHostCard(), cause.getActivatingPlayer(), name, source.getImageKey(), timestamp);
eff.setRenderForUI(false);
StaticAbility stAb = eff.addStaticAbility(AbilityUtils.getSVar(cause, cause.getParam("StaticEffect")));
stAb.setActiveZone(EnumSet.of(ZoneType.Command));
// needed for ETB lookahead like Bronzehide Lion
stAb.putParam("AffectedZone", "Battlefield,Hand,Graveyard,Exile,Stack,Library,Command");
stAb.putParam("AffectedZone", "All");
SpellAbilityEffect.addForgetOnMovedTrigger(eff, "Battlefield");
game.getAction().moveToCommand(eff, cause);
eff.getOwner().getZone(ZoneType.Command).add(eff);
}
eff.addRemembered(copied);
@@ -772,7 +724,6 @@ public class GameAction {
}
}
private void storeChangesZoneAll(Card c, Zone zoneFrom, Zone zoneTo, Map<AbilityKey, Object> params) {
if (params != null && params.containsKey(AbilityKey.InternalTriggerTable)) {
((CardZoneTable) params.get(AbilityKey.InternalTriggerTable)).put(zoneFrom != null ? zoneFrom.getZoneType() : null, zoneTo.getZoneType(), c);
@@ -982,6 +933,10 @@ public class GameAction {
final PlayerZone removed = c.getOwner().getZone(ZoneType.Exile);
final Card copied = moveTo(removed, c, cause, params);
if (c.isImmutable()) {
return copied;
}
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c);
runParams.put(AbilityKey.Cause, cause);
if (origin != null) { // is generally null when adding via dev mode
@@ -1038,7 +993,8 @@ public class GameAction {
lki = CardCopyService.getLKICopy(c);
}
game.addChangeZoneLKIInfo(lki);
if (lki.isInPlay()) {
// CR 702.26k
if (lki.isInPlay() && !lki.isPhasedOut()) {
if (game.getCombat() != null) {
game.getCombat().saveLKI(lki);
game.getCombat().removeFromCombat(c);
@@ -1156,6 +1112,10 @@ public class GameAction {
// search for cards with static abilities
final FCollection<StaticAbility> staticAbilities = new FCollection<>();
final CardCollection staticList = new CardCollection();
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies = null;
if (preList.isEmpty()) {
dependencies = HashBasedTable.create();
}
game.forEachCardInGame(new Visitor<Card>() {
@Override
@@ -1163,7 +1123,7 @@ public class GameAction {
// need to get Card from preList if able
final Card co = preList.get(c);
for (StaticAbility stAb : co.getStaticAbilities()) {
if (stAb.checkMode("Continuous")) {
if (stAb.checkMode("Continuous") && stAb.zonesCheck()) {
staticAbilities.add(stAb);
}
}
@@ -1190,7 +1150,7 @@ public class GameAction {
StaticAbility stAb = staticsForLayer.get(0);
// dependency with CDA seems unlikely
if (!stAb.isCharacteristicDefining()) {
stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility);
stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility, dependencies);
}
staticsForLayer.remove(stAb);
final CardCollectionView previouslyAffected = affectedPerAbility.get(stAb);
@@ -1210,7 +1170,7 @@ public class GameAction {
if (affectedHere != null) {
for (final Card c : affectedHere) {
for (final StaticAbility st2 : c.getStaticAbilities()) {
if (!staticAbilities.contains(st2)) {
if (!staticAbilities.contains(st2) && st2.checkMode("Continuous") && st2.zonesCheck()) {
toAdd.add(st2);
st2.applyContinuousAbilityBefore(layer, preList);
}
@@ -1277,14 +1237,16 @@ public class GameAction {
game.getTriggerHandler().runTrigger(TriggerType.Always, runParams, false);
game.getTriggerHandler().runTrigger(TriggerType.Immediate, runParams, false);
game.getView().setDependencies(dependencies);
}
// Update P/T and type in the view only once after all the cards have been processed, to avoid flickering
for (Card c : affectedCards) {
c.updateNameforView();
c.updatePowerToughnessForView();
c.updatePTforView();
c.updateTypesForView();
c.updateAbilityTextForView(); // only update keywords and text for view to avoid flickering
c.updateKeywords();
}
// TODO filter out old copies from zone change
@@ -1295,7 +1257,8 @@ public class GameAction {
game.getTracker().unfreeze();
}
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility) {
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility,
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
if (staticsForLayer.size() == 1) {
return staticsForLayer.get(0);
}
@@ -1309,12 +1272,11 @@ public class GameAction {
dependencyGraph.addVertex(stAb);
boolean exists = stAb.getHostCard().getStaticAbilities().contains(stAb);
boolean compareAffected = true;
boolean compareAffected = false;
CardCollectionView affectedHere = affectedPerAbility.get(stAb);
if (affectedHere == null) {
affectedHere = StaticAbilityContinuous.getAffectedCards(stAb, preList);
} else {
compareAffected = false;
compareAffected = true;
}
List<Object> effectResults = generateStaticAbilityResult(layer, stAb);
@@ -1342,21 +1304,24 @@ public class GameAction {
// ...what it applies to...
if (!dependency && compareAffected) {
CardCollectionView affectedAfterOther = StaticAbilityContinuous.getAffectedCards(stAb, preList);
if (!Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator())) {
dependency = true;
}
dependency = !Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator());
}
// ...or what it does to any of the things it applies to
if (!dependency) {
List<Object> effectResultsAfterOther = generateStaticAbilityResult(layer, stAb);
if (!effectResults.equals(effectResultsAfterOther)) {
dependency = true;
}
dependency = !effectResults.equals(effectResultsAfterOther);
}
if (dependency) {
dependencyGraph.addVertex(otherStAb);
dependencyGraph.addEdge(stAb, otherStAb);
if (dependencies != null) {
if (dependencies.contains(stAb, otherStAb)) {
dependencies.get(stAb, otherStAb).add(layer);
} else {
dependencies.put(stAb, otherStAb, EnumSet.of(layer));
}
}
}
// undo changes and check next pair
@@ -1389,7 +1354,7 @@ public class GameAction {
}
dependencyGraph.removeAllVertices(toRemove);
// now the earlist one left is the correct choice
// now the earliest one left is the correct choice
List<StaticAbility> statics = Lists.newArrayList(dependencyGraph.vertexSet());
statics.sort(Comparator.comparing(s -> s.getHostCard().getLayerTimestamp()));
@@ -1505,14 +1470,9 @@ public class GameAction {
checkAgainCard |= stateBasedAction704_attach(c, unAttachList); // Attachment
checkAgainCard |= stateBasedAction_Contraption(c, noRegCreats);
checkAgainCard |= stateBasedAction704_5r(c); // annihilate +1/+1 counters with -1/-1 ones
checkAgainCard |= stateBasedAction704_5q(c); // annihilate +1/+1 counters with -1/-1 ones
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
if (c.getCounters(dreamType) > 7 && c.hasKeyword("CARDNAME can't have more than seven dream counters on it.")) {
c.subtractCounter(dreamType, c.getCounters(dreamType) - 7, null);
checkAgainCard = true;
}
checkAgainCard |= stateBasedAction704_5r(c);
if (c.hasKeyword("The number of loyalty counters on CARDNAME is equal to the number of Beebles you control.")) {
int beeble = CardLists.getValidCardCount(game.getCardsIn(ZoneType.Battlefield), "Beeble.YouCtrl", c.getController(), c, null);
@@ -1569,6 +1529,12 @@ public class GameAction {
}
}
// 704.5z If a player controls a permanent with start your engines! and that player has no speed, that players speed becomes 1.
if (p.getSpeed() == 0 && p.getCardsIn(ZoneType.Battlefield).anyMatch(c -> c.hasKeyword(Keyword.START_YOUR_ENGINES))) {
p.increaseSpeed();
checkAgain = true;
}
if (handlePlaneswalkerRule(p, noRegCreats)) {
checkAgain = true;
}
@@ -1578,6 +1544,7 @@ public class GameAction {
}
// 704.5m World rule
checkAgain |= handleWorldRule(noRegCreats);
// only check static abilities once after destroying all the creatures
// (e.g. helpful for Erebos's Titan and another creature dealing lethal damage to each other simultaneously)
setHoldCheckingStaticAbilities(true);
@@ -1686,11 +1653,23 @@ public class GameAction {
private boolean stateBasedAction_Battle(Card c, CardCollection removeList) {
boolean checkAgain = false;
if (!c.getType().isBattle()) {
return false;
if (!c.isBattle()) {
return checkAgain;
}
if (((c.getProtectingPlayer() == null || !c.getProtectingPlayer().isInGame()) &&
(game.getCombat() == null || game.getCombat().getAttackersOf(c).isEmpty())) ||
(c.getType().hasStringType("Siege") && c.getController().equals(c.getProtectingPlayer()))) {
Player newProtector = c.getController().getController().chooseSingleEntityForEffect(c.getController().getOpponents(), new SpellAbility.EmptySa(ApiType.ChoosePlayer, c), "Choose an opponent to protect this battle", null);
// seems unlikely unless range of influence gets implemented
if (newProtector == null) {
removeList.add(c);
} else {
c.setProtectingPlayer(newProtector);
}
checkAgain = true;
}
if (c.getCounters(CounterEnumType.DEFENSE) > 0) {
return false;
return checkAgain;
}
// 704.5v If a battle has defense 0 and it isn't the source of an ability that has triggered but not yet left the stack,
// its put into its owners graveyard.
@@ -1832,13 +1811,17 @@ public class GameAction {
return false;
}
private boolean stateBasedAction704_5r(Card c) {
private boolean stateBasedAction704_5q(Card c) {
boolean checkAgain = false;
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
int plusOneCounters = c.getCounters(p1p1);
int minusOneCounters = c.getCounters(m1m1);
if (plusOneCounters > 0 && minusOneCounters > 0) {
if (!c.canRemoveCounters(p1p1) || !c.canRemoveCounters(m1m1)) {
return checkAgain;
}
int remove = Math.min(plusOneCounters, minusOneCounters);
// If a permanent has both a +1/+1 counter and a -1/-1 counter on it,
// N +1/+1 and N -1/-1 counters are removed from it, where N is the
@@ -1850,6 +1833,26 @@ public class GameAction {
}
return checkAgain;
}
private boolean stateBasedAction704_5r(Card c) {
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
int old = c.getCounters(dreamType);
if (old <= 0) {
return false;
}
Integer max = c.getCounterMax(dreamType);
if (max == null) {
return false;
}
if (old > max) {
if (!c.canRemoveCounters(dreamType)) {
return false;
}
c.subtractCounter(dreamType, old - max, null);
return true;
}
return false;
}
// If a token is in a zone other than the battlefield, it ceases to exist.
private boolean stateBasedAction704_5d(Card c) {
@@ -1872,19 +1875,10 @@ public class GameAction {
public void checkGameOverCondition() {
// award loses as SBE
List<Player> losers = null;
FCollectionView<Player> allPlayers = game.getPlayers();
for (Player p : allPlayers) {
if (p.checkLoseCondition()) { // this will set appropriate outcomes
if (losers == null) {
losers = Lists.newArrayListWithCapacity(3);
}
losers.add(p);
}
}
GameEndReason reason = null;
List<Player> losers = null;
FCollectionView<Player> allPlayers = game.getPlayers();
// Has anyone won by spelleffect?
for (Player p : allPlayers) {
if (!p.hasWon()) {
@@ -1910,24 +1904,17 @@ public class GameAction {
break;
}
// loop through all the non-losing players that can't win
// see if all of their opponents are in that "about to lose" collection
if (losers != null) {
if (reason == null) {
for (Player p : allPlayers) {
if (losers.contains(p)) {
continue;
}
if (p.cantWin()) {
if (losers.containsAll(p.getOpponents())) {
// what to do here?!?!?!
System.err.println(p.toString() + " is about to win, but can't!");
if (p.checkLoseCondition()) { // this will set appropriate outcomes
if (losers == null) {
losers = Lists.newArrayListWithCapacity(3);
}
losers.add(p);
}
}
}
// need a separate loop here, otherwise ConcurrentModificationException is raised
if (losers != null) {
for (Player p : losers) {
game.onPlayerLost(p);
@@ -2684,8 +2671,8 @@ public class GameAction {
if (isCombat) {
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {
final GameEntity ge = et.getKey();
if (ge instanceof Card) {
((Card) ge).clearAssignedDamage();
if (ge instanceof Card c) {
c.clearAssignedDamage();
}
}
}
@@ -2705,8 +2692,7 @@ public class GameAction {
continue;
}
if (e.getKey() instanceof Card && !lethalDamage.containsKey(e.getKey())) {
Card c = (Card) e.getKey();
if (e.getKey() instanceof Card c && !lethalDamage.containsKey(c)) {
lethalDamage.put(c, c.getExcessDamageValue(false));
}

View File

@@ -183,6 +183,34 @@ public final class GameActionUtil {
flashback.setKeyword(inst);
flashback.setIntrinsic(inst.isIntrinsic());
alternatives.add(flashback);
} else if (keyword.startsWith("Harmonize")) {
if (!source.isInZone(ZoneType.Graveyard)) {
continue;
}
if (keyword.equals("Harmonize") && source.getManaCost().isNoCost()) {
continue;
}
SpellAbility harmonize = null;
if (keyword.contains(":")) {
final String[] k = keyword.split(":");
harmonize = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
String extraParams = k.length > 2 ? k[2] : "";
if (!extraParams.isEmpty()) {
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
harmonize.putParam(param.getKey(), param.getValue());
}
}
} else {
harmonize = sa.copy(activator);
}
harmonize.setAlternativeCost(AlternativeCost.Harmonize);
harmonize.getRestrictions().setZone(ZoneType.Graveyard);
harmonize.setKeyword(inst);
harmonize.setIntrinsic(inst.isIntrinsic());
alternatives.add(harmonize);
} else if (keyword.startsWith("Foretell")) {
// Foretell cast only from Exile
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() ||
@@ -582,9 +610,8 @@ public final class GameActionUtil {
" or greater>";
final Cost cost = new Cost(casualtyCost, false);
String str = "Pay for Casualty? " + cost.toSimpleString();
boolean v = pc.addKeywordCost(sa, cost, ki, str);
if (v) {
if (pc.addKeywordCost(sa, cost, ki, str)) {
if (result == null) {
result = sa.copy();
}
@@ -630,9 +657,7 @@ public final class GameActionUtil {
final Cost cost = new Cost(k[1], false);
String str = "Pay for Offspring? " + cost.toSimpleString();
boolean v = pc.addKeywordCost(sa, cost, ki, str);
if (v) {
if (pc.addKeywordCost(sa, cost, ki, str)) {
if (result == null) {
result = sa.copy();
}
@@ -679,6 +704,25 @@ public final class GameActionUtil {
}
}
if (sa.isHarmonize()) {
CardCollectionView creatures = activator.getCreaturesInPlay();
if (!creatures.isEmpty()) {
int max = Aggregates.max(creatures, Card::getNetPower);
int n = pc.chooseNumber(sa, "Choose power of creature to tap", 0, max);
final String harmonizeCost = "tapXType<1/Creature.powerEQ" + n + "/creature for Harmonize>";
final Cost cost = new Cost(harmonizeCost, false);
if (pc.addKeywordCost(sa, cost, sa.getKeyword(), "Tap creature?")) {
if (result == null) {
result = sa.copy();
}
result.getPayCosts().add(cost);
reset = true;
result.setOptionalKeywordAmount(sa.getKeyword(), n);
}
}
}
if (host.isCreature()) {
String kw = "As an additional cost to cast creature spells," +
" you may pay any amount of mana. If you do, that creature enters " +

View File

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

View File

@@ -318,11 +318,20 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return canReceiveCounters(CounterType.get(type));
}
public final void addCounter(final CounterType counterType, final int n, final Player source, GameEntityCounterTable table) {
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
if (n <= 0 || !canReceiveCounters(counterType)) {
// As per rule 107.1b
return;
}
Integer max = getCounterMax(counterType);
if (max != null) {
n = Math.min(n, max - getCounters(counterType));
if (n <= 0) {
return;
}
}
// doesn't really add counters, but is just a helper to add them to the Table
// so the Table can handle the Replacement Effect
table.put(source, this, counterType, n);
@@ -340,6 +349,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params);
}
public Integer getCounterMax(final CounterType counterType) {
return null;
}
public List<Pair<Integer, Boolean>> getDamageReceivedThisTurn() {
return damageReceivedThisTurn;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -180,26 +180,19 @@ public final class AbilityFactory {
}
public static Cost parseAbilityCost(final CardState state, Map<String, String> mapParams, AbilityRecordType type) {
Cost abCost = null;
if (type != AbilityRecordType.SubAbility) {
String cost = mapParams.get("Cost");
if (cost == null) {
if (type == AbilityRecordType.Spell) {
SpellAbility firstAbility = state.getFirstAbility();
if (firstAbility != null && firstAbility.isSpell()) {
// TODO might remove when Enchant Keyword is refactored
System.err.println(state.getName() + " already has Spell using mana cost");
}
// for a Spell if no Cost is used, use the card states ManaCost
abCost = new Cost(state.getManaCost(), false);
} else {
throw new RuntimeException("AbilityFactory : getAbility -- no Cost in " + state.getName());
}
} else {
abCost = new Cost(cost, type == AbilityRecordType.Ability);
}
if (type == AbilityRecordType.SubAbility) {
return null;
}
String cost = mapParams.get("Cost");
if (cost != null) {
return new Cost(cost, type == AbilityRecordType.Ability);
}
if (type == AbilityRecordType.Spell) {
// for a Spell if no Cost is used, use the card states ManaCost
return new Cost(state.getManaCost(), false);
} else {
throw new RuntimeException("AbilityFactory : getAbility -- no Cost in " + state.getName());
}
return abCost;
}
public static SpellAbility getAbility(AbilityRecordType type, ApiType api, Map<String, String> mapParams,

View File

@@ -38,7 +38,6 @@ public enum AbilityKey {
Causer("Causer"),
Championed("Championed"),
ClassLevel("ClassLevel"),
Cost("Cost"),
CostStack("CostStack"),
CounterAmount("CounterAmount"),
CounteredSA("CounteredSA"),

View File

@@ -14,6 +14,7 @@ import forge.game.*;
import forge.game.ability.AbilityFactory.AbilityRecordType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.IndividualCostPaymentInstance;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana;
@@ -537,6 +538,8 @@ public class AbilityUtils {
val = handlePaid(card.getEmerged(), calcX[1], card, ability);
} else if (calcX[0].startsWith("Crewed")) {
val = handlePaid(card.getCrewedByThisTurn(), calcX[1], card, ability);
} else if (calcX[0].startsWith("ChosenCard")) {
val = handlePaid(card.getChosenCards(), calcX[1], card, ability);
} else if (calcX[0].startsWith("Remembered")) {
// Add whole Remembered list to handlePaid
final CardCollection list = new CardCollection();
@@ -1362,10 +1365,8 @@ public class AbilityUtils {
}
// do blessing there before condition checks
if (source.hasKeyword(Keyword.ASCEND)) {
if (controller.getZone(ZoneType.Battlefield).size() >= 10) {
controller.setBlessing(true);
}
if (source.hasKeyword(Keyword.ASCEND) && controller.getZone(ZoneType.Battlefield).size() >= 10) {
controller.setBlessing(true);
}
if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) {
@@ -1855,6 +1856,10 @@ public class AbilityUtils {
return doXMath(list.size(), expr, c, ctb);
}
if (sq[0].equals("ActivatedThisGame")) {
return doXMath(sa.getActivationsThisGame(), expr, c, ctb);
}
if (sq[0].equals("ResolvedThisTurn")) {
return doXMath(sa.getResolvedThisTurn(), expr, c, ctb);
}
@@ -2267,6 +2272,9 @@ public class AbilityUtils {
if (sq[0].equals("Delirium")) {
return doXMath(calculateAmount(c, sq[player.hasDelirium() ? 1 : 2], ctb), expr, c, ctb);
}
if (sq[0].equals("MaxSpeed")) {
return doXMath(calculateAmount(c, sq[player.maxSpeed() ? 1 : 2], ctb), expr, c, ctb);
}
if (sq[0].equals("FatefulHour")) {
return doXMath(calculateAmount(c, sq[player.getLife() <= 5 ? 1 : 2], ctb), expr, c, ctb);
}
@@ -2839,7 +2847,13 @@ public class AbilityUtils {
final String[] workingCopy = paidparts[0].split("_");
final String validFilter = workingCopy[1];
// use objectXCount ?
return CardUtil.getThisTurnActivated(validFilter, c, ctb, player).size();
int activated = CardUtil.getThisTurnActivated(validFilter, c, ctb, player).size();
for (IndividualCostPaymentInstance i : game.costPaymentStack) {
if (i.getPayment().getAbility().isValid(validFilter, player, c, ctb)) {
activated++;
}
}
return activated;
}
// Count$ThisTurnEntered <ZoneDestination> [from <ZoneOrigin>] <Valid>
@@ -3699,6 +3713,10 @@ public class AbilityUtils {
return doXMath(amount, m, source, ctb);
}
if (value.equals("AttractionsVisitedThisTurn")) {
return doXMath(player.getAttractionsVisitedThisTurn(), m, source, ctb);
}
if (value.startsWith("PlaneswalkedToThisTurn")) {
int found = 0;
String name = value.split(" ")[1];

View File

@@ -59,7 +59,6 @@ public enum ApiType {
Cleanup (CleanUpEffect.class),
Cloak (CloakEffect.class),
Clone (CloneEffect.class),
CompanionChoose (ChooseCompanionEffect.class),
Connive (ConniveEffect.class),
CopyPermanent (CopyPermanentEffect.class),
CopySpellAbility (CopySpellAbilityEffect.class),
@@ -86,6 +85,7 @@ public enum ApiType {
Encode (EncodeEffect.class),
EndCombatPhase (EndCombatPhaseEffect.class),
EndTurn (EndTurnEffect.class),
Endure (EndureEffect.class),
ExchangeLife (LifeExchangeEffect.class),
ExchangeLifeVariant (LifeExchangeVariantEffect.class),
ExchangeControl (ControlExchangeEffect.class),
@@ -207,6 +207,7 @@ public enum ApiType {
BlankLine (BlankLineEffect.class),
DamageResolve (DamageResolveEffect.class),
ChangeZoneResolve (ChangeZoneResolveEffect.class),
CompanionChoose (CharmEffect.class),
InternalLegendaryRule (CharmEffect.class),
InternalIgnoreEffect (CharmEffect.class),
InternalRadiation (InternalRadiationEffect.class),

View File

@@ -83,8 +83,8 @@ public abstract class SpellAbilityEffect {
if ("SpellDescription".equalsIgnoreCase(stackDesc)) {
if (params.containsKey("SpellDescription")) {
String rawSDesc = params.get("SpellDescription");
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replaceAll(",,,,,,", " ");
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replaceAll(",,,", " ");
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replace(",,,,,,", " ");
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replace(",,,", " ");
String spellDesc = CardTranslation.translateSingleDescriptionText(rawSDesc, sa.getHostCard());
//trim reminder text from StackDesc
@@ -588,11 +588,10 @@ public abstract class SpellAbilityEffect {
// create a basic template for Effect to be used somewhere els
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image) {
return createEffect(sa, controller, name, image, controller.getGame().getNextTimestamp());
return createEffect(sa, sa.getHostCard(), controller, name, image, controller.getGame().getNextTimestamp());
}
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image, final long timestamp) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
public static Card createEffect(final SpellAbility sa, final Card hostCard, final Player controller, final String name, final String image, final long timestamp) {
final Game game = controller.getGame();
final Card eff = new Card(game.nextCardId(), game);
eff.setGameTimestamp(timestamp);
@@ -608,12 +607,7 @@ public abstract class SpellAbilityEffect {
eff.setRarity(hostCard.getRarity());
}
if (sa.hasParam("Boon")) {
eff.setBoon(true);
}
eff.setOwner(controller);
eff.setSVars(sa.getSVars());
eff.setSetCode(hostCard.getSetCode());
if (image != null) {
@@ -621,7 +615,12 @@ public abstract class SpellAbilityEffect {
}
eff.setGamePieceType(GamePieceType.EFFECT);
eff.setEffectSource(sa);
if (sa != null) {
eff.setEffectSource(sa);
eff.setSVars(sa.getSVars());
} else {
eff.setEffectSource(hostCard);
}
return eff;
}
@@ -1041,7 +1040,9 @@ public abstract class SpellAbilityEffect {
exilingSource = cause.getOriginalHost();
}
movedCard.setExiledWith(exilingSource);
movedCard.setExiledBy(cause.getActivatingPlayer());
Player exiler = cause.hasParam("DefinedExiler") ?
getDefinedPlayersOrTargeted(cause, "DefinedExiler").get(0) : cause.getActivatingPlayer();
movedCard.setExiledBy(exiler);
}
public static GameCommand exileEffectCommand(final Game game, final Card effect) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -121,6 +121,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
if (perpetual) {
Map <String, Object> params = new HashMap<>();
params.put("AddKeywords", keywords);
params.put("RemoveKeywords", removeKeywords);
params.put("RemoveAll", removeAll);
params.put("Timestamp", timestamp);
params.put("Category", "Keywords");

View File

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

View File

@@ -132,7 +132,6 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
source = changingTgtSA.getTargetCard();
}
Predicate<GameObject> filter = sa.hasParam("TargetRestriction") ? GameObjectPredicates.restriction(sa.getParam("TargetRestriction").split(","), activator, source, sa) : null;
// TODO Creature.Other might not work yet as it should
TargetChoices newTarget = chooser.getController().chooseNewTargetsFor(changingTgtSA, filter, false);
changingTgtSI.updateTarget(newTarget, sa.getHostCard());
}

View File

@@ -107,10 +107,8 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
final Zone originZone = game.getZoneOf(c);
// Fizzle spells so that they are removed from stack (e.g. Summary Dismissal)
if (sa.hasParam("Fizzle")) {
if (originZone.is(ZoneType.Exile) || originZone.is(ZoneType.Hand) || originZone.is(ZoneType.Stack)) {
game.getStack().remove(c);
}
if (originZone.is(ZoneType.Stack)) {
game.getStack().remove(c);
}
if (remLKI) {

View File

@@ -558,22 +558,15 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
continue;
}
if (originZone.is(ZoneType.Stack)) {
game.getStack().remove(gameCard);
}
Card movedCard = null;
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
AbilityKey.addCardZoneTableParams(moveParams, triggerList);
if (destination.equals(ZoneType.Library)) {
// If a card is moved to library from the stack, remove its spells from the stack
if (sa.hasParam("Fizzle")) {
// TODO only AI still targets as card, try to remove it
if (gameCard.isInZone(ZoneType.Exile) || gameCard.isInZone(ZoneType.Hand) || gameCard.isInZone(ZoneType.Stack)) {
// This only fizzles spells, not anything else.
game.getStack().remove(gameCard);
}
}
movedCard = game.getAction().moveToLibrary(gameCard, libraryPosition, sa, moveParams);
} else if (destination.equals(ZoneType.Battlefield)) {
if (destination.equals(ZoneType.Battlefield)) {
moveParams.put(AbilityKey.SimultaneousETB, tgtCards);
if (sa.isReplacementAbility()) {
ReplacementEffect re = sa.getReplacementEffect();
@@ -725,15 +718,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
commandCards.add(movedCard); //add to list to reveal the commandzone cards
}
// If a card is Exiled from the stack, remove its spells from the stack
if (sa.hasParam("Fizzle")) {
if (gameCard.isInZone(ZoneType.Exile) || gameCard.isInZone(ZoneType.Hand)
|| gameCard.isInZone(ZoneType.Stack) || gameCard.isInZone(ZoneType.Command)) {
// This only fizzles spells, not anything else.
game.getStack().remove(gameCard);
}
}
if (sa.hasParam("WithCountersType")) {
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
int cAmount = AbilityUtils.calculateAmount(hostCard, sa.getParamOrDefault("WithCountersAmount", "1"), sa);
@@ -1145,7 +1129,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
}
// If we're choosing multiple cards, only need to show the reveal dialog the first time through.
boolean shouldReveal = (i == 0);
Card c = null;

View File

@@ -118,16 +118,9 @@ public class ChooseCardEffect extends SpellAbilityEffect {
}
boolean dontRevealToOwner = true;
if (sa.hasParam("EachBasicType")) {
// Get all lands,
List<Card> land = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS);
String eachBasic = sa.getParam("EachBasicType");
if (eachBasic.equals("Controlled")) {
land = CardLists.filterControlledBy(land, p);
}
// Choose one of each BasicLand given special place
for (final String type : CardType.getBasicTypes()) {
final CardCollectionView cl = CardLists.getType(land, type);
final CardCollectionView cl = CardLists.getType(pChoices, type);
if (!cl.isEmpty()) {
final String prompt = Localizer.getInstance().getMessage("lblChoose") + " " + Lang.nounWithAmount(1, type);
Card c = p.getController().chooseSingleEntityForEffect(cl, sa, prompt, false, null);
@@ -138,7 +131,7 @@ public class ChooseCardEffect extends SpellAbilityEffect {
}
} else if (sa.hasParam("ChooseEach")) {
final String s = sa.getParam("ChooseEach");
final String[] types = s.equals("Party") ? new String[]{"Cleric","Thief","Warrior","Wizard"}
final String[] types = s.equals("Party") ? new String[]{"Cleric","Rogue","Warrior","Wizard"}
: s.split(" & ");
for (final String type : types) {
CardCollection valids = CardLists.filter(pChoices, CardPredicates.isType(type));
@@ -291,11 +284,9 @@ public class ChooseCardEffect extends SpellAbilityEffect {
allChosen.addAll(chosen);
}
if (sa.hasParam("Reveal") && sa.hasParam("Secretly")) {
for (final Player p : tgtPlayers) {
game.getAction().reveal(allChosen, p, true, revealTitle ?
sa.getParam("RevealTitle") : Localizer.getInstance().getMessage("lblChosenCards") + " ",
!revealTitle);
}
game.getAction().revealTo(allChosen, game.getPlayers(), revealTitle ?
sa.getParam("RevealTitle") : Localizer.getInstance().getMessage("lblChosenCards") + " ",
!revealTitle);
}
host.setChosenCards(allChosen);
if (sa.hasParam("ForgetOtherRemembered")) {

View File

@@ -90,7 +90,7 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
} else {
chosen = p.getController().chooseCardName(sa, faces, message);
}
} else {
} else {
// use CardFace because you might name a alternate names
Predicate<ICardFace> cpp = x -> true;
if (sa.hasParam("ValidCards")) {
@@ -112,8 +112,7 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
}
if (randomChoice) {
chosen = StaticData.instance().getCommonCards().streamAllFaces()
.filter(cpp).collect(StreamUtil.random()).get()
.getName();
.filter(cpp).collect(StreamUtil.random()).map(ICardFace::getName).orElse("");
} else {
chosen = p.getController().chooseCardName(sa, cpp, valid, message);
}

View File

@@ -1,12 +0,0 @@
package forge.game.ability.effects;
import forge.game.ability.SpellAbilityEffect;
import forge.game.spellability.SpellAbility;
public class ChooseCompanionEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
// This isn't a real effect. Just need it for AI choosing.
}
}

View File

@@ -122,7 +122,7 @@ public class ChooseTypeEffect extends SpellAbilityEffect {
}
}
if (validTypes.isEmpty() && sa.hasParam("Note")) {
if (validTypes.isEmpty() && sa.hasParam("TypesFromDefined")) {
// OK to end up with no choices/have nothing new to note
} else if (!validTypes.isEmpty()) {
for (final Player p : tgtPlayers) {

View File

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

View File

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

View File

@@ -63,8 +63,7 @@ public class ControlSpellEffect extends SpellAbilityEffect {
// Expand this area as it becomes needed
// Use "DefinedExchange" to Reference Object that is Exchanging the other direction
GameObject obj = Iterables.getFirst(getDefinedOrTargeted(sa, "DefinedExchange"), null);
if (obj instanceof Card) {
Card c = (Card)obj;
if (obj instanceof Card c) {
if (!c.isInPlay() || si == null) {
// Exchanging object isn't available, continue
continue;

View File

@@ -257,7 +257,7 @@ public class CounterEffect extends SpellAbilityEffect {
params.put(AbilityKey.StackSa, tgtSA);
String destination = srcSA.hasParam("Destination") ? srcSA.getParam("Destination") : tgtSA.isAftermath() ? "Exile" : "Graveyard";
String destination = srcSA.getParamOrDefault("Destination", "Graveyard");
if (srcSA.hasParam("DestinationChoice")) { //Hinder
List<String> pos = Arrays.asList(srcSA.getParam("DestinationChoice").split(","));
destination = srcSA.getActivatingPlayer().getController().chooseSomeType(Localizer.getInstance().getMessage("lblRemoveDestination"), tgtSA, pos);

View File

@@ -7,6 +7,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameEntityCounterTable;
@@ -252,8 +253,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
if (sa.hasParam("DividedRandomly")) {
CardCollection targets = new CardCollection();
for (final GameEntity obj : tgtObjects) { // check if each target is still OK
if (obj instanceof Card) {
Card tgtCard = (Card) obj;
if (obj instanceof Card tgtCard) {
Card gameCard = game.getCardState(tgtCard, null);
if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) {
tgtObjects.remove(obj);
@@ -284,8 +284,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
for (final GameEntity obj : tgtObjects) {
// check if the object is still in game or if it was moved
Card gameCard = null;
if (obj instanceof Card) {
Card tgtCard = (Card) obj;
if (obj instanceof Card tgtCard) {
gameCard = game.getCardState(tgtCard, null);
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
@@ -572,9 +571,8 @@ public class CountersPutEffect extends SpellAbilityEffect {
if (sa.isDividedAsYouChoose() && !sa.usesTargeting()) {
counterRemain = counterRemain - counterAmount;
}
} else if (obj instanceof Player) {
} else if (obj instanceof Player pl) {
// Add Counters to players!
Player pl = (Player) obj;
pl.addCounter(counterType, counterAmount, placer, table);
}
}
@@ -621,6 +619,23 @@ public class CountersPutEffect extends SpellAbilityEffect {
for (String k : keywords) {
resolvePerType(sa, placer, CounterType.getType(k), counterAmount, table, false);
}
} else if (sa.hasParam("ForColor")) {
Iterable<String> oldColors = card.getChosenColors();
CounterType counterType = null;
try {
counterType = chooseTypeFromList(sa, sa.getParam("CounterType"), null, placer.getController());
} catch (Exception e) {
System.out.println("Counter type doesn't match, nor does an SVar exist with the type name.");
return;
}
for (String color : MagicColor.Constant.ONLY_COLORS) {
card.setChosenColors(Lists.newArrayList(color));
if (sa.getOriginalParam("ChoiceTitle") != null) {
sa.getMapParams().put("ChoiceTitle", sa.getOriginalParam("ChoiceTitle").replace("chosenColor", color));
}
resolvePerType(sa, placer, counterType, counterAmount, table, true);
}
card.setChosenColors(Lists.newArrayList(oldColors));
} else {
CounterType counterType = null;
if (!sa.hasParam("EachExistingCounter") && !sa.hasParam("EachFromSource")

View File

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

View File

@@ -128,14 +128,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
String typeforPrompt = counterType == null ? "" : counterType.getName();
String title = Localizer.getInstance().getMessage("lblChooseCardsToTakeTargetCounters", typeforPrompt);
title = title.replace(" ", " ");
if (sa.hasParam("ValidSource")) {
srcCards = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("ValidSource"), activator, card, sa);
if (num.equals("Any")) {
Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", counterType);
srcCards = pc.chooseCardsForEffect(srcCards, sa, title, 0, srcCards.size(), true, params);
}
} else if (sa.hasParam("Choices") && counterType != null) {
if (sa.hasParam("Choices") && counterType != null) {
ZoneType choiceZone = sa.hasParam("ChoiceZone") ? ZoneType.smartValueOf(sa.getParam("ChoiceZone"))
: ZoneType.Battlefield;
@@ -148,7 +141,9 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
min = 0;
max = choices.size();
}
srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, null);
Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", counterType);
srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, params);
} else {
srcCards = getTargetCards(sa);
}
@@ -168,39 +163,45 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
totalRemoved += gameCard.subtractCounter(e.getKey(), e.getValue(), activator);
}
game.updateLastStateForCard(gameCard);
continue;
} else if (num.equals("All") || num.equals("Any")) {
cntToRemove = gameCard.getCounters(counterType);
}
if (type.equals("Any")) {
} else if (type.equals("Any")) {
totalRemoved += removeAnyType(gameCard, cntToRemove, sa);
} else {
if (!tgtCard.canRemoveCounters(counterType)) {
if (!gameCard.canRemoveCounters(counterType)) {
continue;
}
cntToRemove = Math.min(cntToRemove, gameCard.getCounters(counterType));
if (zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Exile)) {
if (sa.hasParam("UpTo") || num.equals("Any")) {
Map<String, Object> params = Maps.newHashMap();
params.put("Target", gameCard);
params.put("CounterType", counterType);
title = Localizer.getInstance().getMessage("lblSelectRemoveCountersNumberOfTarget", type);
cntToRemove = pc.chooseNumber(sa, title, 0, cntToRemove, params);
int removeFromCard = cntToRemove;
if (num.equals("All") || num.equals("Any")) {
removeFromCard = gameCard.getCounters(counterType);
} else {
if (sa.hasParam("CounterNumShared")) {
removeFromCard -= totalRemoved;
if (removeFromCard < 1) {
break;
}
}
removeFromCard = Math.min(removeFromCard, gameCard.getCounters(counterType));
}
if (cntToRemove > 0) {
gameCard.subtractCounter(counterType, cntToRemove, activator);
if ((zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Exile)) &&
(sa.hasParam("UpTo") || num.equals("Any"))) {
Map<String, Object> params = Maps.newHashMap();
params.put("Target", gameCard);
params.put("CounterType", counterType);
title = Localizer.getInstance().getMessage("lblSelectRemoveCountersNumberOfTarget", type);
removeFromCard = pc.chooseNumber(sa, title, 0, removeFromCard, params);
}
if (removeFromCard > 0) {
gameCard.subtractCounter(counterType, removeFromCard, activator);
if (rememberRemoved) {
for (int i = 0; i < cntToRemove; i++) {
for (int i = 0; i < removeFromCard; i++) {
// TODO might need to be more specific
card.addRemembered(Pair.of(counterType, i));
}
}
game.updateLastStateForCard(gameCard);
totalRemoved += cntToRemove;
totalRemoved += removeFromCard;
}
}
}
@@ -248,8 +249,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
if (chosenAmount > 0) {
removed += chosenAmount;
entity.subtractCounter(chosenType, chosenAmount, activator);
if (entity instanceof Card) {
Card gameCard = (Card) entity;
if (entity instanceof Card gameCard) {
game.updateLastStateForCard(gameCard);
}

View File

@@ -252,16 +252,14 @@ public class DamageDealEffect extends DamageBaseEffect {
continue;
}
}
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
final Card gc = game.getCardState(c, null);
if (gc == null || !c.equalsWithGameTimestamp(gc) || !gc.isInPlay() || gc.isPhasedOut()) {
// timestamp different or not in play
continue;
}
internalDamageDeal(sa, sourceLKI, gc, dmg, damageMap);
} else if (o instanceof Player) {
final Player p = (Player) o;
} else if (o instanceof Player p) {
damageMap.put(sourceLKI, p, dmg);
}
}

View File

@@ -95,8 +95,7 @@ public class DamageEachEffect extends DamageBaseEffect {
}
} else for (GameEntity ge : getTargetEntities(sa)) {
// check before checking sources
if (ge instanceof Card) {
final Card c = (Card) ge;
if (ge instanceof Card c) {
if (!c.isInPlay() || c.isPhasedOut()) {
continue;
}

View File

@@ -47,8 +47,7 @@ public class DamagePreventEffect extends SpellAbilityEffect {
}
final Object o = tgts.get(i);
if (o instanceof Card) {
final Card tgtC = (Card) o;
if (o instanceof Card tgtC) {
if (tgtC.isFaceDown()) {
sb.append("Morph");
} else {
@@ -104,8 +103,7 @@ public class DamagePreventEffect extends SpellAbilityEffect {
for (final GameEntity o : tgts) {
numDam = sa.usesTargeting() && sa.isDividedAsYouChoose() ? sa.getDividedValue(o) : numDam;
if (o instanceof Card) {
final Card c = (Card) o;
if (o instanceof Card c) {
if (c.isInPlay()) {
addPreventNextDamage(sa, o, numDam);
}

View File

@@ -127,7 +127,8 @@ public class DigEffect extends SpellAbilityEffect {
final boolean skipReorder = sa.hasParam("SkipReorder");
// A hack for cards like Explorer's Scope that need to ensure that a card is revealed to the player activating the ability
final boolean forceRevealToController = sa.hasParam("ForceRevealToController");
final boolean forceReveal = sa.hasParam("ForceRevealToController") ||
sa.hasParam("ForceReveal");
// These parameters are used to indicate that a dialog box must be show to the player asking if the player wants to proceed
// with an optional ability, otherwise the optional ability is skipped.
@@ -236,9 +237,12 @@ public class DigEffect extends SpellAbilityEffect {
valid = top;
}
if (forceRevealToController) {
// Force revealing the card to the player activating the ability (e.g. Explorer's Scope)
game.getAction().revealTo(top, activator);
if (forceReveal) {
// Force revealing the card to defined (e.g. Gonti, Night Minister) or the player activating the
// ability (e.g. Explorer's Scope)
Player revealTo = sa.hasParam("ForceReveal") ?
getDefinedPlayersOrTargeted(sa, "ForceReveal").get(0) : activator;
game.getAction().revealTo(top, revealTo);
delayedReveal = null; // top is already seen by the player, do not reveal twice
}
@@ -421,9 +425,6 @@ public class DigEffect extends SpellAbilityEffect {
if (sa.hasParam("Imprint")) {
host.addImprintedCard(c);
}
if (sa.hasParam("ForgetOtherRemembered")) {
host.clearRemembered();
}
if (remZone1) {
host.addRemembered(c);
}

View File

@@ -166,14 +166,6 @@ public class DiscardEffect extends SpellAbilityEffect {
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
}
if (mode.equals("NotRemembered")) {
if (!p.canDiscardBy(sa, true)) {
continue;
}
toBeDiscarded = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), "Card.IsNotRemembered", p, source, sa);
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
}
int numCards = 1;
if (sa.hasParam("NumCards")) {
numCards = AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa);

View File

@@ -161,6 +161,9 @@ public class EffectEffect extends SpellAbilityEffect {
for (Player controller : effectOwner) {
final Card eff = createEffect(sa, controller, name, image);
if (sa.hasParam("Boon")) {
eff.setBoon(true);
}
// Abilities and triggers work the same as they do for Token
// Grant abilities

View File

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

View File

@@ -303,6 +303,6 @@ public class FlipCoinEffect extends SpellAbilityEffect {
public static int getFlipMultiplier(final Player flipper) {
String str = "If you would flip a coin, instead flip two coins and ignore one.";
return 1 << flipper.getKeywords().getAmount(str);
return 1 << flipper.getAmountOfKeyword(str);
}
}

View File

@@ -1,6 +1,5 @@
package forge.game.ability.effects;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.Player;
@@ -18,6 +17,9 @@ public class GameWinEffect extends SpellAbilityEffect {
for (final Player p : getTargetPlayers(sa)) {
p.altWinBySpellEffect(card.getName());
}
// CR 104.1. A game ends immediately when a player wins
card.getGame().getAction().checkGameOverCondition();
}
}

View File

@@ -32,6 +32,7 @@ public class ManaEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
final Game game = card.getGame();
final AbilityManaPart abMana = sa.getManaPart();
final List<Player> tgtPlayers = getDefinedPlayersOrTargeted(sa);
final Player activator = sa.getActivatingPlayer();
@@ -39,10 +40,7 @@ public class ManaEffect extends SpellAbilityEffect {
// Spells are not undoable
sa.setUndoable(sa.isAbility() && sa.isUndoable() && tgtPlayers.size() < 2 && !sa.hasParam("ActivationLimit"));
final boolean optional = sa.hasParam("Optional");
final Game game = activator.getGame();
if (optional && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantAddMana"), null)) {
if (sa.hasParam("Optional") && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantAddMana"), null)) {
return;
}
@@ -53,6 +51,13 @@ public class ManaEffect extends SpellAbilityEffect {
continue;
}
final Player chooser;
if (sa.hasParam("Chooser")) {
chooser = AbilityUtils.getDefinedPlayers(card, sa.getParam("Chooser"), sa).get(0);
} else {
chooser = p;
}
if (abMana.isComboMana()) {
int amount = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(card, sa.getParam("Amount"), sa) : 1;
if(amount <= 0)
@@ -69,7 +74,7 @@ public class ManaEffect extends SpellAbilityEffect {
ColorSet fullOptions = colorOptions;
// Use specifyManaCombo if possible
if (colorsNeeded == null && amount > 1 && !sa.hasParam("TwoEach")) {
Map<Byte, Integer> choices = p.getController().specifyManaCombo(sa, colorOptions, amount, differentChoice);
Map<Byte, Integer> choices = chooser.getController().specifyManaCombo(sa, colorOptions, amount, differentChoice);
for (Map.Entry<Byte, Integer> e : choices.entrySet()) {
Byte chosenColor = e.getKey();
String choice = MagicColor.toShortString(chosenColor);
@@ -96,7 +101,7 @@ public class ManaEffect extends SpellAbilityEffect {
// just use the first possible color.
choice = colorsProduced[differentChoice ? nMana : 0];
} else {
byte chosenColor = p.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa,
byte chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa,
differentChoice && (colorsNeeded == null || colorsNeeded.length <= nMana) ? fullOptions : colorOptions);
if (chosenColor == 0)
throw new RuntimeException("ManaEffect::resolve() /*combo mana*/ - " + p + " color mana choice is empty for " + card.getName());
@@ -139,7 +144,7 @@ public class ManaEffect extends SpellAbilityEffect {
mask |= MagicColor.fromName(colorsNeeded.charAt(nChar));
}
colorMenu = mask == 0 ? ColorSet.ALL_COLORS : ColorSet.fromMask(mask);
byte val = p.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa, colorMenu);
byte val = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa, colorMenu);
if (0 == val) {
throw new RuntimeException("ManaEffect::resolve() /*any mana*/ - " + p + " color mana choice is empty for " + card.getName());
}
@@ -164,7 +169,7 @@ public class ManaEffect extends SpellAbilityEffect {
if (cs.isColorless())
continue;
if (s.isOr2Generic()) { // CR 106.8
chosenColor = p.getController().chooseColorAllowColorless(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), card, cs);
chosenColor = chooser.getController().chooseColorAllowColorless(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), card, cs);
if (chosenColor == MagicColor.COLORLESS) {
generic += 2;
continue;
@@ -173,7 +178,7 @@ public class ManaEffect extends SpellAbilityEffect {
else if (cs.isMonoColor())
chosenColor = s.getColorMask();
else /* (cs.isMulticolor()) */ {
chosenColor = p.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
}
sb.append(MagicColor.toShortString(chosenColor));
sb.append(' ');
@@ -221,7 +226,7 @@ public class ManaEffect extends SpellAbilityEffect {
if (cs.isMonoColor())
sb.append(MagicColor.toShortString(s.getColorMask()));
else /* (cs.isMulticolor()) */ {
byte chosenColor = p.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
byte chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
sb.append(MagicColor.toShortString(chosenColor));
}
}

View File

@@ -17,24 +17,25 @@ public class ManifestDreadEffect extends ManifestEffect {
final Game game = p.getGame();
for (int i = 0; i < amount; i++) {
CardCollection tgtCards = p.getTopXCardsFromLibrary(2);
Card manifest = null;
Card toGrave = null;
CardCollection toGrave = new CardCollection();
if (!tgtCards.isEmpty()) {
manifest = p.getController().chooseSingleEntityForEffect(tgtCards, sa, getDefaultMessage(), null);
Card manifest = p.getController().chooseSingleEntityForEffect(tgtCards, sa, getDefaultMessage(), null);
tgtCards.remove(manifest);
toGrave = tgtCards.isEmpty() ? null : tgtCards.getFirst();
// CR 701.34d If an effect instructs a player to manifest multiple cards from their library, those cards are manifested one at a time.
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
CardZoneTable triggerList = AbilityKey.addCardZoneTableParams(moveParams, sa);
internalEffect(manifest, p, sa, moveParams);
if (toGrave != null) {
toGrave = game.getAction().moveToGraveyard(toGrave, sa, moveParams);
manifest = internalEffect(manifest, p, sa, moveParams);
// CR 701.60a
if (!manifest.isManifested()) {
tgtCards.add(manifest);
}
for (Card c : tgtCards) {
toGrave.add(game.getAction().moveToGraveyard(c, sa, moveParams));
}
triggerList.triggerChangesZoneAll(game, sa);
}
Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p);
runParams.put(AbilityKey.Card, toGrave);
runParams.put(AbilityKey.Cards, toGrave);
game.getTriggerHandler().runTrigger(TriggerType.ManifestDread, runParams, true);
}
}

View File

@@ -361,7 +361,6 @@ public class PlayEffect extends SpellAbilityEffect {
continue;
}
boolean unpayableCost = tgtSA.getPayCosts().getCostMana().getMana().isNoCost();
if (sa.hasParam("WithoutManaCost")) {
tgtSA = tgtSA.copyWithNoManaCost();
} else if (sa.hasParam("PlayCost")) {
@@ -380,7 +379,8 @@ public class PlayEffect extends SpellAbilityEffect {
}
tgtSA = tgtSA.copyWithManaCostReplaced(tgtSA.getActivatingPlayer(), abCost);
} else if (unpayableCost) {
} else if (tgtSA.getPayCosts().hasManaCost() && tgtSA.getPayCosts().getCostMana().getMana().isNoCost()) {
// unpayable
continue;
}

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