Compare commits

..

233 Commits

Author SHA1 Message Date
Chris H
9bf567a966 Migrate SPM (and some other early spoilers) 2025-09-10 19:01:19 -04:00
Hans Mackowiak
a0be5e25cb CardEdition: fix tokenCN with ☇ 2025-09-10 22:10:00 +02:00
Hans Mackowiak
f2e3cdc111 fix c_1_1_myr to c_1_1_a_myr 2025-09-10 22:02:38 +02:00
Hans Mackowiak
7384aada40 Update pictures_of_spider_man.txt 2025-09-10 06:34:11 +02:00
Eradev
1a8a4f63ae Update Recruit Instructor 2025-09-10 06:31:01 +02:00
Eradev
530d1efcd8 Fix tokens and some images ID (#8684)
* Fig hags ID

* Double sided tokens should now be supported

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

* Update PIP

* Update FRF

* Update 5DN

* Update 5ED

* Update FDN

* Update 4ED

* Update F12

* Update V15

* Update V14

* Update DRB

* Update V11

* Update V16

* Update V12

* Update V10

* Update V17

* Update FUT

* Update F17

* Update GDY

* Update GNT

* Update GN2

* Update GTC

* Update GS1

* Update PGPX

* Update GPT

* Update GK1

* Update GRN

* Update GK1

* Added old numbers back for now.

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

* Update DST

* Update DIS

* Update WHO

* Update DMR

* Update DMU

* Update DOM

* Update 2X2

* Update 2XM

* Update DGM

* Update DTK

* Update DS0

* Update DDQ

* Update DDC

* Update DDO

* Update DDU

* Update DDT

* Update DDS

* Update TD2

* Update DDN

* Update DDP

* Update DPA

* Update AFR

* Update DSC

* Update P8ED

* Update 8ED

* Update EMN

* Update EMA

* Update EVE

* Update EXO

* Update E02

* Fixed emblem name

* Add u_2_1_a_myr back

* Add b_0_0_germ back

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

* Update NPH

* Update 9ED

* Update OGW

* Update ODY

* Update ONS

* Update PHEL

* Update OTJ

* Update ONE

* Update PLC

* Update PC2

* Re-add planes

* Update HOP

* Update PLS

* Update PCA

* Update PWCS

* Update PCY

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

* Update SCG

* Update ALA

* Update SOM

* Update SHM

* Update SOI

* Update SCD

* Update SNC

* Update STX

* Update STH

* Update PSVC

* Update RNA

* Update RAV

* Update RTR

* Update REV

* Update ROE

* Update PRIX

* Update RIX

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

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

The order of the Trigger will be handled later in MagicStack

---------

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

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

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

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

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

Tested with JDK 25.

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

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

* Proper fix

* Revert "Proper fix"

This reverts commit f3a46e40d9.

* Use collector number
2025-08-23 14:14:57 -04:00
kevlahnota
9b53144976 Merge pull request #8509 from kevlahnota/master3
remove Normalizer from clipboard and PaperCard artist.
2025-08-24 00:53:22 +08:00
Anthony Calosa
31020296d8 unused import 2025-08-24 00:46:02 +08:00
Anthony Calosa
65fb3414d8 remove Normalizer from clipboard and PaperCard artist. 2025-08-24 00:42:40 +08:00
kevlahnota
98fee0d86b Merge pull request #8497 from kevlahnota/master3
update InventoryScene, add repair cracked item for gold
2025-08-23 23:17:08 +08:00
Anthony Calosa
becdadb279 NPE prevention for rewards 2025-08-23 23:08:11 +08:00
Hans Mackowiak
26da0ab0d4 Create rw_1_1_soldier.txt
Fix missing TokenScript
2025-08-23 16:46:16 +02:00
Anthony Calosa
e1f4d755e0 minor fix for refreshing overlay 2025-08-23 21:38:52 +08:00
Anthony Calosa
efe7d67d9f add unusable indicator 2025-08-23 21:34:57 +08:00
Anthony Calosa
3a5e11504a add migration message, fix cloning of itemData 2025-08-23 20:07:28 +08:00
Anthony Calosa
1ff16ca509 prevent NPE 2025-08-22 16:43:17 +08:00
Anthony Calosa
fb624458f0 update InventoryScene, add repair cracked item for gold 2025-08-22 12:32:12 +08:00
Michael Rappazzo
67b6c2c03f fix: move CHANGES.txt generation to target directory
- Generate CHANGES.txt in forge-gui-desktop/target/ instead of source tree
- Update installer to copy from target directory for all build profiles
- Add CHANGES.txt to .gitignore since it's generated
- Remove hardcoded fromRef to use latest tag automatically
- Remove maven-release-plugin exclusion for untracked file
2025-08-19 06:49:42 -04:00
878 changed files with 9427 additions and 5142 deletions

View File

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

3
.gitignore vendored
View File

@@ -66,6 +66,9 @@ forge-gui-mobile-dev/testAssets
forge-gui/res/cardsfolder/*.bat
# Generated changelog file
forge-gui/release-files/CHANGES.txt
forge-gui/res/PerSetTrackingResults
forge-gui/res/decks
forge-gui/res/layouts

View File

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

View File

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

View File

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

View File

@@ -563,7 +563,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) {
removed += thisRemove;
table.put(null, prefCard, CounterType.get(cType), thisRemove);
table.put(null, prefCard, cType, thisRemove);
}
}
}
@@ -573,7 +573,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostRemoveAnyCounter cost) {
final int c = cost.getAbilityAmount(ability);
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source);
if (c <= 0) {
return null;
@@ -716,7 +716,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) {
toRemove += over;
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
table.put(null, crd, CounterEnumType.QUEST, over);
}
}
}

View File

@@ -767,7 +767,7 @@ public class ComputerUtil {
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
if (untap) {
typeList.remove(activate);
@@ -2542,7 +2542,7 @@ public class ComputerUtil {
boolean opponent = controller.isOpponentOf(ai);
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
final CounterType p1p1Type = CounterEnumType.P1P1;
if (!sa.hasParam("AILogic")) {
return Aggregates.random(options);

View File

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

View File

@@ -291,6 +291,12 @@ public class ComputerUtilMana {
continue;
}
int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1;
if (amount <= 0) {
// wrong gamestate for variable amount
continue;
}
if (sa.getApi() == ApiType.Animate) {
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
if (saHost.isAura() && "Enchanted".equals(sa.getParam("Defined"))
@@ -443,7 +449,6 @@ public class ComputerUtilMana {
manaProduced = manaProduced.replace(s, color);
}
} else if (saMana.hasParam("ReplaceColor")) {
// replace color
String color = saMana.getParam("ReplaceColor");
if ("Chosen".equals(color)) {
if (card.hasChosenColor()) {
@@ -735,7 +740,8 @@ public class ComputerUtilMana {
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) {
saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
// not a good idea to sac a card that you're targeting with the SA you're paying for
saExcludeList.add(saPayment);
continue;
}
}
@@ -1496,7 +1502,7 @@ public class ComputerUtilMana {
}
if (!cost.isReusuableResource()) {
for(CostPart part : cost.getCostParts()) {
for (CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice && !part.payCostFromSource()) {
unpreferredCost = true;
}
@@ -1587,10 +1593,8 @@ public class ComputerUtilMana {
// don't use abilities with dangerous drawbacks
AbilitySub sub = m.getSubAbility();
if (sub != null) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
}
if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
}
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list

View File

@@ -1347,6 +1347,11 @@ public class PlayerControllerAi extends PlayerController {
// Ai won't understand that anyway
}
@Override
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
// Ai won't understand that anyway
}
@Override
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
// TODO check if profile detection set to Auto

View File

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

View File

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

View File

@@ -154,7 +154,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
}
if (counterType == null || counterType.is(type)) {
addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
addTargetsByCounterType(ai, sa, aiList, type);
}
}
}
@@ -163,7 +163,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (!oppList.isEmpty()) {
// not enough targets
if (sa.canAddMoreTarget()) {
final CounterType type = CounterType.get(CounterEnumType.M1M1);
final CounterType type = CounterEnumType.M1M1;
if (counterType == null || counterType == type) {
addTargetsByCounterType(ai, sa, oppList, type);
}

View File

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

View File

@@ -170,7 +170,7 @@ public class CountersPutAi extends CountersAi {
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)));
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterEnumType.M1M1));
Card best = ComputerUtilCard.getBestAI(oppCreatM1);
if (best != null) {
@@ -336,7 +336,7 @@ public class CountersPutAi extends CountersAi {
Game game = ai.getGame();
Combat combat = game.getCombat();
if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return doCombatAdaptLogic(source, amount, combat);
@@ -442,11 +442,9 @@ public class CountersPutAi extends CountersAi {
}
sa.addDividedAllocation(c, amount);
return decision;
} else {
if (!hasSacCost) {
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision;
}
} else if (!hasSacCost) {
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision;
}
}
}
@@ -608,7 +606,21 @@ public class CountersPutAi extends CountersAi {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
final int currCounters = cards.get(0).getCounters(CounterType.getType(type));
// adding counters would cause counter amount to overflow
if (Integer.MAX_VALUE - currCounters <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (type.equals("P1P1")) {
if (Integer.MAX_VALUE - cards.get(0).getNetPower() <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (Integer.MAX_VALUE - cards.get(0).getNetToughness() <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// each non +1/+1 counter on the card is a 10% chance of not
// activating this ability.
@@ -623,7 +635,7 @@ public class CountersPutAi extends CountersAi {
}
// Useless since the card already has the keyword (or for another reason)
if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -670,14 +682,12 @@ public class CountersPutAi extends CountersAi {
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
if (sa.usesTargeting()) {
CardCollection list = null;
CardCollection list;
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
}
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty() && isMandatoryTrigger) {
@@ -693,9 +703,8 @@ public class CountersPutAi extends CountersAi {
|| sa.getTargets().isEmpty()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
break;
}
break;
}
if (sa.isCurse()) {
@@ -738,8 +747,6 @@ public class CountersPutAi extends CountersAi {
final SpellAbility root = sa.getRootAbility();
final Card source = sa.getHostCard();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
boolean preferred = true;
CardCollection list;
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.isDividedAsYouChoose();
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
@@ -761,11 +768,11 @@ public class CountersPutAi extends CountersAi {
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
if (decision.willingToPlay()) {
return decision;
} else if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (!sa.usesTargeting()) {
@@ -816,19 +823,19 @@ public class CountersPutAi extends CountersAi {
}
}
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
}
list = CardLists.getTargetableCards(list, sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false);
int totalTargets = list.size();
sa.resetTargets();
Iterable<Card> filteredField;
if (sa.isCurse()) {
filteredField = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
filteredField = ai.getCardsIn(ZoneType.Battlefield);
}
CardCollection list = CardLists.getTargetableCards(filteredField, sa);
list = ComputerUtil.filterAITgts(sa, ai, list, false);
int totalTargets = list.size();
boolean preferred = true;
while (sa.canAddMoreTarget()) {
if (mandatory) {
// When things are mandatory, gotta handle a little differently
@@ -865,27 +872,21 @@ public class CountersPutAi extends CountersAi {
if (choice == null && mandatory) {
choice = Aggregates.random(list);
}
} else if (type.equals("M1M1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
if (type.equals("M1M1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = Aggregates.random(list);
}
choice = Aggregates.random(list);
}
} else if (preferred) {
list = ComputerUtil.getSafeTargets(ai, sa, list);
choice = chooseBoonTarget(list, type);
if (choice == null && mandatory) {
choice = Aggregates.random(list);
}
} else if (type.equals("P1P1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
if (preferred) {
list = ComputerUtil.getSafeTargets(ai, sa, list);
choice = chooseBoonTarget(list, type);
if (choice == null && mandatory) {
choice = Aggregates.random(list);
}
} else {
if (type.equals("P1P1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = Aggregates.random(list);
}
}
choice = Aggregates.random(list);
}
if (choice != null && divided) {
int alloc = Math.max(amount / totalTargets, 1);
@@ -961,8 +962,8 @@ public class CountersPutAi extends CountersAi {
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Bolster does use this
// TODO need more or less logic there?
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
final CounterType m1m1 = CounterEnumType.M1M1;
final CounterType p1p1 = CounterEnumType.P1P1;
// no logic if there is no options or no to choice
if (!isOptional && Iterables.size(options) <= 1) {
@@ -1080,11 +1081,10 @@ public class CountersPutAi extends CountersAi {
Player ai = sa.getActivatingPlayer();
GameEntity e = (GameEntity) params.get("Target");
// for Card try to select not useless counter
if (e instanceof Card) {
Card c = (Card) e;
if (e instanceof Card c) {
if (c.getController().isOpponentOf(ai)) {
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterType.get(CounterEnumType.M1M1);
if (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterEnumType.M1M1;
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, c)) {
@@ -1098,15 +1098,14 @@ public class CountersPutAi extends CountersAi {
}
}
}
} else if (e instanceof Player) {
Player p = (Player) e;
} else if (e instanceof Player p) {
if (p.isOpponentOf(ai)) {
if (options.contains(CounterType.get(CounterEnumType.POISON))) {
return CounterType.get(CounterEnumType.POISON);
if (options.contains(CounterEnumType.POISON)) {
return CounterEnumType.POISON;
}
} else {
if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
return CounterType.get(CounterEnumType.EXPERIENCE);
if (options.contains(CounterEnumType.EXPERIENCE)) {
return CounterEnumType.EXPERIENCE;
}
}
@@ -1233,9 +1232,8 @@ public class CountersPutAi extends CountersAi {
if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

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

View File

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

View File

@@ -133,9 +133,9 @@ public class DamageAllAi extends SpellAbilityAi {
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
// When using Pestilence to hurt players, do it at
// the end of the opponent's turn only
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))
|| (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)))
// Need further improvement : if able to kill immediately with repeated activations, do not wait
// for phases! Will also need to implement considering repeated activations for killed creatures!
// || (ai.sa.getPayCosts(). ??? )

View File

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

View File

@@ -159,7 +159,7 @@ public class ManaAi extends SpellAbilityAi {
int numCounters = 0;
int manaSurplus = 0;
if ("Count$xPaid".equals(host.getSVar("X")) && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
CounterType ctrType = CounterType.get(CounterEnumType.KI); // Petalmane Baku
CounterType ctrType = CounterEnumType.KI; // Petalmane Baku
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
ctrType = ((CostRemoveCounter)part).counter;

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,8 @@ public final class ImageKeys {
public static final String MONARCH_IMAGE = "monarch";
public static final String THE_RING_IMAGE = "the_ring";
public static final String RADIATION_IMAGE = "radiation";
public static final String SPEED_IMAGE = "speed";
public static final String MAX_SPEED_IMAGE = "max_speed";
public static final String BACKFACE_POSTFIX = "$alt";
public static final String SPECFACE_W = "$wspec";

View File

@@ -29,8 +29,6 @@ import java.util.stream.Collectors;
public class StaticData {
private final CardStorageReader cardReader;
private final CardStorageReader tokenReader;
private final CardStorageReader customCardReader;
private final String blockDataFolder;
private final CardDb commonCards;
private final CardDb variantCards;
@@ -79,7 +77,6 @@ public class StaticData {
this.tokenReader = tokenReader;
this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder)));
this.blockDataFolder = blockDataFolder;
this.customCardReader = customCardReader;
this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance;
this.enableSmartCardArtSelection = enableSmartCardArtSelection;
this.loadNonLegalCards = loadNonLegalCards;

View File

@@ -45,8 +45,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public final static char NameSetSeparator = '|';
public final static String FlagPrefix = "#";
public static final String FlagSeparator = "\t";
private final String exlcudedCardName = "Concentrate";
private final String exlcudedCardSet = "DS0";
// need this to obtain cardReference by name+set+artindex
private final ListMultimap<String, PaperCard> allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
@@ -303,7 +301,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// create faces list from rules
for (final CardRules rule : rules.values()) {
if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName()))
if (filteredCards.contains(rule.getName()))
continue;
for (ICardFace face : rule.getAllFaces()) {
addFaceToDbNames(face);
@@ -501,8 +499,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
public void addCard(PaperCard paperCard) {
if (excludeCard(paperCard.getName(), paperCard.getEdition()))
if (filtered.contains(paperCard.getName())) {
return;
}
allCardsByName.put(paperCard.getName(), paperCard);
@@ -523,17 +522,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
}
private boolean excludeCard(String cardName, String cardEdition) {
if (filtered.isEmpty())
return false;
if (filtered.contains(cardName)) {
if (exlcudedCardSet.equalsIgnoreCase(cardEdition) && exlcudedCardName.equalsIgnoreCase(cardName))
return true;
else return !exlcudedCardName.equalsIgnoreCase(cardName);
}
return false;
}
private void reIndex() {
uniqueCardsByName.clear();
for (Entry<String, Collection<PaperCard>> kv : allCardsByName.asMap().entrySet()) {

View File

@@ -52,6 +52,14 @@ import java.util.stream.Collectors;
*/
public final class CardEdition implements Comparable<CardEdition> {
public DraftOptions getDraftOptions() {
return draftOptions;
}
public void setDraftOptions(DraftOptions draftOptions) {
this.draftOptions = draftOptions;
}
// immutable
public enum Type {
UNKNOWN,
@@ -275,18 +283,22 @@ public final class CardEdition implements Comparable<CardEdition> {
// Booster/draft info
private List<BoosterSlot> boosterSlots = null;
private boolean smallSetOverride = false;
private boolean foilAlwaysInCommonSlot = false;
private String additionalUnlockSet = "";
private FoilType foilType = FoilType.NOT_SUPPORTED;
// Replace all of these things with booster slots
private boolean foilAlwaysInCommonSlot = false;
private double foilChanceInBooster = 0;
private double chanceReplaceCommonWith = 0;
private String slotReplaceCommonWith = "Common";
private String additionalSheetForFoils = "";
private String additionalUnlockSet = "";
private String boosterMustContain = "";
private String boosterReplaceSlotFromPrintSheet = "";
private String sheetReplaceCardFromSheet = "";
private String sheetReplaceCardFromSheet2 = "";
private String doublePickDuringDraft = "";
// Draft options
private DraftOptions draftOptions = null;
private String[] chaosDraftThemes = new String[0];
private final ListMultimap<String, EditionEntry> cardMap;
@@ -373,7 +385,6 @@ public final class CardEdition implements Comparable<CardEdition> {
public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; }
public String getAdditionalSheetForFoils() { return additionalSheetForFoils; }
public String getAdditionalUnlockSet() { return additionalUnlockSet; }
public String getDoublePickDuringDraft() { return doublePickDuringDraft; }
public String getBoosterMustContain() { return boosterMustContain; }
public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; }
public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; }
@@ -619,7 +630,7 @@ public final class CardEdition implements Comparable<CardEdition> {
* functional variant name - grouping #9
*/
// "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$"
"(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
"(^(.?[0-9A-Z-]+\\S*[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
);
final Pattern tokenPattern = Pattern.compile(
@@ -628,7 +639,7 @@ public final class CardEdition implements Comparable<CardEdition> {
* name - grouping #3
* artist name - grouping #5
*/
"(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
"(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
);
ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create();
@@ -649,31 +660,37 @@ public final class CardEdition implements Comparable<CardEdition> {
continue;
}
// parse sections of the format "<collector number> <rarity> <name>"
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
for(String line : contents.get(sectionName)) {
Matcher matcher = pattern.matcher(line);
if (!matcher.matches()) {
continue;
}
String collectorNumber = matcher.group(2);
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
String cardName = matcher.group(5);
String artistName = matcher.group(7);
String functionalVariantName = matcher.group(9);
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
cardMap.put(sectionName, cis);
}
} else if (boosterSlotsToParse.contains(sectionName)) {
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
if (sectionName.endsWith("Types")) {
CardType.Helper.parseTypes(sectionName, contents.get(sectionName));
} else {
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
// Parse cards
// parse sections of the format "<collector number> <rarity> <name>"
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
for(String line : contents.get(sectionName)) {
Matcher matcher = pattern.matcher(line);
if (!matcher.matches()) {
continue;
}
String collectorNumber = matcher.group(2);
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
String cardName = matcher.group(5);
String artistName = matcher.group(7);
String functionalVariantName = matcher.group(9);
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
cardMap.put(sectionName, cis);
}
} else if (boosterSlotsToParse.contains(sectionName)) {
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
} else {
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
}
}
}
@@ -802,7 +819,6 @@ public final class CardEdition implements Comparable<CardEdition> {
res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral
res.smallSetOverride = metadata.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon)
res.doublePickDuringDraft = metadata.get("DoublePick", ""); // "FirstPick" or "Always"
res.boosterMustContain = metadata.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature
res.boosterReplaceSlotFromPrintSheet = metadata.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card
@@ -810,6 +826,23 @@ public final class CardEdition implements Comparable<CardEdition> {
res.sheetReplaceCardFromSheet2 = metadata.get("SheetReplaceCardFromSheet2", "");
res.chaosDraftThemes = metadata.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names
// Draft options
String doublePick = metadata.get("DoublePick", "Never");
int maxPodSize = metadata.getInt("MaxPodSize", 8);
int recommendedPodSize = metadata.getInt("RecommendedPodSize", 8);
int maxMatchPlayers = metadata.getInt("MaxMatchPlayers", 2);
String deckType = metadata.get("DeckType", "Normal");
String freeCommander = metadata.get("FreeCommander", "");
res.draftOptions = new DraftOptions(
doublePick,
maxPodSize,
recommendedPodSize,
maxMatchPlayers,
deckType,
freeCommander
);
return res;
}
@@ -840,7 +873,7 @@ public final class CardEdition implements Comparable<CardEdition> {
@Override
public void add(CardEdition item) { //Even though we want it to be read only, make an exception for custom content.
if(lock) throw new UnsupportedOperationException("This is a read-only storage");
else map.put(item.getName(), item);
else map.put(item.getCode(), item);
}
public void append(CardEdition.Collection C){ //Append custom editions
if (lock) throw new UnsupportedOperationException("This is a read-only storage");

View File

@@ -53,6 +53,7 @@ public final class CardRules implements ICardCharacteristics {
private boolean addsWildCardColor;
private int setColorID;
private boolean custom;
private boolean unsupported;
private String path;
public CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
@@ -220,7 +221,9 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean isCustom() { return custom; }
public void setCustom() { custom = true; }
public void setCustom() { custom = true; }
public boolean isUnsupported() { return unsupported; }
@Override
public CardType getType() {
@@ -373,6 +376,9 @@ public final class CardRules implements ICardCharacteristics {
public boolean canBeOathbreaker() {
CardType type = mainPart.getType();
if (mainPart.getOracleText().contains("can be your commander")) {
return true;
}
return type.isPlaneswalker();
}
@@ -825,6 +831,8 @@ public final class CardRules implements ICardCharacteristics {
faces[0].assignMissingFields();
final CardRules result = new CardRules(faces, CardSplitType.None, cah);
result.unsupported = true;
return result;
}

View File

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

View File

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

View File

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

View File

@@ -115,6 +115,20 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return parts.get(DeckSection.Main);
}
public Pair<Deck, List<PaperCard>> getValid() {
List<PaperCard> unsupported = new ArrayList<>();
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
CardPool pool = kv.getValue();
for (Entry<PaperCard, Integer> pc : pool) {
if (pc.getKey().getRules() != null && pc.getKey().getRules().isUnsupported()) {
unsupported.add(pc.getKey());
pool.remove(pc.getKey());
}
}
}
return Pair.of(this, unsupported);
}
public List<PaperCard> getCommanders() {
List<PaperCard> result = Lists.newArrayList();
final CardPool cp = get(DeckSection.Commander);

View File

@@ -250,7 +250,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
this.foil = foil;
this.rarity = rarity;
this.artist = TextUtil.normalizeText(artist);
this.artist = artist;
this.collectorNumber = (collectorNumber != null && !collectorNumber.isEmpty()) ? collectorNumber : IPaperCard.NO_COLLECTOR_NUMBER;
// If the user changes the language this will make cards sort by the old language until they restart the game.
// This is a good tradeoff
@@ -375,7 +375,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
System.out.println("PaperCard: " + name + " not found with set and index " + edition + ", " + artIndex);
pc = readObjectAlternate(name, edition);
if (pc == null) {
throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
pc = StaticData.instance().getCommonCards().createUnsupportedCard(name);
//throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
}
System.out.println("Alternate object found: " + pc.getName() + ", " + pc.getEdition() + ", " + pc.getArtIndex());
}

View File

@@ -207,8 +207,6 @@ public class ImageUtil {
else
editionCode = cp.getEdition().toLowerCase();
String cardCollectorNumber = cp.getCollectorNumber();
// Hack to account for variations in Arabian Nights
cardCollectorNumber = cardCollectorNumber.replace("+", "");
// override old planechase sets from their modified id since scryfall move the planechase cards outside their original setcode
if (cardCollectorNumber.startsWith("OHOP")) {
editionCode = "ohop";
@@ -252,6 +250,11 @@ public class ImageUtil {
: "&face=front");
}
if (cardCollectorNumber.endsWith("")) {
faceParam = "&face=back";
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1);
}
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, encodeUtf8(cardCollectorNumber),
langCode, versionParam, faceParam);
}
@@ -261,6 +264,10 @@ public class ImageUtil {
if (!faceParam.isEmpty()) {
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
}
if (collectorNumber.endsWith("")) {
faceParam = "&face=back";
collectorNumber = collectorNumber.substring(0, collectorNumber.length() - 1);
}
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, encodeUtf8(collectorNumber),
langCode, versionParam, faceParam);
}
@@ -281,8 +288,7 @@ public class ImageUtil {
char c;
for (int i = 0; i < in.length(); i++) {
c = in.charAt(i);
if ((c == '"') || (c == '/') || (c == ':') || (c == '?')) {
} else {
if ((c != '"') && (c != '/') && (c != ':') && (c != '?')) {
out.append(c);
}
}

View File

@@ -195,7 +195,7 @@ public class ForgeScript {
return sa.isKeyword(Keyword.SADDLE);
} else if (property.equals("Station")) {
return sa.isKeyword(Keyword.STATION);
}else if (property.equals("Cycling")) {
} else if (property.equals("Cycling")) {
return sa.isCycling();
} else if (property.equals("Dash")) {
return sa.isDash();
@@ -237,6 +237,8 @@ public class ForgeScript {
return sa.isBoast();
} else if (property.equals("Exhaust")) {
return sa.isExhaust();
} else if (property.equals("Mayhem")) {
return sa.isMayhem();
} else if (property.equals("Mutate")) {
return sa.isMutate();
} else if (property.equals("Ninjutsu")) {

View File

@@ -414,19 +414,6 @@ public class Game {
return players;
}
/**
* Gets the nonactive players who are still fighting to win, in turn order.
*/
public final PlayerCollection getNonactivePlayers() {
// Don't use getPlayersInTurnOrder to prevent copying the player collection twice
final PlayerCollection players = new PlayerCollection(ingamePlayers);
players.remove(phaseHandler.getPlayerTurn());
if (!getTurnOrder().isDefaultDirection()) {
Collections.reverse(players);
}
return players;
}
/**
* Gets the players who participated in match (regardless of outcome).
* <i>Use this in UI and after match calculations</i>

View File

@@ -1822,8 +1822,8 @@ public class GameAction {
private boolean stateBasedAction704_5q(Card c) {
boolean checkAgain = false;
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
final CounterType p1p1 = CounterEnumType.P1P1;
final CounterType m1m1 = CounterEnumType.M1M1;
int plusOneCounters = c.getCounters(p1p1);
int minusOneCounters = c.getCounters(m1m1);
if (plusOneCounters > 0 && minusOneCounters > 0) {
@@ -1843,7 +1843,7 @@ public class GameAction {
return checkAgain;
}
private boolean stateBasedAction704_5r(Card c) {
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
final CounterType dreamType = CounterEnumType.DREAM;
int old = c.getCounters(dreamType);
if (old <= 0) {
@@ -2222,6 +2222,13 @@ public class GameAction {
}
}
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
// Notify players
for (Player p : game.getPlayers()) {
p.getController().revealUnsupported(unsupported);
}
}
/** Delivers a message to all players. (use reveal to show Cards) */
public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) {
if (saSource != null) {

View File

@@ -125,10 +125,22 @@ public final class GameActionUtil {
// need to be done there before static abilities does reset the card
// These Keywords depend on the Mana Cost of for Split Cards
if (sa.isBasicSpell() && !sa.isLandAbility()) {
if (sa.isBasicSpell()) {
for (final KeywordInterface inst : source.getKeywords()) {
final String keyword = inst.getOriginal();
if (keyword.startsWith("Mayhem")) {
if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) {
continue;
}
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem));
}
if (sa.isLandAbility()) {
continue;
}
if (keyword.startsWith("Escape")) {
if (!source.isInZone(ZoneType.Graveyard)) {
continue;
@@ -166,18 +178,6 @@ public final class GameActionUtil {
}
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Flashback));
} else if (keyword.startsWith("Mayhem")) {
if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) {
continue;
}
// if source has No Mana cost, and Mayhem doesn't have own one,
// Mayhem can't work
if (keyword.equals("Mayhem") && source.getManaCost().isNoCost()) {
continue;
}
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem));
} else if (keyword.startsWith("Harmonize")) {
if (!source.isInZone(ZoneType.Graveyard)) {
continue;
@@ -242,6 +242,7 @@ public final class GameActionUtil {
}
stackCopy.setLastKnownZone(game.getStackZone());
stackCopy.setCastFrom(oldZone);
stackCopy.setCastSA(sa);
lkicheck = true;
stackCopy.clearStaticChangedCardKeywords(false);

View File

@@ -33,7 +33,6 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.event.GameEventCardAttachment;
import forge.game.keyword.Keyword;
@@ -305,9 +304,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
Integer value = counters.get(counterName);
return value == null ? 0 : value;
}
public final int getCounters(final CounterEnumType counterType) {
return getCounters(CounterType.get(counterType));
}
public void setCounters(final CounterType counterType, final Integer num) {
if (num <= 0) {
@@ -316,9 +312,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
counters.put(counterType, num);
}
}
public void setCounters(final CounterEnumType counterType, final Integer num) {
setCounters(CounterType.get(counterType), num);
}
abstract public void setCounters(final Map<CounterType, Integer> allCounters);
@@ -328,10 +321,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
abstract public int subtractCounter(final CounterType counterName, final int n, final Player remover);
abstract public void clearCounters();
public boolean canReceiveCounters(final CounterEnumType type) {
return canReceiveCounters(CounterType.get(type));
}
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
if (n <= 0 || !canReceiveCounters(counterType)) {
// As per rule 107.1b
@@ -351,18 +340,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
table.put(source, this, counterType, n);
}
public final void addCounter(final CounterEnumType counterType, final int n, final Player source, GameEntityCounterTable table) {
addCounter(CounterType.get(counterType), n, source, table);
}
public int subtractCounter(final CounterEnumType counterName, final int n, final Player remover) {
return subtractCounter(CounterType.get(counterName), n, remover);
}
abstract public void addCounterInternal(final CounterType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params);
public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params);
}
public Integer getCounterMax(final CounterType counterType) {
return null;
}

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ import forge.item.PaperCard;
import forge.util.Localizer;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
@@ -224,6 +225,7 @@ public class Match {
// friendliness
Map<Player, Map<DeckSection, List<? extends PaperCard>>> rAICards = new HashMap<>();
Multimap<Player, PaperCard> removedAnteCards = ArrayListMultimap.create();
Map<Player, List<PaperCard>> unsupported = new HashMap<>();
final FCollectionView<Player> players = game.getPlayers();
final List<RegisteredPlayer> playersConditions = game.getMatch().getPlayers();
@@ -288,22 +290,32 @@ public class Match {
}
}
Deck myDeck = psc.getDeck();
player.setDraftNotes(myDeck.getDraftNotes());
Deck toCheck = psc.getDeck();
if (toCheck == null) {
try {
System.err.println(psc.getPlayer().getName() + " Deck is NULL...");
int val = rules.getGameType().getDeckFormat().getMainRange().getMinimum();
toCheck = new Deck("NULL");
if (val > 0)
toCheck.getMain().add("Wastes", val);
} catch (Exception ignored) {}
}
Pair<Deck, List<PaperCard>> myDeck = toCheck.getValid();
player.setDraftNotes(myDeck.getLeft().getDraftNotes());
Set<PaperCard> myRemovedAnteCards = null;
if (!rules.useAnte()) {
myRemovedAnteCards = getRemovedAnteCards(myDeck);
myRemovedAnteCards = getRemovedAnteCards(myDeck.getLeft());
for (PaperCard cp: myRemovedAnteCards) {
for (Entry<DeckSection, CardPool> ds : myDeck) {
for (Entry<DeckSection, CardPool> ds : myDeck.getLeft()) {
ds.getValue().removeAll(cp);
}
}
}
preparePlayerZone(player, ZoneType.Library, myDeck.getMain(), psc.useRandomFoil());
if (myDeck.has(DeckSection.Sideboard)) {
preparePlayerZone(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil());
preparePlayerZone(player, ZoneType.Library, myDeck.getLeft().getMain(), psc.useRandomFoil());
if (myDeck.getLeft().has(DeckSection.Sideboard)) {
preparePlayerZone(player, ZoneType.Sideboard, myDeck.getLeft().get(DeckSection.Sideboard), psc.useRandomFoil());
// Assign Companion
Card companion = player.assignCompanion(game, person);
@@ -322,7 +334,7 @@ public class Match {
player.shuffle(null);
if (isFirstGame) {
Map<DeckSection, List<? extends PaperCard>> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck);
Map<DeckSection, List<? extends PaperCard>> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck.getLeft());
if (cardsComplained != null && !cardsComplained.isEmpty()) {
rAICards.put(player, cardsComplained);
}
@@ -337,6 +349,7 @@ public class Match {
if (myRemovedAnteCards != null && !myRemovedAnteCards.isEmpty()) {
removedAnteCards.putAll(player, myRemovedAnteCards);
}
unsupported.put(player, myDeck.getRight());
}
final Localizer localizer = Localizer.getInstance();
@@ -347,6 +360,10 @@ public class Match {
if (!removedAnteCards.isEmpty()) {
game.getAction().revealAnte(localizer.getMessage("lblAnteCardsRemoved"), removedAnteCards);
}
if (!unsupported.isEmpty()) {
game.getAction().revealUnsupported(unsupported);
}
}
private void executeAnte(Game lastGame) {

View File

@@ -383,6 +383,9 @@ public final class AbilityFactory {
if (mapParams.containsKey("TargetsWithDifferentCMC")) {
abTgt.setDifferentCMC(true);
}
if (mapParams.containsKey("TargetsWithDifferentNames")) {
abTgt.setDifferentNames(true);
}
if (mapParams.containsKey("TargetsWithEqualToughness")) {
abTgt.setEqualToughness(true);
}

View File

@@ -1328,12 +1328,6 @@ public class AbilityUtils {
game.getTriggerHandler().resetActiveTriggers();
}
if (sa.hasParam("Precalc")) {
for (String s : sa.getParam("Precalc").split(",")) {
sa.setSVar(s, String.valueOf(calculateAmount(sa.getHostCard(), s, sa)));
}
}
resolvePreAbilities(sa, game);
// count times ability resolves this turn

View File

@@ -39,7 +39,6 @@ public class AirbendEffect extends SpellAbilityEffect {
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
final Card hostCard = sa.getHostCard();

View File

@@ -46,6 +46,9 @@ public class AlterAttributeEffect extends SpellAbilityEffect {
boolean altered = false;
switch (attr.trim()) {
case "Harnessed":
altered = gameCard.setHarnessed(activate);
break;
case "Plotted":
altered = gameCard.setPlotted(activate);

View File

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

View File

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

View File

@@ -969,12 +969,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
String prompt;
if (sa.hasParam("OptionalPrompt")) {
prompt = sa.getParam("OptionalPrompt");
} else if (defined) {
prompt = Localizer.getInstance().getMessage("lblPutThatCardFromPlayerOriginToDestination", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase(), destination.getTranslatedName().toLowerCase());
} else {
if (defined) {
prompt = Localizer.getInstance().getMessage("lblPutThatCardFromPlayerOriginToDestination", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase(), destination.getTranslatedName().toLowerCase());
} else {
prompt = Localizer.getInstance().getMessage("lblSearchPlayerZoneConfirm", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase());
}
prompt = Localizer.getInstance().getMessage("lblSearchPlayerZoneConfirm", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase());
}
String message = MessageUtil.formatMessage(prompt , decider, player);
if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message, null)) {

View File

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

View File

@@ -40,6 +40,7 @@ public class EarthbendEffect extends SpellAbilityEffect {
TargetRestrictions abTgt = new TargetRestrictions("Select target land you control", "Land.YouCtrl".split(","), "1", "1");
sa.setTargetRestrictions(abTgt);
}
@Override
public void resolve(SpellAbility sa) {
final Card source = sa.getHostCard();

View File

@@ -34,6 +34,9 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
final Game game = host.getGame();
// CR 603.12a if the trigger event or events occur multiple times during the resolution of the spell or ability that created it,
// the reflexive triggered ability will trigger once for each of those times
int amt = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("TriggerAmount", "1"), sa);
if (amt <= 0) {
return;

View File

@@ -54,33 +54,29 @@ public class LifeExchangeEffect extends SpellAbilityEffect {
final int life1 = p1.getLife();
final int life2 = p2.getLife();
final int diff = Math.abs(life1 - life2);
if (sa.hasParam("RememberDifference")) {
final int diff = life1 - life2;
source.addRemembered(diff);
if (life2 > life1) {
// swap players
Player tmp = p2;
p2 = p1;
p1 = tmp;
}
final Map<Player, Integer> lossMap = Maps.newHashMap();
if ((life1 > life2) && p1.canLoseLife() && p2.canGainLife()) {
final int diff = life1 - life2;
if (diff > 0 && p1.canLoseLife() && p2.canGainLife()) {
final int lost = p1.loseLife(diff, false, false);
p2.gainLife(diff, source, sa);
if (lost > 0) {
final Map<Player, Integer> lossMap = Maps.newHashMap();
lossMap.put(p1, lost);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPIMap(lossMap);
source.getGame().getTriggerHandler().runTrigger(TriggerType.LifeLostAll, runParams, false);
if (sa.hasParam("RememberOwnLoss") && p1.equals(sa.getActivatingPlayer())) {
source.addRemembered(lost);
}
}
} else if ((life2 > life1) && p2.canLoseLife() && p1.canGainLife()) {
final int diff = life2 - life1;
final int lost = p2.loseLife(diff, false, false);
p1.gainLife(diff, source, sa);
if (lost > 0) {
lossMap.put(p2, lost);
}
} else {
// they are equal or can't be exchanged, so nothing to do
}
if (!lossMap.isEmpty()) { // Run triggers if any player actually lost life
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPIMap(lossMap);
source.getGame().getTriggerHandler().runTrigger(TriggerType.LifeLostAll, runParams, false);
if (sa.hasParam("RememberDifference")) {
source.addRemembered(p1.getLife() - p2.getLife());
}
}
}

View File

@@ -271,10 +271,11 @@ public class ManaEffect extends SpellAbilityEffect {
producedMana.append(abMana.produceMana(mana, p, sa));
}
abMana.tapsForMana(sa.getRootAbility(), producedMana.toString());
// Only clear express choice after mana has been produced
abMana.clearExpressChoice();
abMana.tapsForMana(sa.getRootAbility(), producedMana.toString());
if (sa.isKeyword(Keyword.FIREBENDING)) {
activator.triggerElementalBend(TriggerType.Firebend);
}

View File

@@ -15,8 +15,9 @@ public class PermanentCreatureEffect extends PermanentEffect {
public String getStackDescription(final SpellAbility sa) {
final CardState source = sa.getCardState();
final StringBuilder sb = new StringBuilder();
sb.append(CardTranslation.getTranslatedName(source.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" ").append(source.getBasePowerString());
sb.append(" / ").append(source.getBaseToughnessString());
sb.append(CardTranslation.getTranslatedName(source.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" ");
sb.append(sa.getParamOrDefault("SetPower", source.getBasePowerString()));
sb.append(" / ").append(sa.getParamOrDefault("SetToughness", source.getBaseToughnessString()));
return sb.toString();
}
}

View File

@@ -17,7 +17,6 @@ import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardUtil;
import forge.game.card.perpetual.PerpetualKeywords;
@@ -282,6 +281,17 @@ public class PumpEffect extends SpellAbilityEffect {
List<Card> tgtCards = getCardsfromTargets(sa);
List<Player> tgtPlayers = getTargetPlayers(sa);
if (sa.hasParam("Optional")) {
final String targets = Lang.joinHomogenous(tgtCards);
final String message = sa.hasParam("OptionQuestion")
? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets)
: Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets);
if (!activator.getController().confirmAction(sa, null, message, null)) {
return;
}
}
List<String> keywords = Lists.newArrayList();
if (sa.hasParam("KW")) {
keywords.addAll(Arrays.asList(sa.getParam("KW").split(" & ")));
@@ -307,8 +317,6 @@ public class PumpEffect extends SpellAbilityEffect {
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, host, sa);
}
final CardCollection untargetedCards = CardUtil.getRadiance(sa);
if (sa.hasParam("DefinedKW")) {
String defined = sa.getParam("DefinedKW");
if (defined.equals("ChosenType")) {
@@ -394,17 +402,6 @@ public class PumpEffect extends SpellAbilityEffect {
keywords = choice;
}
if (sa.hasParam("Optional")) {
final String targets = Lang.joinHomogenous(tgtCards);
final String message = sa.hasParam("OptionQuestion")
? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets)
: Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets);
if (!activator.getController().confirmAction(sa, null, message, null)) {
return;
}
}
if (sa.hasParam("RememberObjects")) {
host.addRemembered(AbilityUtils.getDefinedObjects(host, sa.getParam("RememberObjects"), sa));
}
@@ -494,7 +491,7 @@ public class PumpEffect extends SpellAbilityEffect {
registerDelayedTrigger(sa, sa.getParam("AtEOT"), tgtCards);
}
for (final Card tgtC : untargetedCards) {
for (final Card tgtC : CardUtil.getRadiance(sa)) {
// only pump things in PumpZone
if (!tgtC.isInZones(pumpZones)) {
continue;

View File

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

View File

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

View File

@@ -203,6 +203,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
private boolean unearthed;
private boolean ringbearer;
private boolean monstrous;
private boolean harnessed;
private boolean renowned;
private boolean solved;
private boolean tributed;
@@ -2462,10 +2463,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
}
sbLong.append("Enchant ").append(desc).append("\r\n");
} else if (keyword.startsWith("Ripple")) {
sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n");
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")
|| keyword.startsWith("Disguise")
|| keyword.startsWith("Disguise") || keyword.startsWith("Reflect")
|| keyword.startsWith("Escape") || keyword.startsWith("Foretell:")
|| keyword.startsWith("Madness:")|| keyword.startsWith("Recover")
|| keyword.startsWith("Reconfigure") || keyword.startsWith("Squad")
@@ -2504,17 +2503,15 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
sbLong.append(".");
}
if (k.length > 3) {
sbLong.append(". " + k[3]);
sbLong.append(". ").append(k[3]);
}
}
sbLong.append(" (").append(inst.getReminderText()).append(")");
sbLong.append("\r\n");
} else if (keyword.equals("Mayhem")) {
sbLong.append(" (").append(inst.getReminderText()).append(")");
sbLong.append("\r\n");
}
} else if (keyword.startsWith("Reflect")) {
final String[] k = keyword.split(":");
sbLong.append(k[0]).append(" ").append(ManaCostParser.parse(k[1]));
sbLong.append(" (").append(inst.getReminderText()).append(")");
sbLong.append("\r\n");
} else if (keyword.startsWith("Echo")) {
sbLong.append("Echo ");
final String[] upkeepCostParams = keyword.split(":");
@@ -2653,7 +2650,7 @@ 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("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Ripple")) {
final String[] k = keyword.split(":");
sbLong.append(k[0]).append(" ").append(k[1]).append(" (").append(inst.getReminderText()).append(")");
} else if (keyword.startsWith("Crew")) {
@@ -2969,6 +2966,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (monstrous) {
sb.append("Monstrous\r\n");
}
if (harnessed) {
sb.append("Harnessed\r\n");
}
if (renowned) {
sb.append("Renowned\r\n");
}
@@ -3561,7 +3561,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (!getStaticAbilities().isEmpty()) {
return false;
}
if (!getReplacementEffects().isEmpty()) {
if (!getReplacementEffects().isEmpty()
&& (getReplacementEffects().size() > 1 || !isSaga() || hasKeyword(Keyword.READ_AHEAD))) {
return false;
}
if (!getTriggers().isEmpty()) {
@@ -6441,10 +6442,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
DamageType damageType = DamageType.Normal;
if (isPlaneswalker()) { // 120.3c
subtractCounter(CounterType.get(CounterEnumType.LOYALTY), damageIn, null, true);
subtractCounter(CounterEnumType.LOYALTY, damageIn, null, true);
}
if (isBattle()) {
subtractCounter(CounterType.get(CounterEnumType.DEFENSE), damageIn, null, true);
subtractCounter(CounterEnumType.DEFENSE, damageIn, null, true);
}
if (isCreature()) {
if (source.isWitherDamage()) { // 120.3d
@@ -6692,6 +6693,14 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
setRingBearer(false);
}
public final boolean isHarnessed() {
return harnessed;
}
public final boolean setHarnessed(final boolean harnessed0) {
harnessed = harnessed0;
return true;
}
public final boolean isMonstrous() {
return monstrous;
}
@@ -6849,6 +6858,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return exiledSA.isKeyword(Keyword.WARP);
}
public boolean isWebSlinged() {
return getCastSA() != null & getCastSA().isAlternativeCost(AlternativeCost.WebSlinging);
}
public boolean isSpecialized() {
return specialized;
}
@@ -7144,7 +7157,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return false;
}
if (StaticAbilityCantTarget.cantTarget(this, sa)) {
if (StaticAbilityCantTarget.cantTarget(this, sa) != null) {
return false;
}

View File

@@ -376,22 +376,28 @@ public class CardFactory {
}
}
// Build English oracle and translated oracle mapping
// Negative card Id's are for view purposes only
if (c.getId() >= 0) {
// Build English oracle and translated oracle mapping
CardTranslation.buildOracleMapping(face.getName(), face.getOracleText(), variantName);
}
// Name first so Senty has the Card name
// Set name for Sentry reports to be identifiable
c.setName(face.getName());
for (Entry<String, String> v : face.getVariables()) c.setSVar(v.getKey(), v.getValue());
if (c.getId() >= 0) { // Set Triggers & Abilities if not for view
for (Entry<String, String> v : face.getVariables())
c.setSVar(v.getKey(), v.getValue());
for (String r : face.getReplacements())
c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true, c.getCurrentState()));
for (String s : face.getStaticAbilities())
c.addStaticAbility(s);
for (String t : face.getTriggers())
c.addTrigger(TriggerHandler.parseTrigger(t, c, true, c.getCurrentState()));
for (String r : face.getReplacements()) c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true, c.getCurrentState()));
for (String s : face.getStaticAbilities()) c.addStaticAbility(s);
for (String t : face.getTriggers()) c.addTrigger(TriggerHandler.parseTrigger(t, c, true, c.getCurrentState()));
// keywords not before variables
c.addIntrinsicKeywords(face.getKeywords(), false);
// keywords not before variables
c.addIntrinsicKeywords(face.getKeywords(), false);
}
if (face.getDraftActions() != null) {
face.getDraftActions().forEach(c::addDraftAction);
}
@@ -420,7 +426,8 @@ public class CardFactory {
c.setAttractionLights(face.getAttractionLights());
CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities());
if (c.getId() > 0) // Set FactoryAbilities if not for view
CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities());
}
public static void copySpellAbility(SpellAbility from, SpellAbility to, final Card host, final Player p, final boolean lki, final boolean keepTextChanges) {

View File

@@ -2568,7 +2568,7 @@ public class CardFactoryUtil {
} else if (keyword.equals("Sunburst")) {
// Rule 702.43a If this object is entering the battlefield as a creature,
// ignoring any type-changing effects that would affect it
CounterType t = CounterType.get(host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE);
CounterType t = host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE;
StringBuilder sb = new StringBuilder("etbCounter:");
sb.append(t).append(":Sunburst:no Condition:");
@@ -2776,10 +2776,9 @@ public class CardFactoryUtil {
final String cost = params[1];
final StringBuilder sbAttach = new StringBuilder();
sbAttach.append("SP$ Attach | Cost$ ");
sbAttach.append("SP$ Attach | ValidTgts$ Creature | Cost$ ");
sbAttach.append(cost);
sbAttach.append(" | AILogic$ ").append(params.length > 2 ? params[2] : "Pump");
sbAttach.append(" | Bestow$ True | ValidTgts$ Creature");
final SpellAbility sa = AbilityFactory.getAbility(sbAttach.toString(), card);
final StringBuilder sbDesc = new StringBuilder();
@@ -4113,7 +4112,7 @@ public class CardFactoryUtil {
sbValid.append("| ").append(param).append(k[1]);
}
String effect = "Mode$ CantTarget | ValidCard$ Card.Self | Secondary$ True"
String effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True"
+ sbValid.toString() + " | Activator$ Opponent | Description$ "
+ sbDesc.toString() + " (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
@@ -4154,7 +4153,7 @@ public class CardFactoryUtil {
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
// Target
effect = "Mode$ CantTarget | Protection$ True | ValidCard$ Card.Self | Secondary$ True ";
effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True ";
if (!valid.isEmpty()) {
effect += "| ValidSource$ " + valid;
}
@@ -4162,7 +4161,7 @@ public class CardFactoryUtil {
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
// Attach
effect = "Mode$ CantAttach | Protection$ True | Target$ Card.Self | Secondary$ True ";
effect = "Mode$ CantAttach | Target$ Card.Self | Secondary$ True ";
if (!valid.isEmpty()) {
effect += "| ValidCard$ " + valid;
}
@@ -4183,7 +4182,7 @@ public class CardFactoryUtil {
" | Description$ Chapter abilities of this Saga can't trigger the turn it entered the battlefield unless it has exactly the number of lore counters on it specified in the chapter symbol of that ability.";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Shroud")) {
String effect = "Mode$ CantTarget | ValidCard$ Card.Self | Secondary$ True"
String effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True"
+ " | Description$ Shroud (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Skulk")) {

View File

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

View File

@@ -1244,7 +1244,8 @@ public class CardProperty {
if (property.contains("ControlledBy")) {
FCollectionView<Player> p = AbilityUtils.getDefinedPlayers(source, property.split("ControlledBy")[1], spellAbility);
cards = CardLists.filterControlledBy(cards, p);
if (!cards.contains(card)) {
// Kraven the Hunter LTB trigger
if (!card.isLKI() && !cards.contains(card)) {
return false;
}
}
@@ -1819,6 +1820,10 @@ public class CardProperty {
if (!card.isWarped()) {
return false;
}
} else if (property.equals("webSlinged")) {
if (!card.isWebSlinged()) {
return false;
}
} else if (property.equals("CrewedThisTurn")) {
if (!hasTimestampMatch(card, source.getCrewedByThisTurn())) return false;
} else if (property.equals("CrewedBySourceThisTurn")) {
@@ -1827,6 +1832,10 @@ public class CardProperty {
if (card.getDevouredCards().isEmpty()) {
return false;
}
} else if (property.equals("harnessed")) {
if (!card.isHarnessed()) {
return false;
}
} else if (property.equals("IsMonstrous")) {
if (!card.isMonstrous()) {
return false;

View File

@@ -468,6 +468,9 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
return Iterables.getFirst(getIntrinsicSpellAbilities(), null);
}
public final SpellAbility getFirstSpellAbility() {
if (this.card.getCastSA() != null) {
return this.card.getCastSA();
}
return Iterables.getFirst(getNonManaAbilities(), null);
}
@@ -605,18 +608,18 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
result.add(loyaltyRep);
}
if (type.isBattle()) {
// TODO This is currently breaking for Battle/Defense
// Going to script the cards to work but ideally it would happen here
if (defenseRep == null) {
defenseRep = CardFactoryUtil.makeEtbCounter("etbCounter:DEFENSE:" + this.baseDefense, this, true);
}
result.add(defenseRep);
// TODO add Siege "Choose a player to protect it"
}
card.updateReplacementEffects(result, this);
// below are global rules
if (type.hasSubtype("Saga") && !hasKeyword(Keyword.READ_AHEAD)) {
if (sagaRep == null) {
sagaRep = CardFactoryUtil.makeEtbCounter("etbCounter:LORE:1", this, true);
sagaRep = CardFactoryUtil.makeEtbCounter("etbCounter:LORE:1", this, false);
}
result.add(sagaRep);
}
@@ -633,7 +636,6 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
result.add(omenRep);
}
card.updateReplacementEffects(result, this);
return result;
}
public boolean addReplacementEffect(final ReplacementEffect replacementEffect) {

View File

@@ -60,13 +60,12 @@ public class CardView extends GameEntityView {
}
public static TrackableCollection<CardView> getCollection(Iterable<Card> cards) {
if (cards == null) {
return null;
}
TrackableCollection<CardView> collection = new TrackableCollection<>();
for (Card c : cards) {
if (c.getRenderForUI()) { //only add cards that match their card for UI
collection.add(c.getView());
if (cards != null) {
for (Card c : cards) {
if (c != null && c.getRenderForUI()) { //only add cards that match their card for UI
collection.add(c.getView());
}
}
}
return collection;

View File

@@ -28,7 +28,7 @@ import java.util.Locale;
* @author Clemens Koza
* @version V0.0 17.02.2010
*/
public enum CounterEnumType {
public enum CounterEnumType implements CounterType {
M1M1("-1/-1", "-1/-1", 255, 110, 106),
P1P1("+1/+1", "+1/+1", 96, 226, 23),
@@ -167,6 +167,8 @@ public enum CounterEnumType {
FILIBUSTER("FLBTR", 255, 179, 119),
FILM("FILM", 255, 255, 255),
FINALITY("FINAL", 255, 255, 255),
FIRE("FIRE", 240, 30, 35),
@@ -555,4 +557,14 @@ public enum CounterEnumType {
public static final ImmutableList<CounterEnumType> values = ImmutableList.copyOf(values());
@Override
public boolean is(CounterEnumType eType) {
return this == eType;
}
@Override
public boolean isKeywordCounter() {
return false;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
package forge.game.combat;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -39,10 +38,12 @@ public class AttackRequirement {
//MustAttack static check
final List<GameEntity> mustAttack = StaticAbilityMustAttack.entitiesMustAttack(attacker);
nAttackAnything += Collections.frequency(mustAttack, attacker);
for (GameEntity e : mustAttack) {
if (e.equals(attacker)) continue;
defenderSpecific.add(e);
if (e.equals(attacker)) {
nAttackAnything++;
} else {
defenderSpecific.add(e);
}
}
for (final GameEntity defender : possibleDefenders) {

View File

@@ -225,7 +225,7 @@ public class CombatUtil {
if (!ge.equals(defender) && ge instanceof Player) {
// found a player which does not goad that creature
// and creature can attack this player or planeswalker
if (!attacker.isGoadedBy((Player) ge) && !ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) {
if (!attacker.isGoadedBy((Player) ge) && canAttack(attacker, ge)) {
return false;
}
}
@@ -233,17 +233,6 @@ public class CombatUtil {
}
}
// Quasi-goad logic for "Kardur, Doomscourge" etc. that isn't goad but behaves the same
if (defender != null && defender.hasKeyword("Creatures your opponents control attack a player other than you if able.")) {
for (GameEntity ge : getAllPossibleDefenders(attacker.getController())) {
if (!ge.equals(defender) && ge instanceof Player) {
if (!ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) {
return false;
}
}
}
}
// CantAttack static abilities
if (StaticAbilityCantAttackBlock.cantAttack(attacker, defender)) {
return false;

View File

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

View File

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

View File

@@ -86,7 +86,7 @@ public class CostUntapType extends CostPartWithList {
if (!canUntapSource) {
typeList.remove(source);
}
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
final int amount = this.getAbilityAmount(ability);
return (typeList.size() != 0) && (typeList.size() >= amount);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,11 +5,9 @@ import forge.game.card.Card;
import java.util.Arrays;
import java.util.Collection;
public class GameEventCardRegenerated extends GameEvent {
public final Collection<Card> cards;
public record GameEventCardRegenerated(Collection<Card> cards) implements GameEvent {
public GameEventCardRegenerated(Card affected) {
cards = Arrays.asList(affected);
this(Arrays.asList(affected));
}
@Override

View File

@@ -1,9 +1,19 @@
package forge.game.event;
public class GameEventCardSacrificed extends GameEvent {
import forge.game.card.Card;
public record GameEventCardSacrificed(Card card) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "" + card.getController() + " sacrificed " + card;
}
}

View File

@@ -11,23 +11,20 @@ import forge.game.card.Card;
/**
* This means card's characteristics have changed on server, clients must re-request them
*/
public class GameEventCardStatsChanged extends GameEvent {
public record GameEventCardStatsChanged(Collection<Card> cards, boolean transform) implements GameEvent {
public final Collection<Card> cards;
public boolean transform = false;
public GameEventCardStatsChanged(Card affected) {
this(affected, false);
}
public GameEventCardStatsChanged(Card affected, boolean isTransform) {
cards = Arrays.asList(affected);
this(Arrays.asList(affected), false);
//the transform should only fire once so the flip effect sound will trigger once every transformation...
// disable for now
transform = false;
}
public GameEventCardStatsChanged(Collection<Card> affected) {
cards = affected;
this(affected, false);
}
/* (non-Javadoc)
@@ -35,7 +32,6 @@ public class GameEventCardStatsChanged extends GameEvent {
*/
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
// TODO Auto-generated method stub
return visitor.visit(this);
}

View File

@@ -2,17 +2,18 @@ package forge.game.event;
import forge.game.card.Card;
public class GameEventCardTapped extends GameEvent {
public final boolean tapped;
public final Card card;
public GameEventCardTapped(final Card card, final boolean tapped) {
this.tapped = tapped;
this.card = card;
}
public record GameEventCardTapped(Card card, boolean tapped) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "" + card.getController() + (tapped ? " tapped " : " untapped ") + card;
}
}

View File

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

View File

@@ -4,19 +4,18 @@ import java.util.List;
import forge.game.card.Card;
public class GameEventCombatEnded extends GameEvent {
public final List<Card> attackers;
public final List<Card> blockers;
public GameEventCombatEnded(List<Card> attackers, List<Card> blockers) {
this.attackers = attackers;
this.blockers = blockers;
}
public record GameEventCombatEnded(List<Card> attackers, List<Card> blockers) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Combat ended. Attackers: " + attackers + " Blockers: " + blockers;
}
}

View File

@@ -4,15 +4,7 @@ import java.util.List;
import forge.game.card.Card;
public class GameEventCombatUpdate extends GameEvent {
public final List<Card> attackers;
public final List<Card> blockers;
public GameEventCombatUpdate(List<Card> attackers, List<Card> blockers) {
this.attackers = attackers;
this.blockers = blockers;
}
public record GameEventCombatUpdate(List<Card> attackers, List<Card> blockers) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {

View File

@@ -1,11 +1,6 @@
package forge.game.event;
public class GameEventDayTimeChanged extends GameEvent {
public final boolean daytime;
public GameEventDayTimeChanged(final boolean daytime) {
this.daytime = daytime;
}
public record GameEventDayTimeChanged(boolean daytime) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {

View File

@@ -6,18 +6,7 @@ import forge.game.player.Player;
import forge.util.CardTranslation;
import forge.util.Lang;
public class GameEventDoorChanged extends GameEvent {
public final Player activatingPlayer;
public final Card card;
public final CardStateName state;
public boolean unlock;
public GameEventDoorChanged(Player player, Card c, CardStateName state, boolean unlock) {
activatingPlayer = player;
card = c;
this.state = state;
this.unlock = unlock;
}
public record GameEventDoorChanged(Player activatingPlayer, Card card, CardStateName state, boolean unlock) implements GameEvent {
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {

View File

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

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