Compare commits

..

341 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
2789 changed files with 33471 additions and 26262 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

@@ -122,3 +122,11 @@ jobs:
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

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

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

@@ -2891,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));
}
/**
@@ -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);

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

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

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

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

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

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

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

@@ -671,11 +671,9 @@ public class DeckRecognizer {
// ok so the card has been found - let's see if there's any restriction on the set
return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine,
currentDeckSection, true);
// On the off chance we accidentally interpreted part of the card's name as a set code, e.g. "Tyrranax Rex"
if (data.isMTGCard(cardName + " " + setCode))
continue;
// 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

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

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

@@ -1272,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);
@@ -1355,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()));
@@ -1471,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);
@@ -1535,7 +1529,7 @@ 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. See rule 702.179, “Start Your Engines!”
// 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;
@@ -1550,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);
@@ -1658,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.
@@ -1804,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
@@ -1822,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) {

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

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

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

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

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

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

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

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

@@ -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;
@@ -618,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;
}
}
}

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

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

View File

@@ -71,7 +71,6 @@ public class RepeatEachEffect extends SpellAbilityEffect {
else if (sa.hasParam("DefinedCards")) {
repeatCards = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa);
}
boolean loopOverCards = repeatCards != null && !repeatCards.isEmpty();
if (sa.hasParam("ClearRemembered")) {
source.clearRemembered();
@@ -89,7 +88,7 @@ public class RepeatEachEffect extends SpellAbilityEffect {
sa.setLoseLifeMap(Maps.newHashMap());
}
if (loopOverCards) {
if (repeatCards != null && !repeatCards.isEmpty()) {
if (sa.hasParam("ChooseOrder") && repeatCards.size() > 1) {
final Player chooser = sa.getParam("ChooseOrder").equals("True") ? activator :
AbilityUtils.getDefinedPlayers(source, sa.getParam("ChooseOrder"), sa).get(0);

View File

@@ -1,17 +1,10 @@
package forge.game.ability.effects;
import java.util.Map;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantSetSchemesInMotion;
import forge.game.trigger.TriggerType;
public class SetInMotionEffect extends SpellAbilityEffect {
@@ -25,25 +18,13 @@ public class SetInMotionEffect extends SpellAbilityEffect {
boolean again = sa.hasParam("Again");
int repeats = 1;
if (sa.hasParam("RepeatNum")) {
repeats = AbilityUtils.calculateAmount(source, sa.getParam("RepeatNum"), sa);
}
for (int i = 0; i < repeats; i++) {
if (again) {
// Set the current scheme in motion again
Game game = controller.getGame();
if (StaticAbilityCantSetSchemesInMotion.any(game)) {
return;
}
game.getAction().moveToCommand(controller.getActiveScheme(), sa);
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Scheme, controller.getActiveScheme());
game.getTriggerHandler().runTrigger(TriggerType.SetInMotion, runParams, false);
controller.setSchemeInMotion(sa, controller.getActiveScheme());
} else {
controller.setSchemeInMotion(sa);
}

View File

@@ -9,6 +9,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.*;
import forge.game.event.GameEventCardStatsChanged;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.trigger.TriggerHandler;
@@ -53,7 +54,6 @@ public class SetStateEffect extends SpellAbilityEffect {
final Game game = host.getGame();
final boolean remChanged = sa.hasParam("RememberChanged");
final boolean hiddenAgenda = sa.hasParam("HiddenAgenda");
final boolean optional = sa.hasParam("Optional");
final CardCollection transformedCards = new CardCollection();
@@ -194,14 +194,12 @@ public class SetStateEffect extends SpellAbilityEffect {
} else if (sa.isCloakUp()) {
String sb = p + " has uncloaked " + gameCard.getName();
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
} else if (hiddenAgenda) {
if (gameCard.hasKeyword("Double agenda")) {
String sb = p + " has revealed " + gameCard.getName() + " with the chosen names: " + gameCard.getNamedCards();
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
} else {
String sb = p + " has revealed " + gameCard.getName() + " with the chosen name " + gameCard.getNamedCard();
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
}
} else if (sa.isKeyword(Keyword.DOUBLE_AGENDA)) {
String sb = p + " has revealed " + gameCard.getName() + " with the chosen names: " + gameCard.getNamedCards();
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
} else if (sa.isKeyword(Keyword.HIDDEN_AGENDA)) {
String sb = p + " has revealed " + gameCard.getName() + " with the chosen name " + gameCard.getNamedCard();
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
}
game.fireEvent(new GameEventCardStatsChanged(gameCard));
if (sa.hasParam("Mega")) { // TODO move Megamorph into an Replacement Effect

View File

@@ -84,7 +84,7 @@ public class TapOrUntapAllEffect extends SpellAbilityEffect {
if (toTap) {
if (gameCard.tap(true, sa, activator)) tapped.add(gameCard);
} else {
if (gameCard.untap(true)) untapped.add(gameCard);
if (gameCard.untap()) untapped.add(gameCard);
}
}
if (!tapped.isEmpty()) {

View File

@@ -65,7 +65,7 @@ public class TapOrUntapEffect extends SpellAbilityEffect {
!gameCard.getController().equals(tapper));
if (tap) {
if (gameCard.tap(true, sa, tapper)) tapped.add(gameCard);
} else if (gameCard.untap(true)) {
} else if (gameCard.untap()) {
untapMap.computeIfAbsent(tapper, i -> new CardCollection()).add(gameCard);
}
}

View File

@@ -25,7 +25,7 @@ public class UnattachEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) {
final Game game = sa.getHostCard().getGame();
for (final Card tgtC : getTargetCards(sa)) {
if (tgtC.isInPlay()) {
if (!tgtC.isInPlay()) {
continue;
}
// check if the object is still in game or if it was moved

View File

@@ -44,7 +44,7 @@ public class UntapAllEffect extends SpellAbilityEffect {
if (sa.hasParam("ControllerUntaps")) {
untapper = c.getController();
}
if (c.untap(true)) {
if (c.untap()) {
untapMap.computeIfAbsent(untapper, i -> new CardCollection()).add(c);
if (sa.hasParam("RememberUntapped")) card.addRemembered(c);

View File

@@ -66,7 +66,7 @@ public class UntapEffect extends SpellAbilityEffect {
if (gameCard == null || !tgtC.equalsWithGameTimestamp(gameCard)) {
continue;
}
if (gameCard.untap(true)) untapped.add(gameCard);
if (gameCard.untap()) untapped.add(gameCard);
}
if (etb) {
// do not fire triggers
@@ -114,7 +114,7 @@ public class UntapEffect extends SpellAbilityEffect {
final CardCollectionView selected = p.getController().chooseCardsForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectCardToUntap"), mandatory ? num : 0, num, !mandatory, null);
if (selected != null) {
for (final Card c : selected) {
if (c.untap(true)) untapped.add(c);
if (c.untap()) untapped.add(c);
}
}
if (!untapped.isEmpty()) {

View File

@@ -188,7 +188,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
private boolean startsGameInPlay = false;
private boolean drawnThisTurn = false;
private boolean foughtThisTurn = false;
private boolean becameTargetThisTurn, valiant = false;
private boolean enlistedThisCombat = false;
private boolean startedTheTurnUntapped = false;
private boolean cameUnderControlSinceLastUpkeep = true; // for Echo
@@ -250,6 +249,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
private int exertThisTurn = 0;
private PlayerCollection exertedByPlayer = new PlayerCollection();
private PlayerCollection targetedFromThisTurn = new PlayerCollection();
private long bestowTimestamp = -1;
private long transformedTimestamp = 0;
private long prototypeTimestamp = -1;
@@ -1075,7 +1076,22 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public final boolean isAdventureCard() {
return hasState(CardStateName.Adventure);
if (!hasState(CardStateName.Secondary))
return false;
return getState(CardStateName.Secondary).getType().hasSubtype("Adventure");
}
public final boolean isOnAdventure() {
if (!isAdventureCard())
return false;
if (getExiledWith() == null)
return false;
if (!CardStateName.Secondary.equals(getExiledWith().getCurrentStateName()))
return false;
if (!getExiledWith().getType().hasSubtype("Adventure")) {
return false;
}
return true;
}
public final boolean isBackSide() {
@@ -1689,19 +1705,32 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return true;
}
@Override
public Integer getCounterMax(final CounterType counterType) {
if (counterType.is(CounterEnumType.DREAM)) {
return StaticAbilityMaxCounter.maxCounter(this, counterType);
}
return null;
}
@Override
public void addCounterInternal(final CounterType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
int addAmount = n;
// Rules say it is only a SBA, but is it checked there too?
if (counterType.is(CounterEnumType.DREAM) && hasKeyword("CARDNAME can't have more than seven dream counters on it.")) {
addAmount = Math.min(addAmount, 7 - getCounters(CounterEnumType.DREAM));
}
if (addAmount <= 0 || !canReceiveCounters(counterType)) {
// As per rule 107.1b
// CR 107.1b
return;
}
final int oldValue = getCounters(counterType);
Integer max = getCounterMax(counterType);
if (max != null) {
addAmount = Math.min(addAmount, max - oldValue);
if (addAmount <= 0) {
return;
}
}
final int newValue = addAmount + oldValue;
if (fireEvents) {
getGame().updateLastStateForCard(this);
@@ -2590,7 +2619,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing:")
|| keyword.startsWith("Afterlife") || keyword.startsWith("Hideaway") || keyword.startsWith("Toxic")
|| keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")) {
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")
|| keyword.startsWith("Mobilize")) {
final String[] k = keyword.split(":");
sbLong.append(k[0]).append(" ").append(k[1]).append(" (").append(inst.getReminderText()).append(")");
} else if (keyword.startsWith("Crew")) {
@@ -3014,7 +3044,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
// add Adventure to AbilityText
if (sa.isAdventure() && state.getStateName().equals(CardStateName.Original)) {
CardState advState = getState(CardStateName.Adventure);
CardState advState = getState(CardStateName.Secondary);
StringBuilder sbSA = new StringBuilder();
sbSA.append(Localizer.getInstance().getMessage("lblAdventure"));
sbSA.append("").append(CardTranslation.getTranslatedName(advState.getName()));
@@ -3022,6 +3052,15 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
sbSA.append(": ");
sbSA.append(sAbility);
sAbility = sbSA.toString();
} else if (sa.isOmen() && state.getStateName().equals(CardStateName.Original)) {
CardState advState = getState(CardStateName.Secondary);
StringBuilder sbSA = new StringBuilder();
sbSA.append(Localizer.getInstance().getMessage("lblOmen"));
sbSA.append("").append(CardTranslation.getTranslatedName(advState.getName()));
sbSA.append(" ").append(sa.getPayCosts().toSimpleString());
sbSA.append(": ");
sbSA.append(sAbility);
sAbility = sbSA.toString();
} else if (sa.isSpell() && sa.isBasicSpell()) {
continue;
} else if (sa.hasParam("DescriptionFromChosenName") && !getNamedCard().isEmpty()) {
@@ -3561,9 +3600,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
}
} else {
// Adenture may only be cast not from Battlefield
if (isAdventureCard() && state.getView().getState() == CardStateName.Original) {
for (SpellAbility sa : getState(CardStateName.Adventure).getSpellAbilities()) {
// Adventure and Omen may only be cast not from Battlefield
if (hasState(CardStateName.Secondary) && state.getView().getState() == CardStateName.Original) {
for (SpellAbility sa : getState(CardStateName.Secondary).getSpellAbilities()) {
if (mana == null || mana == sa.isManaAbility()) {
list.add(sa);
}
@@ -3820,16 +3859,13 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public boolean hasBecomeTargetThisTurn() {
return becameTargetThisTurn;
return !targetedFromThisTurn.isEmpty();
}
public void setBecameTargetThisTurn(boolean becameTargetThisTurn0) {
becameTargetThisTurn = becameTargetThisTurn0;
public void addTargetFromThisTurn(Player p) {
targetedFromThisTurn.add(p);
}
public boolean isValiant() {
return valiant;
}
public void setValiant(boolean v) {
valiant = v;
public boolean isValiant(Player p) {
return getController().equals(p) && !targetedFromThisTurn.contains(p);
}
public boolean hasStartedTheTurnUntapped() {
@@ -4898,10 +4934,31 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return true;
}
public final boolean untap(boolean untapAnimation) {
if (!tapped) { return false; }
public final boolean canUntap(Player phase, boolean predict) {
if (!predict && !tapped) { return false; }
if (phase != null && isExertedBy(phase)) {
return false;
}
if (phase != null && hasKeyword("This card doesn't untap during your next untap step.")) {
return false;
}
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(this);
runParams.put(AbilityKey.Player, phase);
return !getGame().getReplacementHandler().cantHappenCheck(ReplacementType.Untap, runParams);
}
if (getGame().getReplacementHandler().run(ReplacementType.Untap, AbilityKey.mapFromAffected(this)) != ReplacementResult.NotReplaced) {
public final boolean untap() {
return untap(null);
}
public final boolean untap(Player phase) {
if (!tapped) { return false; }
if (phase != null && isExertedBy(phase)) {
return false;
}
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(this);
runParams.put(AbilityKey.Player, phase);
if (getGame().getReplacementHandler().run(ReplacementType.Untap, runParams) != ReplacementResult.NotReplaced) {
return false;
}
@@ -4909,7 +4966,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
runUntapCommands();
setTapped(false);
view.updateNeedsUntapAnimation(untapAnimation);
view.updateNeedsUntapAnimation(true);
getGame().fireEvent(new GameEventCardTapped(this, false));
return true;
}
@@ -5518,10 +5575,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (StringUtils.isNumeric(s)) {
count += Integer.parseInt(s);
} else {
StaticAbility st = inst.getStatic();
// TODO make keywordinterface inherit from CardTrait somehow, or invent new interface
if (st != null && st.hasSVar(s)) {
count += AbilityUtils.calculateAmount(this, st.getSVar(s), null);
if (inst.hasSVar(s)) {
count += AbilityUtils.calculateAmount(this, inst.getSVar(s), null);
} else {
String svar = StringUtils.join(parse);
if (state.hasSVar(svar)) {
@@ -6550,7 +6606,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
public void removeExertedBy(final Player player) {
exertedByPlayer.remove(player);
view.updateExertedThisTurn(this, getExertedThisTurn() > 0);
// removeExertedBy is called on Untap phase, where it can't be exerted yet
}
protected void resetExertedThisTurn() {
@@ -7355,8 +7411,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
resetExcessDamage();
setRegeneratedThisTurn(0);
resetShieldCount();
setBecameTargetThisTurn(false);
setValiant(false);
targetedFromThisTurn.clear();
setFoughtThisTurn(false);
turnedFaceUpThisTurn = false;
clearMustBlockCards();
@@ -7577,9 +7632,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
final List<SpellAbility> abilities = Lists.newArrayList();
for (SpellAbility sa : getSpellAbilities()) {
//adventure spell check
if (isAdventureCard() && sa.isAdventure()) {
if (getExiledWith() != null && getExiledWith().equals(this) && CardStateName.Adventure.equals(getExiledWith().getCurrentStateName()))
continue; // skip since it's already on adventure
if (isAdventureCard() && sa.isAdventure() && isOnAdventure()) {
continue; // skip since it's already on adventure
}
//add alternative costs as additional spell abilities
abilities.add(sa);
@@ -7591,6 +7645,14 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player, false));
}
}
if (isFaceDown() && isInZone(ZoneType.Command)) {
for (KeywordInterface k : oState.getCachedKeyword(Keyword.HIDDEN_AGENDA)) {
abilities.addAll(k.getAbilities());
}
for (KeywordInterface k : oState.getCachedKeyword(Keyword.DOUBLE_AGENDA)) {
abilities.addAll(k.getAbilities());
}
}
// Add Modal Spells
if (isModal() && hasState(CardStateName.Modal)) {
for (SpellAbility sa : getState(CardStateName.Modal).getSpellAbilities()) {
@@ -8127,12 +8189,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public boolean isWitherDamage() {
if (this.hasKeyword(Keyword.WITHER) || this.hasKeyword(Keyword.INFECT)) {
if (hasKeyword(Keyword.WITHER) || hasKeyword(Keyword.INFECT)) {
return true;
}
return StaticAbilityWitherDamage.isWitherDamage(this);
}
public boolean isInfectDamage(Player target) {
return hasKeyword(Keyword.INFECT) || StaticAbilityInfectDamage.isInfectDamage(target);
}
public Set<CardStateName> getUnlockedRooms() {
return this.unlockedRooms;
}
@@ -8209,9 +8275,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (!isRoom()) {
return;
}
if (!isInPlay()) {
return;
}
if (isFaceDown()) {
return;
}

View File

@@ -252,10 +252,10 @@ public class CardCopyService {
newCopy.getState(CardStateName.Original).copyFrom(copyFrom.getState(CardStateName.Original), true);
newCopy.addAlternateState(CardStateName.Transformed, false);
newCopy.getState(CardStateName.Transformed).copyFrom(copyFrom.getState(CardStateName.Transformed), true);
} else if (copyFrom.isAdventureCard()) {
} else if (copyFrom.hasState(CardStateName.Secondary)) {
newCopy.getState(CardStateName.Original).copyFrom(copyFrom.getState(CardStateName.Original), true);
newCopy.addAlternateState(CardStateName.Adventure, false);
newCopy.getState(CardStateName.Adventure).copyFrom(copyFrom.getState(CardStateName.Adventure), true);
newCopy.addAlternateState(CardStateName.Secondary, false);
newCopy.getState(CardStateName.Secondary).copyFrom(copyFrom.getState(CardStateName.Secondary), true);
} else if (copyFrom.isSplitCard()) {
newCopy.getState(CardStateName.Original).copyFrom(copyFrom.getState(CardStateName.Original), true);
newCopy.addAlternateState(CardStateName.LeftSplit, false);

View File

@@ -211,8 +211,8 @@ public class CardFactory {
c.setRarity(cp.getRarity());
c.setState(CardStateName.RightSplit, false);
c.setImageKey(originalPicture);
} else if (c.isAdventureCard()) {
c.setState(CardStateName.Adventure, false);
} else if (c.hasState(CardStateName.Secondary)) {
c.setState(CardStateName.Secondary, false);
c.setImageKey(originalPicture);
} else if (c.canSpecialize()) {
c.setState(CardStateName.SpecializeW, false);
@@ -281,9 +281,6 @@ public class CardFactory {
} else if (state != CardStateName.Original) {
CardFactoryUtil.setupKeywordedAbilities(card);
}
if (state == CardStateName.Adventure) {
CardFactoryUtil.setupAdventureAbility(card);
}
}
card.setState(CardStateName.Original, false);
@@ -564,14 +561,14 @@ public class CardFactory {
final CardState ret2 = new CardState(out, CardStateName.Flipped);
ret2.copyFrom(in.getState(CardStateName.Flipped), false, sa);
result.put(CardStateName.Flipped, ret2);
} else if (in.isAdventureCard()) {
} else if (in.hasState(CardStateName.Secondary)) {
final CardState ret1 = new CardState(out, CardStateName.Original);
ret1.copyFrom(in.getState(CardStateName.Original), false, sa);
result.put(CardStateName.Original, ret1);
final CardState ret2 = new CardState(out, CardStateName.Adventure);
ret2.copyFrom(in.getState(CardStateName.Adventure), false, sa);
result.put(CardStateName.Adventure, ret2);
final CardState ret2 = new CardState(out, CardStateName.Secondary);
ret2.copyFrom(in.getState(CardStateName.Secondary), false, sa);
result.put(CardStateName.Secondary, ret2);
} else if (in.isTransformable() && sa instanceof SpellAbility && (
ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi()) ||
ApiType.CopySpellAbility.equals(((SpellAbility)sa).getApi()) ||

View File

@@ -215,7 +215,7 @@ public class CardFactoryUtil {
return manifestUp;
}
public static boolean handleHiddenAgenda(Player player, Card card) {
public static boolean handleHiddenAgenda(Player player, Card card, KeywordInterface ki) {
SpellAbility sa = new SpellAbility.EmptySa(card);
sa.putParam("AILogic", card.getSVar("AgendaLogic"));
Predicate<ICardFace> cpp = x -> true;
@@ -228,7 +228,7 @@ public class CardFactoryUtil {
}
card.addNamedCard(name);
if (card.hasKeyword("Double agenda")) {
if (ki.getKeyword().equals(Keyword.DOUBLE_AGENDA)) {
String name2 = player.getController().chooseCardName(sa, cpp, "Card.!NamedCard",
"Name a second card for " + card.getName());
if (name2 == null || name2.isEmpty()) {
@@ -239,14 +239,14 @@ public class CardFactoryUtil {
card.turnFaceDown();
card.addMayLookAt(player.getGame().getNextTimestamp(), ImmutableList.of(player));
card.addSpellAbility(abilityRevealHiddenAgenda(card));
ki.addSpellAbility(abilityRevealHiddenAgenda(card));
return true;
}
private static SpellAbility abilityRevealHiddenAgenda(final Card sourceCard) {
String ab = "ST$ SetState | Cost$ 0"
+ " | ConditionDefined$ Self | ConditionPresent$ Card.faceDown+inZoneCommand"
+ " | HiddenAgenda$ True"
+ " | PresentDefined$ Self | IsPresent$ Card.faceDown+inZoneCommand"
+ " | ActivationZone$ Command | Secondary$ True"
+ " | Mode$ TurnFaceUp | SpellDescription$ Reveal this Hidden Agenda at any time.";
return AbilityFactory.getAbility(ab, sourceCard);
}
@@ -519,14 +519,6 @@ public class CardFactoryUtil {
return filteredkw;
}
public static int getCardTypesFromList(final CardCollectionView list) {
EnumSet<CardType.CoreType> types = EnumSet.noneOf(CardType.CoreType.class);
for (Card c1 : list) {
c1.getType().getCoreTypes().forEach(types::add);
}
return types.size();
}
/**
* Adds the ability factory abilities.
*
@@ -1050,7 +1042,7 @@ public class CardFactoryUtil {
inst.addTrigger(dethroneTrigger);
} else if (keyword.equals("Double team")) {
final String trigString = "Mode$ Attacks | ValidCard$ Card.Self+nonToken | TriggerZones$ Battlefield" +
final String trigString = "Mode$ Attacks | ValidCard$ Card.Self+!token | TriggerZones$ Battlefield" +
" | Secondary$ True | TriggerDescription$ Double team (" + inst.getReminderText() + ")";
final String maSt = "DB$ MakeCard | DefinedName$ Self | Zone$ Hand | RememberMade$ True | Conjure$ True";
final String puSt = "DB$ Pump | RememberObjects$ Self";
@@ -1075,8 +1067,7 @@ public class CardFactoryUtil {
" | IsPresent$ Card.Self+cameUnderControlSinceLastUpkeep | Secondary$ True | " +
"TriggerDescription$ " + inst.getReminderText();
String effect = "DB$ Sacrifice | SacValid$ Self | "
+ "Echo$ " + cost;
String effect = "DB$ Sacrifice | SacValid$ Self | Echo$ " + cost;
final Trigger trigger = TriggerHandler.parseTrigger(upkeepTrig, card, intrinsic);
trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
@@ -1534,6 +1525,20 @@ public class CardFactoryUtil {
triggerDrawn.setOverridingAbility(revealSA);
inst.addTrigger(triggerDrawn);
} else if (keyword.startsWith("Mobilize")) {
final String[] k = keyword.split(":");
final String n = k[1];
final String trigStr = "Mode$ Attacks | ValidCard$ Card.Self | Secondary$ True"
+ " | TriggerDescription$ Mobilize " + n + " (" + inst.getReminderText() + ")";
final String effect = "DB$ Token | TokenAmount$ " + n + " | TokenScript$ r_1_1_warrior"
+ " | TokenTapped$ True | TokenAttacking$ True | AtEOT$ Sacrifice";
final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);
trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
inst.addTrigger(trigger);
} else if (keyword.startsWith("Modular")) {
final String abStr = "DB$ PutCounter | ValidTgts$ Artifact.Creature | " +
"TgtPrompt$ Select target artifact creature | CounterType$ P1P1 | CounterNum$ ModularX";
@@ -1706,8 +1711,7 @@ public class CardFactoryUtil {
+ " | IsPresent$ Card.Self+!IsRenowned | CombatDamage$ True | Secondary$ True"
+ " | TriggerDescription$ Renown " + k[1] +" (" + inst.getReminderText() + ")";
final String effect = "DB$ PutCounter | Defined$ Self | "
+ "CounterType$ P1P1 | CounterNum$ " + k[1];
final String effect = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ " + k[1];
final Trigger parsedTrigger = TriggerHandler.parseTrigger(renownTrig, card, intrinsic);
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
@@ -2308,6 +2312,40 @@ public class CardFactoryUtil {
re.setOverridingAbility(saExile);
inst.addReplacement(re);
} else if (keyword.startsWith("Harmonize")) {
StringBuilder sb = new StringBuilder();
sb.append("Event$ Moved | ValidCard$ Card.Self | Origin$ Stack | ExcludeDestination$ Exile ");
sb.append("| ValidStackSa$ Spell.Harmonize+castKeyword | Description$ Harmonize");
if (keyword.contains(":")) {
final String[] k = keyword.split(":");
final Cost cost = new Cost(k[1], false);
sb.append(cost.isOnlyManaCost() ? " " : "").append(cost.toSimpleString());
sb.append(cost.isOnlyManaCost() ? "" : ".");
String extraDesc = k.length > 3 ? k[3] : "";
if (!extraDesc.isEmpty()) { // extra params added in GameActionUtil, desc added here
sb.append(cost.isOnlyManaCost() ? ". " : " ").append(extraDesc);
}
}
sb.append(" (").append(inst.getReminderText()).append(")");
String repeffstr = sb.toString();
String abExile = "DB$ ChangeZone | Defined$ Self | Origin$ Stack | Destination$ Exile";
SpellAbility saExile = AbilityFactory.getAbility(abExile, card);
if (!intrinsic) {
saExile.setIntrinsic(false);
}
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, host, intrinsic, card);
re.setOverridingAbility(saExile);
inst.addReplacement(re);
} else if (keyword.startsWith("Graft")) {
final String[] k = keyword.split(":");
@@ -3596,8 +3634,7 @@ public class CardFactoryUtil {
newSA.setAlternativeCost(AlternativeCost.Surge);
String desc = "Surge " + surgeCost.toSimpleString() + " (" + inst.getReminderText()
+ ")";
String desc = "Surge " + surgeCost.toSimpleString() + " (" + inst.getReminderText() + ")";
newSA.setDescription(desc);
newSA.setIntrinsic(intrinsic);
@@ -3959,6 +3996,12 @@ public class CardFactoryUtil {
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutFlying+withoutReach | Secondary$ True" +
" | Description$ Flying (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Harmonize")) {
String reduceEffect = "Mode$ ReduceCost | ValidCard$ Card.Self | ValidSpell$ Spell.Harmonize | Secondary$ True"
+ " | Amount$ AffectedX | EffectZone$ All | Description$ Harmonize (" + inst.getReminderText() + ")";
StaticAbility stAb = StaticAbility.create(reduceEffect, state.getCard(), state, intrinsic);
stAb.setSVar("AffectedX", "Count$OptionalKeywordAmount");
inst.addStaticAbility(stAb);
} else if (keyword.startsWith("Hexproof")) {
final StringBuilder sbDesc = new StringBuilder("Hexproof");
final StringBuilder sbValid = new StringBuilder();
@@ -4109,16 +4152,7 @@ public class CardFactoryUtil {
card.addTrigger(defeatedTrigger);
}
public static void setupAdventureAbility(Card card) {
if (card.getCurrentStateName() != CardStateName.Adventure) {
return;
}
SpellAbility sa = card.getFirstSpellAbility();
if (sa == null) {
return;
}
sa.setCardState(card.getCurrentState());
public static ReplacementEffect setupAdventureAbility(CardState card) {
StringBuilder sb = new StringBuilder();
sb.append("Event$ Moved | ValidCard$ Card.Self | Origin$ Stack | ExcludeDestination$ Exile ");
sb.append("| ValidStackSa$ Spell.Adventure | Fizzle$ False | Secondary$ True | Description$ Adventure");
@@ -4129,20 +4163,37 @@ public class CardFactoryUtil {
SpellAbility saExile = AbilityFactory.getAbility(abExile, card);
String abEffect = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell+nonToken";
String abEffect = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell+!token";
AbilitySub saEffect = (AbilitySub)AbilityFactory.getAbility(abEffect, card);
StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonAdventure");
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+!Adventure");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
saEffect.setSVar("Play", sbPlay.toString());
saExile.setSubAbility(saEffect);
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, card, true);
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, card.getCard(), true);
re.setOverridingAbility(saExile);
card.addReplacementEffect(re);
return re;
}
public static ReplacementEffect setupOmenAbility(CardState card) {
StringBuilder sb = new StringBuilder();
sb.append("Event$ Moved | ValidCard$ Card.Self | Origin$ Stack ");
sb.append("| ValidStackSa$ Spell.Omen | Fizzle$ False | Secondary$ True | Description$ Omen");
String repeffstr = sb.toString();
String abShuffle = "DB$ ChangeZone | Defined$ Self | Origin$ Stack | Destination$ Library | Shuffle$ True | StackDescription$ None";
AbilitySub saShuffle = (AbilitySub)AbilityFactory.getAbility(abShuffle, card);
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, card.getCard(), true);
re.setOverridingAbility(saShuffle);
return re;
}
public static void setFaceDownState(Card c, SpellAbility sa) {

View File

@@ -250,6 +250,19 @@ public class CardLists {
return result;
}
public static CardCollection canSubsequentlyTarget(Iterable<Card> list, SpellAbility source) {
if (source.getTargets().isEmpty()) {
return (CardCollection) list;
}
return CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean test(Card card) {
return source.canTarget(card);
}
});
}
public static CardCollection getKeyword(Iterable<Card> cardList, final String keyword) {
return CardLists.filter(cardList, CardPredicates.hasKeyword(keyword));
}

View File

@@ -129,7 +129,7 @@ public final class CardPredicates {
}
public static Predicate<Card> possibleBlockers(final Card attacker) {
return c -> c.isCreature() && CombatUtil.canBlock(attacker, c);
return c -> CombatUtil.canBlock(attacker, c);
}
public static Predicate<Card> possibleBlockerForAtLeastOne(final Iterable<Card> attackers) {

View File

@@ -1163,7 +1163,7 @@ public class CardProperty {
}
else if (prop.isEmpty() && dmgSource.equalsWithGameTimestamp(source)) {
found = true;
} else if (dmgSource.isValid(prop.split(","), sourceController, source, spellAbility)) {
} else if (dmgSource.isValid(prop.split(";"), sourceController, source, spellAbility)) {
found = true;
}
if (found) {
@@ -1265,10 +1265,6 @@ public class CardProperty {
if (card.getBlockedByThisTurn().isEmpty()) {
return false;
}
} else if (property.startsWith("notAttackedThisTurn")) {
if (card.getDamageHistory().getCreatureAttacksThisTurn() > 0) {
return false;
}
} else if (property.startsWith("greatestPower")) {
CardCollectionView cards = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.CREATURES);
if (property.contains("ControlledBy")) {
@@ -1410,10 +1406,6 @@ public class CardProperty {
if (property.contains("Created") && card.getCastSA() != null) {
return false;
}
} else if (property.startsWith("nonToken")) {
if (card.isToken() || card.isTokenCard()) {
return false;
}
} else if (property.startsWith("copiedSpell")) {
if (!card.isCopiedSpell()) {
return false;
@@ -1879,10 +1871,6 @@ public class CardProperty {
if (!card.isSolved()) {
return false;
}
} else if (property.equals("IsUnsolved")) {
if (card.isSolved()) {
return false;
}
} else if (property.equals("IsSaddled")) {
if (!card.isSaddled()) {
return false;
@@ -2027,10 +2015,6 @@ public class CardProperty {
if (!card.isCommander()) {
return false;
}
} else if (property.equals("IsNotCommander")) {
if (card.isCommander()) {
return false;
}
} else if (property.startsWith("NotedFor")) {
final String key = property.substring("NotedFor".length());
for (String note : sourceController.getNotesForName(key)) {

View File

@@ -84,8 +84,9 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
private ReplacementEffect loyaltyRep;
private ReplacementEffect defenseRep;
private ReplacementEffect battleTypeRep;
private ReplacementEffect sagaRep;
private ReplacementEffect adventureRep;
private ReplacementEffect omenRep;
private SpellAbility manifestUp;
private SpellAbility cloakUp;
@@ -513,13 +514,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
}
result.add(defenseRep);
if (battleTypeRep == null) {
if(type.hasSubtype("Siege")) {
// battleTypeRep; // - Choose a player to protect it
}
}
//result.add(battleTypeRep);
// TODO add Siege "Choose a player to protect it"
}
if (type.hasSubtype("Saga") && !hasKeyword(Keyword.READ_AHEAD)) {
if (sagaRep == null) {
@@ -527,6 +522,18 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
}
result.add(sagaRep);
}
if (type.hasSubtype("Adventure")) {
if (this.adventureRep == null) {
adventureRep = CardFactoryUtil.setupAdventureAbility(this);
}
result.add(adventureRep);
}
if (type.hasSubtype("Omen")) {
if (this.omenRep == null) {
omenRep = CardFactoryUtil.setupOmenAbility(this);
}
result.add(omenRep);
}
card.updateReplacementEffects(result, this);
return result;
@@ -687,6 +694,12 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
if (source.sagaRep != null) {
sagaRep = source.sagaRep.copy(card, true);
}
if (source.adventureRep != null) {
adventureRep = source.adventureRep.copy(card, true);
}
if (source.omenRep != null) {
omenRep = source.omenRep.copy(card, true);
}
}
}

View File

@@ -161,8 +161,8 @@ public class CardView extends GameEntityView {
return get(TrackableProperty.DoubleFaced);
}
public boolean isAdventureCard() {
return get(TrackableProperty.Adventure);
public boolean hasSecondaryState() {
return get(TrackableProperty.Secondary);
}
public boolean isModalCard() {
@@ -1029,7 +1029,7 @@ public class CardView extends GameEntityView {
set(TrackableProperty.Foretold, c.isForetold());
set(TrackableProperty.Manifested, c.isManifested());
set(TrackableProperty.Cloaked, c.isCloaked());
set(TrackableProperty.Adventure, c.isAdventureCard());
set(TrackableProperty.Secondary, c.hasState(CardStateName.Secondary));
set(TrackableProperty.DoubleFaced, c.isDoubleFaced());
set(TrackableProperty.Modal, c.isModal());
set(TrackableProperty.Room, c.isRoom());

View File

@@ -355,6 +355,8 @@ public enum CounterEnumType {
QUEST("QUEST", 251, 189, 0),
RALLY("RALLY", 25, 230, 225),
RELEASE("RELEASE", 200, 210, 50),
REPRIEVE("REPR", 240, 120, 50),
@@ -458,6 +460,8 @@ public enum CounterEnumType {
WIND("WIND", 0, 236, 255),
WISH("WISH", 255, 85, 206),
WRECK("WRECK", 208, 55, 255),
// Player Counters
@@ -477,6 +481,7 @@ public enum CounterEnumType {
FIRSTSTRIKE("First Strike"),
DOUBLESTRIKE("Double Strike"),
DEATHTOUCH("Deathtouch"),
DECAYED("Decayed"),
HASTE("Haste"),
HEXPROOF("Hexproof"),
INDESTRUCTIBLE("Indestructible"),
@@ -484,8 +489,8 @@ public enum CounterEnumType {
MENACE("Menace"),
REACH("Reach"),
TRAMPLE("Trample"),
VIGILANCE("Vigilance")
SHADOW("Shadow")
VIGILANCE("Vigilance"),
SHADOW("Shadow"),
EXALTED("Exalted")
//*/
;

View File

@@ -18,7 +18,7 @@ public class CounterType implements Comparable<CounterType>, Serializable {
// Rule 122.1b
static ImmutableList<String> keywordCounter = ImmutableList.of(
"Flying", "First Strike", "Double Strike", "Deathtouch", "Exalted", "Haste", "Hexproof",
"Flying", "First Strike", "Double Strike", "Deathtouch", "Decayed", "Exalted", "Haste", "Hexproof",
"Indestructible", "Lifelink", "Menace", "Reach", "Shadow", "Trample", "Vigilance");
private static Map<CounterEnumType, CounterType> eMap = Maps.newEnumMap(CounterEnumType.class);

View File

@@ -281,7 +281,11 @@ public class TokenInfo {
final Card host = sa.getHostCard();
final Game game = host.getGame();
String edition = ObjectUtils.firstNonNull(sa.getOriginalHost(), host).getSetCode();
Card editionHost = sa.getOriginalHost();
if (sa.getKeyword() != null && sa.getKeyword().getStatic() != null) {
editionHost = sa.getKeyword().getStatic().getHostCard();
}
String edition = ObjectUtils.firstNonNull(editionHost, host).getSetCode();
PaperToken token = StaticData.instance().getAllTokens().getToken(script, edition);
if (token == null) {

View File

@@ -33,6 +33,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityBlockRestrict;
import forge.game.staticability.StaticAbilityBlockTapped;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityMustBlock;
import forge.game.trigger.TriggerType;
@@ -201,6 +202,10 @@ public class CombatUtil {
private static boolean canAttack(final Card attacker, final GameEntity defender, final boolean forNextTurn) {
final Game game = attacker.getGame();
if (attacker.isBattle()) {
return false;
}
// Basic checks (unless is for next turn)
if (!forNextTurn && (
!attacker.isCreature()
@@ -476,16 +481,23 @@ public class CombatUtil {
* @return a boolean.
*/
public static boolean canBlock(final Card blocker, final boolean nextTurn) {
if (blocker == null) {
if (blocker == null || !blocker.isCreature()) {
return false;
}
if (!nextTurn && blocker.isTapped() && !blocker.hasKeyword("CARDNAME can block as though it were untapped.")) {
if (blocker.isBattle()) {
return false;
}
if (blocker.hasKeyword("CARDNAME can't block.") || blocker.hasKeyword("CARDNAME can't attack or block.")
|| blocker.isPhasedOut()) {
if (!nextTurn && blocker.isPhasedOut()) {
return false;
}
if (!nextTurn && blocker.isTapped() && !StaticAbilityBlockTapped.canBlockTapped(blocker)) {
return false;
}
if (blocker.hasKeyword("CARDNAME can't block.") || blocker.hasKeyword("CARDNAME can't attack or block.")) {
return false;
}
@@ -988,7 +1000,7 @@ public class CombatUtil {
* @return a boolean.
*/
public static boolean canBlock(final Card attacker, final Card blocker, final boolean nextTurn) {
if (attacker == null || blocker == null) {
if (attacker == null || blocker == null || !blocker.isCreature()) {
return false;
}

View File

@@ -495,6 +495,12 @@ public class Cost implements Serializable {
return new CostReveal(splitStr[0], splitStr[1], description, "Hand,Battlefield");
}
if (parse.startsWith("Behold<")) {
final String[] splitStr = abCostParse(parse, 3);
final String description = splitStr.length > 2 ? splitStr[2] : null;
return new CostBehold(splitStr[0], splitStr[1], description);
}
if (parse.startsWith("ExiledMoveToGrave<")) {
final String[] splitStr = abCostParse(parse, 3);
final String description = splitStr.length > 2 ? splitStr[2] : null;

View File

@@ -0,0 +1,29 @@
package forge.game.cost;
public class CostBehold extends CostReveal {
private static final long serialVersionUID = 1L;
public CostBehold(String amount, String type, String description) {
super(amount, type, description, "Hand,Battlefield");
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("Behold ");
final Integer i = this.convertAmount();
final String desc = this.getTypeDescription() == null ? this.getType() : this.getTypeDescription();
sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), desc));
return sb.toString();
}
// Inputs
public <T> T accept(ICostVisitor<T> visitor) {
return visitor.visit(this);
}
}

View File

@@ -222,7 +222,7 @@ public class CostExile extends CostPartWithList {
int amount = this.getAbilityAmount(ability);
if (nTypes > -1) {
if (CardFactoryUtil.getCardTypesFromList(list) < nTypes) return false;
if (AbilityUtils.countCardTypesFromList(list, false) < nTypes) return false;
}
if (sharedType) { // will need more logic if cost ever wants more than 2 that share a type

View File

@@ -21,7 +21,7 @@ public class CostForage extends CostPartWithList {
if (graveyard.size() >= 3) {
return true;
}
CardCollection food = CardLists.filter(payer.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Food"), CardPredicates.canBeSacrificedBy(ability, effect));
if (!food.isEmpty()) {
return true;

View File

@@ -96,7 +96,7 @@ public class CostReveal extends CostPartWithList {
}
@Override
public final String toString() {
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("Reveal ");

View File

@@ -69,7 +69,9 @@ public class CostSacrifice extends CostPartWithList {
}
CardCollectionView typeList = payer.getCardsIn(ZoneType.Battlefield);
typeList = CardLists.getValidCards(typeList, type.split(";"), payer, source, ability);
if (!type.contains("X")) {
typeList = CardLists.getValidCards(typeList, type.split(";"), payer, source, ability);
}
typeList = CardLists.filter(typeList, CardPredicates.canBeSacrificedBy(ability, effect));
if (differentNames) {
// TODO rewrite with sharesName to respect Spy Kit

View File

@@ -87,7 +87,7 @@ public class CostUntap extends CostPart {
@Override
public boolean payAsDecided(Player ai, PaymentDecision decision, SpellAbility ability, final boolean effect) {
final Card c = ability.getHostCard();
if (c.untap(true)) {
if (c.untap()) {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
final Map<Player, CardCollection> map = Maps.newHashMap();
map.put(ai, new CardCollection(c));

View File

@@ -94,7 +94,7 @@ public class CostUntapType extends CostPartWithList {
@Override
protected Card doPayment(Player payer, SpellAbility ability, Card targetCard, final boolean effect) {
targetCard.untap(true);
targetCard.untap();
return targetCard;
}
@@ -107,7 +107,7 @@ public class CostUntapType extends CostPartWithList {
protected CardCollectionView doListPayment(Player payer, SpellAbility ability, CardCollectionView targetCards, final boolean effect) {
CardCollection untapped = new CardCollection();
for (Card c : targetCards) {
if (c.untap(true)) untapped.add(c);
if (c.untap()) untapped.add(c);
}
if (!untapped.isEmpty()) {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();

View File

@@ -2,6 +2,7 @@ package forge.game.cost;
public interface ICostVisitor<T> {
T visit(CostBehold cost);
T visit(CostGainControl cost);
T visit(CostChooseColor cost);
T visit(CostChooseCreatureType cost);
@@ -65,6 +66,10 @@ public interface ICostVisitor<T> {
public T visit(CostDiscard cost) {
return null;
}
@Override
public T visit(CostBehold cost) {
return null;
}
@Override
public T visit(CostDamage cost) {

View File

@@ -57,6 +57,7 @@ public enum Keyword {
DISGUISE("Disguise", KeywordWithCost.class, false, "You may cast this card face down for {3} as a 2/2 creature with ward {2}. Turn it face up any time for its disguise cost."),
DISTURB("Disturb", KeywordWithCost.class, false, "You may cast this card from your graveyard transformed for its disturb cost."),
DOCTORS_COMPANION("Doctor's companion", Partner.class, true, "You can have two commanders if the other is the Doctor."),
DOUBLE_AGENDA("Double agenda", SimpleKeyword.class, false, "Start the game with this conspiracy face down in the command zone and secretly choose two different card names. You may turn this conspiracy face up any time and reveal those names."),
DOUBLE_STRIKE("Double Strike", SimpleKeyword.class, true, "This creature deals both first-strike and regular combat damage."),
DOUBLE_TEAM("Double team", SimpleKeyword.class, true, "When this creature attacks, if it's not a token, conjure a duplicate of it into your hand. Then both cards perpetually lose double team."),
DREDGE("Dredge", KeywordWithAmount.class, false, "If you would draw a card, instead you may put exactly {%d:card} from the top of your library into your graveyard. If you do, return this card from your graveyard to your hand. Otherwise, draw a card."),
@@ -95,10 +96,12 @@ public enum Keyword {
GIFT("Gift", SimpleKeyword.class, true, "You may promise an opponent a gift as you cast this spell. If you do, when it enters, they %s."),
GRAFT("Graft", KeywordWithAmount.class, false, "This permanent enters with {%d:+1/+1 counter} on it. Whenever another creature enters, you may move a +1/+1 counter from this permanent onto it."),
GRAVESTORM("Gravestorm", SimpleKeyword.class, false, "When you cast this spell, copy it for each permanent that was put into a graveyard from the battlefield this turn. If the spell has any targets, you may choose new targets for any of the copies."),
HARMONIZE("Harmonize", KeywordWithCost.class, false, "You may cast this card from your graveyard for its harmonize cost. You may tap a creature you control to reduce that cost by {X}, where X is its power. Then exile this spell."),
HASTE("Haste", SimpleKeyword.class, true, "This creature can attack and {T} as soon as it comes under your control."),
HAUNT("Haunt", SimpleKeyword.class, false, "When this is put into a graveyard, exile it haunting target creature."),
HEXPROOF("Hexproof", Hexproof.class, true, "This can't be the target of %s spells or abilities your opponents control."),
HIDEAWAY("Hideaway", KeywordWithAmount.class, false, "When this permanent enters, look at the top {%d:card} of your library, exile one face down, then put the rest on the bottom of your library."),
HIDDEN_AGENDA("Hidden agenda", SimpleKeyword.class, false, "Start the game with this conspiracy face down in the command zone and secretly choose a card name. You may turn this conspiracy face up any time and reveal that name."),
HORSEMANSHIP("Horsemanship", SimpleKeyword.class, true, "This creature can't be blocked except by creatures with horsemanship."),
IMPENDING("Impending", KeywordWithCostAndAmount.class, false, "If you cast this spell for its impending cost, it enters with {%2$d:time counter} and isn't a creature until the last is removed. At the beginning of your end step, remove a time counter from it."),
IMPROVISE("Improvise", SimpleKeyword.class, true, "Your artifacts can help cast this spell. Each artifact you tap after you're done activating mana abilities pays for {1}."),
@@ -119,6 +122,7 @@ public enum Keyword {
MENACE("Menace", SimpleKeyword.class, true, "This creature can't be blocked except by two or more creatures."),
MEGAMORPH("Megamorph", KeywordWithCost.class, false, "You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its megamorph cost and put a +1/+1 counter on it."),
MIRACLE("Miracle", KeywordWithCost.class, false, "You may cast this card for its miracle cost when you draw it if it's the first card you drew this turn."),
MOBILIZE("Mobilize", KeywordWithAmount.class, false, "When this creature attacks, create {%1$d:tapped and attacking 1/1 red Warrior creature token}. Sacrifice them at the beginning of the next end step."),
// technically not a keyword but easier this way
MONSTROSITY("Monstrosity", KeywordWithCostAndAmount.class, false, "If this creature isn't monstrous, put {%2$d:+1/+1 counter} on it and it becomes monstrous."),
MODULAR("Modular", Modular.class, false, "This creature enters with {%d:+1/+1 counter} on it. When it dies, you may put its +1/+1 counters on target artifact creature."),

View File

@@ -2,11 +2,13 @@ package forge.game.keyword;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.collect.Lists;
import forge.game.IHasSVars;
import forge.game.card.Card;
import forge.game.card.CardFactoryUtil;
import forge.game.player.Player;
@@ -381,4 +383,43 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
idx = i;
}
protected IHasSVars getSVarFallback() {
if (getStatic() != null) {
return getStatic();
}
return getHostCard();
}
@Override
public String getSVar(final String name) {
return getSVarFallback().getSVar(name);
}
@Override
public boolean hasSVar(final String name) {
return getSVarFallback().hasSVar(name);
}
@Override
public final void setSVar(final String name, final String value) {
}
@Override
public Map<String, String> getSVars() {
return getSVarFallback().getSVars();
}
@Override
public Map<String, String> getDirectSVars() {
return null;
}
@Override
public void setSVars(Map<String, String> newSVars) {
}
@Override
public void removeSVar(String var) {
}
}

View File

@@ -2,6 +2,7 @@ package forge.game.keyword;
import java.util.Collection;
import forge.game.IHasSVars;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
@@ -9,7 +10,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
public interface KeywordInterface extends Cloneable {
public interface KeywordInterface extends Cloneable, IHasSVars {
Card getHostCard();
void setHostCard(final Card host);

View File

@@ -142,7 +142,7 @@ public class PhaseHandler implements java.io.Serializable {
private void advanceToNextPhase() {
PhaseType oldPhase = phase;
boolean isTopsy = playerTurn.getAmountOfKeyword("The phases of your turn are reversed.") % 2 == 1;
boolean isTopsy = playerTurn.isPhasesReversed();
boolean turnEnded = false;
game.getStack().clearUndoStack(); //can't undo action from previous phase
@@ -185,9 +185,8 @@ public class PhaseHandler implements java.io.Serializable {
playerTurn.setNumPowerSurgeLands(lands);
}
// Replacement effects
final Map<AbilityKey, Object> repRunParams = AbilityKey.mapFromAffected(playerTurn);
repRunParams.put(AbilityKey.Phase, phase.nameForScripts);
repRunParams.put(AbilityKey.Phase, phase);
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.BeginPhase, repRunParams);
if (repres != ReplacementResult.NotReplaced) {
// Currently there is no effect to skip entire beginning phase
@@ -432,7 +431,7 @@ public class PhaseHandler implements java.io.Serializable {
if (!skipped) {
// Run triggers if phase isn't being skipped
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(playerTurn);
runParams.put(AbilityKey.Phase, phase.nameForScripts);
//runParams.put(AbilityKey.Phase, phase.nameForScripts);
game.getTriggerHandler().runTrigger(TriggerType.Phase, runParams, false);
}
@@ -1165,7 +1164,7 @@ public class PhaseHandler implements java.io.Serializable {
return devAdvanceToPhase(targetPhase, null);
}
public final boolean devAdvanceToPhase(PhaseType targetPhase, Runnable resolver) {
boolean isTopsy = playerTurn.getAmountOfKeyword("The phases of your turn are reversed.") % 2 == 1;
boolean isTopsy = playerTurn.isPhasesReversed();
while (phase.isBefore(targetPhase, isTopsy)) {
if (checkStateBasedEffects()) {
return false;

View File

@@ -21,9 +21,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.game.Game;
@@ -79,54 +77,28 @@ public class Untap extends Phase {
doUntap();
}
/**
* <p>
* canUntap.
* </p>
*
* @param c
* a {@link forge.game.card.Card} object.
* @return a boolean.
*/
public static boolean canUntap(final Card c) {
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")
|| c.hasKeyword("This card doesn't untap during your next untap step.")
|| c.hasKeyword("This card doesn't untap during your next two untap steps.")
|| c.hasKeyword("This card doesn't untap.")) {
return false;
}
//exerted need current player turn
final Player playerTurn = c.getGame().getPhaseHandler().getPlayerTurn();
return !c.isExertedBy(playerTurn);
}
public static final Predicate<Card> CANUNTAP = Untap::canUntap;
/**
* <p>
* doUntap.
* </p>
*/
private void doUntap() {
final Player player = game.getPhaseHandler().getPlayerTurn();
final Predicate<Card> tappedCanUntap = CardPredicates.TAPPED.and(CANUNTAP);
final Player active = game.getPhaseHandler().getPlayerTurn();
Map<Player, CardCollection> untapMap = Maps.newHashMap();
CardCollection list = new CardCollection(player.getCardsIn(ZoneType.Battlefield));
CardCollection untapList = new CardCollection(active.getCardsIn(ZoneType.Battlefield));
CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
CardCollection bounceList = CardLists.getKeyword(list, "During your next untap step, as you untap your permanents, return CARDNAME to its owner's hand.");
CardCollection bounceList = CardLists.getKeyword(untapList, "During your next untap step, as you untap your permanents, return CARDNAME to its owner's hand.");
for (final Card c : bounceList) {
Card moved = game.getAction().moveToHand(c, null);
triggerList.put(ZoneType.Battlefield, moved.getZone().getZoneType(), moved);
}
triggerList.triggerChangesZoneAll(game, null);
list.removeAll(bounceList);
untapList.removeAll(bounceList);
final Map<String, Integer> restrictUntap = Maps.newHashMap();
boolean hasChosen = false;
for (KeywordInterface ki : player.getKeywords()) {
for (KeywordInterface ki : active.getKeywords()) {
String kw = ki.getOriginal();
if (kw.startsWith("UntapAdjust")) {
String[] parse = kw.split(":");
@@ -135,103 +107,79 @@ public class Untap extends Phase {
restrictUntap.put(parse[1], Integer.parseInt(parse[2]));
}
}
if (kw.startsWith("OnlyUntapChosen") && !hasChosen) {
if (kw.startsWith("OnlyUntapChosen")) {
List<String> validTypes = Arrays.asList(kw.split(":")[1].split(","));
final String chosen = player.getController().chooseSomeType("Card", new SpellAbility.EmptySa(ApiType.ChooseType, null, player), validTypes);
list = CardLists.getType(list, chosen);
hasChosen = true;
final String chosen = active.getController().chooseSomeType("Card", new SpellAbility.EmptySa(ApiType.ChooseType, null, active), validTypes);
untapList = CardLists.getType(untapList, chosen);
}
}
final CardCollection untapList = new CardCollection(list);
final String[] restrict = restrictUntap.keySet().toArray(new String[0]);
list = CardLists.filter(list, c -> {
if (!Untap.canUntap(c)) {
return false;
}
return !c.isValid(restrict, player, null, null);
});
for (final Card c : list) {
if (optionalUntap(c)) {
untapMap.computeIfAbsent(player, i -> new CardCollection()).add(c);
untapList = CardLists.filter(untapList, c -> c.canUntap(active, false));
final String[] restrict = restrictUntap.keySet().toArray(new String[0]);
final CardCollection restrictList = CardLists.getValidCards(untapList, restrict, active, null, null);
untapList.removeAll(restrictList);
CardCollection restrictUntapped = new CardCollection();
while (!restrictList.isEmpty()) {
Map<String, Integer> remaining = Maps.newHashMap(restrictUntap);
for (Entry<String, Integer> entry : remaining.entrySet()) {
if (entry.getValue() == 0) {
restrictList.removeAll(CardLists.getValidCards(restrictList, entry.getKey(), active, null, null));
restrictUntap.remove(entry.getKey());
}
}
Card chosen = active.getController().chooseSingleEntityForEffect(restrictList, new SpellAbility.EmptySa(ApiType.Untap, null, active),
"Select a card to untap\r\n(Selected:" + restrictUntapped + ")\r\n" + "Remaining cards that can untap: " + remaining, null);
if (chosen != null) {
for (Entry<String, Integer> rest : restrictUntap.entrySet()) {
if (chosen.isValid(rest.getKey(), active, null, null)) {
restrictUntap.put(rest.getKey(), rest.getValue() - 1);
}
}
untapList.add(chosen);
restrictList.remove(chosen);
}
}
for (final Card c : untapList) {
if (optionalUntap(c, active)) {
untapMap.computeIfAbsent(active, i -> new CardCollection()).add(c);
}
}
// other players untapping during your untap phase
List<Card> cardsWithKW = CardLists.getKeyword(player.getAllOtherPlayers().getCardsIn(ZoneType.Battlefield),
List<Card> cardsWithKW = CardLists.getKeyword(active.getAllOtherPlayers().getCardsIn(ZoneType.Battlefield),
"CARDNAME untaps during each other player's untap step.");
cardsWithKW = CardLists.getNotKeyword(cardsWithKW, "This card doesn't untap.");
List<Card> cardsWithKW2 = CardLists.getKeyword(player.getOpponents().getCardsIn(ZoneType.Battlefield),
List<Card> cardsWithKW2 = CardLists.getKeyword(active.getOpponents().getCardsIn(ZoneType.Battlefield),
"CARDNAME untaps during each opponent's untap step.");
cardsWithKW2 = CardLists.getNotKeyword(cardsWithKW2, "This card doesn't untap.");
cardsWithKW.addAll(cardsWithKW2);
for (final Card cardWithKW : cardsWithKW) {
if (cardWithKW.isExertedBy(player)) {
continue;
}
if (cardWithKW.untap(true)) {
untapMap.computeIfAbsent(cardWithKW.getController(),
i -> new CardCollection()).add(cardWithKW);
}
}
// end other players untapping during your untap phase
CardCollection restrictUntapped = new CardCollection();
CardCollection cardList = CardLists.filter(untapList, tappedCanUntap);
cardList = CardLists.getValidCards(cardList, restrict, player, null, null);
while (!cardList.isEmpty()) {
Map<String, Integer> remaining = Maps.newHashMap(restrictUntap);
for (Entry<String, Integer> entry : remaining.entrySet()) {
if (entry.getValue() == 0) {
cardList.removeAll(CardLists.getValidCards(cardList, entry.getKey(), player, null, null));
restrictUntap.remove(entry.getKey());
}
}
Card chosen = player.getController().chooseSingleEntityForEffect(cardList, new SpellAbility.EmptySa(ApiType.Untap, null, player),
"Select a card to untap\r\n(Selected:" + restrictUntapped + ")\r\n" + "Remaining cards that can untap: " + remaining, null);
if (chosen != null) {
for (Entry<String, Integer> rest : restrictUntap.entrySet()) {
if (chosen.isValid(rest.getKey(), player, null, null)) {
restrictUntap.put(rest.getKey(), rest.getValue() - 1);
}
}
restrictUntapped.add(chosen);
cardList.remove(chosen);
}
}
for (Card c : restrictUntapped) {
if (optionalUntap(c)) {
untapMap.computeIfAbsent(player, i -> new CardCollection()).add(c);
if (cardWithKW.untap(active)) {
untapMap.computeIfAbsent(cardWithKW.getController(), i -> new CardCollection()).add(cardWithKW);
}
}
// Remove temporary keywords
// TODO Replace with Static Abilities
for (final Card c : player.getCardsIn(ZoneType.Battlefield)) {
for (final Card c : active.getCardsIn(ZoneType.Battlefield)) {
c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next untap step.");
if (c.hasKeyword("This card doesn't untap during your next two untap steps.")) {
c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next two untap steps.");
c.addHiddenExtrinsicKeywords(game.getNextTimestamp(), 0, Lists.newArrayList("This card doesn't untap during your next untap step."));
}
}
// remove exerted flags from all things in play
// even if they are not creatures
for (final Card c : game.getCardsIn(ZoneType.Battlefield)) {
c.removeExertedBy(player);
c.removeExertedBy(active);
}
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Map, untapMap);
game.getTriggerHandler().runTrigger(TriggerType.UntapAll, runParams, false);
}
private static boolean optionalUntap(final Card c) {
private static boolean optionalUntap(final Card c, Player phase) {
boolean untap = true;
if (c.hasKeyword("You may choose not to untap CARDNAME during your untap step.") && c.isTapped()) {
if (c.hasKeyword("You may choose not to untap CARDNAME during your untap step.")) {
StringBuilder prompt = new StringBuilder("Untap " + c.toString() + "?");
boolean defaultChoice = true;
if (c.hasGainControlTarget()) {
@@ -246,8 +194,8 @@ public class Untap extends Phase {
}
untap = c.getController().getController().chooseBinary(new SpellAbility.EmptySa(c, c.getController()), prompt.toString(), BinaryChoiceType.UntapOrLeaveTapped, defaultChoice);
}
if (untap) {
if (!c.untap(true)) untap = false;
if (untap && !c.untap(phase)) {
untap = false;
}
return untap;
}

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