mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-13 17:27:46 +00:00
Compare commits
860 Commits
additional
...
paperCardI
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f14454e992 | ||
|
|
e4799a4f73 | ||
|
|
7fa89cbc2e | ||
|
|
567e13c92b | ||
|
|
6f5a933de3 | ||
|
|
e40567c9c8 | ||
|
|
928ac875b5 | ||
|
|
f4831bc51b | ||
|
|
d3e0e79325 | ||
|
|
ac3015eff1 | ||
|
|
554e73e352 | ||
|
|
fe86bb1be3 | ||
|
|
f98eca3925 | ||
|
|
8506fd2bab | ||
|
|
c2ffa227e2 | ||
|
|
f33f780b25 | ||
|
|
855e4dcabd | ||
|
|
40c9a06b21 | ||
|
|
7c4cf9425f | ||
|
|
d2a6329e03 | ||
|
|
6e2717e371 | ||
|
|
2152b7ca7d | ||
|
|
08387f12cb | ||
|
|
01800d3c49 | ||
|
|
2b3735662e | ||
|
|
5ab2b44343 | ||
|
|
0bdb9d7d27 | ||
|
|
1f4cebf186 | ||
|
|
f651a7d73d | ||
|
|
453bae6849 | ||
|
|
292cca0727 | ||
|
|
24f568101f | ||
|
|
ad73651382 | ||
|
|
476f3dfe9c | ||
|
|
5977129096 | ||
|
|
44e243d428 | ||
|
|
491e275b8a | ||
|
|
7bf6ba0779 | ||
|
|
ea33d589ae | ||
|
|
1314c98a5a | ||
|
|
66c1c485f2 | ||
|
|
fe1d37bed6 | ||
|
|
64d54dc7c7 | ||
|
|
050cebf944 | ||
|
|
42e5d9437b | ||
|
|
67aaf9902e | ||
|
|
3d0436aeca | ||
|
|
827d60610d | ||
|
|
69a1d76778 | ||
|
|
7bd5dbbe28 | ||
|
|
39613a8413 | ||
|
|
225ff335e8 | ||
|
|
23a3de5446 | ||
|
|
75fd7bbfda | ||
|
|
ed6a14e180 | ||
|
|
42b6d2c17d | ||
|
|
9a93f0a16c | ||
|
|
1c0d3031d3 | ||
|
|
724697391c | ||
|
|
7820c3f519 | ||
|
|
fe80b3850e | ||
|
|
16a2ae6741 | ||
|
|
ce19c4fb9d | ||
|
|
4911a7f951 | ||
|
|
c6f994d47a | ||
|
|
f0a9077791 | ||
|
|
b6941a9b38 | ||
|
|
7023eb10d3 | ||
|
|
007e132559 | ||
|
|
9b9fdb2e5e | ||
|
|
f1f4310608 | ||
|
|
8634f96e4b | ||
|
|
6eed4c31a2 | ||
|
|
c7b9934072 | ||
|
|
cc50d72d63 | ||
|
|
fbceab3252 | ||
|
|
780491ae4b | ||
|
|
994471e9b6 | ||
|
|
e1bf639e90 | ||
|
|
4386cead3e | ||
|
|
9b0d0f7924 | ||
|
|
b9be1bc647 | ||
|
|
513e4c6c8d | ||
|
|
0f5b67a504 | ||
|
|
72f455067f | ||
|
|
6dc508b631 | ||
|
|
deecf128c3 | ||
|
|
5f497669e5 | ||
|
|
90952d95c9 | ||
|
|
5a5ef52492 | ||
|
|
3b4df7211c | ||
|
|
45329a6df0 | ||
|
|
22607adf57 | ||
|
|
bd9c72fee9 | ||
|
|
35641265dd | ||
|
|
fe37a8358b | ||
|
|
bb7ae64ef8 | ||
|
|
4d9db78cc3 | ||
|
|
ce46c684b5 | ||
|
|
29d4e716f5 | ||
|
|
c6bfdd5b78 | ||
|
|
559daf9fce | ||
|
|
9caa024fa5 | ||
|
|
3605b4e34e | ||
|
|
56832ff987 | ||
|
|
338bb09747 | ||
|
|
b57a5e9ad1 | ||
|
|
67f8f4760e | ||
|
|
485427c682 | ||
|
|
8852732e6b | ||
|
|
6264761117 | ||
|
|
a3b1c98abf | ||
|
|
c566a4bed8 | ||
|
|
a16e2a9c37 | ||
|
|
f80bb13ed7 | ||
|
|
1c350b1766 | ||
|
|
0b5ce9c8fc | ||
|
|
74c7f4b164 | ||
|
|
6128f1d720 | ||
|
|
9e2dcbb630 | ||
|
|
ec9fc88734 | ||
|
|
59c404f6c4 | ||
|
|
fd727a909b | ||
|
|
ce6ad65e12 | ||
|
|
6b65f8972c | ||
|
|
2e7fe8a81b | ||
|
|
6f9db790a6 | ||
|
|
645aff52cb | ||
|
|
4d4afbdf03 | ||
|
|
a8c1f5c969 | ||
|
|
0f4c94d6f8 | ||
|
|
53479c60b4 | ||
|
|
0c7b8c5b04 | ||
|
|
7860957d8f | ||
|
|
07bc31f4c1 | ||
|
|
99390f5967 | ||
|
|
ee8ca02128 | ||
|
|
1bc7efba65 | ||
|
|
efc2357905 | ||
|
|
d5d01011f4 | ||
|
|
c1a19ea4ae | ||
|
|
ad3fb1137a | ||
|
|
c5c6b36f4f | ||
|
|
f2f92212bc | ||
|
|
3226be58be | ||
|
|
46cfcff0ca | ||
|
|
b161c9612b | ||
|
|
91dee5c379 | ||
|
|
349129f88f | ||
|
|
57ea25bbbd | ||
|
|
d1725af64d | ||
|
|
72626ef214 | ||
|
|
e00d5ee30b | ||
|
|
d6c42c3c8c | ||
|
|
950a1f2a44 | ||
|
|
c895b0eeab | ||
|
|
82c8fb20e8 | ||
|
|
cf54f3e04e | ||
|
|
f4958a4b49 | ||
|
|
760f9412ff | ||
|
|
71f2d41eb0 | ||
|
|
51d7933ef3 | ||
|
|
f5938b47e1 | ||
|
|
807d078799 | ||
|
|
bedb97183b | ||
|
|
08919e3375 | ||
|
|
a117d65f51 | ||
|
|
b61e3015f3 | ||
|
|
83a512a075 | ||
|
|
c1630a2e47 | ||
|
|
1d0b50356f | ||
|
|
43d82ce1ce | ||
|
|
c4a8765d6c | ||
|
|
15d49bc1b1 | ||
|
|
d84712b65d | ||
|
|
eac94f7249 | ||
|
|
d134c3dfcd | ||
|
|
0a03327299 | ||
|
|
0a673aeadc | ||
|
|
40f7a9a22a | ||
|
|
e2ccf8960a | ||
|
|
66acc6b920 | ||
|
|
83abcf7c44 | ||
|
|
ef9d807b6e | ||
|
|
83f334888e | ||
|
|
8359cfda35 | ||
|
|
93b2d7c795 | ||
|
|
ea74cc8f7f | ||
|
|
4e16a5ea60 | ||
|
|
00325fb511 | ||
|
|
fe042fa6d9 | ||
|
|
6830f15859 | ||
|
|
fb3a8ce003 | ||
|
|
edc6edc44b | ||
|
|
40b3edd426 | ||
|
|
f50aea1576 | ||
|
|
4ab67b1d3b | ||
|
|
a4e4525769 | ||
|
|
1d664145e0 | ||
|
|
32355499aa | ||
|
|
a475855c4c | ||
|
|
598d3a4958 | ||
|
|
35a4053212 | ||
|
|
9d770a9bca | ||
|
|
de15ab3a71 | ||
|
|
35bc80767f | ||
|
|
ef28a92dff | ||
|
|
99f3ccb8d6 | ||
|
|
16148ec992 | ||
|
|
93f7987312 | ||
|
|
c7816daaf6 | ||
|
|
bc458ecde4 | ||
|
|
e4da62c254 | ||
|
|
53588dfc24 | ||
|
|
61c11b38a0 | ||
|
|
fdcc33198b | ||
|
|
dd7a0e99e2 | ||
|
|
cb0e594a6e | ||
|
|
059881a7b5 | ||
|
|
ca59ac925c | ||
|
|
0b5f14cd9c | ||
|
|
1d987c25e9 | ||
|
|
5cb68bc2f0 | ||
|
|
1715efced7 | ||
|
|
aeb39b99bb | ||
|
|
778066a622 | ||
|
|
75a056abe6 | ||
|
|
3915f316e2 | ||
|
|
b52646ee7e | ||
|
|
cb1d48a566 | ||
|
|
733d56246e | ||
|
|
7902ad71d7 | ||
|
|
2666de0adb | ||
|
|
c2613a03b0 | ||
|
|
b077cca3ac | ||
|
|
43fc281e65 | ||
|
|
7b1f5d0a34 | ||
|
|
330fe0a0a8 | ||
|
|
30a4c24c35 | ||
|
|
01a6a38285 | ||
|
|
fe5aa37791 | ||
|
|
42f1fba7c4 | ||
|
|
0c6f1ff58f | ||
|
|
8678b5ec5b | ||
|
|
71256972c4 | ||
|
|
a76f23fc25 | ||
|
|
3c7993d640 | ||
|
|
71d984a5ff | ||
|
|
be171c011a | ||
|
|
becdd180f4 | ||
|
|
41efdb095a | ||
|
|
86453a6fa7 | ||
|
|
8448a6e20e | ||
|
|
194662c9c7 | ||
|
|
d3a33a0092 | ||
|
|
2bab13247c | ||
|
|
da30b886c9 | ||
|
|
da38641ff8 | ||
|
|
fcbc83c3ff | ||
|
|
4ae93980b6 | ||
|
|
10b96138c3 | ||
|
|
8706edbb01 | ||
|
|
4f5c074fe6 | ||
|
|
50ca71fc87 | ||
|
|
84134341e3 | ||
|
|
988518284c | ||
|
|
1476621df1 | ||
|
|
cb8e13e933 | ||
|
|
ea4d878a2d | ||
|
|
eeafc6400c | ||
|
|
875b4557b0 | ||
|
|
bec25c68f2 | ||
|
|
f98aae35a5 | ||
|
|
b1acb9f0a3 | ||
|
|
89049db6a0 | ||
|
|
77378855e3 | ||
|
|
709582086f | ||
|
|
7eda9e204e | ||
|
|
cf94d96d95 | ||
|
|
a78d361b2e | ||
|
|
d01619c09c | ||
|
|
fd76098765 | ||
|
|
9fb08f1695 | ||
|
|
4eef86daf1 | ||
|
|
d74d2da755 | ||
|
|
f69fd12ebd | ||
|
|
d41cdca1bd | ||
|
|
acfc413465 | ||
|
|
7a2ef6c9fd | ||
|
|
b214b6f8bf | ||
|
|
4d75f7b225 | ||
|
|
b7db4b4cbc | ||
|
|
efaca50d97 | ||
|
|
0157113103 | ||
|
|
bf70d8e6f6 | ||
|
|
30ac5ae381 | ||
|
|
dd94169c9a | ||
|
|
4c24e7248d | ||
|
|
40eb8de90c | ||
|
|
8f60973d95 | ||
|
|
bc9df8872f | ||
|
|
1071089879 | ||
|
|
28ae7fa729 | ||
|
|
e9c5bc46ae | ||
|
|
9308dac257 | ||
|
|
f5fa87410b | ||
|
|
c3167a9928 | ||
|
|
c533bee4b3 | ||
|
|
01a3b2723f | ||
|
|
8640a2aaad | ||
|
|
9f5e5f3068 | ||
|
|
3db209f128 | ||
|
|
351a94281c | ||
|
|
f190e7e678 | ||
|
|
e01d3e0a6d | ||
|
|
cb3fcb259f | ||
|
|
0940e7433b | ||
|
|
8e48ff3dfa | ||
|
|
07b10f736b | ||
|
|
d0e4e5bd17 | ||
|
|
fd082ffabb | ||
|
|
520dcb88d6 | ||
|
|
a4138ebd4d | ||
|
|
1118f5b135 | ||
|
|
e78f921e4d | ||
|
|
a6ec235052 | ||
|
|
613cc549f7 | ||
|
|
69c7083592 | ||
|
|
7f64e25691 | ||
|
|
2cb107380d | ||
|
|
2a4eaea90a | ||
|
|
8fdbaf3611 | ||
|
|
55edec665f | ||
|
|
f11c6a7038 | ||
|
|
36150be7f3 | ||
|
|
b861994d2f | ||
|
|
0242189012 | ||
|
|
69ddfdc18c | ||
|
|
bef56d1caa | ||
|
|
c7f43e245d | ||
|
|
939b7a22e0 | ||
|
|
a79a3503f5 | ||
|
|
9109207495 | ||
|
|
dbac513027 | ||
|
|
3152ac93f6 | ||
|
|
fc56076f81 | ||
|
|
ecc763faf6 | ||
|
|
0130367873 | ||
|
|
c36b9b7610 | ||
|
|
774ab38335 | ||
|
|
251ce2f83b | ||
|
|
2af47fe6df | ||
|
|
332c982695 | ||
|
|
eb970665a6 | ||
|
|
4714319204 | ||
|
|
c85214b9e3 | ||
|
|
d5854c9d1c | ||
|
|
a3a2a5dd1b | ||
|
|
8f155c9cca | ||
|
|
366ed643a7 | ||
|
|
e7becacd57 | ||
|
|
2cf9293ab3 | ||
|
|
88300de6e5 | ||
|
|
a4cb0924f3 | ||
|
|
e0001f8348 | ||
|
|
e380590c4c | ||
|
|
7410a2844f | ||
|
|
b4af62eda7 | ||
|
|
9c14ba49a3 | ||
|
|
25900ee10c | ||
|
|
443ba2c1e0 | ||
|
|
6ade60cd59 | ||
|
|
436697e0b3 | ||
|
|
e7d8664386 | ||
|
|
e2e3c658a0 | ||
|
|
2a4ea4cb5d | ||
|
|
93215e6ce4 | ||
|
|
f4a0de6392 | ||
|
|
579033abb3 | ||
|
|
8328866645 | ||
|
|
890ce2505c | ||
|
|
7e6697d100 | ||
|
|
84d793b786 | ||
|
|
386550da39 | ||
|
|
6d3d11398d | ||
|
|
5db9c189ba | ||
|
|
8d3e3cb253 | ||
|
|
2042aaa033 | ||
|
|
6fc5d218c9 | ||
|
|
6332f5cbfc | ||
|
|
44340998fd | ||
|
|
5274c976ef | ||
|
|
280d2fed6d | ||
|
|
9ecea646d2 | ||
|
|
c4e5101a42 | ||
|
|
48a555817d | ||
|
|
2cc2ae421a | ||
|
|
9109b26484 | ||
|
|
98e5eb9652 | ||
|
|
4e93f95dff | ||
|
|
44e1332fb4 | ||
|
|
e2972acad0 | ||
|
|
76086ffd35 | ||
|
|
3c3c47616b | ||
|
|
de5a660a6a | ||
|
|
d348a72ae4 | ||
|
|
4055512698 | ||
|
|
b4b2346fb8 | ||
|
|
caa5443874 | ||
|
|
c7fb0bd3c3 | ||
|
|
1f5f62b21a | ||
|
|
04937b6447 | ||
|
|
94ea88b171 | ||
|
|
2188582e16 | ||
|
|
fbc73fa22b | ||
|
|
31790c3dc9 | ||
|
|
cdf3038ab6 | ||
|
|
ff1781b734 | ||
|
|
49d7351eac | ||
|
|
3b372eead7 | ||
|
|
c815f7ed0c | ||
|
|
04ac8d8de9 | ||
|
|
6b961ed3e1 | ||
|
|
17a2a23981 | ||
|
|
74e83f2a94 | ||
|
|
8e25dd0e25 | ||
|
|
7f9260f54c | ||
|
|
505d4b4f9a | ||
|
|
baca196066 | ||
|
|
b08cf3bf5b | ||
|
|
b5ee3eb43c | ||
|
|
081e1a2960 | ||
|
|
d0310e257b | ||
|
|
a3733f1fa8 | ||
|
|
b4bd7947f9 | ||
|
|
b56da433eb | ||
|
|
64b5906f08 | ||
|
|
f7d94fabc9 | ||
|
|
87e0810603 | ||
|
|
93f2fa8f43 | ||
|
|
c86bf402fd | ||
|
|
9926004cf1 | ||
|
|
d0c24f49a9 | ||
|
|
661f3b8e7a | ||
|
|
14249150a0 | ||
|
|
93dccdeace | ||
|
|
706ef4ac6c | ||
|
|
bd3994a217 | ||
|
|
e722c4b63c | ||
|
|
a062e0040d | ||
|
|
7fdd645026 | ||
|
|
3670891ec9 | ||
|
|
6f3dd8deba | ||
|
|
63ac4a3ee4 | ||
|
|
4616ee715e | ||
|
|
5ef6bf1c15 | ||
|
|
862b4e19b6 | ||
|
|
378524dc39 | ||
|
|
6d5f45a311 | ||
|
|
0e00a52eb4 | ||
|
|
4b69d16c6d | ||
|
|
92e17a66f2 | ||
|
|
b601431591 | ||
|
|
76db40189e | ||
|
|
8ddf8225c0 | ||
|
|
359dd8d641 | ||
|
|
878da9b06f | ||
|
|
d843004ad6 | ||
|
|
7f6024f81f | ||
|
|
5a37b49fcd | ||
|
|
1c8cdac5be | ||
|
|
20bd27d487 | ||
|
|
fc761220d2 | ||
|
|
144681012c | ||
|
|
8fe3bd3c79 | ||
|
|
da65308cf2 | ||
|
|
011457e949 | ||
|
|
c296025837 | ||
|
|
ac4c501629 | ||
|
|
fe062a9312 | ||
|
|
65d4505b67 | ||
|
|
073d7e537c | ||
|
|
77dc367c95 | ||
|
|
7553d164f4 | ||
|
|
ee01e3d29f | ||
|
|
58f8c39197 | ||
|
|
7304fa862a | ||
|
|
1ef8b9ca47 | ||
|
|
5191d2f9c4 | ||
|
|
ff74e36fe5 | ||
|
|
0121619e93 | ||
|
|
aeb279a6f8 | ||
|
|
20815552b9 | ||
|
|
ac67a36ccf | ||
|
|
fcd8b8fd35 | ||
|
|
fbe4ad5c44 | ||
|
|
ee17483fff | ||
|
|
a451f1a234 | ||
|
|
52c4c01a7d | ||
|
|
b1bb0d669f | ||
|
|
eb1f9783aa | ||
|
|
0724d224fa | ||
|
|
e7775cdfa9 | ||
|
|
f8836f0c40 | ||
|
|
4c342cfc6a | ||
|
|
df05ab34fb | ||
|
|
5da0e75252 | ||
|
|
92ec5d8f64 | ||
|
|
6515fed9d2 | ||
|
|
5a7cd40614 | ||
|
|
65b01e0822 | ||
|
|
643f893d43 | ||
|
|
e0c6b43214 | ||
|
|
0f5d71f933 | ||
|
|
e6a8b5ed74 | ||
|
|
24c11e47c4 | ||
|
|
cb5f805767 | ||
|
|
3788e01f38 | ||
|
|
a9df4ea424 | ||
|
|
25ba06d530 | ||
|
|
9b81644f11 | ||
|
|
4b27536ed3 | ||
|
|
148da24456 | ||
|
|
d1be43fd83 | ||
|
|
f2df505237 | ||
|
|
bd37e26fab | ||
|
|
7954473476 | ||
|
|
13287cefbd | ||
|
|
9afbc91de1 | ||
|
|
dfe5bd9ec9 | ||
|
|
e2411e34bd | ||
|
|
f2998bdf9a | ||
|
|
c52f886e89 | ||
|
|
f972aa44ba | ||
|
|
ccafe0557f | ||
|
|
f9f9b1a1f9 | ||
|
|
e867aacbf5 | ||
|
|
a09e9e4fd6 | ||
|
|
fc320e6524 | ||
|
|
2b6a1c9f3d | ||
|
|
3e9cd2c226 | ||
|
|
3f722abba2 | ||
|
|
06508f70a3 | ||
|
|
8580108d1e | ||
|
|
300f34377c | ||
|
|
c4828f510f | ||
|
|
04c400553a | ||
|
|
d2508333bc | ||
|
|
fc901f1ebb | ||
|
|
c365f5a3d1 | ||
|
|
49697c863c | ||
|
|
4431c40de6 | ||
|
|
a0f6efb959 | ||
|
|
44fea0ae75 | ||
|
|
95c970e23f | ||
|
|
8596151fa1 | ||
|
|
2eac43734c | ||
|
|
dff91eb2aa | ||
|
|
aa122700a9 | ||
|
|
80f267df59 | ||
|
|
235618c3bb | ||
|
|
b7e55e785e | ||
|
|
050c986d08 | ||
|
|
2b83541ebc | ||
|
|
d40894ef6a | ||
|
|
b756bda988 | ||
|
|
0e64a88005 | ||
|
|
0d952a54bd | ||
|
|
d3ff7f3b61 | ||
|
|
0f9e7eca89 | ||
|
|
207b786fcd | ||
|
|
0f6fa87da0 | ||
|
|
995c1167dc | ||
|
|
1ab1d9c002 | ||
|
|
4b1a6a2f87 | ||
|
|
dacecd9006 | ||
|
|
dd5d75613e | ||
|
|
d59a316d8c | ||
|
|
9c4f855f71 | ||
|
|
90131b4a70 | ||
|
|
fb6725f2d7 | ||
|
|
475c57af55 | ||
|
|
6ae119a415 | ||
|
|
8b5fe276e7 | ||
|
|
5e4d5c262d | ||
|
|
9b82f1ef1f | ||
|
|
321d2d7e33 | ||
|
|
5b7cca95e1 | ||
|
|
2e0d53c6fe | ||
|
|
9d4f6d2cbb | ||
|
|
9546b434e4 | ||
|
|
f230522657 | ||
|
|
a57a1f566a | ||
|
|
8f8d6e6e30 | ||
|
|
3b8694483c | ||
|
|
4328a12967 | ||
|
|
fdc85b85c3 | ||
|
|
bb2eed23b7 | ||
|
|
72e33146de | ||
|
|
e371617938 | ||
|
|
b363db2bbd | ||
|
|
37a5958750 | ||
|
|
9091cfe3b0 | ||
|
|
e2614187ac | ||
|
|
5c69bf0470 | ||
|
|
383dc85166 | ||
|
|
7e47208888 | ||
|
|
137d87c3df | ||
|
|
fbff1fe10a | ||
|
|
3acd4490c5 | ||
|
|
2952ed79f8 | ||
|
|
cb23eda5a6 | ||
|
|
ced87c8aea | ||
|
|
daf87e26ad | ||
|
|
0c30c4e32c | ||
|
|
bf5f9f69ca | ||
|
|
b4828d3b4d | ||
|
|
617df8e07c | ||
|
|
deaba89f46 | ||
|
|
d5b13d56cc | ||
|
|
f893c7ddf8 | ||
|
|
c6cf450ac4 | ||
|
|
fa123023c7 | ||
|
|
0b285fa045 | ||
|
|
fe6c4243ee | ||
|
|
1a2c18d25e | ||
|
|
12e6de4697 | ||
|
|
f2b72d4234 | ||
|
|
25a84e9aee | ||
|
|
488171b02b | ||
|
|
eb22a449f4 | ||
|
|
c21c043f5f | ||
|
|
94808ea73e | ||
|
|
7fe486cba6 | ||
|
|
a3517e260c | ||
|
|
377f1fad41 | ||
|
|
17448f99c9 | ||
|
|
8c83886c2e | ||
|
|
9cef38af60 | ||
|
|
288ecc0d72 | ||
|
|
bb514f6d08 | ||
|
|
ecb21abb9a | ||
|
|
9445093d68 | ||
|
|
8f3f83051b | ||
|
|
5750892edf | ||
|
|
db74b2b70b | ||
|
|
a5c036be05 | ||
|
|
b60ee73ce5 | ||
|
|
b743f1cbc5 | ||
|
|
0f0bb56f7d | ||
|
|
8b593f8356 | ||
|
|
79ac73fb15 | ||
|
|
da0bb4c0ce | ||
|
|
9d7617add9 | ||
|
|
f38ed39c87 | ||
|
|
8ff5f45449 | ||
|
|
0447299e4f | ||
|
|
b797317f1a | ||
|
|
388f334fd3 | ||
|
|
e47f623b0a | ||
|
|
93e71bded8 | ||
|
|
8a93283398 | ||
|
|
aafd9b8f49 | ||
|
|
32fec183d5 | ||
|
|
d1751262df | ||
|
|
d05523360d | ||
|
|
a32f9a3c1c | ||
|
|
0468247c5a | ||
|
|
d02dd67016 | ||
|
|
27d5766abb | ||
|
|
be88c63414 | ||
|
|
c0d965857a | ||
|
|
e1763c45af | ||
|
|
177f7f64ac | ||
|
|
a2096aa753 | ||
|
|
bc9fac1da6 | ||
|
|
e0dcf24c90 | ||
|
|
db5eb095aa | ||
|
|
16b87f20ca | ||
|
|
00b82fe9f9 | ||
|
|
6af0ad100e | ||
|
|
f14fda5d1b | ||
|
|
1ef027e7e2 | ||
|
|
61aaba268f | ||
|
|
b041eee060 | ||
|
|
8ed65b95f2 | ||
|
|
3fe53f601c | ||
|
|
5ce0374426 | ||
|
|
e4aba70090 | ||
|
|
b17824c20a | ||
|
|
976dd18fa2 | ||
|
|
56bfcec656 | ||
|
|
f935706d22 | ||
|
|
e2322ee7ef | ||
|
|
d553a7cac4 | ||
|
|
f75b2ad9ee | ||
|
|
333b25eeaf | ||
|
|
0db70261f9 | ||
|
|
504db590db | ||
|
|
650b667148 | ||
|
|
4afdd0c264 | ||
|
|
2816bdef85 | ||
|
|
855fe70281 | ||
|
|
e4071a4f4e | ||
|
|
18ee17f7c8 | ||
|
|
f84f694351 | ||
|
|
0a15a0352d | ||
|
|
0e46d436de | ||
|
|
a16b4ffe75 | ||
|
|
608d4c5bda | ||
|
|
dbb8d8c93a | ||
|
|
e30f9a6cb1 | ||
|
|
bd4f5a2aa4 | ||
|
|
2d352b110b | ||
|
|
2802c61abd | ||
|
|
62c27f9142 | ||
|
|
c358f4e71f | ||
|
|
a05ecbc810 | ||
|
|
6b299693ca | ||
|
|
bb3413c1e5 | ||
|
|
854267d521 | ||
|
|
c4e05a5d9b | ||
|
|
27b61a79c8 | ||
|
|
70183bcc85 | ||
|
|
94da663287 | ||
|
|
3c7c1cc4c7 | ||
|
|
3b773da60d | ||
|
|
afee15cf44 | ||
|
|
a424aa65df | ||
|
|
fa93a7dfdd | ||
|
|
446f60b331 | ||
|
|
e136368ce3 | ||
|
|
5f1b54860c | ||
|
|
23eb008d5f | ||
|
|
40882c20d6 | ||
|
|
9054e01273 | ||
|
|
c5fe9b2667 | ||
|
|
0b382d3a9a | ||
|
|
45319ddf73 | ||
|
|
76c725843d | ||
|
|
ee09d6ca6a | ||
|
|
02173ce357 | ||
|
|
105bfdc489 | ||
|
|
9b90d04376 | ||
|
|
6233fc09af | ||
|
|
ec20b59ff3 | ||
|
|
049eb19be4 | ||
|
|
e5e8fa4cdd | ||
|
|
80c11b9f11 | ||
|
|
af055f37dc | ||
|
|
49c0db5280 | ||
|
|
8478835c4d | ||
|
|
804a9d9f20 | ||
|
|
9060dd786f | ||
|
|
802fee2e86 | ||
|
|
8f6fc751dd | ||
|
|
770dbc31cd | ||
|
|
a49ab150f9 | ||
|
|
4bc07e5311 | ||
|
|
8706ba7b68 | ||
|
|
99bc83ae84 | ||
|
|
16e871be7b | ||
|
|
afc4024287 | ||
|
|
0da1681c96 | ||
|
|
da19214754 | ||
|
|
44fca5ee5e | ||
|
|
2f33c24414 | ||
|
|
380f289887 | ||
|
|
a5ab069f5b | ||
|
|
e880a83df2 | ||
|
|
80cc7218a3 | ||
|
|
4809fb858a | ||
|
|
3af62888dc | ||
|
|
79e1d0a0f0 | ||
|
|
c447dfc888 | ||
|
|
8149966915 | ||
|
|
472f9481e8 | ||
|
|
2209ce3cee | ||
|
|
258c89e65d | ||
|
|
11913085ef | ||
|
|
53fca12a57 | ||
|
|
8e8a795f19 | ||
|
|
a4b27321ac | ||
|
|
ead83d932f | ||
|
|
900bd4327d | ||
|
|
1a2bb054f4 | ||
|
|
f908df46c8 | ||
|
|
f8c97842c4 | ||
|
|
c324b45025 | ||
|
|
5061ceda0e | ||
|
|
d1e677eb4f | ||
|
|
493a8f351b | ||
|
|
04172eead0 | ||
|
|
f0ed9288b3 | ||
|
|
03fe3d63ea | ||
|
|
83438ef72b | ||
|
|
cf18808a70 | ||
|
|
49dc2c1c42 | ||
|
|
692400db2a | ||
|
|
751c31b226 | ||
|
|
7be252c509 | ||
|
|
288eac743c | ||
|
|
d0bd80f158 | ||
|
|
7fe8154bcb | ||
|
|
87cd5c90a3 | ||
|
|
12399fca48 | ||
|
|
aaf17553c1 | ||
|
|
9dedd24d3e | ||
|
|
2443f1486d | ||
|
|
cfd1822198 | ||
|
|
7930c4949b | ||
|
|
ef6d0707ac | ||
|
|
cfd792cb69 | ||
|
|
f5352662cd | ||
|
|
bf1192f80d | ||
|
|
dee2150cf9 | ||
|
|
c44b105d9f | ||
|
|
b624fb3cf8 | ||
|
|
45396c1bf4 | ||
|
|
f562ae6fdb | ||
|
|
6615090bda | ||
|
|
eaf6f117a2 | ||
|
|
a8488502e7 | ||
|
|
309e36827c | ||
|
|
fe7883ddd8 | ||
|
|
06e5ff5174 | ||
|
|
2c31dd01dd | ||
|
|
d8a92c4879 | ||
|
|
16baeadf0c | ||
|
|
88ed81f75f | ||
|
|
70d9df1db2 | ||
|
|
aaa04570f2 | ||
|
|
9365d55964 | ||
|
|
0c61139f51 | ||
|
|
34bd623e45 | ||
|
|
0e31bb8565 | ||
|
|
0b87094f96 | ||
|
|
42e53c66f6 | ||
|
|
25a7d80146 | ||
|
|
a6170745b1 | ||
|
|
db32547a6e | ||
|
|
4df6d9998b | ||
|
|
2f42f6ca28 | ||
|
|
6617c10946 | ||
|
|
1d34e02957 | ||
|
|
132f8d3d4f | ||
|
|
d3961b1a53 | ||
|
|
2c04ef9e1f | ||
|
|
f599e3ead6 | ||
|
|
e16da84a75 | ||
|
|
0a622f5282 | ||
|
|
bb40138c52 | ||
|
|
2a7bd8bbd2 | ||
|
|
e6fc666012 | ||
|
|
a1297e593c | ||
|
|
2026c7eca0 | ||
|
|
137076f224 | ||
|
|
5538650681 | ||
|
|
0e36e6b6d9 | ||
|
|
f4c786763a |
101
.github/workflows/maven-publish.yml
vendored
101
.github/workflows/maven-publish.yml
vendored
@@ -2,10 +2,21 @@ name: Publish Desktop Forge
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug_enabled:
|
||||
type: boolean
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
release_android:
|
||||
type: boolean
|
||||
description: 'Also try to release android build'
|
||||
required: false
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
if: github.repository_owner == 'Card-Forge'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -32,10 +43,94 @@ jobs:
|
||||
run: |
|
||||
git config user.email "actions@github.com"
|
||||
git config user.name "GitHub Actions"
|
||||
- name: Build/Install/Publish to GitHub Packages Apache Maven
|
||||
|
||||
- name: Install old maven (3.8.1)
|
||||
run: |
|
||||
curl -o apache-maven-3.8.1-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz
|
||||
tar xf apache-maven-3.8.1-bin.tar.gz
|
||||
export PATH=$PWD/apache-maven-3.8.1/bin:$PATH
|
||||
export MAVEN_HOME=$PWD/apache-maven-3.8.1
|
||||
mvn --version
|
||||
|
||||
- name: Setup tmate session
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
|
||||
|
||||
- name: Setup android requirements
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
|
||||
run: |
|
||||
JAVA_HOME=${JAVA_HOME_17_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;35.0.0" "platform-tools" "platforms;android-35"
|
||||
cd forge-gui-android
|
||||
echo "${{ secrets.FORGE_KEYSTORE }}" > forge.keystore.asc
|
||||
gpg -d --passphrase "${{ secrets.FORGE_KEYSTORE_PASSPHRASE }}" --batch forge.keystore.asc > forge.keystore
|
||||
cd -
|
||||
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
|
||||
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
|
||||
curl -L -o android-maven-plugin-4.6.2.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.jar
|
||||
curl -L -o android-maven-plugin-4.6.2.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.pom
|
||||
cd -
|
||||
mvn install -Dmaven.test.skip=true
|
||||
mvn dependency:tree
|
||||
|
||||
|
||||
- name: Build/Install/Publish Desktop to GitHub Packages Apache Maven
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_android }}
|
||||
run: |
|
||||
export DISPLAY=":1"
|
||||
Xvfb :1 -screen 0 800x600x8 &
|
||||
mvn -U -B clean -P windows-linux install release:clean release:prepare release:perform -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }}
|
||||
export _JAVA_OPTIONS="-Xmx2g"
|
||||
d=$(date +%m.%d)
|
||||
# build only desktop and only try to move desktop files
|
||||
mvn -U -B clean -P windows-linux install -e -T 1C release:clean release:prepare release:perform -DskipTests
|
||||
mkdir izpack
|
||||
# move bz2 and jar from work dir to izpack dir
|
||||
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
|
||||
# move desktop build.txt and version.txt to izpack
|
||||
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
|
||||
cd izpack
|
||||
ls
|
||||
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Build/Install/Publish Desktop+Android to GitHub Packages Apache Maven
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
|
||||
run: |
|
||||
export DISPLAY=":1"
|
||||
Xvfb :1 -screen 0 800x600x8 &
|
||||
export _JAVA_OPTIONS="-Xmx2g"
|
||||
d=$(date +%m.%d)
|
||||
# build both desktop and android
|
||||
mvn -U -B clean -P windows-linux,android-release-build install -e -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=35.0.0
|
||||
mkdir izpack
|
||||
# move bz2 and jar from work dir to izpack dir
|
||||
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
|
||||
# move desktop build.txt and version.txt to izpack
|
||||
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
|
||||
# move android apk and assets.zip
|
||||
mv /home/runner/work/forge/forge/forge-gui-android/target/*-signed-aligned.apk izpack/
|
||||
mv /home/runner/work/forge/forge/forge-gui-android/target/assets.zip izpack/
|
||||
cd izpack
|
||||
ls
|
||||
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Upload snapshot to GitHub Prerelease
|
||||
uses: ncipollo/release-action@v1
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
name: Release ${{ env.GIT_TAG }}
|
||||
tag: ${{ env.GIT_TAG }}
|
||||
artifacts: izpack/*
|
||||
allowUpdates: true
|
||||
removeArtifacts: true
|
||||
makeLatest: true
|
||||
|
||||
- name: Send failure notification to Discord
|
||||
if: failure() # This step runs only if the job fails
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d "{\"content\": \"🔴 Release Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
|
||||
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}
|
||||
|
||||
32
.github/workflows/snapshot-both-pc-android.yml
vendored
32
.github/workflows/snapshot-both-pc-android.yml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: '00 18 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -109,16 +112,21 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: 📂 Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
|
||||
- name: Upload snapshot to GitHub Prerelease
|
||||
uses: ncipollo/release-action@v1
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
server: ftp.cardforge.org
|
||||
username: ${{ secrets.FTP_USERNAME }}
|
||||
password: ${{ secrets.FTP_PASSWORD }}
|
||||
local-dir: izpack/
|
||||
server-dir: downloads/dailysnapshots/
|
||||
state-name: .ftp-deploy-both-sync-state.json
|
||||
exclude: |
|
||||
*.pom
|
||||
*.repositories
|
||||
*.xml
|
||||
name: Daily Snapshot
|
||||
tag: daily-snapshots
|
||||
prerelease: true
|
||||
artifacts: izpack/*
|
||||
allowUpdates: true
|
||||
removeArtifacts: true
|
||||
|
||||
- name: Send failure notification to Discord
|
||||
if: failure() # This step runs only if the job fails
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d "{\"content\": \"🔴 Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
|
||||
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}
|
||||
|
||||
4
.github/workflows/snapshots-android.yml
vendored
4
.github/workflows/snapshots-android.yml
vendored
@@ -13,10 +13,6 @@ on:
|
||||
# description: 'Upload the completed Android package'
|
||||
# required: false
|
||||
# default: true
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: '00 19 * * *'
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
3
.github/workflows/snapshots-pc.yml
vendored
3
.github/workflows/snapshots-pc.yml
vendored
@@ -8,9 +8,6 @@ on:
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: '30 18 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -26,13 +26,13 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
||||
|
||||
### 📥 Desktop Installation
|
||||
1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest).
|
||||
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/).
|
||||
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots).
|
||||
- **Tip:** Extract to a new folder to prevent version conflicts.
|
||||
3. **User Data Management:** Previous players’ data is preserved during upgrades.
|
||||
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
|
||||
|
||||
### 📱 Android Installation
|
||||
- Download the **APK** from the [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/). On the first launch, Forge will automatically download all necessary assets.
|
||||
- Download the **APK** from the [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots). On the first launch, Forge will automatically download all necessary assets.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -45,17 +45,16 @@ public class BiomeStructureDataMappingEditor extends JComponent {
|
||||
JList list, Object value, int index,
|
||||
boolean isSelected, boolean cellHasFocus) {
|
||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping))
|
||||
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping biomeData))
|
||||
return label;
|
||||
BiomeStructureData.BiomeStructureDataMapping data=(BiomeStructureData.BiomeStructureDataMapping) value;
|
||||
// Get the renderer component from parent class
|
||||
|
||||
label.setText(data.name);
|
||||
label.setText(biomeData.name);
|
||||
if(editor.data!=null)
|
||||
{
|
||||
SwingAtlas itemAtlas=new SwingAtlas(Config.instance().getFile(editor.data.structureAtlasPath));
|
||||
if(itemAtlas.has(data.name))
|
||||
label.setIcon(itemAtlas.get(data.name));
|
||||
if(itemAtlas.has(biomeData.name))
|
||||
label.setIcon(itemAtlas.get(biomeData.name));
|
||||
else
|
||||
{
|
||||
ImageIcon img=itemAtlas.getAny();
|
||||
|
||||
@@ -25,9 +25,8 @@ public class DialogOptionEditor extends JComponent{
|
||||
JList list, Object value, int index,
|
||||
boolean isSelected, boolean cellHasFocus) {
|
||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||
if(!(value instanceof DialogData))
|
||||
if(!(value instanceof DialogData dialog))
|
||||
return label;
|
||||
DialogData dialog=(DialogData) value;
|
||||
StringBuilder builder=new StringBuilder();
|
||||
if(dialog.name==null||dialog.name.isEmpty())
|
||||
builder.append("[[Blank Option]]");
|
||||
|
||||
@@ -27,17 +27,16 @@ public class ItemsEditor extends JComponent {
|
||||
JList list, Object value, int index,
|
||||
boolean isSelected, boolean cellHasFocus) {
|
||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||
if(!(value instanceof ItemData))
|
||||
if(!(value instanceof ItemData item))
|
||||
return label;
|
||||
ItemData Item=(ItemData) value;
|
||||
// Get the renderer component from parent class
|
||||
|
||||
label.setText(Item.name);
|
||||
label.setText(item.name);
|
||||
if(itemAtlas==null)
|
||||
itemAtlas=new SwingAtlas(Config.instance().getFile(Paths.ITEMS_ATLAS));
|
||||
|
||||
if(itemAtlas.has(Item.iconName))
|
||||
label.setIcon(itemAtlas.get(Item.iconName));
|
||||
if(itemAtlas.has(item.iconName))
|
||||
label.setIcon(itemAtlas.get(item.iconName));
|
||||
else
|
||||
{
|
||||
ImageIcon img=itemAtlas.getAny();
|
||||
|
||||
@@ -26,9 +26,8 @@ public class QuestEditor extends JComponent {
|
||||
JList list, Object value, int index,
|
||||
boolean isSelected, boolean cellHasFocus) {
|
||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||
if(!(value instanceof AdventureQuestData))
|
||||
if(!(value instanceof AdventureQuestData quest))
|
||||
return label;
|
||||
AdventureQuestData quest=(AdventureQuestData) value;
|
||||
// Get the renderer component from parent class
|
||||
|
||||
label.setText(quest.name);
|
||||
|
||||
@@ -26,9 +26,8 @@ public class QuestStageEditor extends JComponent{
|
||||
JList list, Object value, int index,
|
||||
boolean isSelected, boolean cellHasFocus) {
|
||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||
if(!(value instanceof AdventureQuestStage))
|
||||
if(!(value instanceof AdventureQuestStage stageData))
|
||||
return label;
|
||||
AdventureQuestStage stageData=(AdventureQuestStage) value;
|
||||
label.setText(stageData.name);
|
||||
//label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually?
|
||||
return label;
|
||||
|
||||
@@ -43,9 +43,8 @@ public class WorldEditor extends JComponent {
|
||||
JList list, Object value, int index,
|
||||
boolean isSelected, boolean cellHasFocus) {
|
||||
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||
if(!(value instanceof BiomeData))
|
||||
if(!(value instanceof BiomeData biome))
|
||||
return label;
|
||||
BiomeData biome=(BiomeData) value;
|
||||
// Get the renderer component from parent class
|
||||
|
||||
label.setText(biome.name);
|
||||
|
||||
@@ -37,6 +37,7 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityPredicates;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -115,8 +116,8 @@ public class AiAttackController {
|
||||
} // overloaded constructor to evaluate single specified attacker
|
||||
|
||||
private void refreshCombatants(GameEntity defender) {
|
||||
if (defender instanceof Card && ((Card) defender).isBattle()) {
|
||||
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
|
||||
if (defender instanceof Card card && card.isBattle()) {
|
||||
this.oppList = getOpponentCreatures(card.getProtectingPlayer());
|
||||
} else {
|
||||
this.oppList = getOpponentCreatures(defendingOpponent);
|
||||
}
|
||||
@@ -312,7 +313,8 @@ public class AiAttackController {
|
||||
}
|
||||
}
|
||||
// Poison opponent if unblocked
|
||||
if (defender instanceof Player && ComputerUtilCombat.poisonIfUnblocked(attacker, (Player) defender) > 0) {
|
||||
if (defender instanceof Player player
|
||||
&& ComputerUtilCombat.poisonIfUnblocked(attacker, player) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -849,10 +851,9 @@ public class AiAttackController {
|
||||
// decided to attack another defender so related lists need to be updated
|
||||
// (though usually rather try to avoid this situation for performance reasons)
|
||||
if (defender != defendingOpponent) {
|
||||
if (defender instanceof Player) {
|
||||
defendingOpponent = (Player) defender;
|
||||
} else if (defender instanceof Card) {
|
||||
Card defCard = (Card) defender;
|
||||
if (defender instanceof Player p) {
|
||||
defendingOpponent = p;
|
||||
} else if (defender instanceof Card defCard) {
|
||||
if (defCard.isBattle()) {
|
||||
defendingOpponent = defCard.getProtectingPlayer();
|
||||
} else {
|
||||
@@ -946,8 +947,8 @@ public class AiAttackController {
|
||||
return 1;
|
||||
}
|
||||
// or weakest player
|
||||
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) {
|
||||
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife();
|
||||
if (r1.getKey() instanceof Player p1 && r2.getKey() instanceof Player p2) {
|
||||
return p1.getLife() - p2.getLife();
|
||||
}
|
||||
}
|
||||
return r2.getValue() - r1.getValue();
|
||||
@@ -1314,7 +1315,7 @@ public class AiAttackController {
|
||||
attackersAssigned.add(attacker);
|
||||
|
||||
// check if attackers are enough to finish the attacked planeswalker
|
||||
if (i < left.size() - 1 && defender instanceof Card) {
|
||||
if (i < left.size() - 1 && defender instanceof Card card) {
|
||||
final int blockNum = this.blockers.size();
|
||||
int attackNum = 0;
|
||||
int damage = 0;
|
||||
@@ -1328,7 +1329,7 @@ public class AiAttackController {
|
||||
}
|
||||
}
|
||||
// if enough damage: switch to next planeswalker
|
||||
if (damage >= ComputerUtilCombat.getDamageToKill((Card) defender, true)) {
|
||||
if (damage >= ComputerUtilCombat.getDamageToKill(card, true)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1587,7 +1588,7 @@ public class AiAttackController {
|
||||
// but there are no creatures it can target, no need to exert with it
|
||||
boolean missTarget = false;
|
||||
for (StaticAbility st : c.getStaticAbilities()) {
|
||||
if (!"OptionalAttackCost".equals(st.getParam("Mode"))) {
|
||||
if (!st.checkMode(StaticAbilityMode.OptionalAttackCost)) {
|
||||
continue;
|
||||
}
|
||||
SpellAbility sa = st.getPayingTrigSA();
|
||||
@@ -1754,10 +1755,12 @@ public class AiAttackController {
|
||||
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final Queue<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
|
||||
// TODO: detect Revenge of Ravens by the trigger instead of by name
|
||||
boolean revengeOfRavens = false;
|
||||
if (defender instanceof Player) {
|
||||
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||
} else if (defender instanceof Card) {
|
||||
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||
if (defender instanceof Player player) {
|
||||
revengeOfRavens = !CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||
} else if (defender instanceof Card card) {
|
||||
revengeOfRavens = !CardLists.filter(card.getController().getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||
}
|
||||
|
||||
if (!revengeOfRavens) {
|
||||
|
||||
@@ -161,12 +161,12 @@ public class AiBlockController {
|
||||
// defend battles with fewer defense counters before battles with more defense counters,
|
||||
// if planeswalker/battle will be too difficult to defend don't even bother
|
||||
for (GameEntity defender : defenders) {
|
||||
if ((defender instanceof Card && ((Card) defender).getController().equals(ai))
|
||||
|| (defender instanceof Card && ((Card) defender).isBattle() && ((Card) defender).getProtectingPlayer().equals(ai))) {
|
||||
final CardCollection attackers = combat.getAttackersOf(defender);
|
||||
if ((defender instanceof Card card1 && card1.getController().equals(ai))
|
||||
|| (defender instanceof Card card2 && card2.isBattle() && card2.getProtectingPlayer().equals(ai))) {
|
||||
final CardCollection ccAttackers = combat.getAttackersOf(defender);
|
||||
// Begin with the attackers that pose the biggest threat
|
||||
CardLists.sortByPowerDesc(attackers);
|
||||
sortedAttackers.addAll(attackers);
|
||||
CardLists.sortByPowerDesc(ccAttackers);
|
||||
sortedAttackers.addAll(ccAttackers);
|
||||
} else if (defender instanceof Player && defender.equals(ai)) {
|
||||
firstAttacker = combat.getAttackersOf(defender);
|
||||
CardLists.sortByPowerDesc(firstAttacker);
|
||||
@@ -872,9 +872,9 @@ public class AiBlockController {
|
||||
CardCollection threatenedPWs = new CardCollection();
|
||||
for (final Card attacker : attackers) {
|
||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||
if (def instanceof Card) {
|
||||
if (def instanceof Card card) {
|
||||
if (!onlyIfLethal) {
|
||||
threatenedPWs.add((Card) def);
|
||||
threatenedPWs.add(card);
|
||||
} else {
|
||||
int damageToPW = 0;
|
||||
for (final Card pwatkr : combat.getAttackersOf(def)) {
|
||||
@@ -906,12 +906,12 @@ public class AiBlockController {
|
||||
continue;
|
||||
}
|
||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||
if (def instanceof Card && threatenedPWs.contains(def)) {
|
||||
if (def instanceof Card card && threatenedPWs.contains(def)) {
|
||||
Card blockerDecided = null;
|
||||
for (final Card blocker : chumpPWDefenders) {
|
||||
if (CombatUtil.canBlock(attacker, blocker, combat)) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
pwsWithChumpBlocks.add((Card) def);
|
||||
pwsWithChumpBlocks.add(card);
|
||||
chosenChumpBlockers.add(blocker);
|
||||
blockerDecided = blocker;
|
||||
blockersLeft.remove(blocker);
|
||||
@@ -1346,8 +1346,8 @@ public class AiBlockController {
|
||||
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.CREATURES)
|
||||
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
|
||||
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
|
||||
&& combat.getDefenderByAttacker(attacker) instanceof Card
|
||||
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
|
||||
&& combat.getDefenderByAttacker(attacker) instanceof Card card
|
||||
&& card.isPlaneswalker();
|
||||
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
|
||||
|
||||
return ((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
|
||||
|
||||
@@ -54,6 +54,7 @@ import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.*;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityDisableTriggers;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
@@ -68,8 +69,10 @@ import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@@ -292,7 +295,7 @@ public class AiController {
|
||||
}
|
||||
|
||||
// can't fetch partner isn't problematic
|
||||
if (tr.getKeyword() != null && tr.getKeyword().getOriginal().startsWith("Partner")) {
|
||||
if (tr.isKeyword(Keyword.PARTNER)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -689,7 +692,6 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO handle fetchlands and what they can fetch for
|
||||
// determine new color pips
|
||||
int[] card_counts = new int[6]; // in WUBRGC order
|
||||
@@ -1128,7 +1130,7 @@ public class AiController {
|
||||
// Memory Crystal-like effects need special handling
|
||||
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
for (StaticAbility s : c.getStaticAbilities()) {
|
||||
if ("ReduceCost".equals(s.getParam("Mode"))
|
||||
if (s.checkMode(StaticAbilityMode.ReduceCost)
|
||||
&& "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
|
||||
neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
|
||||
}
|
||||
@@ -1708,7 +1710,8 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
CompletableFuture<SpellAbility> future = CompletableFuture.supplyAsync(() -> {
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<SpellAbility> future = executor.submit(() -> {
|
||||
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
|
||||
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
|
||||
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
|
||||
@@ -1788,11 +1791,9 @@ public class AiController {
|
||||
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
try {
|
||||
if (game.AI_CAN_USE_TIMEOUT)
|
||||
return future.completeOnTimeout(null, game.getAITimeout(), TimeUnit.SECONDS).get();
|
||||
else
|
||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
future.cancel(true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2368,7 +2369,7 @@ public class AiController {
|
||||
|
||||
// TODO move to more common place
|
||||
public static <T extends TriggerReplacementBase> List<T> filterList(List<T> input, Function<SpellAbility, Object> pred, Object value) {
|
||||
return filterList(input, trb -> pred.apply(trb.ensureAbility()) == value);
|
||||
return filterList(input, trb -> trb.ensureAbility() != null && pred.apply(trb.ensureAbility()) == value);
|
||||
}
|
||||
|
||||
public static List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {
|
||||
|
||||
@@ -46,6 +46,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
return PaymentDecision.number(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostBehold cost) {
|
||||
final String type = cost.getType();
|
||||
CardCollectionView hand = player.getCardsIn(cost.getRevealFrom());
|
||||
hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability);
|
||||
return hand.isEmpty() ? null : PaymentDecision.card(getBestCreatureAI(hand));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostChooseColor cost) {
|
||||
int c = cost.getAbilityAmount(ability);
|
||||
|
||||
@@ -48,6 +48,7 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.trigger.WrappedAbility;
|
||||
@@ -1099,6 +1100,11 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// if AI has no speed, play start your engines on Main1
|
||||
if (ai.noSpeed() && cardState.hasKeyword(Keyword.START_YOUR_ENGINES)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// cast Blitz in main 1 if the creature attacks
|
||||
if (sa.isBlitz() && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, card)) {
|
||||
return true;
|
||||
@@ -1408,9 +1414,7 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
for (final CostPart part : abCost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
|
||||
if (part instanceof CostSacrifice sac) {
|
||||
final String type = sac.getType();
|
||||
|
||||
if (type.equals("CARDNAME")) {
|
||||
@@ -1455,15 +1459,14 @@ public class ComputerUtil {
|
||||
// check for Continuous abilities that grant Haste
|
||||
for (final Card c : all) {
|
||||
for (StaticAbility stAb : c.getStaticAbilities()) {
|
||||
Map<String, String> params = stAb.getMapParams();
|
||||
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
|
||||
&& params.get("AddKeyword").contains("Haste")) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
|
||||
&& stAb.getParam("AddKeyword").contains("Haste")) {
|
||||
|
||||
if (c.isEquipment() && c.getEquipping() == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final String affected = params.get("Affected");
|
||||
final String affected = stAb.getParam("Affected");
|
||||
if (affected.contains("Creature.YouCtrl")
|
||||
|| affected.contains("Other+YouCtrl")) {
|
||||
return true;
|
||||
@@ -1516,11 +1519,10 @@ public class ComputerUtil {
|
||||
|
||||
for (final Card c : opp) {
|
||||
for (StaticAbility stAb : c.getStaticAbilities()) {
|
||||
Map<String, String> params = stAb.getMapParams();
|
||||
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
|
||||
&& params.get("AddKeyword").contains("Haste")) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
|
||||
&& stAb.getParam("AddKeyword").contains("Haste")) {
|
||||
|
||||
final ArrayList<String> affected = Lists.newArrayList(params.get("Affected").split(","));
|
||||
final ArrayList<String> affected = Lists.newArrayList(stAb.getParam("Affected").split(","));
|
||||
if (affected.contains("Creature")) {
|
||||
return true;
|
||||
}
|
||||
@@ -1776,9 +1778,7 @@ public class ComputerUtil {
|
||||
noRegen = true;
|
||||
}
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
|
||||
if (o instanceof Card c) {
|
||||
// indestructible
|
||||
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||
continue;
|
||||
@@ -1842,9 +1842,7 @@ public class ComputerUtil {
|
||||
if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c, false)) {
|
||||
threatened.add(c);
|
||||
}
|
||||
} else if (o instanceof Player) {
|
||||
final Player p = (Player) o;
|
||||
|
||||
} else if (o instanceof Player p) {
|
||||
if (source.hasKeyword(Keyword.INFECT)) {
|
||||
if (p.canReceiveCounters(CounterEnumType.POISON) && ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= 10 - p.getPoisonCounters()) {
|
||||
threatened.add(p);
|
||||
@@ -1862,8 +1860,7 @@ public class ComputerUtil {
|
||||
|| saviourApi == null)) {
|
||||
final int dmg = -AbilityUtils.calculateAmount(source, topStack.getParam("NumDef"), topStack);
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
if (o instanceof Card c) {
|
||||
final boolean canRemove = (c.getNetToughness() <= dmg)
|
||||
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && dmg >= ComputerUtilCombat.getDamageToKill(c, false));
|
||||
if (!canRemove) {
|
||||
@@ -1909,9 +1906,7 @@ public class ComputerUtil {
|
||||
|| saviourApi == ApiType.Protection || saviourApi == null
|
||||
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
// indestructible
|
||||
if (o instanceof Card c) {
|
||||
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1960,8 +1955,7 @@ public class ComputerUtil {
|
||||
&& topStack.hasParam("Destination")
|
||||
&& topStack.getParam("Destination").equals("Exile")) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
if (o instanceof Card c) {
|
||||
// give Shroud to targeted creatures
|
||||
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
|
||||
continue;
|
||||
@@ -1988,8 +1982,7 @@ public class ComputerUtil {
|
||||
&& (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|
||||
|| saviourApi == ApiType.Protection || saviourApi == null)) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
if (o instanceof Card c) {
|
||||
// give Shroud to targeted creatures
|
||||
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
|
||||
continue;
|
||||
@@ -2011,8 +2004,7 @@ public class ComputerUtil {
|
||||
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
|
||||
if (enableCurseAuraRemoval) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
if (o instanceof Card c) {
|
||||
// give Shroud to targeted creatures
|
||||
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
|
||||
continue;
|
||||
@@ -2436,7 +2428,7 @@ public class ComputerUtil {
|
||||
// Are we picking a type to reduce costs for that type?
|
||||
boolean reducingCost = false;
|
||||
for (StaticAbility s : sa.getHostCard().getStaticAbilities()) {
|
||||
if ("ReduceCost".equals(s.getParam("Mode")) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
|
||||
if (s.checkMode(StaticAbilityMode.ReduceCost) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
|
||||
reducingCost = true;
|
||||
break;
|
||||
}
|
||||
@@ -2898,7 +2890,7 @@ public class ComputerUtil {
|
||||
// Iceberg does use Ice as Storage
|
||||
|| (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName()))
|
||||
// some lands does use Depletion as Storage Counter
|
||||
|| (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step."))
|
||||
|| (type.is(CounterEnumType.DEPLETION) && c.getReplacementEffects().anyMatch(r -> r.getMode().equals(ReplacementType.Untap) && r.getLayer().equals(ReplacementLayer.CantHappen)))
|
||||
// treat Time Counters on suspended Cards as Bad,
|
||||
// and also on Chronozoa
|
||||
|| (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
|
||||
|
||||
@@ -48,6 +48,7 @@ import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.zone.MagicStack;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -691,6 +692,8 @@ public class ComputerUtilCard {
|
||||
public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) {
|
||||
AiBlockController aiBlk = new AiBlockController(ai, checkingOther);
|
||||
Combat combat = new Combat(ai);
|
||||
// avoid removing original attacker
|
||||
attacker.setCombatLKI(null);
|
||||
combat.addAttacker(attacker, ai);
|
||||
final List<Card> attackers = Lists.newArrayList(attacker);
|
||||
aiBlk.assignBlockersGivenAttackers(combat, attackers);
|
||||
@@ -1211,8 +1214,7 @@ public class ComputerUtilCard {
|
||||
// if this thing is both owned and controlled by an opponent and it has a continuous ability,
|
||||
// assume it either benefits the player or disrupts the opponent
|
||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||
final Map<String, String> params = stAb.getMapParams();
|
||||
if (params.get("Mode").equals("Continuous") && stAb.isIntrinsic()) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.isIntrinsic()) {
|
||||
priority = true;
|
||||
break;
|
||||
}
|
||||
@@ -1243,17 +1245,16 @@ public class ComputerUtilCard {
|
||||
}
|
||||
} else {
|
||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||
final Map<String, String> params = stAb.getMapParams();
|
||||
//continuous buffs
|
||||
if (params.get("Mode").equals("Continuous") && "Creature.YouCtrl".equals(params.get("Affected"))) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && "Creature.YouCtrl".equals(stAb.getParam("Affected"))) {
|
||||
int bonusPT = 0;
|
||||
if (params.containsKey("AddPower")) {
|
||||
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
|
||||
if (stAb.hasParam("AddPower")) {
|
||||
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
|
||||
}
|
||||
if (params.containsKey("AddToughness")) {
|
||||
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
|
||||
if (stAb.hasParam("AddToughness")) {
|
||||
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
|
||||
}
|
||||
String kws = params.get("AddKeyword");
|
||||
String kws = stAb.getParam("AddKeyword");
|
||||
if (kws != null) {
|
||||
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
|
||||
}
|
||||
@@ -1784,7 +1785,7 @@ public class ComputerUtilCard {
|
||||
// remove old boost that might be copied
|
||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||
vCard.removePTBoost(c.getLayerTimestamp(), stAb.getId());
|
||||
if (!stAb.checkMode("Continuous")) {
|
||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
continue;
|
||||
}
|
||||
if (!stAb.hasParam("Affected")) {
|
||||
@@ -1862,7 +1863,7 @@ public class ComputerUtilCard {
|
||||
if (!c.isCreature()) {
|
||||
return false;
|
||||
}
|
||||
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.hasKeyword("CARDNAME doesn't untap during your untap step.") && c.isTapped()) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
|
||||
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.isTapped() && !c.canUntap(ai, true)) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -31,7 +31,7 @@ import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayment;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
@@ -39,6 +39,7 @@ import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.staticability.StaticAbilityMustAttack;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
@@ -101,7 +102,7 @@ public class ComputerUtilCombat {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), "BeginCombat")) {
|
||||
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), PhaseType.COMBAT_BEGIN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ public class ComputerUtilCombat {
|
||||
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
|
||||
// || attacker.hasSVar("EndOfTurnLeavePlay"));
|
||||
// The creature won't untap next turn
|
||||
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && Untap.canUntap(attacker));
|
||||
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && attacker.canUntap(attacker.getController(), true));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +177,7 @@ public class ComputerUtilCombat {
|
||||
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
|
||||
int damage = attacker.getNetCombatDamage();
|
||||
int sum = 0;
|
||||
if (attacked instanceof Player && !((Player) attacked).canLoseLife()) {
|
||||
if (attacked instanceof Player player && !player.canLoseLife()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -214,7 +215,7 @@ public class ComputerUtilCombat {
|
||||
int damage = attacker.getNetCombatDamage();
|
||||
int poison = 0;
|
||||
damage += predictPowerBonusOfAttacker(attacker, null, null, false);
|
||||
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||
if (attacker.isInfectDamage(attacked)) {
|
||||
int pd = predictDamageTo(attacked, damage, attacker, true);
|
||||
// opponent can always order it so that he gets 0
|
||||
if (pd == 1 && attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
|
||||
@@ -357,7 +358,7 @@ public class ComputerUtilCombat {
|
||||
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
|
||||
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
|
||||
if (trampleDamage > 0) {
|
||||
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||
if (attacker.isInfectDamage(ai)) {
|
||||
poison += trampleDamage;
|
||||
}
|
||||
poison += predictExtraPoisonWithDamage(attacker, ai, trampleDamage);
|
||||
@@ -900,7 +901,7 @@ public class ComputerUtilCombat {
|
||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
if (!stAb.checkMode("Continuous")) {
|
||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
continue;
|
||||
}
|
||||
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
|
||||
@@ -1196,7 +1197,7 @@ public class ComputerUtilCombat {
|
||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
if (!stAb.checkMode("Continuous")) {
|
||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
continue;
|
||||
}
|
||||
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
|
||||
@@ -1387,7 +1388,7 @@ public class ComputerUtilCombat {
|
||||
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
if (!"Continuous".equals(stAb.getParam("Mode"))) {
|
||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
continue;
|
||||
}
|
||||
if (!stAb.hasParam("Affected")) {
|
||||
@@ -1734,6 +1735,7 @@ public class ComputerUtilCombat {
|
||||
final int attackerLife = getDamageToKill(attacker, false)
|
||||
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
|
||||
// AI should be less worried about Deathtouch
|
||||
if (blocker.hasDoubleStrike()) {
|
||||
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
||||
return true;
|
||||
@@ -1963,6 +1965,7 @@ public class ComputerUtilCombat {
|
||||
final int attackerLife = getDamageToKill(attacker, false)
|
||||
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
|
||||
// AI should be less worried about deathtouch
|
||||
if (attacker.hasDoubleStrike()) {
|
||||
if (attackerDamage >= defenderLife) {
|
||||
return true;
|
||||
@@ -2539,20 +2542,20 @@ public class ComputerUtilCombat {
|
||||
if (combat != null) {
|
||||
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
|
||||
// 1. If the card that spawned the attacker was sent at a card, attack the same. Consider improving.
|
||||
if (def instanceof Card && Iterables.contains(defenders, def)) {
|
||||
if (((Card) def).isPlaneswalker()) {
|
||||
if (def instanceof Card card && Iterables.contains(defenders, def)) {
|
||||
if (card.isPlaneswalker()) {
|
||||
return def;
|
||||
}
|
||||
if (((Card) def).isBattle()) {
|
||||
if (card.isBattle()) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
// 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably.
|
||||
for (GameEntity p : defenders) {
|
||||
if (p instanceof Player && !ComputerUtilCard.canBeBlockedProfitably((Player)p, attacker, true)) {
|
||||
if (p instanceof Player p1 && !ComputerUtilCard.canBeBlockedProfitably(p1, attacker, true)) {
|
||||
return p;
|
||||
}
|
||||
if (p instanceof Card && !ComputerUtilCard.canBeBlockedProfitably(((Card)p).getController(), attacker, true)) {
|
||||
if (p instanceof Card card && !ComputerUtilCard.canBeBlockedProfitably(card.getController(), attacker, true)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,7 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostPutCounter) {
|
||||
final CostPutCounter addCounter = (CostPutCounter) part;
|
||||
if (part instanceof CostPutCounter addCounter) {
|
||||
final CounterType type = addCounter.getCounter();
|
||||
|
||||
if (type.is(CounterEnumType.M1M1)) {
|
||||
@@ -77,9 +76,7 @@ public class ComputerUtilCost {
|
||||
}
|
||||
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
|
||||
|
||||
if (part instanceof CostRemoveCounter remCounter) {
|
||||
final CounterType type = remCounter.counter;
|
||||
if (!part.payCostFromSource()) {
|
||||
if (type.is(CounterEnumType.P1P1)) {
|
||||
@@ -106,9 +103,7 @@ public class ComputerUtilCost {
|
||||
&& !source.hasKeyword(Keyword.UNDYING)) {
|
||||
return false;
|
||||
}
|
||||
} else if (part instanceof CostRemoveAnyCounter) {
|
||||
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
|
||||
|
||||
} else if (part instanceof CostRemoveAnyCounter remCounter) {
|
||||
PaymentDecision pay = decision.visit(remCounter);
|
||||
return pay != null;
|
||||
}
|
||||
@@ -133,9 +128,7 @@ public class ComputerUtilCost {
|
||||
CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand));
|
||||
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostDiscard) {
|
||||
final CostDiscard disc = (CostDiscard) part;
|
||||
|
||||
if (part instanceof CostDiscard disc) {
|
||||
final String type = disc.getType();
|
||||
final CardCollection typeList;
|
||||
int num;
|
||||
@@ -187,8 +180,7 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostDamage) {
|
||||
final CostDamage pay = (CostDamage) part;
|
||||
if (part instanceof CostDamage pay) {
|
||||
int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false);
|
||||
if (ai.getLife() - realDamage < remainingLife
|
||||
&& realDamage > 0 && !ai.cantLoseForZeroOrLessLife()
|
||||
@@ -220,9 +212,7 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostPayLife) {
|
||||
final CostPayLife payLife = (CostPayLife) part;
|
||||
|
||||
if (part instanceof CostPayLife payLife) {
|
||||
int amount = payLife.getAbilityAmount(sourceAbility);
|
||||
|
||||
// check if there's override for the remainingLife threshold
|
||||
@@ -296,8 +286,7 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
if (part instanceof CostSacrifice sac) {
|
||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||
|
||||
if (sac.payCostFromSource() && source.isCreature()) {
|
||||
@@ -346,12 +335,11 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
if (part instanceof CostSacrifice sac) {
|
||||
if (suppressRecursiveSacCostCheck) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||
|
||||
String type = sac.getType();
|
||||
@@ -620,7 +608,7 @@ public class ComputerUtilCost {
|
||||
}
|
||||
|
||||
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
|
||||
&& CostPayment.canPayAdditionalCosts(cost, sa, effect);
|
||||
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
|
||||
}
|
||||
|
||||
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
||||
|
||||
@@ -158,7 +158,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
// Mana abilities on the same card
|
||||
String shardMana = shard.toString().replaceAll("\\{", "").replaceAll("\\}", "");
|
||||
String shardMana = shard.toShortString();
|
||||
|
||||
boolean payWithAb1 = ability1.getManaPart().mana(ability1).contains(shardMana);
|
||||
boolean payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana);
|
||||
@@ -642,7 +642,8 @@ public class ComputerUtilMana {
|
||||
List<SpellAbility> paymentList = Lists.newArrayList();
|
||||
final ManaPool manapool = ai.getManaPool();
|
||||
|
||||
// Apply the color/type conversion matrix if necessary
|
||||
// Apply color/type conversion matrix if necessary (already done via autopay)
|
||||
if (ai.getControllingPlayer() == null) {
|
||||
manapool.restoreColorReplacements();
|
||||
CardPlayOption mayPlay = sa.getMayPlayOption();
|
||||
if (!effect) {
|
||||
@@ -656,10 +657,13 @@ public class ComputerUtilMana {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
|
||||
}
|
||||
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
|
||||
}
|
||||
|
||||
// not worth checking if it makes sense to not spend floating first
|
||||
if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) {
|
||||
CostPayment.handleOfferings(sa, test, cost.isPaid());
|
||||
return true; // paid all from floating mana
|
||||
// paid all from floating mana
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
|
||||
@@ -1326,7 +1330,9 @@ public class ComputerUtilMana {
|
||||
}
|
||||
}
|
||||
|
||||
if (!effect) {
|
||||
CostAdjustment.adjust(manaCost, sa, null, test);
|
||||
}
|
||||
|
||||
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
|
||||
ManaCost mkCost = sa.getPayCosts().getTotalMana();
|
||||
|
||||
@@ -160,12 +160,6 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(20, "protection");
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
|
||||
}
|
||||
}
|
||||
|
||||
// paired creatures are more valuable because they grant a bonus to the other creature
|
||||
if (c.isPaired()) {
|
||||
value += addValue(14, "paired");
|
||||
@@ -213,11 +207,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(1, "untapped");
|
||||
}
|
||||
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
value += addValue(10, "manadork");
|
||||
}
|
||||
|
||||
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
|
||||
if (!c.canUntap(c.getController(), true)) {
|
||||
if (c.isTapped()) {
|
||||
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
|
||||
} else {
|
||||
@@ -226,6 +216,17 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
} else {
|
||||
value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned");
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
|
||||
}
|
||||
}
|
||||
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
value += addValue(10, "manadork");
|
||||
}
|
||||
|
||||
// use scaling because the creature is only available halfway
|
||||
if (c.hasKeyword(Keyword.PHASING)) {
|
||||
value -= subValue(Math.max(20, value / 2), "phasing");
|
||||
|
||||
@@ -13,6 +13,7 @@ import forge.card.mana.ManaAtom;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.effects.DetachedCardEffect;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
@@ -1305,10 +1306,10 @@ public abstract class GameState {
|
||||
} else if (info.startsWith("FaceDown")) {
|
||||
c.turnFaceDown(true);
|
||||
if (info.endsWith("Manifested")) {
|
||||
c.setManifested(true);
|
||||
c.setManifested(new SpellAbility.EmptySa(ApiType.Manifest, c));
|
||||
}
|
||||
if (info.endsWith("Cloaked")) {
|
||||
c.setCloaked(true);
|
||||
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
|
||||
}
|
||||
} else if (info.startsWith("Transformed")) {
|
||||
c.setState(CardStateName.Transformed, true);
|
||||
@@ -1408,7 +1409,7 @@ public abstract class GameState {
|
||||
} else if (info.equals("Foretold")) {
|
||||
c.setForetold(true);
|
||||
c.turnFaceDown(true);
|
||||
c.addMayLookTemp(c.getOwner());
|
||||
c.addMayLookFaceDownExile(c.getOwner());
|
||||
} else if (info.equals("ForetoldThisTurn")) {
|
||||
c.setTurnInZone(turn);
|
||||
} else if (info.equals("IsToken")) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import forge.game.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.effects.CharmEffect;
|
||||
import forge.game.ability.effects.RollDiceEffect;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
@@ -745,6 +746,30 @@ public class PlayerControllerAi extends PlayerController {
|
||||
return Aggregates.random(rolls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> chooseDiceToReroll(List<Integer> rolls) {
|
||||
//TODO create AI logic for this
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer chooseRollToModify(List<Integer> rolls) {
|
||||
//TODO create AI logic for this
|
||||
return Aggregates.random(rolls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RollDiceEffect.DieRollResult chooseRollToSwap(List<RollDiceEffect.DieRollResult> rolls) {
|
||||
//TODO create AI logic for this
|
||||
return Aggregates.random(rolls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseRollSwapValue(List<String> swapChoices, Integer currentResult, int power, int toughness) {
|
||||
//TODO create AI logic for this
|
||||
return Aggregates.random(swapChoices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
|
||||
return !ComputerUtil.wantMulligan(player, cardsToReturn);
|
||||
@@ -1207,6 +1232,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView<Player> allPayers) {
|
||||
// TODO logic for AI to pay rerolls and modification costs
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) {
|
||||
for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) {
|
||||
@@ -1265,8 +1295,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
public boolean playSaFromPlayEffect(SpellAbility tgtSA) {
|
||||
boolean optional = !tgtSA.getPayCosts().isMandatory();
|
||||
boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
|
||||
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
|
||||
Spell spell = (Spell) tgtSA;
|
||||
if (tgtSA instanceof Spell spell) { // Isn't it ALWAYS a spell?
|
||||
// TODO if mandatory AI is only forced to use mana when it's already in the pool
|
||||
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
|
||||
return ComputerUtil.playStack(tgtSA, player, getGame());
|
||||
@@ -1390,11 +1419,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa);
|
||||
}
|
||||
|
||||
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
if (source != null && source.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
|
||||
// If any Conspiracies are present, try not to choose the same name twice
|
||||
// (otherwise the AI will spam the same name)
|
||||
for (Card consp : player.getCardsIn(ZoneType.Command)) {
|
||||
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
if (consp.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
|
||||
String chosenName = consp.getNamedCard();
|
||||
if (!chosenName.isEmpty()) {
|
||||
aiLibrary = CardLists.filter(aiLibrary, CardPredicates.nameNotEquals(chosenName));
|
||||
|
||||
@@ -1469,6 +1469,7 @@ public class SpecialCardAi {
|
||||
if (best != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(best);
|
||||
sa.setXManaCostPaid(best.getCMC());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ public abstract class SpellAbilityAi {
|
||||
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
|
||||
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|
||||
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|
||||
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
|
||||
}
|
||||
|
||||
@@ -342,9 +342,9 @@ public abstract class SpellAbilityAi {
|
||||
for (T ent : options) {
|
||||
if (ent instanceof Player) {
|
||||
hasPlayer = true;
|
||||
} else if (ent instanceof Card) {
|
||||
} else if (ent instanceof Card card) {
|
||||
hasCard = true;
|
||||
if (((Card)ent).isPlaneswalker() || ((Card)ent).isBattle()) {
|
||||
if (card.isPlaneswalker() || card.isBattle()) {
|
||||
hasAttackableCard = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.EachDamage, DamageEachAi.class)
|
||||
.put(ApiType.Effect, EffectAi.class)
|
||||
.put(ApiType.Encode, EncodeAi.class)
|
||||
.put(ApiType.Endure, EndureAi.class)
|
||||
.put(ApiType.EndCombatPhase, EndTurnAi.class)
|
||||
.put(ApiType.EndTurn, EndTurnAi.class)
|
||||
.put(ApiType.ExchangeLife, LifeExchangeAi.class)
|
||||
|
||||
@@ -24,6 +24,7 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityContinuous;
|
||||
import forge.game.staticability.StaticAbilityLayer;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.FileSection;
|
||||
import forge.util.collect.FCollectionView;
|
||||
@@ -562,7 +563,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0);
|
||||
if (traits != null) {
|
||||
for (StaticAbility stAb : traits.getStaticAbilities()) {
|
||||
if ("Continuous".equals(stAb.getParam("Mode"))) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
for (final StaticAbilityLayer layer : stAb.getLayers()) {
|
||||
StaticAbilityContinuous.applyContinuousAbility(stAb, new CardCollection(card), layer);
|
||||
}
|
||||
|
||||
@@ -15,20 +15,23 @@ import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityCantAttackBlock;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -130,7 +133,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
int power = 0, toughness = 0;
|
||||
List<String> keywords = Lists.newArrayList();
|
||||
for (StaticAbility stAb : source.getStaticAbilities()) {
|
||||
if ("Continuous".equals(stAb.getParam("Mode"))) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
if (stAb.hasParam("AddPower")) {
|
||||
power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
|
||||
}
|
||||
@@ -307,9 +310,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
String type = "";
|
||||
|
||||
for (final StaticAbility stAb : attachSource.getStaticAbilities()) {
|
||||
final Map<String, String> stab = stAb.getMapParams();
|
||||
if (stab.get("Mode").equals("Continuous") && stab.containsKey("AddType")) {
|
||||
type = stab.get("AddType");
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddType")) {
|
||||
type = stAb.getParam("AddType");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,9 +373,39 @@ public class AttachAi extends SpellAbilityAi {
|
||||
*/
|
||||
private static Card attachAIKeepTappedPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory, final Card attachSource) {
|
||||
// AI For Cards like Paralyzing Grasp and Glimmerdust Nap
|
||||
|
||||
// check for ETB Trigger
|
||||
boolean tapETB = isAuraSpell(sa) && attachSource.getTriggers().anyMatch(t -> {
|
||||
if (t.getMode() != TriggerType.ChangesZone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ZoneType.Battlefield.toString().equals(t.getParam("Destination"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (t.hasParam("ValidCard") && !t.getParam("ValidCard").contains("Self")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SpellAbility tSa = t.ensureAbility();
|
||||
if (tSa == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ApiType.Tap.equals(tSa.getApi())) {
|
||||
return false;
|
||||
}
|
||||
if (!"Enchanted".equals(tSa.getParam("Defined"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
final List<Card> prefList = CardLists.filter(list, c -> {
|
||||
// Don't do Untapped Vigilance cards
|
||||
if (c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
|
||||
if (!tapETB && c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -388,21 +420,10 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!c.isEnchanted()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Iterable<Card> auras = c.getEnchantedBy();
|
||||
for (Card aura : auras) {
|
||||
SpellAbility auraSA = aura.getSpells().get(0);
|
||||
if (auraSA.getApi() == ApiType.Attach) {
|
||||
if ("KeepTapped".equals(auraSA.getParam("AILogic"))) {
|
||||
// Don't attach multiple KeepTapped Auras to one card
|
||||
// already affected
|
||||
if (!c.canUntap(c.getController(), true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -549,28 +570,46 @@ public class AttachAi extends SpellAbilityAi {
|
||||
final Card attachSource) {
|
||||
// AI For choosing a Card to Animate.
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final Card attachSourceLki = CardCopyService.getLKICopy(attachSource);
|
||||
Card attachSourceLki = null;
|
||||
for (Trigger t : attachSource.getTriggers()) {
|
||||
if (!t.getMode().equals(TriggerType.ChangesZone)) {
|
||||
continue;
|
||||
}
|
||||
if (!"Battlefield".equals(t.getParam("Destination"))) {
|
||||
continue;
|
||||
}
|
||||
if (!"Card.Self".equals(t.getParam("ValidCard"))) {
|
||||
continue;
|
||||
}
|
||||
SpellAbility trigSa = t.ensureAbility();
|
||||
SpellAbility animateSa = trigSa.findSubAbilityByType(ApiType.Animate);
|
||||
if (animateSa == null) {
|
||||
continue;
|
||||
}
|
||||
animateSa.setActivatingPlayer(sa.getActivatingPlayer());
|
||||
attachSourceLki = AnimateAi.becomeAnimated(attachSource, animateSa);
|
||||
}
|
||||
if (attachSourceLki == null) {
|
||||
return null;
|
||||
}
|
||||
attachSourceLki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||
// Suppress original attach Spell to replace it with another
|
||||
attachSourceLki.getFirstAttachSpell().setSuppressed(true);
|
||||
final Card finalAttachSourceLki = attachSourceLki;
|
||||
|
||||
//TODO for Reanimate Auras i need the new Attach Spell, in later versions it might be part of the Enchant Keyword
|
||||
attachSourceLki.addSpellAbility(AbilityFactory.getAbility(attachSourceLki, "NewAttach"));
|
||||
List<Card> betterList = CardLists.filter(list, c -> {
|
||||
final Card lki = CardCopyService.getLKICopy(c);
|
||||
// need to fake it as if lki would be on the battlefield
|
||||
lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||
|
||||
// Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered
|
||||
attachSourceLki.clearRemembered();
|
||||
attachSourceLki.addRemembered(lki);
|
||||
finalAttachSourceLki.clearRemembered();
|
||||
finalAttachSourceLki.addRemembered(lki);
|
||||
|
||||
// need to check what the cards would be on the battlefield
|
||||
// do not attach yet, that would cause Events
|
||||
CardCollection preList = new CardCollection(lki);
|
||||
preList.add(attachSourceLki);
|
||||
preList.add(finalAttachSourceLki);
|
||||
c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList);
|
||||
boolean result = lki.canBeAttached(attachSourceLki, null);
|
||||
boolean result = lki.canBeAttached(finalAttachSourceLki, null);
|
||||
|
||||
//reset static abilities
|
||||
c.getGame().getAction().checkStaticAbilities(false);
|
||||
@@ -795,27 +834,45 @@ public class AttachAi extends SpellAbilityAi {
|
||||
int totPower = 0;
|
||||
final List<String> keywords = new ArrayList<>();
|
||||
|
||||
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
|
||||
final Map<String, String> stabMap = stAbility.getMapParams();
|
||||
boolean cantAttack = false;
|
||||
boolean cantBlock = false;
|
||||
|
||||
if (!stabMap.get("Mode").equals("Continuous")) {
|
||||
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
|
||||
if (stAbility.checkMode(StaticAbilityMode.CantAttack)) {
|
||||
String valid = stAbility.getParam("ValidCard");
|
||||
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
|
||||
cantAttack = true;
|
||||
}
|
||||
} else if (stAbility.checkMode(StaticAbilityMode.CantBlock)) {
|
||||
String valid = stAbility.getParam("ValidCard");
|
||||
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
|
||||
cantBlock = true;
|
||||
}
|
||||
} else if (stAbility.checkMode(StaticAbilityMode.CantBlockBy)) {
|
||||
String valid = stAbility.getParam("ValidBlocker");
|
||||
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
|
||||
cantBlock = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String affected = stabMap.get("Affected");
|
||||
final String affected = stAbility.getParam("Affected");
|
||||
|
||||
if (affected == null) {
|
||||
continue;
|
||||
}
|
||||
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), sa);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), sa);
|
||||
|
||||
String kws = stabMap.get("AddKeyword");
|
||||
String kws = stAbility.getParam("AddKeyword");
|
||||
if (kws != null) {
|
||||
keywords.addAll(Arrays.asList(kws.split(" & ")));
|
||||
}
|
||||
kws = stabMap.get("AddHiddenKeyword");
|
||||
kws = stAbility.getParam("AddHiddenKeyword");
|
||||
if (kws != null) {
|
||||
keywords.addAll(Arrays.asList(kws.split(" & ")));
|
||||
}
|
||||
@@ -851,6 +908,12 @@ public class AttachAi extends SpellAbilityAi {
|
||||
prefList = CardLists.filter(prefList, c -> c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c));
|
||||
}
|
||||
|
||||
if (cantAttack) {
|
||||
prefList = CardLists.filter(prefList, c -> c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c));
|
||||
} else if (cantBlock) { // TODO better can block filter?
|
||||
prefList = CardLists.filter(prefList, c -> c.isCreature() && !ComputerUtilCard.isUselessCreature(ai, c));
|
||||
}
|
||||
|
||||
//some auras aren't useful in multiples
|
||||
if (attachSource.hasSVar("NonStackingAttachEffect")) {
|
||||
prefList = CardLists.filter(prefList,
|
||||
@@ -925,6 +988,10 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isAuraSpell(final SpellAbility sa) {
|
||||
return sa.isSpell() && sa.getHostCard().isAura();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach preference.
|
||||
*
|
||||
@@ -940,7 +1007,23 @@ public class AttachAi extends SpellAbilityAi {
|
||||
*/
|
||||
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
|
||||
GameObject o;
|
||||
if (tgt.canTgtPlayer()) {
|
||||
boolean spellCanTargetPlayer = false;
|
||||
if (isAuraSpell(sa)) {
|
||||
Card source = sa.getHostCard();
|
||||
if (!source.hasKeyword(Keyword.ENCHANT)) {
|
||||
return false;
|
||||
}
|
||||
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
|
||||
String ko = ki.getOriginal();
|
||||
String m[] = ko.split(":");
|
||||
String v = m[1];
|
||||
if (v.contains("Player") || v.contains("Opponent")) {
|
||||
spellCanTargetPlayer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tgt.canTgtPlayer() && (!isAuraSpell(sa) || spellCanTargetPlayer)) {
|
||||
List<Player> targetable = new ArrayList<>();
|
||||
for (final Player player : sa.getHostCard().getGame().getPlayers()) {
|
||||
if (sa.canTarget(player)) {
|
||||
@@ -1005,9 +1088,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
CardCollection toRemove = new CardCollection();
|
||||
for (Trigger t : attachSource.getTriggers()) {
|
||||
if (t.getMode() == TriggerType.ChangesZone) {
|
||||
final Map<String, String> params = t.getMapParams();
|
||||
if ("Card.Self".equals(params.get("ValidCard"))
|
||||
&& "Battlefield".equals(params.get("Destination"))) {
|
||||
if ("Card.Self".equals(t.getParam("ValidCard"))
|
||||
&& "Battlefield".equals(t.getParam("Destination"))) {
|
||||
SpellAbility trigSa = t.ensureAbility();
|
||||
if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
|
||||
for (Card target : list) {
|
||||
@@ -1067,29 +1149,27 @@ public class AttachAi extends SpellAbilityAi {
|
||||
boolean grantingExtraBlock = false;
|
||||
|
||||
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
|
||||
final Map<String, String> stabMap = stAbility.getMapParams();
|
||||
|
||||
if (!"Continuous".equals(stabMap.get("Mode"))) {
|
||||
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String affected = stabMap.get("Affected");
|
||||
final String affected = stAbility.getParam("Affected");
|
||||
|
||||
if (affected == null) {
|
||||
continue;
|
||||
}
|
||||
if (affected.contains(stCheck) || affected.contains("AttachedBy")) {
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), stAbility);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), stAbility);
|
||||
|
||||
grantingAbilities |= stabMap.containsKey("AddAbility");
|
||||
grantingExtraBlock |= stabMap.containsKey("CanBlockAmount") || stabMap.containsKey("CanBlockAny");
|
||||
grantingAbilities |= stAbility.hasParam("AddAbility");
|
||||
grantingExtraBlock |= stAbility.hasParam("CanBlockAmount") || stAbility.hasParam("CanBlockAny");
|
||||
|
||||
String kws = stabMap.get("AddKeyword");
|
||||
String kws = stAbility.getParam("AddKeyword");
|
||||
if (kws != null) {
|
||||
keywords.addAll(Arrays.asList(kws.split(" & ")));
|
||||
}
|
||||
kws = stabMap.get("AddHiddenKeyword");
|
||||
kws = stAbility.getParam("AddHiddenKeyword");
|
||||
if (kws != null) {
|
||||
keywords.addAll(Arrays.asList(kws.split(" & ")));
|
||||
}
|
||||
@@ -1158,12 +1238,17 @@ public class AttachAi extends SpellAbilityAi {
|
||||
// TODO Somehow test for definitive advantage (e.g. opponent low on health, AI is attacking)
|
||||
// to be able to deal the final blow with an enchanted vehicle like that
|
||||
boolean canOnlyTargetCreatures = true;
|
||||
for (String valid : ObjectUtils.firstNonNull(attachSource.getFirstAttachSpell(), sa).getTargetRestrictions().getValidTgts()) {
|
||||
if (!valid.startsWith("Creature")) {
|
||||
if (attachSource.isAura()) {
|
||||
for (KeywordInterface ki : attachSource.getKeywords(Keyword.ENCHANT)) {
|
||||
String o = ki.getOriginal();
|
||||
String m[] = o.split(":");
|
||||
String v = m[1];
|
||||
if (!v.startsWith("Creature")) {
|
||||
canOnlyTargetCreatures = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (canOnlyTargetCreatures && (attachSource.isAura() || attachSource.isEquipment())) {
|
||||
prefList = CardLists.filter(prefList, c -> c.getTimesCrewedThisTurn() == 0 || (attachSource.isEquipment() && attachSource.getGame().getPhaseHandler().is(PhaseType.MAIN1, ai)));
|
||||
}
|
||||
@@ -1387,8 +1472,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("ChangeType".equals(logic)) {
|
||||
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("KeepTapped".equals(logic)) {
|
||||
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("Animate".equals(logic)) {
|
||||
c = attachAIAnimatePreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("Reanimate".equals(logic)) {
|
||||
@@ -1399,6 +1482,12 @@ public class AttachAi extends SpellAbilityAi {
|
||||
c = attachAIHighestEvaluationPreference(prefList);
|
||||
}
|
||||
|
||||
if (isAuraSpell(sa)) {
|
||||
if (attachSource.getReplacementEffects().anyMatch(re -> re.getMode().equals(ReplacementType.Untap) && re.getLayer().equals(ReplacementLayer.CantHappen))) {
|
||||
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
|
||||
}
|
||||
}
|
||||
|
||||
// Consider exceptional cases which break the normal evaluation rules
|
||||
if (!isUsefulAttachAction(ai, c, sa)) {
|
||||
return null;
|
||||
@@ -1551,8 +1640,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|
||||
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
|
||||
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
|
||||
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
|
||||
return !card.isUntapped();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -138,8 +137,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (aiLogic != null) {
|
||||
if (aiLogic.equals("Always")) {
|
||||
return true;
|
||||
} else if (aiLogic.startsWith("ExileSpell")) {
|
||||
return doExileSpellLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
|
||||
return doSacAndUpgradeLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
|
||||
@@ -878,6 +875,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
origin.addAll(ZoneType.listValueOf(sa.getParam("TgtZone")));
|
||||
}
|
||||
|
||||
if (origin.contains(ZoneType.Stack) && doExileSpellLogic(ai, sa, mandatory)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
final Game game = ai.getGame();
|
||||
|
||||
@@ -902,7 +903,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
|
||||
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
|
||||
@@ -914,6 +914,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (sa.isSpell()) {
|
||||
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
|
||||
}
|
||||
|
||||
// list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (sa.hasParam("AttachedTo")) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1282,8 +1285,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
if (sa.canTarget(choice)) {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
|
||||
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
|
||||
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
|
||||
@@ -1448,6 +1453,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// AI Targeting
|
||||
Card choice = null;
|
||||
|
||||
// Filter out cards TargetsForEachPlayer
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
if (mostExpensivePermanent.isCreature()
|
||||
@@ -2061,33 +2069,26 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doExileSpellLogic(final Player aiPlayer, final SpellAbility sa) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
SpellAbilityStackInstance top = aiPlayer.getGame().getStack().peek();
|
||||
List<ApiType> dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
|
||||
int manaCost = 0;
|
||||
int minCost = 0;
|
||||
|
||||
if (aiLogic.contains(".")) {
|
||||
minCost = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1));
|
||||
private static boolean doExileSpellLogic(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
List<ApiType> dangerousApi = null;
|
||||
CardCollection spells = new CardCollection(ai.getGame().getStackZone().getCards());
|
||||
Collections.reverse(spells);
|
||||
if (!mandatory && !spells.isEmpty()) {
|
||||
spells = spells.subList(0, 1);
|
||||
spells = ComputerUtil.filterAITgts(sa, ai, spells, true);
|
||||
dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
|
||||
}
|
||||
|
||||
if (top != null) {
|
||||
SpellAbility topSA = top.getSpellAbility();
|
||||
if (topSA != null) {
|
||||
if (topSA.getPayCosts().hasManaCost()) {
|
||||
manaCost = topSA.getPayCosts().getTotalMana().getCMC();
|
||||
}
|
||||
|
||||
if ((manaCost >= minCost || dangerousApi.contains(topSA.getApi()))
|
||||
&& topSA.getActivatingPlayer().isOpponentOf(aiPlayer)
|
||||
&& sa.canTargetSpellAbility(topSA)) {
|
||||
for (Card c : spells) {
|
||||
SpellAbility topSA = ai.getGame().getStack().getSpellMatchingHost(c);
|
||||
if (topSA != null && (dangerousApi == null ||
|
||||
(dangerousApi.contains(topSA.getApi()) && topSA.getActivatingPlayer().isOpponentOf(ai)))
|
||||
&& sa.canTarget(topSA)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(topSA);
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,10 +161,10 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
// FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "Draw")) {
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.DRAW)) {
|
||||
return skipDraw;
|
||||
}
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "BeginCombat")) {
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.COMBAT_BEGIN)) {
|
||||
return skipCombat;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public class CloakAi extends ManifestBaseAi {
|
||||
// (e.g. Grafdigger's Cage)
|
||||
Card topCopy = CardCopyService.getLKICopy(card);
|
||||
topCopy.turnFaceDownNoUpdate();
|
||||
topCopy.setCloaked(true);
|
||||
topCopy.setCloaked(sa);
|
||||
|
||||
if (ComputerUtil.isETBprevented(topCopy)) {
|
||||
return false;
|
||||
|
||||
@@ -4,6 +4,7 @@ import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -18,6 +19,13 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
return false; // can't draw anything
|
||||
}
|
||||
|
||||
Card host = sa.getHostCard();
|
||||
|
||||
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
|
||||
if (num == 0) {
|
||||
return false; // Won't do anything
|
||||
}
|
||||
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
|
||||
@@ -205,6 +205,9 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
while (t == null) {
|
||||
// filter by MustTarget requirement
|
||||
CardCollection originalList = new CardCollection(list);
|
||||
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
|
||||
|
||||
if (planeswalkers > 0) {
|
||||
|
||||
@@ -152,6 +152,8 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
|
||||
// target loop
|
||||
while (sa.canAddMoreTarget()) {
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
|
||||
sa.resetTargets();
|
||||
|
||||
@@ -95,7 +95,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
final String damage = sa.getParam("NumDmg");
|
||||
int dmg = calculateDamageAmount(sa, source, damage);
|
||||
|
||||
if (damage.equals("X") || source.getSVar("X").equals("Count$xPaid")) {
|
||||
if (damage.equals("X") || (dmg == 0 && source.getSVar("X").equals("Count$xPaid"))) {
|
||||
if (sa.getSVar("X").equals("Count$xPaid") || sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
|
||||
|
||||
@@ -216,6 +216,8 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
CardCollection originalList = new CardCollection(list);
|
||||
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
|
||||
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
@@ -275,6 +277,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
choice = aura;
|
||||
}
|
||||
}
|
||||
// TODO What about stolen permanents we're getting back at the end of the turn?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,8 +287,10 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
if (sa.canTarget(choice)) {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
} else if (sa.hasParam("Defined")) {
|
||||
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if ("WillSkipTurn".equals(logic) && (source.getController().equals(ai)
|
||||
@@ -361,7 +366,10 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
Card c = ComputerUtilCard.getBestAI(preferred);
|
||||
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
}
|
||||
preferred.remove(c);
|
||||
}
|
||||
}
|
||||
@@ -382,7 +390,9 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
} else {
|
||||
c = ComputerUtilCard.getCheapestPermanentAI(list, sa, false);
|
||||
}
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
}
|
||||
list.remove(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,12 +515,17 @@ public class DrawAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((computerHandSize + numCards > computerMaxHandSize)
|
||||
&& game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& !sa.isTrigger()
|
||||
&& !assumeSafeX) {
|
||||
if ((computerHandSize + numCards > computerMaxHandSize)) {
|
||||
// Don't draw too many cards and then risk discarding cards at EOT
|
||||
if (!drawback) {
|
||||
if (game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& !sa.isTrigger()
|
||||
&& !assumeSafeX
|
||||
&& !drawback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (computerHandSize > computerMaxHandSize) {
|
||||
// Don't make my hand size get too big if already at max
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
140
forge-ai/src/main/java/forge/ai/ability/EndureAi.java
Normal file
140
forge-ai/src/main/java/forge/ai/ability/EndureAi.java
Normal file
@@ -0,0 +1,140 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class EndureAi extends SpellAbilityAi {
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
|
||||
if (bestCreature == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreature);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean shouldPutCounters(Player ai, SpellAbility sa) {
|
||||
// TODO: adapted from Fabricate AI in TokenAi, maybe can be refactored to a single method
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
final String num = sa.getParamOrDefault("Num", "1");
|
||||
final int amount = AbilityUtils.calculateAmount(source, num, sa);
|
||||
|
||||
// if host would leave the play or if host is useless, create the token
|
||||
if (source.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(ai, source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// need a copy for one with extra +1/+1 counter boost,
|
||||
// without causing triggers to run
|
||||
final Card copy = CardCopyService.getLKICopy(source);
|
||||
copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + amount);
|
||||
copy.setZone(source.getZone());
|
||||
|
||||
// if host would put into the battlefield attacking
|
||||
Combat combat = source.getGame().getCombat();
|
||||
if (combat != null && combat.isAttacking(source)) {
|
||||
final Player defender = combat.getDefenderPlayerByAttacker(source);
|
||||
return defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true);
|
||||
}
|
||||
|
||||
// if the host has haste and can attack
|
||||
if (CombatUtil.canAttack(copy)) {
|
||||
for (final Player opp : ai.getOpponents()) {
|
||||
if (CombatUtil.canAttack(copy, opp) &&
|
||||
opp.canLoseLife() &&
|
||||
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check for trigger to turn token ETB into +1/+1 counter for host
|
||||
// TODO check for trigger to turn token ETB into damage or life loss for opponent
|
||||
// in these cases token might be preferred even if they would not survive
|
||||
|
||||
// evaluate creature with counters
|
||||
int evalCounter = ComputerUtilCard.evaluateCreature(copy);
|
||||
|
||||
// spawn the token so it's possible to evaluate it
|
||||
final Card token = TokenInfo.getProtoType("w_x_x_spirit", sa, ai, false);
|
||||
|
||||
token.setController(ai, 0);
|
||||
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||
token.setTokenSpawningAbility(sa);
|
||||
|
||||
// evaluate the generated token
|
||||
token.setBasePowerString(num);
|
||||
token.setBasePower(amount);
|
||||
token.setBaseToughnessString(num);
|
||||
token.setBaseToughness(amount);
|
||||
|
||||
boolean result = true;
|
||||
|
||||
// need to check what the cards would be on the battlefield
|
||||
// do not attach yet, that would cause Events
|
||||
CardCollection preList = new CardCollection(token);
|
||||
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
|
||||
|
||||
// token would not survive
|
||||
if (!token.isCreature() || token.getNetToughness() < 1) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
int evalToken = ComputerUtilCard.evaluateCreature(token);
|
||||
result = evalToken < evalCounter;
|
||||
}
|
||||
|
||||
//reset static abilities
|
||||
game.getAction().checkStaticAbilities(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
return shouldPutCounters(player, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public class ManifestAi extends ManifestBaseAi {
|
||||
// (e.g. Grafdigger's Cage)
|
||||
Card topCopy = CardCopyService.getLKICopy(card);
|
||||
topCopy.turnFaceDownNoUpdate();
|
||||
topCopy.setManifested(true);
|
||||
topCopy.setManifested(sa);
|
||||
|
||||
if (ComputerUtil.isETBprevented(topCopy)) {
|
||||
return false;
|
||||
|
||||
@@ -14,6 +14,7 @@ import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -47,7 +48,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
if (sa.isDash()) {
|
||||
//only checks that the dashed creature will attack
|
||||
if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat"))
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))
|
||||
return false;
|
||||
if (ComputerUtilCost.canPayCost(sa.getHostCard().getSpellPermanent(), ai, false)) {
|
||||
//do not dash if creature can be played normally
|
||||
@@ -70,7 +71,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
// after attacking
|
||||
if (card.hasSVar("EndOfTurnLeavePlay")
|
||||
&& (!ph.isPlayerTurn(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
|| game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat"))) {
|
||||
|| game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))) {
|
||||
// AiPlayDecision.AnotherTime
|
||||
return false;
|
||||
}
|
||||
@@ -154,7 +155,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
boolean canCastAtOppTurn = true;
|
||||
for (Card c : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
|
||||
for (StaticAbility s : c.getStaticAbilities()) {
|
||||
if ("CantBeCast".equals(s.getParam("Mode")) && StringUtils.contains(s.getParam("Activator"), "NonActive")
|
||||
if (s.checkMode(StaticAbilityMode.CantBeCast) && StringUtils.contains(s.getParam("Activator"), "NonActive")
|
||||
&& (!s.getParam("Activator").startsWith("You") || c.getController().equals(ai))) {
|
||||
canCastAtOppTurn = false;
|
||||
break;
|
||||
|
||||
@@ -542,6 +542,8 @@ public class PumpAi extends PumpAiBase {
|
||||
Card t = null;
|
||||
// boolean goodt = false;
|
||||
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
if (mandatory || ComputerUtil.activateForCost(sa, ai)) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import forge.game.combat.CombatUtil;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -137,7 +136,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
return CombatUtil.canBlockAtLeastOne(card, attackers);
|
||||
} else if (keyword.endsWith("This card doesn't untap during your next untap step.")) {
|
||||
return !ph.getPhase().isBefore(PhaseType.MAIN2) && !card.isUntapped() && ph.isPlayerTurn(ai)
|
||||
&& Untap.canUntap(card);
|
||||
&& card.canUntap(card.getController(), true);
|
||||
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")
|
||||
|| keyword.endsWith("Prevent all damage that would be dealt by CARDNAME.")) {
|
||||
if (ph.isPlayerTurn(ai) && (!(CombatUtil.canBlock(card) || combat != null && combat.isBlocking(card))
|
||||
|
||||
@@ -6,6 +6,7 @@ import forge.ai.SpellAbilityAi;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -142,7 +143,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
// hidden agenda
|
||||
if (card.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")
|
||||
if (card.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)
|
||||
&& card.isInZone(ZoneType.Command)) {
|
||||
String chosenName = card.getNamedCard();
|
||||
for (Card cast : ai.getGame().getStack().getSpellsCastThisTurn()) {
|
||||
|
||||
@@ -206,7 +206,8 @@ public class TokenAi extends SpellAbilityAi {
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
|
||||
&& game.getCombat() != null
|
||||
&& !game.getCombat().getAttackers().isEmpty()
|
||||
&& alwaysOnOppAttack) {
|
||||
&& alwaysOnOppAttack
|
||||
&& actualToken.isCreature()) {
|
||||
for (Card attacker : game.getCombat().getAttackers()) {
|
||||
if (CombatUtil.canBlock(attacker, actualToken)) {
|
||||
return true;
|
||||
@@ -366,8 +367,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
private boolean tgtRoleAura(final Player ai, final SpellAbility sa, final Card tok, final boolean mandatory) {
|
||||
boolean isCurse = "Curse".equals(sa.getParam("AILogic")) ||
|
||||
tok.getFirstAttachSpell().getParamOrDefault("AILogic", "").equals("Curse");
|
||||
boolean isCurse = "Curse".equals(sa.getParam("AILogic")) || "Curse".equals(tok.getSVar("AttachAILogic"));
|
||||
List<Card> tgts = CardUtil.getValidCardsToTarget(sa);
|
||||
|
||||
// look for card without role from ai
|
||||
|
||||
@@ -16,7 +16,6 @@ import forge.game.cost.CostTap;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -338,7 +337,7 @@ public class UntapAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// See if there's anything to untap that is tapped and that doesn't untap during the next untap step by itself
|
||||
CardCollection noAutoUntap = CardLists.filter(untapList, Untap.CANUNTAP.negate());
|
||||
CardCollection noAutoUntap = CardLists.filter(untapList, c -> !c.canUntap(c.getController(), true));
|
||||
if (!noAutoUntap.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(noAutoUntap);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public class GameCopier {
|
||||
newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn());
|
||||
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
|
||||
newPlayer.setSpeed(origPlayer.getSpeed());
|
||||
newPlayer.setBlessing(origPlayer.hasBlessing());
|
||||
newPlayer.setBlessing(origPlayer.hasBlessing(), null);
|
||||
newPlayer.setRevolt(origPlayer.hasRevolt());
|
||||
newPlayer.setDescended(origPlayer.getDescended());
|
||||
newPlayer.setLibrarySearched(origPlayer.getLibrarySearched());
|
||||
@@ -373,10 +373,10 @@ public class GameCopier {
|
||||
if (c.isFaceDown()) {
|
||||
newCard.turnFaceDown(true);
|
||||
if (c.isManifested()) {
|
||||
newCard.setManifested(true);
|
||||
newCard.setManifested(c.getManifestedSA());
|
||||
}
|
||||
if (c.isCloaked()) {
|
||||
newCard.setCloaked(true);
|
||||
newCard.setCloaked(c.getCloakedSA());
|
||||
}
|
||||
}
|
||||
if (c.isMonstrous()) {
|
||||
|
||||
@@ -23,17 +23,16 @@ public final class ImageKeys {
|
||||
|
||||
public static final String HIDDEN_CARD = "hidden";
|
||||
public static final String MORPH_IMAGE = "morph";
|
||||
public static final String DISGUISED_IMAGE = "disguised";
|
||||
public static final String MANIFEST_IMAGE = "manifest";
|
||||
public static final String CLOAKED_IMAGE = "cloaked";
|
||||
public static final String FORETELL_IMAGE = "foretell";
|
||||
public static final String BLESSING_IMAGE = "blessing";
|
||||
public static final String INITIATIVE_IMAGE = "initiative";
|
||||
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 BACKFACE_POSTFIX = "$alt";
|
||||
public static final String SPECFACE_W = "$wspec";
|
||||
public static final String SPECFACE_U = "$uspec";
|
||||
public static final String SPECFACE_B = "$bspec";
|
||||
public static final String SPECFACE_R = "$rspec";
|
||||
public static final String SPECFACE_G = "$gspec";
|
||||
|
||||
private static String CACHE_CARD_PICS_DIR, CACHE_TOKEN_PICS_DIR, CACHE_ICON_PICS_DIR, CACHE_BOOSTER_PICS_DIR,
|
||||
CACHE_FATPACK_PICS_DIR, CACHE_BOOSTERBOX_PICS_DIR, CACHE_PRECON_PICS_DIR, CACHE_TOURNAMENTPACK_PICS_DIR;
|
||||
@@ -93,13 +92,38 @@ public final class ImageKeys {
|
||||
return cachedCards.get(key);
|
||||
}
|
||||
public static File getImageFile(String key) {
|
||||
return getImageFile(key, false);
|
||||
}
|
||||
public static File getImageFile(String key, boolean artCrop) {
|
||||
if (StringUtils.isEmpty(key))
|
||||
return null;
|
||||
|
||||
final String dir;
|
||||
final String filename;
|
||||
if (key.startsWith(ImageKeys.TOKEN_PREFIX)) {
|
||||
filename = key.substring(ImageKeys.TOKEN_PREFIX.length());
|
||||
String[] tempdata = null;
|
||||
if (key.startsWith(ImageKeys.CARD_PREFIX)) {
|
||||
tempdata = key.substring(ImageKeys.CARD_PREFIX.length()).split("\\|");
|
||||
String tokenname = tempdata[0];
|
||||
if (tempdata.length > 1) {
|
||||
tokenname += "_" + tempdata[1];
|
||||
}
|
||||
if (tempdata.length > 2) {
|
||||
tokenname += "_" + tempdata[2];
|
||||
}
|
||||
filename = tokenname ;
|
||||
|
||||
dir = CACHE_CARD_PICS_DIR;
|
||||
} else if (key.startsWith(ImageKeys.TOKEN_PREFIX)) {
|
||||
tempdata = key.substring(ImageKeys.TOKEN_PREFIX.length()).split("\\|");
|
||||
String tokenname = tempdata[0];
|
||||
if (tempdata.length > 1) {
|
||||
tokenname += "_" + tempdata[1];
|
||||
}
|
||||
if (tempdata.length > 2) {
|
||||
tokenname += "_" + tempdata[2];
|
||||
}
|
||||
filename = tokenname;
|
||||
|
||||
dir = CACHE_TOKEN_PICS_DIR;
|
||||
} else if (key.startsWith(ImageKeys.ICON_PREFIX)) {
|
||||
filename = key.substring(ImageKeys.ICON_PREFIX.length());
|
||||
@@ -140,6 +164,54 @@ public final class ImageKeys {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
if (tempdata != null && dir.equals(CACHE_CARD_PICS_DIR)) {
|
||||
String setlessFilename = tempdata[0] + (artCrop ? ".artcrop" : ".fullborder");
|
||||
String setCode = tempdata.length > 1 ? tempdata[1] : "";
|
||||
String collectorNumber = tempdata.length > 2 ? tempdata[2] : "";
|
||||
if (!setCode.isEmpty()) {
|
||||
if (!collectorNumber.isEmpty()) {
|
||||
file = findFile(dir, setCode + "/" + collectorNumber + "_" + setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
file = findFile(dir, setCode + "/" + setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
file = findFile(dir, setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
if (tempdata != null && dir.equals(CACHE_TOKEN_PICS_DIR)) {
|
||||
String setlessFilename = tempdata[0];
|
||||
String setCode = tempdata.length > 1 ? tempdata[1] : "";
|
||||
String collectorNumber = tempdata.length > 2 ? tempdata[2] : "";
|
||||
if (!setCode.isEmpty()) {
|
||||
if (!collectorNumber.isEmpty()) {
|
||||
file = findFile(dir, setCode + "/" + collectorNumber + "_" + setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
file = findFile(dir, setCode + "/" + setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
file = findFile(dir, setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
// AE -> Ae and Ae -> AE for older cards with different file names
|
||||
// on case-sensitive file systems
|
||||
@@ -221,39 +293,7 @@ public final class ImageKeys {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
if (dir.equals(CACHE_TOKEN_PICS_DIR)) {
|
||||
int index = filename.lastIndexOf('_');
|
||||
if (index != -1) {
|
||||
String setlessFilename = filename.substring(0, index);
|
||||
String setCode = filename.substring(index + 1);
|
||||
// try with upper case set
|
||||
file = findFile(dir, setlessFilename + "_" + setCode.toUpperCase());
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
// try with lower case set
|
||||
file = findFile(dir, setlessFilename + "_" + setCode.toLowerCase());
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
// try without set name
|
||||
file = findFile(dir, setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
// if there's an art variant try without it
|
||||
if (setlessFilename.matches(".*[0-9]*$")) {
|
||||
file = findFile(dir, setlessFilename.replaceAll("[0-9]*$", ""));
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (filename.contains("/")) {
|
||||
if (filename.contains("/")) {
|
||||
String setlessFilename = filename.substring(filename.indexOf('/') + 1);
|
||||
file = findFile(dir, setlessFilename);
|
||||
if (file != null) {
|
||||
|
||||
@@ -95,12 +95,12 @@ public class StaticData {
|
||||
if (!loadNonLegalCards) {
|
||||
for (CardEdition e : editions) {
|
||||
if (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER) {
|
||||
List<CardEdition.CardInSet> eternalCards = e.getFunnyEternalCards();
|
||||
List<CardEdition.EditionEntry> eternalCards = e.getFunnyEternalCards();
|
||||
|
||||
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) {
|
||||
for (CardEdition.EditionEntry cis : e.getAllCardsInSet()) {
|
||||
if (eternalCards.contains(cis))
|
||||
continue;
|
||||
funnyCards.add(cis.name);
|
||||
funnyCards.add(cis.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,6 +217,9 @@ public class StaticData {
|
||||
}
|
||||
|
||||
public CardEdition getCardEdition(String setCode) {
|
||||
if (CardEdition.UNKNOWN_CODE.equals(setCode)) {
|
||||
return CardEdition.UNKNOWN;
|
||||
}
|
||||
CardEdition edition = this.editions.get(setCode);
|
||||
return edition;
|
||||
}
|
||||
@@ -248,6 +251,15 @@ public class StaticData {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a PaperCard by looking at all available card databases for any matching print.
|
||||
* @param cardName The name of the card
|
||||
* @return PaperCard instance found in one of the available CardDb databases, or <code>null</code> if not found.
|
||||
*/
|
||||
public PaperCard fetchCard(final String cardName) {
|
||||
return fetchCard(cardName, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a PaperCard by looking at all available card databases;
|
||||
* @param cardName The name of the card
|
||||
@@ -778,11 +790,11 @@ public class StaticData {
|
||||
|
||||
Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>();
|
||||
List<CompletableFuture<?>> futures = new ArrayList<>();
|
||||
for (CardEdition.CardInSet c : e.getAllCardsInSet()) {
|
||||
if (cardCount.containsKey(c.name)) {
|
||||
cardCount.put(c.name, Pair.of(c.collectorNumber != null && c.collectorNumber.startsWith("F"), cardCount.get(c.name).getRight() + 1));
|
||||
for (CardEdition.EditionEntry c : e.getAllCardsInSet()) {
|
||||
if (cardCount.containsKey(c.name())) {
|
||||
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), cardCount.get(c.name()).getRight() + 1));
|
||||
} else {
|
||||
cardCount.put(c.name, Pair.of(c.collectorNumber != null && c.collectorNumber.startsWith("F"), 1));
|
||||
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,9 +856,9 @@ public class StaticData {
|
||||
futures.clear();
|
||||
|
||||
// TODO: Audit token images here...
|
||||
for(Map.Entry<String, Integer> tokenEntry : e.getTokens().entrySet()) {
|
||||
for(Map.Entry<String, Collection<CardEdition.EditionEntry>> tokenEntry : e.getTokens().asMap().entrySet()) {
|
||||
final String name = tokenEntry.getKey();
|
||||
final int artIndex = tokenEntry.getValue();
|
||||
final int artIndex = tokenEntry.getValue().size();
|
||||
try {
|
||||
PaperToken token = getAllTokens().getToken(name, e.getCode());
|
||||
if (token == null) {
|
||||
@@ -983,4 +995,23 @@ public class StaticData {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public String getOtherImageKey(String name, String set) {
|
||||
if (this.editions.get(set) != null) {
|
||||
String realSetCode = this.editions.get(set).getOtherSet(name);
|
||||
if (realSetCode != null) {
|
||||
CardEdition.EditionEntry ee = this.editions.get(realSetCode).findOther(name);
|
||||
if (ee != null) { // TODO add collector Number and new ImageKey format
|
||||
return ImageKeys.getTokenKey(String.format("%s|%s|%s", name, realSetCode, ee.collectorNumber()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (CardEdition e : this.editions) {
|
||||
CardEdition.EditionEntry ee = e.findOther(name);
|
||||
if (ee != null) { // TODO add collector Number and new ImageKey format
|
||||
return ImageKeys.getTokenKey(String.format("%s|%s|%s", name, e.getCode(), ee.collectorNumber()));
|
||||
}
|
||||
}
|
||||
// final fallback
|
||||
return ImageKeys.getTokenKey(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import forge.StaticData;
|
||||
import forge.card.CardEdition.CardInSet;
|
||||
import forge.card.CardEdition.EditionEntry;
|
||||
import forge.card.CardEdition.Type;
|
||||
import forge.deck.generation.IDeckGenPool;
|
||||
import forge.item.IPaperCard;
|
||||
@@ -42,7 +42,8 @@ import java.util.stream.Stream;
|
||||
public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public final static String foilSuffix = "+";
|
||||
public final static char NameSetSeparator = '|';
|
||||
public final static String colorIDPrefix = "#";
|
||||
public final static String FlagPrefix = "#";
|
||||
public static final String FlagSeparator = "\t";
|
||||
private final String exlcudedCardName = "Concentrate";
|
||||
private final String exlcudedCardSet = "DS0";
|
||||
|
||||
@@ -93,19 +94,19 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public int artIndex;
|
||||
public boolean isFoil;
|
||||
public String collectorNumber;
|
||||
public Set<String> colorID;
|
||||
public Map<String, String> flags;
|
||||
|
||||
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber) {
|
||||
this(name, edition, artIndex, isFoil, collectorNumber, null);
|
||||
}
|
||||
|
||||
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber, Set<String> colorID) {
|
||||
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber, Map<String, String> flags) {
|
||||
cardName = name;
|
||||
this.edition = edition;
|
||||
this.artIndex = artIndex;
|
||||
this.isFoil = isFoil;
|
||||
this.collectorNumber = collectorNumber;
|
||||
this.colorID = colorID;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
public static boolean isFoilCardName(final String cardName){
|
||||
@@ -120,7 +121,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode) {
|
||||
setCode = setCode != null ? setCode : "";
|
||||
if(setCode == null || StringUtils.isBlank(setCode) || setCode.equals(CardEdition.UNKNOWN_CODE))
|
||||
setCode = "";
|
||||
cardName = cardName != null ? cardName : "";
|
||||
if (cardName.indexOf(NameSetSeparator) != -1)
|
||||
// If cardName is another RequestString, just get card name and forget about the rest.
|
||||
@@ -134,14 +136,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return requestInfo + NameSetSeparator + artIndex;
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, int artIndex, Set<String> colorID) {
|
||||
String requestInfo = compose(cardName, setCode);
|
||||
artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
|
||||
String cid = colorID == null ? "" : NameSetSeparator +
|
||||
colorID.toString().replace("[", colorIDPrefix).replace(", ", colorIDPrefix).replace("]", "");
|
||||
return requestInfo + NameSetSeparator + artIndex + cid;
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, String collectorNumber) {
|
||||
String requestInfo = compose(cardName, setCode);
|
||||
// CollectorNumber will be wrapped in square brackets
|
||||
@@ -149,6 +143,34 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return requestInfo + NameSetSeparator + collectorNumber;
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, int artIndex, Map<String, String> flags) {
|
||||
String requestInfo = compose(cardName, setCode);
|
||||
artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
|
||||
if(flags == null)
|
||||
return requestInfo + NameSetSeparator + artIndex;
|
||||
return requestInfo + NameSetSeparator + artIndex + getFlagSegment(flags);
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, String collectorNumber, Map<String, String> flags) {
|
||||
String requestInfo = compose(cardName, setCode);
|
||||
collectorNumber = preprocessCollectorNumber(collectorNumber);
|
||||
if(flags == null || flags.isEmpty())
|
||||
return requestInfo + NameSetSeparator + collectorNumber;
|
||||
return requestInfo + NameSetSeparator + collectorNumber + getFlagSegment(flags);
|
||||
}
|
||||
|
||||
public static String compose(PaperCard card) {
|
||||
String name = compose(card.getName(), card.isFoil());
|
||||
return compose(name, card.getEdition(), card.getCollectorNumber(), card.getMarkedFlags().toMap());
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, int artIndex, String collectorNumber) {
|
||||
String requestInfo = compose(cardName, setCode, artIndex);
|
||||
// CollectorNumber will be wrapped in square brackets
|
||||
collectorNumber = preprocessCollectorNumber(collectorNumber);
|
||||
return requestInfo + NameSetSeparator + collectorNumber;
|
||||
}
|
||||
|
||||
private static String preprocessCollectorNumber(String collectorNumber) {
|
||||
if (collectorNumber == null)
|
||||
return "";
|
||||
@@ -160,19 +182,21 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return collectorNumber;
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, int artIndex, String collectorNumber) {
|
||||
String requestInfo = compose(cardName, setCode, artIndex);
|
||||
// CollectorNumber will be wrapped in square brackets
|
||||
collectorNumber = preprocessCollectorNumber(collectorNumber);
|
||||
return requestInfo + NameSetSeparator + collectorNumber;
|
||||
private static String getFlagSegment(Map<String, String> flags) {
|
||||
if(flags == null)
|
||||
return "";
|
||||
String flagText = flags.entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining(FlagSeparator));
|
||||
return NameSetSeparator + FlagPrefix + "{" + flagText + "}";
|
||||
}
|
||||
|
||||
private static boolean isCollectorNumber(String s) {
|
||||
return s.startsWith("[") && s.endsWith("]");
|
||||
}
|
||||
|
||||
private static boolean isColorIDString(String s) {
|
||||
return s.startsWith(colorIDPrefix);
|
||||
private static boolean isFlagSegment(String s) {
|
||||
return s.startsWith(FlagPrefix);
|
||||
}
|
||||
|
||||
private static boolean isArtIndex(String s) {
|
||||
@@ -201,44 +225,36 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return null;
|
||||
|
||||
String[] info = TextUtil.split(reqInfo, NameSetSeparator);
|
||||
int setPos;
|
||||
int artPos;
|
||||
int cNrPos;
|
||||
int clrPos;
|
||||
if (info.length >= 4) { // name|set|artIndex|[collNr]
|
||||
setPos = isSetCode(info[1]) ? 1 : -1;
|
||||
artPos = isArtIndex(info[2]) ? 2 : -1;
|
||||
cNrPos = isCollectorNumber(info[3]) ? 3 : -1;
|
||||
int pos = cNrPos > 0 ? -1 : 3;
|
||||
clrPos = pos > 0 ? isColorIDString(info[pos]) ? pos : -1 : -1;
|
||||
} else if (info.length == 3) { // name|set|artIndex (or CollNr)
|
||||
setPos = isSetCode(info[1]) ? 1 : -1;
|
||||
artPos = isArtIndex(info[2]) ? 2 : -1;
|
||||
cNrPos = isCollectorNumber(info[2]) ? 2 : -1;
|
||||
int pos = cNrPos > 0 ? -1 : 2;
|
||||
clrPos = pos > 0 ? isColorIDString(info[pos]) ? pos : -1 : -1;
|
||||
} else if (info.length == 2) { // name|set (or artIndex, even if not possible via compose)
|
||||
setPos = isSetCode(info[1]) ? 1 : -1;
|
||||
artPos = isArtIndex(info[1]) ? 1 : -1;
|
||||
cNrPos = -1;
|
||||
clrPos = -1;
|
||||
} else {
|
||||
setPos = -1;
|
||||
artPos = -1;
|
||||
cNrPos = -1;
|
||||
clrPos = -1;
|
||||
}
|
||||
int index = 1;
|
||||
String cardName = info[0];
|
||||
boolean isFoil = false;
|
||||
int artIndex = IPaperCard.NO_ART_INDEX;
|
||||
String setCode = null;
|
||||
String collectorNumber = IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
Map<String, String> flags = null;
|
||||
if (isFoilCardName(cardName)) {
|
||||
cardName = cardName.substring(0, cardName.length() - foilSuffix.length());
|
||||
isFoil = true;
|
||||
}
|
||||
int artIndex = artPos > 0 ? Integer.parseInt(info[artPos]) : IPaperCard.NO_ART_INDEX; // default: no art index
|
||||
String collectorNumber = cNrPos > 0 ? info[cNrPos].substring(1, info[cNrPos].length() - 1) : IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
String setCode = setPos > 0 ? info[setPos] : null;
|
||||
Set<String> colorID = clrPos > 0 ? Arrays.stream(info[clrPos].substring(1).split(colorIDPrefix)).collect(Collectors.toSet()) : null;
|
||||
if (setCode != null && setCode.equals(CardEdition.UNKNOWN.getCode())) { // ???
|
||||
|
||||
if(info.length > index && isSetCode(info[index])) {
|
||||
setCode = info[index];
|
||||
index++;
|
||||
}
|
||||
if(info.length > index && isArtIndex(info[index])) {
|
||||
artIndex = Integer.parseInt(info[index]);
|
||||
index++;
|
||||
}
|
||||
if(info.length > index && isCollectorNumber(info[index])) {
|
||||
collectorNumber = info[index].substring(1, info[index].length() - 1);
|
||||
index++;
|
||||
}
|
||||
if (info.length > index && isFlagSegment(info[index])) {
|
||||
String flagText = info[index].substring(FlagPrefix.length());
|
||||
flags = parseRequestFlags(flagText);
|
||||
}
|
||||
|
||||
if (CardEdition.UNKNOWN_CODE.equals(setCode)) { // ???
|
||||
setCode = null;
|
||||
}
|
||||
if (setCode == null) {
|
||||
@@ -253,7 +269,29 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
// finally, check whether any between artIndex and CollectorNumber has been set
|
||||
if (collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER) && artIndex == IPaperCard.NO_ART_INDEX)
|
||||
artIndex = IPaperCard.DEFAULT_ART_INDEX;
|
||||
return new CardRequest(cardName, setCode, artIndex, isFoil, collectorNumber, colorID);
|
||||
return new CardRequest(cardName, setCode, artIndex, isFoil, collectorNumber, flags);
|
||||
}
|
||||
|
||||
private static Map<String, String> parseRequestFlags(String flagText) {
|
||||
flagText = flagText.trim();
|
||||
if(flagText.isEmpty())
|
||||
return null;
|
||||
if(!flagText.startsWith("{")) {
|
||||
//Legacy form for marked colors. They'll be of the form "W#B#R"
|
||||
Map<String, String> flags = new HashMap<>();
|
||||
String normalizedColorString = ColorSet.fromNames(flagText.split(FlagPrefix)).toString();
|
||||
flags.put("markedColors", String.join("", normalizedColorString));
|
||||
return flags;
|
||||
}
|
||||
flagText = flagText.substring(1, flagText.length() - 1); //Trim the braces.
|
||||
//List of flags, a series of "key=value" text broken up by tabs.
|
||||
return Arrays.stream(flagText.split(FlagSeparator))
|
||||
.map(f -> f.split("=", 2))
|
||||
.filter(f -> f.length > 0)
|
||||
.collect(Collectors.toMap(
|
||||
entry -> entry[0],
|
||||
entry -> entry.length > 1 ? entry[1] : "true" //If there's no '=' in the entry, treat it as a boolean flag.
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,27 +332,27 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
}
|
||||
|
||||
private void addSetCard(CardEdition e, CardInSet cis, CardRules cr) {
|
||||
private void addSetCard(CardEdition e, EditionEntry cis, CardRules cr) {
|
||||
int artIdx = IPaperCard.DEFAULT_ART_INDEX;
|
||||
String key = e.getCode() + "/" + cis.name;
|
||||
String key = e.getCode() + "/" + cis.name();
|
||||
if (artIds.containsKey(key)) {
|
||||
artIdx = artIds.get(key) + 1;
|
||||
}
|
||||
|
||||
artIds.put(key, artIdx);
|
||||
addCard(new PaperCard(cr, e.getCode(), cis.rarity, artIdx, false, cis.collectorNumber, cis.artistName, cis.functionalVariantName));
|
||||
addCard(new PaperCard(cr, e.getCode(), cis.rarity(), artIdx, false, cis.collectorNumber(), cis.artistName(), cis.functionalVariantName()));
|
||||
}
|
||||
|
||||
private boolean addFromSetByName(String cardName, CardEdition ed, CardRules cr) {
|
||||
List<CardInSet> cardsInSet = ed.getCardInSet(cardName); // empty collection if not present
|
||||
List<EditionEntry> cardsInSet = ed.getCardInSet(cardName); // empty collection if not present
|
||||
if (cr.hasFunctionalVariants()) {
|
||||
cardsInSet = cardsInSet.stream().filter(c -> StringUtils.isEmpty(c.functionalVariantName)
|
||||
|| cr.getSupportedFunctionalVariants().contains(c.functionalVariantName)
|
||||
cardsInSet = cardsInSet.stream().filter(c -> StringUtils.isEmpty(c.functionalVariantName())
|
||||
|| cr.getSupportedFunctionalVariants().contains(c.functionalVariantName())
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
if (cardsInSet.isEmpty())
|
||||
return false;
|
||||
for (CardInSet cis : cardsInSet) {
|
||||
for (EditionEntry cis : cardsInSet) {
|
||||
addSetCard(ed, cis, cr);
|
||||
}
|
||||
return true;
|
||||
@@ -359,15 +397,15 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
upcomingSet = e;
|
||||
}
|
||||
|
||||
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) {
|
||||
CardRules cr = rulesByName.get(cis.name);
|
||||
for (CardEdition.EditionEntry cis : e.getAllCardsInSet()) {
|
||||
CardRules cr = rulesByName.get(cis.name());
|
||||
if (cr == null) {
|
||||
missingCards.add(cis.name);
|
||||
missingCards.add(cis.name());
|
||||
continue;
|
||||
}
|
||||
if (cr.hasFunctionalVariants()) {
|
||||
if (StringUtils.isNotEmpty(cis.functionalVariantName)
|
||||
&& !cr.getSupportedFunctionalVariants().contains(cis.functionalVariantName)) {
|
||||
if (StringUtils.isNotEmpty(cis.functionalVariantName())
|
||||
&& !cr.getSupportedFunctionalVariants().contains(cis.functionalVariantName())) {
|
||||
//Supported card, unsupported variant.
|
||||
//Could note the card as missing but since these are often un-cards,
|
||||
//it's likely absent because it does something out of scope.
|
||||
@@ -406,7 +444,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown));
|
||||
} else if (enableUnknownCards && !this.filtered.contains(cr.getName())) {
|
||||
System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. ");
|
||||
addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special));
|
||||
addCard(new PaperCard(cr, CardEdition.UNKNOWN_CODE, CardRarity.Special));
|
||||
}
|
||||
} else {
|
||||
System.err.println("The custom card " + cr.getName() + " was not assigned to any set. Adding it to custom USER set, and will try to load custom art from USER edition.");
|
||||
@@ -425,8 +463,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
lang = new LangEnglish();
|
||||
}
|
||||
// for now just check Universes Within
|
||||
for (CardInSet cis : editions.get("SLX").getCards()) {
|
||||
String orgName = alternateName.get(cis.name);
|
||||
for (EditionEntry cis : editions.get("SLX").getCards()) {
|
||||
String orgName = alternateName.get(cis.name());
|
||||
if (orgName != null) {
|
||||
// found original (beyond) print
|
||||
CardRules org = getRules(orgName);
|
||||
@@ -456,7 +494,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
CardRules within = new CardRules(new ICardFace[] { renamedMain, renamedOther, null, null, null, null, null }, org.getSplitType(), org.getAiHints());
|
||||
// so workshop can edit same script
|
||||
within.setNormalizedName(org.getNormalizedName());
|
||||
rulesByName.put(cis.name, within);
|
||||
rulesByName.put(cis.name(), within);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -592,15 +630,15 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperCard getCard(final String cardName, String setCode, int artIndex, String collectorNumber) {
|
||||
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, collectorNumber);
|
||||
public PaperCard getCard(final String cardName, String setCode, int artIndex, Map<String, String> flags) {
|
||||
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, flags);
|
||||
CardRequest request = CardRequest.fromString(reqInfo);
|
||||
return tryGetCard(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperCard getCard(final String cardName, String setCode, int artIndex, Set<String> colorID) {
|
||||
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, colorID);
|
||||
public PaperCard getCard(final String cardName, String setCode, String collectorNumber, Map<String, String> flags) {
|
||||
String reqInfo = CardRequest.compose(cardName, setCode, collectorNumber, flags);
|
||||
CardRequest request = CardRequest.fromString(reqInfo);
|
||||
return tryGetCard(request);
|
||||
}
|
||||
@@ -611,14 +649,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return null;
|
||||
// 1. First off, try using all possible search parameters, to narrow down the actual cards looked for.
|
||||
String reqEditionCode = request.edition;
|
||||
if (reqEditionCode != null && reqEditionCode.length() > 0) {
|
||||
if (reqEditionCode != null && !reqEditionCode.isEmpty()) {
|
||||
// This get is robust even against expansion aliases (e.g. TE and TMP both valid for Tempest) -
|
||||
// MOST of the extensions have two short codes, 141 out of 221 (so far)
|
||||
// ALSO: Set Code are always UpperCase
|
||||
CardEdition edition = editions.get(reqEditionCode.toUpperCase());
|
||||
|
||||
return this.getCardFromSet(request.cardName, edition, request.artIndex,
|
||||
request.collectorNumber, request.isFoil, request.colorID);
|
||||
PaperCard cardFromSet = this.getCardFromSet(request.cardName, edition, request.artIndex, request.collectorNumber, request.isFoil);
|
||||
if(cardFromSet != null && request.flags != null)
|
||||
cardFromSet = cardFromSet.copyWithFlags(request.flags);
|
||||
|
||||
return cardFromSet;
|
||||
}
|
||||
|
||||
// 2. Card lookup in edition with specified filter didn't work.
|
||||
@@ -661,11 +702,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
|
||||
@Override
|
||||
public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil) {
|
||||
return getCardFromSet(cardName, edition, artIndex, collectorNumber, isFoil, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil, Set<String> colorID) {
|
||||
if (edition == null || cardName == null) // preview cards
|
||||
return null; // No cards will be returned
|
||||
|
||||
@@ -674,18 +710,18 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
cardName = cardNameRequest.cardName;
|
||||
isFoil = isFoil || cardNameRequest.isFoil;
|
||||
|
||||
List<PaperCard> candidates = getAllCards(cardName, c -> {
|
||||
boolean artIndexFilter = true;
|
||||
boolean collectorNumberFilter = true;
|
||||
boolean setFilter = c.getEdition().equalsIgnoreCase(edition.getCode()) ||
|
||||
c.getEdition().equalsIgnoreCase(edition.getCode2());
|
||||
String code1 = edition.getCode(), code2 = edition.getCode2();
|
||||
|
||||
Predicate<PaperCard> filter = (c) -> {
|
||||
String ed = c.getEdition();
|
||||
return ed.equalsIgnoreCase(code1) || ed.equalsIgnoreCase(code2);
|
||||
};
|
||||
if (artIndex > 0)
|
||||
artIndexFilter = (c.getArtIndex() == artIndex);
|
||||
if ((collectorNumber != null) && (collectorNumber.length() > 0)
|
||||
&& !(collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)))
|
||||
collectorNumberFilter = (c.getCollectorNumber().equals(collectorNumber));
|
||||
return setFilter && artIndexFilter && collectorNumberFilter;
|
||||
});
|
||||
filter = filter.and((c) -> artIndex == c.getArtIndex());
|
||||
if (collectorNumber != null && !collectorNumber.isEmpty() && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER))
|
||||
filter = filter.and((c) -> collectorNumber.equals(c.getCollectorNumber()));
|
||||
|
||||
List<PaperCard> candidates = getAllCards(cardName, filter);
|
||||
if (candidates.isEmpty())
|
||||
return null;
|
||||
|
||||
@@ -699,7 +735,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
while (!candidate.hasImage() && candidatesIterator.hasNext())
|
||||
candidate = candidatesIterator.next();
|
||||
candidate = candidate.hasImage() ? candidate : firstCandidate;
|
||||
return isFoil ? candidate.getFoiled().getColorIDVersion(colorID) : candidate.getColorIDVersion(colorID);
|
||||
return isFoil ? candidate.getFoiled() : candidate;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -742,11 +778,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperCard getCardFromEditions(final String cardInfo, final CardArtPreference artPreference, int artIndex, Set<String> colorID) {
|
||||
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, null, false, null, colorID);
|
||||
}
|
||||
|
||||
/*
|
||||
* ===============================================
|
||||
* 4. SPECIALISED CARD LOOKUP BASED ON
|
||||
@@ -820,12 +851,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
|
||||
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter){
|
||||
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, releaseDate, releasedBeforeFlag, filter, null);
|
||||
}
|
||||
|
||||
private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
|
||||
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter, Set<String> colorID){
|
||||
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter) {
|
||||
if (cardInfo == null)
|
||||
return null;
|
||||
final CardRequest cr = CardRequest.fromString(cardInfo);
|
||||
@@ -865,7 +891,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
for (PaperCard card : cards) {
|
||||
String setCode = card.getEdition();
|
||||
CardEdition ed;
|
||||
if (setCode.equals(CardEdition.UNKNOWN.getCode()))
|
||||
if (setCode.equals(CardEdition.UNKNOWN_CODE))
|
||||
ed = CardEdition.UNKNOWN;
|
||||
else
|
||||
ed = editions.get(card.getEdition());
|
||||
@@ -906,7 +932,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
candidate = candidate.hasImage() ? candidate : firstCandidate;
|
||||
//If any, we're sure that at least one candidate is always returned despite it having any image
|
||||
return cr.isFoil ? candidate.getFoiled().getColorIDVersion(colorID) : candidate.getColorIDVersion(colorID);
|
||||
return cr.isFoil ? candidate.getFoiled() : candidate;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1017,7 +1043,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public static final Predicate<PaperCard> EDITION_NON_PROMO = paperCard -> {
|
||||
String code = paperCard.getEdition();
|
||||
CardEdition edition = StaticData.instance().getCardEdition(code);
|
||||
if(edition == null && code.equals("???"))
|
||||
if(edition == null && code.equals(CardEdition.UNKNOWN_CODE))
|
||||
return true;
|
||||
return edition != null && edition.getType() != Type.PROMO;
|
||||
};
|
||||
@@ -1025,7 +1051,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public static final Predicate<PaperCard> EDITION_NON_REPRINT = paperCard -> {
|
||||
String code = paperCard.getEdition();
|
||||
CardEdition edition = StaticData.instance().getCardEdition(code);
|
||||
if(edition == null && code.equals("???"))
|
||||
if(edition == null && code.equals(CardEdition.UNKNOWN_CODE))
|
||||
return true;
|
||||
return edition != null && Type.REPRINT_SET_TYPES.contains(edition.getType());
|
||||
};
|
||||
@@ -1081,8 +1107,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public Collection<PaperCard> getAllCards(CardEdition edition) {
|
||||
List<PaperCard> cards = Lists.newArrayList();
|
||||
|
||||
for (CardInSet cis : edition.getAllCardsInSet()) {
|
||||
PaperCard card = this.getCard(cis.name, edition.getCode());
|
||||
for (EditionEntry cis : edition.getAllCardsInSet()) {
|
||||
PaperCard card = this.getCard(cis.name(), edition.getCode());
|
||||
if (card == null) {
|
||||
// Just in case the card is listed in the edition file but Forge doesn't support it
|
||||
continue;
|
||||
@@ -1126,29 +1152,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
.anyMatch(rarity::equals);
|
||||
}
|
||||
|
||||
public StringBuilder appendCardToStringBuilder(PaperCard card, StringBuilder sb) {
|
||||
final boolean hasBadSetInfo = card.getEdition().equals(CardEdition.UNKNOWN.getCode()) || StringUtils.isBlank(card.getEdition());
|
||||
sb.append(card.getName());
|
||||
if (card.isFoil()) {
|
||||
sb.append(CardDb.foilSuffix);
|
||||
}
|
||||
|
||||
if (!hasBadSetInfo) {
|
||||
int artCount = getArtCount(card.getName(), card.getEdition(), card.getFunctionalVariant());
|
||||
sb.append(CardDb.NameSetSeparator).append(card.getEdition());
|
||||
if (artCount >= IPaperCard.DEFAULT_ART_INDEX) {
|
||||
sb.append(CardDb.NameSetSeparator).append(card.getArtIndex()); // indexes start at 1 to match image file name conventions
|
||||
}
|
||||
if (card.getColorID() != null) {
|
||||
sb.append(CardDb.NameSetSeparator);
|
||||
for (String color : card.getColorID())
|
||||
sb.append(CardDb.colorIDPrefix).append(color);
|
||||
}
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
public PaperCard createUnsupportedCard(String cardRequest) {
|
||||
CardRequest request = CardRequest.fromString(cardRequest);
|
||||
CardEdition cardEdition = CardEdition.UNKNOWN;
|
||||
@@ -1157,10 +1160,10 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
// May iterate over editions and find out if there is any card named 'cardRequest' but not implemented with Forge script.
|
||||
if (StringUtils.isBlank(request.edition)) {
|
||||
for (CardEdition edition : editions) {
|
||||
for (CardInSet cardInSet : edition.getAllCardsInSet()) {
|
||||
if (cardInSet.name.equals(request.cardName)) {
|
||||
for (EditionEntry cardInSet : edition.getAllCardsInSet()) {
|
||||
if (cardInSet.name().equals(request.cardName)) {
|
||||
cardEdition = edition;
|
||||
cardRarity = cardInSet.rarity;
|
||||
cardRarity = cardInSet.rarity();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1171,9 +1174,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
} else {
|
||||
cardEdition = editions.get(request.edition);
|
||||
if (cardEdition != null) {
|
||||
for (CardInSet cardInSet : cardEdition.getAllCardsInSet()) {
|
||||
if (cardInSet.name.equals(request.cardName)) {
|
||||
cardRarity = cardInSet.rarity;
|
||||
for (EditionEntry cardInSet : cardEdition.getAllCardsInSet()) {
|
||||
if (cardInSet.name().equals(request.cardName)) {
|
||||
cardRarity = cardInSet.rarity();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1224,9 +1227,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
// @leriomaggio: DONE! re-using here the same strategy implemented for lazy-loading!
|
||||
for (CardEdition e : editions.getOrderedEditions()) {
|
||||
int artIdx = IPaperCard.DEFAULT_ART_INDEX;
|
||||
for (CardInSet cis : e.getCardInSet(cardName))
|
||||
paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity, artIdx++, false,
|
||||
cis.collectorNumber, cis.artistName, cis.functionalVariantName));
|
||||
for (EditionEntry cis : e.getCardInSet(cardName))
|
||||
paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity(), artIdx++, false,
|
||||
cis.collectorNumber(), cis.artistName(), cis.functionalVariantName()));
|
||||
}
|
||||
} else {
|
||||
String lastEdition = null;
|
||||
@@ -1240,17 +1243,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
if (ed == null) {
|
||||
continue;
|
||||
}
|
||||
List<CardInSet> cardsInSet = ed.getCardInSet(cardName);
|
||||
List<EditionEntry> cardsInSet = ed.getCardInSet(cardName);
|
||||
if (cardsInSet.isEmpty())
|
||||
continue;
|
||||
int cardInSetIndex = Math.max(artIdx-1, 0); // make sure doesn't go below zero
|
||||
CardInSet cds = cardsInSet.get(cardInSetIndex); // use ArtIndex to get the right Coll. Number
|
||||
EditionEntry cds = cardsInSet.get(cardInSetIndex); // use ArtIndex to get the right Coll. Number
|
||||
paperCards.add(new PaperCard(rules, lastEdition, tuple.getValue(), artIdx++, false,
|
||||
cds.collectorNumber, cds.artistName, cds.functionalVariantName));
|
||||
cds.collectorNumber(), cds.artistName(), cds.functionalVariantName()));
|
||||
}
|
||||
}
|
||||
if (paperCards.isEmpty()) {
|
||||
paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN.getCode(), CardRarity.Special));
|
||||
paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN_CODE, CardRarity.Special));
|
||||
}
|
||||
// 2. add them to db
|
||||
for (PaperCard paperCard : paperCards) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package forge.card;
|
||||
|
||||
import com.google.common.collect.*;
|
||||
|
||||
import forge.StaticData;
|
||||
import forge.card.CardDb.CardArtPreference;
|
||||
import forge.deck.CardPool;
|
||||
@@ -165,43 +166,6 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
}
|
||||
}
|
||||
|
||||
public static class CardInSet implements Comparable<CardInSet> {
|
||||
public final CardRarity rarity;
|
||||
public final String collectorNumber;
|
||||
public final String name;
|
||||
public final String artistName;
|
||||
public final String functionalVariantName;
|
||||
|
||||
public CardInSet(final String name, final String collectorNumber, final CardRarity rarity, final String artistName, final String functionalVariantName) {
|
||||
this.name = name;
|
||||
this.collectorNumber = collectorNumber;
|
||||
this.rarity = rarity;
|
||||
this.artistName = artistName;
|
||||
this.functionalVariantName = functionalVariantName;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (collectorNumber != null) {
|
||||
sb.append(collectorNumber);
|
||||
sb.append(' ');
|
||||
}
|
||||
if (rarity != CardRarity.Unknown) {
|
||||
sb.append(rarity);
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(name);
|
||||
if (artistName != null) {
|
||||
sb.append(" @");
|
||||
sb.append(artistName);
|
||||
}
|
||||
if (functionalVariantName != null) {
|
||||
sb.append(" $");
|
||||
sb.append(functionalVariantName);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final Map<String, String> sortableCollNumberLookup = new HashMap<>();
|
||||
/**
|
||||
* This method implements the main strategy to allow for natural ordering of collectorNumber
|
||||
@@ -244,8 +208,32 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
return sortableCollNr;
|
||||
}
|
||||
|
||||
public record EditionEntry(String name, String collectorNumber, CardRarity rarity, String artistName, String functionalVariantName) implements Comparable<EditionEntry> {
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (collectorNumber != null) {
|
||||
sb.append(collectorNumber);
|
||||
sb.append(' ');
|
||||
}
|
||||
if (rarity != CardRarity.Unknown && rarity != CardRarity.Token) {
|
||||
sb.append(rarity);
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(name);
|
||||
if (artistName != null) {
|
||||
sb.append(" @");
|
||||
sb.append(artistName);
|
||||
}
|
||||
if (functionalVariantName != null) {
|
||||
sb.append(" $");
|
||||
sb.append(functionalVariantName);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CardInSet o) {
|
||||
public int compareTo(EditionEntry o) {
|
||||
final int nameCmp = name.compareToIgnoreCase(o.name);
|
||||
if (0 != nameCmp) {
|
||||
return nameCmp;
|
||||
@@ -262,11 +250,17 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
public static final CardEdition UNKNOWN = new CardEdition("1990-01-01", "???", "??", Type.UNKNOWN, "Undefined", FoilType.NOT_SUPPORTED, new CardInSet[]{});
|
||||
/**
|
||||
* Equivalent to the set code of CardEdition.UNKNOWN
|
||||
*/
|
||||
public static final String UNKNOWN_CODE = "???";
|
||||
public static final CardEdition UNKNOWN = new CardEdition("1990-01-01", UNKNOWN_CODE, "??", Type.UNKNOWN, "Undefined", FoilType.NOT_SUPPORTED, new EditionEntry[]{});
|
||||
private Date date;
|
||||
private String code;
|
||||
private String code2;
|
||||
private String scryfallCode;
|
||||
private String tokensCode;
|
||||
private String tokenFallbackCode;
|
||||
private String cardsLanguage;
|
||||
private Type type;
|
||||
private String name;
|
||||
@@ -296,31 +290,32 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
private String doublePickDuringDraft = "";
|
||||
private String[] chaosDraftThemes = new String[0];
|
||||
|
||||
private final ListMultimap<String, CardInSet> cardMap;
|
||||
private final List<CardInSet> cardsInSet;
|
||||
private final Map<String, Integer> tokenNormalized;
|
||||
private final ListMultimap<String, EditionEntry> cardMap;
|
||||
private final List<EditionEntry> cardsInSet;
|
||||
private final ListMultimap<String, EditionEntry> tokenMap;
|
||||
// custom print sheets that will be loaded lazily
|
||||
private final Map<String, List<String>> customPrintSheetsToParse;
|
||||
private ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create();
|
||||
|
||||
private int boosterArts = 1;
|
||||
private SealedTemplate boosterTpl = null;
|
||||
private final Map<String, SealedTemplate> boosterTemplates = new HashMap<>();
|
||||
|
||||
private CardEdition(ListMultimap<String, CardInSet> cardMap, Map<String, Integer> tokens, Map<String, List<String>> customPrintSheetsToParse) {
|
||||
private CardEdition(ListMultimap<String, EditionEntry> cardMap, ListMultimap<String, EditionEntry> tokens, Map<String, List<String>> customPrintSheetsToParse) {
|
||||
this.cardMap = cardMap;
|
||||
this.cardsInSet = new ArrayList<>(cardMap.values());
|
||||
Collections.sort(cardsInSet);
|
||||
this.tokenNormalized = tokens;
|
||||
this.tokenMap = tokens;
|
||||
this.customPrintSheetsToParse = customPrintSheetsToParse;
|
||||
}
|
||||
|
||||
private CardEdition(CardInSet[] cards, Map<String, Integer> tokens) {
|
||||
List<CardInSet> cardsList = Arrays.asList(cards);
|
||||
private CardEdition(EditionEntry[] cards, ListMultimap<String, EditionEntry> tokens) {
|
||||
List<EditionEntry> cardsList = Arrays.asList(cards);
|
||||
this.cardMap = ArrayListMultimap.create();
|
||||
this.cardMap.replaceValues("cards", cardsList);
|
||||
this.cardsInSet = new ArrayList<>(cardsList);
|
||||
Collections.sort(cardsInSet);
|
||||
this.tokenNormalized = tokens;
|
||||
this.tokenMap = tokens;
|
||||
this.customPrintSheetsToParse = new HashMap<>();
|
||||
}
|
||||
|
||||
@@ -337,8 +332,8 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
* @param name the name of the set
|
||||
* @param cards the cards in the set
|
||||
*/
|
||||
private CardEdition(String date, String code, String code2, Type type, String name, FoilType foil, CardInSet[] cards) {
|
||||
this(cards, new HashMap<>());
|
||||
private CardEdition(String date, String code, String code2, Type type, String name, FoilType foil, EditionEntry[] cards) {
|
||||
this(cards, ArrayListMultimap.create());
|
||||
this.code = code;
|
||||
this.code2 = code2;
|
||||
this.type = type;
|
||||
@@ -361,6 +356,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
public String getCode() { return code; }
|
||||
public String getCode2() { return code2; }
|
||||
public String getScryfallCode() { return scryfallCode.toLowerCase(); }
|
||||
public String getTokensCode() { return tokensCode.toLowerCase(); }
|
||||
public String getCardsLangCode() { return cardsLanguage.toLowerCase(); }
|
||||
public Type getType() { return type; }
|
||||
public String getName() { return name; }
|
||||
@@ -385,14 +381,14 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
public String getSheetReplaceCardFromSheet2() { return sheetReplaceCardFromSheet2; }
|
||||
public String[] getChaosDraftThemes() { return chaosDraftThemes; }
|
||||
|
||||
public List<CardInSet> getCards() { return cardMap.get(EditionSectionWithCollectorNumbers.CARDS.getName()); }
|
||||
public List<CardInSet> getRebalancedCards() { return cardMap.get(EditionSectionWithCollectorNumbers.REBALANCED.getName()); }
|
||||
public List<CardInSet> getFunnyEternalCards() { return cardMap.get(EditionSectionWithCollectorNumbers.ETERNAL.getName()); }
|
||||
public List<CardInSet> getAllCardsInSet() {
|
||||
public List<EditionEntry> getCards() { return cardMap.get(EditionSectionWithCollectorNumbers.CARDS.getName()); }
|
||||
public List<EditionEntry> getRebalancedCards() { return cardMap.get(EditionSectionWithCollectorNumbers.REBALANCED.getName()); }
|
||||
public List<EditionEntry> getFunnyEternalCards() { return cardMap.get(EditionSectionWithCollectorNumbers.ETERNAL.getName()); }
|
||||
public List<EditionEntry> getAllCardsInSet() {
|
||||
return cardsInSet;
|
||||
}
|
||||
|
||||
private ListMultimap<String, CardInSet> cardsInSetLookupMap = null;
|
||||
private ListMultimap<String, EditionEntry> cardsInSetLookupMap = null;
|
||||
|
||||
/**
|
||||
* Get all the CardInSet instances with the input card name.
|
||||
@@ -400,12 +396,12 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
* @return A List of all the CardInSet instances for a given name.
|
||||
* If not fount, an Empty sequence (view) will be returned instead!
|
||||
*/
|
||||
public List<CardInSet> getCardInSet(String cardName){
|
||||
public List<EditionEntry> getCardInSet(String cardName){
|
||||
if (cardsInSetLookupMap == null) {
|
||||
// initialise
|
||||
cardsInSetLookupMap = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
|
||||
List<CardInSet> cardsInSet = this.getAllCardsInSet();
|
||||
for (CardInSet cis : cardsInSet){
|
||||
List<EditionEntry> cardsInSet = this.getAllCardsInSet();
|
||||
for (EditionEntry cis : cardsInSet){
|
||||
String key = cis.name;
|
||||
cardsInSetLookupMap.put(key, cis);
|
||||
}
|
||||
@@ -413,8 +409,19 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
return this.cardsInSetLookupMap.get(cardName);
|
||||
}
|
||||
|
||||
public EditionEntry getCardFromCollectorNumber(String collectorNumber) {
|
||||
if(collectorNumber == null || collectorNumber.isEmpty())
|
||||
return null;
|
||||
for(EditionEntry c : this.cardsInSet) {
|
||||
//Could build a map for this one too if it's used for more than one-offs.
|
||||
if (c.collectorNumber.equalsIgnoreCase(collectorNumber))
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isRebalanced(String cardName) {
|
||||
for (CardInSet cis : getRebalancedCards()) {
|
||||
for (EditionEntry cis : getRebalancedCards()) {
|
||||
if (cis.name.equals(cardName)) {
|
||||
return true;
|
||||
}
|
||||
@@ -424,7 +431,44 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others
|
||||
|
||||
public Map<String, Integer> getTokens() { return tokenNormalized; }
|
||||
public Multimap<String, EditionEntry> getTokens() { return tokenMap; }
|
||||
|
||||
public EditionEntry getTokenFromCollectorNumber(String collectorNumber) {
|
||||
if(collectorNumber == null || collectorNumber.isEmpty())
|
||||
return null;
|
||||
for(EditionEntry c : this.tokenMap.values()) {
|
||||
//Could build a map for this one too if it's used for more than one-offs.
|
||||
if (c.collectorNumber.equalsIgnoreCase(collectorNumber))
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getTokenSet(String token) {
|
||||
if (tokenMap.containsKey(token)) {
|
||||
return this.getCode();
|
||||
}
|
||||
if (this.tokenFallbackCode != null) {
|
||||
return StaticData.instance().getCardEdition(this.tokenFallbackCode).getTokenSet(token);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public String getOtherSet(String token) {
|
||||
if (otherMap.containsKey(token)) {
|
||||
return this.getCode();
|
||||
}
|
||||
if (this.tokenFallbackCode != null) {
|
||||
return StaticData.instance().getCardEdition(this.tokenFallbackCode).getOtherSet(token);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public EditionEntry findOther(String name) {
|
||||
if (otherMap.containsKey(name)) {
|
||||
return Aggregates.random(otherMap.get(name));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(final CardEdition o) {
|
||||
@@ -508,8 +552,8 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
for (String sectionName : cardMap.keySet()) {
|
||||
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName));
|
||||
|
||||
List<CardInSet> cards = cardMap.get(sectionName);
|
||||
for (CardInSet card : cards) {
|
||||
List<EditionEntry> cards = cardMap.get(sectionName);
|
||||
for (EditionEntry card : cards) {
|
||||
int index = 1;
|
||||
if (cardToIndex.containsKey(card.name)) {
|
||||
index = cardToIndex.get(card.name) + 1;
|
||||
@@ -562,6 +606,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
it should also match the Un-set and older alternate art cards
|
||||
like Merseine from FEM.
|
||||
*/
|
||||
// Collector numbers now should allow hyphens for Planeswalker Championship Promos
|
||||
//"(^(?<cnum>[0-9]+.?) )?((?<rarity>[SCURML]) )?(?<name>.*)$"
|
||||
/* Ideally we'd use the named group above, but Android 6 and
|
||||
earlier don't appear to support named groups.
|
||||
@@ -575,12 +620,20 @@ 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)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
|
||||
);
|
||||
|
||||
ListMultimap<String, CardInSet> cardMap = ArrayListMultimap.create();
|
||||
final Pattern tokenPattern = Pattern.compile(
|
||||
/*
|
||||
* cnum - grouping #2
|
||||
* name - grouping #3
|
||||
* artist name - grouping #5
|
||||
*/
|
||||
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
|
||||
);
|
||||
|
||||
ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create();
|
||||
List<BoosterSlot> boosterSlots = null;
|
||||
Map<String, Integer> tokenNormalized = new HashMap<>();
|
||||
Map<String, List<String>> customPrintSheetsToParse = new HashMap<>();
|
||||
List<String> editionSectionsWithCollectorNumbers = EditionSectionWithCollectorNumbers.getNames();
|
||||
|
||||
@@ -611,7 +664,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
String cardName = matcher.group(5);
|
||||
String artistName = matcher.group(7);
|
||||
String functionalVariantName = matcher.group(9);
|
||||
CardInSet cis = new CardInSet(cardName, collectorNumber, r, artistName, functionalVariantName);
|
||||
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
|
||||
|
||||
cardMap.put(sectionName, cis);
|
||||
}
|
||||
@@ -625,41 +678,59 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
}
|
||||
}
|
||||
|
||||
ListMultimap<String, EditionEntry> tokenMap = ArrayListMultimap.create();
|
||||
ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create();
|
||||
// parse tokens section
|
||||
if (contents.containsKey("tokens")) {
|
||||
for (String line : contents.get("tokens")) {
|
||||
if (StringUtils.isBlank(line))
|
||||
continue;
|
||||
Matcher matcher = tokenPattern.matcher(line);
|
||||
|
||||
if (!tokenNormalized.containsKey(line)) {
|
||||
tokenNormalized.put(line, 1);
|
||||
} else {
|
||||
tokenNormalized.put(line, tokenNormalized.get(line) + 1);
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String collectorNumber = matcher.group(2);
|
||||
String cardName = matcher.group(3);
|
||||
String artistName = matcher.group(5);
|
||||
// rarity isn't used for this anyway
|
||||
EditionEntry tis = new EditionEntry(cardName, collectorNumber, CardRarity.Token, artistName, null);
|
||||
tokenMap.put(cardName, tis);
|
||||
}
|
||||
}
|
||||
if (contents.containsKey("other")) {
|
||||
for (String line : contents.get("other")) {
|
||||
if (StringUtils.isBlank(line))
|
||||
continue;
|
||||
Matcher matcher = tokenPattern.matcher(line);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
String collectorNumber = matcher.group(2);
|
||||
String cardName = matcher.group(3);
|
||||
String artistName = matcher.group(5);
|
||||
EditionEntry tis = new EditionEntry(cardName, collectorNumber, CardRarity.Unknown, artistName, null);
|
||||
otherMap.put(cardName, tis);
|
||||
}
|
||||
}
|
||||
|
||||
CardEdition res = new CardEdition(cardMap, tokenNormalized, customPrintSheetsToParse);
|
||||
CardEdition res = new CardEdition(cardMap, tokenMap, customPrintSheetsToParse);
|
||||
res.boosterSlots = boosterSlots;
|
||||
// parse metadata section
|
||||
res.name = metadata.get("name");
|
||||
res.date = parseDate(metadata.get("date"));
|
||||
res.code = metadata.get("code");
|
||||
res.code2 = metadata.get("code2");
|
||||
if (res.code2 == null) {
|
||||
res.code2 = res.code;
|
||||
}
|
||||
res.scryfallCode = metadata.get("ScryfallCode");
|
||||
if (res.scryfallCode == null) {
|
||||
res.scryfallCode = res.code;
|
||||
}
|
||||
res.cardsLanguage = metadata.get("CardLang");
|
||||
if (res.cardsLanguage == null) {
|
||||
res.cardsLanguage = "en";
|
||||
}
|
||||
|
||||
res.code2 = metadata.get("code2", res.code);
|
||||
res.scryfallCode = metadata.get("ScryfallCode", res.code);
|
||||
res.tokensCode = metadata.get("TokensCode", "T" + res.scryfallCode);
|
||||
res.tokenFallbackCode = metadata.get("TokenFallbackCode");
|
||||
res.cardsLanguage = metadata.get("CardLang", "en");
|
||||
res.boosterArts = metadata.getInt("BoosterCovers", 1);
|
||||
|
||||
res.otherMap = otherMap;
|
||||
|
||||
String boosterDesc = metadata.get("Booster");
|
||||
|
||||
if (metadata.contains("Booster")) {
|
||||
@@ -778,7 +849,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
initAliases(E); //Made a method in case the system changes, so it's consistent.
|
||||
}
|
||||
CardEdition customBucket = new CardEdition("2990-01-01", "USER", "USER",
|
||||
Type.CUSTOM_SET, "USER", FoilType.NOT_SUPPORTED, new CardInSet[]{});
|
||||
Type.CUSTOM_SET, "USER", FoilType.NOT_SUPPORTED, new EditionEntry[]{});
|
||||
this.add(customBucket);
|
||||
initAliases(customBucket);
|
||||
this.lock = true; //Consider it initialized and prevent from writing any more data.
|
||||
@@ -810,7 +881,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public CardEdition getEditionByCodeOrThrow(final String code) {
|
||||
final CardEdition set = this.get(code);
|
||||
if (null == set && code.equals("???")) //Hardcoded set ??? is not with the others, needs special check.
|
||||
if (null == set && code.equals(UNKNOWN_CODE)) //Hardcoded set ??? is not with the others, needs special check.
|
||||
return UNKNOWN;
|
||||
if (null == set) {
|
||||
throw new RuntimeException("Edition with code '" + code + "' not found");
|
||||
@@ -941,4 +1012,12 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean hasBasicLands() {
|
||||
for(String landName : MagicColor.Constant.BASIC_LANDS) {
|
||||
if (null == StaticData.instance().getCommonCards().getCard(landName, this.getCode(), 0))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ final class CardFace implements ICardFace, Cloneable {
|
||||
else variant.replacements.addAll(0, this.replacements);
|
||||
|
||||
if(variant.variables == null) variant.variables = this.variables;
|
||||
else variant.variables.putAll(this.variables);
|
||||
else this.variables.forEach((k, v) -> variant.variables.putIfAbsent(k, v));
|
||||
|
||||
if(variant.nonAbilityText == null) variant.nonAbilityText = this.nonAbilityText;
|
||||
if(variant.draftActions == null) variant.draftActions = this.draftActions;
|
||||
|
||||
@@ -20,6 +20,8 @@ package forge.card;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.StaticData;
|
||||
import forge.card.mana.IParserManaCost;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.card.mana.ManaCostShard;
|
||||
@@ -149,6 +151,10 @@ public final class CardRules implements ICardCharacteristics {
|
||||
return splitType;
|
||||
}
|
||||
|
||||
public boolean hasBackSide() {
|
||||
return CardSplitType.DUAL_FACED_CARDS.contains(splitType) || splitType == CardSplitType.Flip;
|
||||
}
|
||||
|
||||
public ICardFace getMainPart() {
|
||||
return mainPart;
|
||||
}
|
||||
@@ -165,20 +171,32 @@ public final class CardRules implements ICardCharacteristics {
|
||||
return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values());
|
||||
}
|
||||
|
||||
public ICardFace getWSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeW);
|
||||
public String getImageName(CardStateName state) {
|
||||
if (splitType == CardSplitType.Split) {
|
||||
return mainPart.getName() + otherPart.getName();
|
||||
} else if (state.equals(splitType.getChangedStateName())) {
|
||||
if (otherPart != null) {
|
||||
return otherPart.getName();
|
||||
} else if (this.hasBackSide()) {
|
||||
if (!getMeldWith().isEmpty()) {
|
||||
final CardDb db = StaticData.instance().getCommonCards();
|
||||
return db.getRules(getMeldWith()).getOtherPart().getName();
|
||||
}
|
||||
public ICardFace getUSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeU);
|
||||
return null;
|
||||
}
|
||||
public ICardFace getBSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeB);
|
||||
}
|
||||
public ICardFace getRSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeR);
|
||||
|
||||
switch (state) {
|
||||
case SpecializeW:
|
||||
case SpecializeU:
|
||||
case SpecializeB:
|
||||
case SpecializeR:
|
||||
case SpecializeG:
|
||||
ICardFace face = specializedParts.get(state);
|
||||
return face != null ? face.getName() : null;
|
||||
default:
|
||||
return getName();
|
||||
}
|
||||
public ICardFace getGSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeG);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -396,6 +414,7 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
|
||||
public int getSetColorID() {
|
||||
//Could someday generalize this to support other kinds of markings.
|
||||
return setColorID;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ public enum CardSplitType
|
||||
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
|
||||
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
|
||||
Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped),
|
||||
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Adventure),
|
||||
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Omen(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal),
|
||||
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ public enum CardStateName {
|
||||
Original,
|
||||
FaceDown,
|
||||
Flipped,
|
||||
Converted,
|
||||
Transformed,
|
||||
Meld,
|
||||
LeftSplit,
|
||||
RightSplit,
|
||||
Adventure,
|
||||
Secondary,
|
||||
Modal,
|
||||
EmptyRoom,
|
||||
SpecializeW,
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -315,6 +316,12 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
|
||||
return landTypes;
|
||||
}
|
||||
|
||||
public Set<String> getBattleTypes() {
|
||||
if(!isBattle())
|
||||
return Set.of();
|
||||
return subtypes.stream().filter(CardType::isABattleType).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStringType(String t) {
|
||||
if (t.isEmpty()) {
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface CardTypeView extends Iterable<String>, Serializable {
|
||||
|
||||
Set<String> getCreatureTypes();
|
||||
Set<String> getLandTypes();
|
||||
Set<String> getBattleTypes();
|
||||
|
||||
boolean hasStringType(String t);
|
||||
boolean hasType(CoreType type);
|
||||
|
||||
@@ -25,6 +25,8 @@ import forge.util.BinaryUtil;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* <p>CardColor class.</p>
|
||||
@@ -291,14 +293,8 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (this.orderWeight == -1) {
|
||||
return "n/a";
|
||||
}
|
||||
final String toReturn = MagicColor.toLongString(myColor);
|
||||
if (toReturn.equals(MagicColor.Constant.COLORLESS) && myColor != 0) {
|
||||
return "multi";
|
||||
}
|
||||
return toReturn;
|
||||
final ManaCostShard[] orderedShards = getOrderedShards();
|
||||
return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,6 +372,10 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<MagicColor.Color> stream() {
|
||||
return this.toEnumSet().stream();
|
||||
}
|
||||
|
||||
//Get array of mana cost shards for color set in the proper order
|
||||
public ManaCostShard[] getOrderedShards() {
|
||||
return shardOrderLookup[myColor];
|
||||
|
||||
@@ -5,43 +5,42 @@ import forge.item.PaperCard;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.Set;
|
||||
|
||||
public interface ICardDatabase extends Iterable<PaperCard> {
|
||||
/**
|
||||
/**
|
||||
* Magic Cards Database.
|
||||
* --------------------
|
||||
* This interface defines the general API for Database Access and Cards' Lookup.
|
||||
*
|
||||
* <p>
|
||||
* Methods for single Card's lookup currently support three alternative strategies:
|
||||
* 1. [getCard]: Card search based on a single card's attributes
|
||||
* (i.e. name, edition, art, collectorNumber)
|
||||
*
|
||||
* <p>
|
||||
* 2. [getCardFromSet]: Card Lookup from a single Expansion set.
|
||||
* Particularly useful in Deck Editors when a specific Set is specified.
|
||||
*
|
||||
* <p>
|
||||
* 3. [getCardFromEditions]: Card search considering a predefined `SetPreference` policy and/or a specified Date
|
||||
* when no expansion is specified for a card.
|
||||
* This method is particularly useful for Re-prints whenever no specific
|
||||
* Expansion is specified (e.g. in Deck Import) and a decision should be made
|
||||
* on which card to pick. This methods allows to adopt a SetPreference selection
|
||||
* policy to make this decision.
|
||||
*
|
||||
* <p>
|
||||
* The API also includes methods to fetch Collection of Card (i.e. PaperCard instances):
|
||||
* - all cards (no filter)
|
||||
* - all unique cards (by name)
|
||||
* - all prints of a single card
|
||||
* - all cards from a single Expansion Set
|
||||
* - all cards compliant with a filter condition (i.e. Predicate)
|
||||
*
|
||||
* <p>
|
||||
* Finally, various utility methods are supported:
|
||||
* - Get the foil version of a Card (if Any)
|
||||
* - Get the Order Number of a Card in an Expansion Set
|
||||
* - Get the number of Print/Arts for a card in a Set (useful for those exp. having multiple arts)
|
||||
* */
|
||||
|
||||
public interface ICardDatabase extends Iterable<PaperCard> {
|
||||
/* SINGLE CARD RETRIEVAL METHODS
|
||||
* ============================= */
|
||||
// 1. Card Lookup by attributes
|
||||
@@ -50,22 +49,20 @@ public interface ICardDatabase extends Iterable<PaperCard> {
|
||||
PaperCard getCard(String cardName, String edition, int artIndex);
|
||||
// [NEW Methods] Including the card CollectorNumber as criterion for DB lookup
|
||||
PaperCard getCard(String cardName, String edition, String collectorNumber);
|
||||
PaperCard getCard(String cardName, String edition, int artIndex, String collectorNumber);
|
||||
PaperCard getCard(String cardName, String edition, int artIndex, Set<String> colorID);
|
||||
PaperCard getCard(String cardName, String edition, int artIndex, Map<String, String> flags);
|
||||
PaperCard getCard(String cardName, String edition, String collectorNumber, Map<String, String> flags);
|
||||
|
||||
// 2. Card Lookup from a single Expansion Set
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, boolean isFoil); // NOT yet used, included for API symmetry
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, String collectorNumber, boolean isFoil);
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, boolean isFoil);
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil);
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil, Set<String> colorID);
|
||||
|
||||
// 3. Card lookup based on CardArtPreference Selection Policy
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference);
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, Predicate<PaperCard> filter);
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex);
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Predicate<PaperCard> filter);
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Set<String> colorID);
|
||||
|
||||
// 4. Specialised Card Lookup on CardArtPreference Selection and Release Date
|
||||
PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, Date releaseDate);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package forge.card;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import forge.deck.DeckRecognizer;
|
||||
|
||||
/**
|
||||
* Holds byte values for each color magic has.
|
||||
@@ -187,6 +188,12 @@ public final class MagicColor {
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getLocalizedName() {
|
||||
//Should probably move some of this logic back here, or at least to a more general location.
|
||||
return DeckRecognizer.getLocalisedMagicColorName(getName());
|
||||
}
|
||||
|
||||
public byte getColormask() {
|
||||
return colormask;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ public enum ManaCostShard {
|
||||
/** The cmpc. */
|
||||
private final float cmpc;
|
||||
private final String stringValue;
|
||||
private final String shortStringValue;
|
||||
|
||||
/** The image key. */
|
||||
private final String imageKey;
|
||||
@@ -125,6 +126,7 @@ public enum ManaCostShard {
|
||||
this.cmc = this.getCMC();
|
||||
this.cmpc = this.getCmpCost();
|
||||
this.stringValue = "{" + sValue + "}";
|
||||
this.shortStringValue = sValue;
|
||||
this.imageKey = imgKey;
|
||||
}
|
||||
|
||||
@@ -232,16 +234,21 @@ public enum ManaCostShard {
|
||||
return ManaCostShard.valueOf(atoms);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see java.lang.Object#toString()
|
||||
/**
|
||||
* @return the string representation of this shard - e.g. "{W}" "{2/U}" "{G/P}"
|
||||
*/
|
||||
@Override
|
||||
public final String toString() {
|
||||
return this.stringValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The string representation of this shard without brackets - e.g. "W" "2/U" "G/P"
|
||||
*/
|
||||
public final String toShortString() {
|
||||
return this.shortStringValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cmc.
|
||||
*
|
||||
|
||||
@@ -52,7 +52,10 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
|
||||
public void add(final String cardRequest, final int amount) {
|
||||
CardDb.CardRequest request = CardDb.CardRequest.fromString(cardRequest);
|
||||
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.artIndex, amount, false, request.colorID);
|
||||
if(request.collectorNumber != null && !request.collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER))
|
||||
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.collectorNumber, amount, false, request.flags);
|
||||
else
|
||||
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.artIndex, amount, false, request.flags);
|
||||
}
|
||||
|
||||
public void add(final String cardName, final String setCode) {
|
||||
@@ -71,7 +74,20 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
public void add(String cardName, String setCode, int artIndex, final int amount) {
|
||||
this.add(cardName, setCode, artIndex, amount, false, null);
|
||||
}
|
||||
public void add(String cardName, String setCode, int artIndex, final int amount, boolean addAny, Set<String> colorID) {
|
||||
private void add(String cardName, String setCode, String collectorNumber, final int amount, boolean addAny, Map<String, String> flags) {
|
||||
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
|
||||
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
|
||||
CardDb db = entry.getValue();
|
||||
PaperCard paperCard = db.getCard(cardName, setCode, collectorNumber, flags);
|
||||
if (paperCard != null) {
|
||||
this.add(paperCard, amount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
//Failed to find it. Fall back accordingly?
|
||||
this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny, flags);
|
||||
}
|
||||
private void add(String cardName, String setCode, int artIndex, final int amount, boolean addAny, Map<String, String> flags) {
|
||||
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
|
||||
PaperCard paperCard = null;
|
||||
String selectedDbName = "";
|
||||
@@ -81,7 +97,7 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
|
||||
String dbName = entry.getKey();
|
||||
CardDb db = entry.getValue();
|
||||
paperCard = db.getCard(cardName, setCode, artIndex, colorID);
|
||||
paperCard = db.getCard(cardName, setCode, artIndex, flags);
|
||||
if (paperCard != null) {
|
||||
selectedDbName = dbName;
|
||||
break;
|
||||
@@ -123,7 +139,7 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
int cnt = artGroups[i - 1];
|
||||
if (cnt <= 0)
|
||||
continue;
|
||||
PaperCard randomCard = cardDb.getCard(cardName, setCode, i, colorID);
|
||||
PaperCard randomCard = cardDb.getCard(cardName, setCode, i, flags);
|
||||
this.add(randomCard, cnt);
|
||||
}
|
||||
}
|
||||
@@ -430,7 +446,6 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
public String toCardList(String separator) {
|
||||
List<Entry<PaperCard, Integer>> main2sort = Lists.newArrayList(this);
|
||||
main2sort.sort(ItemPoolSorter.BY_NAME_THEN_SET);
|
||||
final CardDb commonDb = StaticData.instance().getCommonCards();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
boolean isFirst = true;
|
||||
@@ -441,10 +456,8 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
else
|
||||
isFirst = false;
|
||||
|
||||
CardDb db = !e.getKey().getRules().isVariant() ? commonDb : StaticData.instance().getVariantCards();
|
||||
sb.append(e.getValue()).append(" ");
|
||||
db.appendCardToStringBuilder(e.getKey(), sb);
|
||||
|
||||
sb.append(CardDb.CardRequest.compose(e.getKey()));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
@@ -463,20 +476,4 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
}
|
||||
return filteredPool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a predicate to this CardPool's cards.
|
||||
* @param predicate the Predicate to apply to this CardPool
|
||||
* @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate
|
||||
*/
|
||||
public CardPool getFilteredPoolWithCardsCount(Predicate<PaperCard> predicate) {
|
||||
CardPool filteredPool = new CardPool();
|
||||
for (Entry<PaperCard, Integer> entry : this.items.entrySet()) {
|
||||
PaperCard pc = entry.getKey();
|
||||
int count = entry.getValue();
|
||||
if (predicate.test(pc))
|
||||
filteredPool.add(pc, count);
|
||||
}
|
||||
return filteredPool;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
|
||||
Map<String, List<String>> referenceDeckLoadingMap;
|
||||
if (deferredSections != null) {
|
||||
this.validateDeferredSections();
|
||||
this.normalizeDeferredSections();
|
||||
referenceDeckLoadingMap = new HashMap<>(this.deferredSections);
|
||||
} else
|
||||
referenceDeckLoadingMap = new HashMap<>(loadedSections);
|
||||
@@ -267,7 +267,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
continue;
|
||||
final List<String> cardsInSection = s.getValue();
|
||||
ArrayList<String> cardNamesWithNoEdition = getAllCardNamesWithNoSpecifiedEdition(cardsInSection);
|
||||
if (cardNamesWithNoEdition.size() > 0) {
|
||||
if (!cardNamesWithNoEdition.isEmpty()) {
|
||||
includeCardsFromUnspecifiedSet = true;
|
||||
if (smartCardArtSelection)
|
||||
cardsWithNoEdition.put(sec, cardNamesWithNoEdition);
|
||||
@@ -281,10 +281,10 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
optimiseCardArtSelectionInDeckSections(cardsWithNoEdition);
|
||||
}
|
||||
|
||||
private void validateDeferredSections() {
|
||||
private void normalizeDeferredSections() {
|
||||
/*
|
||||
Construct a temporary (DeckSection, CardPool) Maps, to be sanitised and finalised
|
||||
before copying into `this.parts`. This sanitisation is applied because of the
|
||||
before copying into `this.parts`. This sanitization is applied because of the
|
||||
validation schema introduced in DeckSections.
|
||||
*/
|
||||
Map<String, List<String>> validatedSections = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
@@ -296,61 +296,33 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
}
|
||||
|
||||
final List<String> cardsInSection = s.getValue();
|
||||
List<Pair<String, Integer>> originalCardRequests = CardPool.processCardList(cardsInSection);
|
||||
CardPool pool = CardPool.fromCardList(cardsInSection);
|
||||
if (pool.countDistinct() == 0)
|
||||
continue; // pool empty, no card has been found!
|
||||
|
||||
// Filter pool by applying DeckSection Validation schema for Card Types (to avoid inconsistencies)
|
||||
CardPool filteredPool = pool.getFilteredPoolWithCardsCount(deckSection::validate);
|
||||
// Add all the cards from ValidPool anyway!
|
||||
List<String> whiteList = validatedSections.getOrDefault(s.getKey(), null);
|
||||
if (whiteList == null)
|
||||
whiteList = new ArrayList<>();
|
||||
for (Entry<PaperCard, Integer> entry : filteredPool) {
|
||||
String poolRequest = getPoolRequest(entry, originalCardRequests);
|
||||
whiteList.add(poolRequest);
|
||||
List<String> validatedSection = validatedSections.computeIfAbsent(s.getKey(), (k) -> new ArrayList<>());
|
||||
for (Entry<PaperCard, Integer> entry : pool) {
|
||||
PaperCard card = entry.getKey();
|
||||
String normalizedRequest = getPoolRequest(entry);
|
||||
if(deckSection.validate(card))
|
||||
validatedSection.add(normalizedRequest);
|
||||
else {
|
||||
// Card was in the wrong section. Move it to the right section.
|
||||
DeckSection cardSection = DeckSection.matchingSection(card);
|
||||
assert(cardSection.validate(card)); //Card doesn't fit in the matchingSection?
|
||||
List<String> sectionCardList = validatedSections.computeIfAbsent(cardSection.name(), (k) -> new ArrayList<>());
|
||||
sectionCardList.add(normalizedRequest);
|
||||
}
|
||||
}
|
||||
validatedSections.put(s.getKey(), whiteList);
|
||||
|
||||
if (filteredPool.countDistinct() != pool.countDistinct()) {
|
||||
CardPool blackList = pool.getFilteredPoolWithCardsCount(input -> !(deckSection.validate(input)));
|
||||
|
||||
for (Entry<PaperCard, Integer> entry : blackList) {
|
||||
DeckSection cardSection = DeckSection.matchingSection(entry.getKey());
|
||||
String poolRequest = getPoolRequest(entry, originalCardRequests);
|
||||
List<String> sectionCardList = validatedSections.getOrDefault(cardSection.name(), null);
|
||||
if (sectionCardList == null)
|
||||
sectionCardList = new ArrayList<>();
|
||||
sectionCardList.add(poolRequest);
|
||||
validatedSections.put(cardSection.name(), sectionCardList);
|
||||
} // end for blacklist
|
||||
} // end if
|
||||
} // end main for on deferredSections
|
||||
|
||||
// Overwrite deferredSections
|
||||
this.deferredSections = validatedSections;
|
||||
}
|
||||
|
||||
private String getPoolRequest(Entry<PaperCard, Integer> entry, List<Pair<String, Integer>> originalCardRequests) {
|
||||
PaperCard card = entry.getKey();
|
||||
private String getPoolRequest(Entry<PaperCard, Integer> entry) {
|
||||
int amount = entry.getValue();
|
||||
String poolCardRequest = CardDb.CardRequest.compose(
|
||||
card.isFoil() ? CardDb.CardRequest.compose(card.getName(), true) : card.getName(),
|
||||
card.getEdition(), card.getArtIndex(), card.getColorID());
|
||||
String originalRequestCandidate = null;
|
||||
for (Pair<String, Integer> originalRequest : originalCardRequests){
|
||||
String cardRequest = originalRequest.getLeft();
|
||||
if (!StringUtils.startsWithIgnoreCase(poolCardRequest, cardRequest))
|
||||
continue;
|
||||
originalRequestCandidate = cardRequest;
|
||||
int cardAmount = originalRequest.getRight();
|
||||
if (amount == cardAmount)
|
||||
return String.format("%d %s", cardAmount, cardRequest);
|
||||
}
|
||||
// This is just in case, it should never happen as we're
|
||||
if (originalRequestCandidate != null)
|
||||
return String.format("%d %s", amount, originalRequestCandidate);
|
||||
String poolCardRequest = CardDb.CardRequest.compose(entry.getKey());
|
||||
return String.format("%d %s", amount, poolCardRequest);
|
||||
}
|
||||
|
||||
@@ -645,9 +617,8 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (o instanceof Deck) {
|
||||
final DeckBase dbase = (DeckBase) o;
|
||||
boolean deckBaseEquals = super.equals(dbase);
|
||||
if (o instanceof DeckBase deckBase) {
|
||||
boolean deckBaseEquals = super.equals(deckBase);
|
||||
if (!deckBaseEquals)
|
||||
return false;
|
||||
// ok so far we made sure they do have the same name. Now onto comparing parts
|
||||
|
||||
@@ -472,7 +472,8 @@ public class DeckRecognizer {
|
||||
"side", "sideboard", "sb",
|
||||
"main", "card", "mainboard",
|
||||
"avatar", "commander", "schemes",
|
||||
"conspiracy", "planes", "deck", "dungeon"};
|
||||
"conspiracy", "planes", "deck", "dungeon",
|
||||
"attractions", "contraptions"};
|
||||
|
||||
private static CharSequence[] allCardTypes(){
|
||||
List<String> cardTypesList = new ArrayList<>();
|
||||
@@ -671,7 +672,8 @@ public class DeckRecognizer {
|
||||
return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine,
|
||||
currentDeckSection, true);
|
||||
// UNKNOWN card as in the Counterspell|FEM case
|
||||
return Token.UnknownCard(cardName, setCode, cardCount);
|
||||
unknownCardToken = Token.UnknownCard(cardName, setCode, cardCount);
|
||||
continue;
|
||||
}
|
||||
// ok so we can simply ignore everything but card name - as set code does not exist
|
||||
// At this stage, we know the card name exists in the DB so a Card MUST be found
|
||||
@@ -985,7 +987,7 @@ public class DeckRecognizer {
|
||||
private static String getMagicColourLabel(MagicColor.Color magicColor) {
|
||||
if (magicColor == null) // Multicolour
|
||||
return String.format("%s {W}{U}{B}{R}{G}", getLocalisedMagicColorName("Multicolour"));
|
||||
return String.format("%s %s", getLocalisedMagicColorName(magicColor.getName()), magicColor.getSymbol());
|
||||
return String.format("%s %s", magicColor.getLocalizedName(), magicColor.getSymbol());
|
||||
}
|
||||
|
||||
private static final HashMap<Integer, String> manaSymbolsMap = new HashMap<Integer, String>() {{
|
||||
@@ -1004,8 +1006,8 @@ public class DeckRecognizer {
|
||||
if (magicColor2 == null || magicColor2 == MagicColor.Color.COLORLESS
|
||||
|| magicColor1 == MagicColor.Color.COLORLESS)
|
||||
return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2));
|
||||
String localisedName1 = getLocalisedMagicColorName(magicColor1.getName());
|
||||
String localisedName2 = getLocalisedMagicColorName(magicColor2.getName());
|
||||
String localisedName1 = magicColor1.getLocalizedName();
|
||||
String localisedName2 = magicColor2.getLocalizedName();
|
||||
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
|
||||
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public enum DeckSection {
|
||||
CardType t = card.getRules().getType();
|
||||
// NOTE: Same rules applies to both Deck and Side, despite "Conspiracy cards" are allowed
|
||||
// in the SideBoard (see Rule 313.2)
|
||||
// Those will be matched later, in case (see `Deck::validateDeferredSections`)
|
||||
// Those will be matched later, in case (see `Deck::normalizeDeferredSections`)
|
||||
return !t.isConspiracy() && !t.isDungeon() && !t.isPhenomenon() && !t.isPlane() && !t.isScheme() && !t.isVanguard();
|
||||
};
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ public class DeckSerializer {
|
||||
}
|
||||
|
||||
for(Entry<DeckSection, CardPool> s : d) {
|
||||
if(s.getValue().isEmpty())
|
||||
continue;
|
||||
out.add(TextUtil.enclosedBracket(s.getKey().toString()));
|
||||
out.add(s.getValue().toCardList(System.lineSeparator()));
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package forge.item;
|
||||
|
||||
import forge.card.CardRarity;
|
||||
import forge.card.CardRules;
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.ICardFace;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Set;
|
||||
|
||||
public interface IPaperCard extends InventoryItem, Serializable {
|
||||
|
||||
@@ -20,7 +20,7 @@ public interface IPaperCard extends InventoryItem, Serializable {
|
||||
String getEdition();
|
||||
String getCollectorNumber();
|
||||
String getFunctionalVariant();
|
||||
Set<String> getColorID();
|
||||
ColorSet getMarkedColors();
|
||||
int getArtIndex();
|
||||
boolean isFoil();
|
||||
boolean isToken();
|
||||
|
||||
@@ -24,12 +24,10 @@ import forge.util.CardTranslation;
|
||||
import forge.util.ImageUtil;
|
||||
import forge.util.Localizer;
|
||||
import forge.util.TextUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A lightweight version of a card that matches real-world cards, to use outside of games (eg. inventory, decks, trade).
|
||||
@@ -39,6 +37,7 @@ import java.util.Set;
|
||||
* @author Forge
|
||||
*/
|
||||
public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet, IPaperCard {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 2942081982620691205L;
|
||||
|
||||
// Reference to rules
|
||||
@@ -55,16 +54,15 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
private String artist;
|
||||
private final int artIndex;
|
||||
private final boolean foil;
|
||||
private Boolean hasImage;
|
||||
private final boolean noSell;
|
||||
private Set<String> colorID;
|
||||
private String sortableName;
|
||||
private final PaperCardFlags flags;
|
||||
private final String sortableName;
|
||||
private final String functionalVariant;
|
||||
|
||||
// Calculated fields are below:
|
||||
private transient CardRarity rarity; // rarity is given in ctor when set is assigned
|
||||
// Reference to a new instance of Self, but foiled!
|
||||
private transient PaperCard foiledVersion, noSellVersion;
|
||||
private transient PaperCard foiledVersion, noSellVersion, flaglessVersion;
|
||||
private transient Boolean hasImage;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -89,8 +87,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getColorID() {
|
||||
return colorID;
|
||||
public ColorSet getMarkedColors() {
|
||||
return this.flags.markedColors;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -147,32 +145,32 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
return unFoiledVersion;
|
||||
}
|
||||
public PaperCard getNoSellVersion() {
|
||||
if (this.noSell)
|
||||
if (this.flags.noSellValue)
|
||||
return this;
|
||||
|
||||
if (this.noSellVersion == null) {
|
||||
this.noSellVersion = new PaperCard(this.rules, this.edition, this.rarity,
|
||||
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, true);
|
||||
}
|
||||
if (this.noSellVersion == null)
|
||||
this.noSellVersion = new PaperCard(this, this.flags.withNoSellValueFlag(true));
|
||||
return this.noSellVersion;
|
||||
}
|
||||
public PaperCard getSellable() {
|
||||
if (!this.noSell)
|
||||
return this;
|
||||
|
||||
PaperCard sellable = new PaperCard(this.rules, this.edition, this.rarity,
|
||||
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, false);
|
||||
return sellable;
|
||||
public PaperCard copyWithoutFlags() {
|
||||
if(this.flaglessVersion == null) {
|
||||
if(this.flags == PaperCardFlags.IDENTITY_FLAGS)
|
||||
this.flaglessVersion = this;
|
||||
else
|
||||
this.flaglessVersion = new PaperCard(this, null);
|
||||
}
|
||||
public PaperCard getColorIDVersion(Set<String> colors) {
|
||||
if (colors == null && this.colorID == null)
|
||||
return flaglessVersion;
|
||||
}
|
||||
public PaperCard copyWithFlags(Map<String, String> flags) {
|
||||
if(flags == null || flags.isEmpty())
|
||||
return this.copyWithoutFlags();
|
||||
return new PaperCard(this, new PaperCardFlags(flags));
|
||||
}
|
||||
public PaperCard copyWithMarkedColors(ColorSet colors) {
|
||||
if(Objects.equals(colors, this.flags.markedColors))
|
||||
return this;
|
||||
if (this.colorID != null && this.colorID.equals(colors))
|
||||
return this;
|
||||
if (colors != null && colors.equals(this.colorID))
|
||||
return this;
|
||||
return new PaperCard(this.rules, this.edition, this.rarity,
|
||||
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, this.noSell, colors);
|
||||
return new PaperCard(this, this.flags.withMarkedColors(colors));
|
||||
}
|
||||
@Override
|
||||
public String getItemType() {
|
||||
@@ -180,8 +178,12 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
return localizer.getMessage("lblCard");
|
||||
}
|
||||
|
||||
public boolean isNoSell() {
|
||||
return noSell;
|
||||
public PaperCardFlags getMarkedFlags() {
|
||||
return this.flags;
|
||||
}
|
||||
|
||||
public boolean hasNoSellValue() {
|
||||
return this.flags.noSellValue;
|
||||
}
|
||||
public boolean hasImage() {
|
||||
return hasImage(false);
|
||||
@@ -198,38 +200,41 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME, IPaperCard.NO_FUNCTIONAL_VARIANT);
|
||||
}
|
||||
|
||||
public PaperCard(final PaperCard copyFrom, final PaperCardFlags flags) {
|
||||
this(copyFrom.rules, copyFrom.edition, copyFrom.rarity, copyFrom.artIndex, copyFrom.foil, copyFrom.collectorNumber,
|
||||
copyFrom.artist, copyFrom.functionalVariant, flags);
|
||||
this.flaglessVersion = copyFrom.flaglessVersion;
|
||||
}
|
||||
|
||||
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
|
||||
final int artIndex0, final boolean foil0, final String collectorNumber0,
|
||||
final String artist0, final String functionalVariant) {
|
||||
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, false);
|
||||
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, null);
|
||||
}
|
||||
|
||||
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
|
||||
final int artIndex0, final boolean foil0, final String collectorNumber0,
|
||||
final String artist0, final String functionalVariant, final boolean noSell0) {
|
||||
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, noSell0, null);
|
||||
}
|
||||
|
||||
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
|
||||
final int artIndex0, final boolean foil0, final String collectorNumber0,
|
||||
final String artist0, final String functionalVariant, final boolean noSell0, final Set<String> colorID0) {
|
||||
if (rules0 == null || edition0 == null || rarity0 == null) {
|
||||
protected PaperCard(final CardRules rules, final String edition, final CardRarity rarity,
|
||||
final int artIndex, final boolean foil, final String collectorNumber,
|
||||
final String artist, final String functionalVariant, final PaperCardFlags flags) {
|
||||
if (rules == null || edition == null || rarity == null) {
|
||||
throw new IllegalArgumentException("Cannot create card without rules, edition or rarity");
|
||||
}
|
||||
rules = rules0;
|
||||
name = rules0.getName();
|
||||
edition = edition0;
|
||||
artIndex = Math.max(artIndex0, IPaperCard.DEFAULT_ART_INDEX);
|
||||
foil = foil0;
|
||||
rarity = rarity0;
|
||||
artist = TextUtil.normalizeText(artist0);
|
||||
collectorNumber = (collectorNumber0 != null) && (collectorNumber0.length() > 0) ? collectorNumber0 : IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
this.rules = rules;
|
||||
name = rules.getName();
|
||||
this.edition = edition;
|
||||
this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
|
||||
this.foil = foil;
|
||||
this.rarity = rarity;
|
||||
this.artist = TextUtil.normalizeText(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
|
||||
sortableName = TextUtil.toSortableName(CardTranslation.getTranslatedName(rules0.getName()));
|
||||
sortableName = TextUtil.toSortableName(CardTranslation.getTranslatedName(rules.getName()));
|
||||
this.functionalVariant = functionalVariant != null ? functionalVariant : IPaperCard.NO_FUNCTIONAL_VARIANT;
|
||||
noSell = noSell0;
|
||||
colorID = colorID0;
|
||||
|
||||
if(flags == null || flags.equals(PaperCardFlags.IDENTITY_FLAGS))
|
||||
this.flags = PaperCardFlags.IDENTITY_FLAGS;
|
||||
else
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
public static PaperCard FAKE_CARD = new PaperCard(CardRules.getUnsupportedCardNamed("Fake Card"), "Fake Edition", CardRarity.Common);
|
||||
@@ -256,8 +261,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
}
|
||||
if (!getCollectorNumber().equals(other.getCollectorNumber()))
|
||||
return false;
|
||||
// colorID can be NULL
|
||||
if (getColorID() != other.getColorID())
|
||||
if (!Objects.equals(flags, other.flags))
|
||||
return false;
|
||||
return (other.foil == foil) && (other.artIndex == artIndex);
|
||||
}
|
||||
@@ -269,13 +273,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int code = (name.hashCode() * 11) + (edition.hashCode() * 59) +
|
||||
(artIndex * 2) + (getCollectorNumber().hashCode() * 383);
|
||||
final int id = Optional.ofNullable(colorID).map(Set::hashCode).orElse(0);
|
||||
if (foil) {
|
||||
return code + id + 1;
|
||||
}
|
||||
return code + id;
|
||||
return Objects.hash(name, edition, collectorNumber, artIndex, foil, flags);
|
||||
}
|
||||
|
||||
// FIXME: Check
|
||||
@@ -307,7 +305,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
String collectorNumber = collectorNumber0;
|
||||
if (collectorNumber.equals(NO_COLLECTOR_NUMBER))
|
||||
collectorNumber = null;
|
||||
return CardEdition.CardInSet.getSortableCollectorNumber(collectorNumber);
|
||||
return CardEdition.getSortableCollectorNumber(collectorNumber);
|
||||
}
|
||||
|
||||
private String sortableCNKey = null;
|
||||
@@ -339,6 +337,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
return Integer.compare(artIndex, o.getArtIndex());
|
||||
}
|
||||
|
||||
@Serial
|
||||
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
|
||||
// default deserialization
|
||||
ois.defaultReadObject();
|
||||
@@ -354,22 +353,24 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
rarity = pc.getRarity();
|
||||
}
|
||||
|
||||
@Serial
|
||||
private Object readResolve() throws ObjectStreamException {
|
||||
//If we deserialize an old PaperCard with no flags, reinitialize as a fresh copy to set default flags.
|
||||
if(this.flags == null)
|
||||
return new PaperCard(this, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImageKey(boolean altState) {
|
||||
String noramlizedName = StringUtils.stripAccents(name);
|
||||
String imageKey = ImageKeys.CARD_PREFIX + noramlizedName + CardDb.NameSetSeparator
|
||||
+ edition + CardDb.NameSetSeparator + artIndex;
|
||||
if (altState) {
|
||||
imageKey += ImageKeys.BACKFACE_POSTFIX;
|
||||
}
|
||||
return imageKey;
|
||||
return altState ? this.getCardAltImageKey() : this.getCardImageKey();
|
||||
}
|
||||
|
||||
private String cardImageKey = null;
|
||||
@Override
|
||||
public String getCardImageKey() {
|
||||
if (this.cardImageKey == null)
|
||||
this.cardImageKey = ImageUtil.getImageKey(this, "", true);
|
||||
this.cardImageKey = ImageUtil.getImageKey(this, CardStateName.Original);
|
||||
return cardImageKey;
|
||||
}
|
||||
|
||||
@@ -378,9 +379,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
public String getCardAltImageKey() {
|
||||
if (this.cardAltImageKey == null){
|
||||
if (this.hasBackFace())
|
||||
this.cardAltImageKey = ImageUtil.getImageKey(this, "back", true);
|
||||
this.cardAltImageKey = ImageUtil.getImageKey(this, this.getRules().getSplitType().getChangedStateName());
|
||||
else // altImageKey will be the same as cardImageKey
|
||||
this.cardAltImageKey = ImageUtil.getImageKey(this, "", true);
|
||||
this.cardAltImageKey = getCardImageKey();
|
||||
}
|
||||
return cardAltImageKey;
|
||||
}
|
||||
@@ -390,9 +391,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
public String getCardWSpecImageKey() {
|
||||
if (this.cardWSpecImageKey == null) {
|
||||
if (this.rules.getSplitType() == CardSplitType.Specialize)
|
||||
this.cardWSpecImageKey = ImageUtil.getImageKey(this, "white", true);
|
||||
this.cardWSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeW);
|
||||
else // just use cardImageKey
|
||||
this.cardWSpecImageKey = ImageUtil.getImageKey(this, "", true);
|
||||
this.cardWSpecImageKey = getCardImageKey();
|
||||
}
|
||||
return cardWSpecImageKey;
|
||||
}
|
||||
@@ -402,9 +403,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
public String getCardUSpecImageKey() {
|
||||
if (this.cardUSpecImageKey == null) {
|
||||
if (this.rules.getSplitType() == CardSplitType.Specialize)
|
||||
this.cardUSpecImageKey = ImageUtil.getImageKey(this, "blue", true);
|
||||
this.cardUSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeU);
|
||||
else // just use cardImageKey
|
||||
this.cardUSpecImageKey = ImageUtil.getImageKey(this, "", true);
|
||||
this.cardUSpecImageKey = getCardImageKey();
|
||||
}
|
||||
return cardUSpecImageKey;
|
||||
}
|
||||
@@ -414,9 +415,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
public String getCardBSpecImageKey() {
|
||||
if (this.cardBSpecImageKey == null) {
|
||||
if (this.rules.getSplitType() == CardSplitType.Specialize)
|
||||
this.cardBSpecImageKey = ImageUtil.getImageKey(this, "black", true);
|
||||
this.cardBSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeB);
|
||||
else // just use cardImageKey
|
||||
this.cardBSpecImageKey = ImageUtil.getImageKey(this, "", true);
|
||||
this.cardBSpecImageKey = getCardImageKey();
|
||||
}
|
||||
return cardBSpecImageKey;
|
||||
}
|
||||
@@ -426,9 +427,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
public String getCardRSpecImageKey() {
|
||||
if (this.cardRSpecImageKey == null) {
|
||||
if (this.rules.getSplitType() == CardSplitType.Specialize)
|
||||
this.cardRSpecImageKey = ImageUtil.getImageKey(this, "red", true);
|
||||
this.cardRSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeR);
|
||||
else // just use cardImageKey
|
||||
this.cardRSpecImageKey = ImageUtil.getImageKey(this, "", true);
|
||||
this.cardRSpecImageKey = getCardImageKey();
|
||||
}
|
||||
return cardRSpecImageKey;
|
||||
}
|
||||
@@ -438,18 +439,16 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
public String getCardGSpecImageKey() {
|
||||
if (this.cardGSpecImageKey == null) {
|
||||
if (this.rules.getSplitType() == CardSplitType.Specialize)
|
||||
this.cardGSpecImageKey = ImageUtil.getImageKey(this, "green", true);
|
||||
this.cardGSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeG);
|
||||
else // just use cardImageKey
|
||||
this.cardGSpecImageKey = ImageUtil.getImageKey(this, "", true);
|
||||
this.cardGSpecImageKey = getCardImageKey();
|
||||
}
|
||||
return cardGSpecImageKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasBackFace(){
|
||||
CardSplitType cst = this.rules.getSplitType();
|
||||
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld
|
||||
|| cst == CardSplitType.Modal;
|
||||
return this.rules.hasBackSide();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -493,4 +492,88 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
public boolean isRebalanced() {
|
||||
return StaticData.instance().isRebalanced(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains properties of a card which distinguish it from an otherwise identical copy of the card with the same
|
||||
* name, edition, and collector number. Examples include permanent markings on the card, and flags for Adventure
|
||||
* mode.
|
||||
*/
|
||||
public static class PaperCardFlags implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = -3924720485840169336L;
|
||||
|
||||
/**
|
||||
* Chosen colors, for cards like Cryptic Spires.
|
||||
*/
|
||||
public final ColorSet markedColors;
|
||||
/**
|
||||
* Removes the sell value of the card in Adventure mode.
|
||||
*/
|
||||
public final boolean noSellValue;
|
||||
|
||||
//TODO: Could probably move foil here.
|
||||
|
||||
static final PaperCardFlags IDENTITY_FLAGS = new PaperCardFlags(Map.of());
|
||||
|
||||
protected PaperCardFlags(Map<String, String> flags) {
|
||||
if(flags.containsKey("markedColors"))
|
||||
markedColors = ColorSet.fromNames(flags.get("markedColors").split(""));
|
||||
else
|
||||
markedColors = null;
|
||||
noSellValue = flags.containsKey("noSellValue");
|
||||
}
|
||||
|
||||
//Copy constructor. There are some better ways to do this, and they should be explored once we have more than 4
|
||||
//or 5 fields here. Just need to ensure it's impossible to accidentally change a field while the PaperCardFlags
|
||||
//object is in use.
|
||||
private PaperCardFlags(PaperCardFlags copyFrom, ColorSet markedColors, Boolean noSellValue) {
|
||||
if(markedColors == null)
|
||||
markedColors = copyFrom.markedColors;
|
||||
else if(markedColors.isColorless())
|
||||
markedColors = null;
|
||||
this.markedColors = markedColors;
|
||||
this.noSellValue = noSellValue != null ? noSellValue : copyFrom.noSellValue;
|
||||
}
|
||||
|
||||
public PaperCardFlags withMarkedColors(ColorSet markedColors) {
|
||||
if(markedColors == null)
|
||||
markedColors = ColorSet.getNullColor();
|
||||
return new PaperCardFlags(this, markedColors, null);
|
||||
}
|
||||
|
||||
public PaperCardFlags withNoSellValueFlag(boolean noSellValue) {
|
||||
return new PaperCardFlags(this, null, noSellValue);
|
||||
}
|
||||
|
||||
private Map<String, String> asMap;
|
||||
public Map<String, String> toMap() {
|
||||
if(asMap != null)
|
||||
return asMap;
|
||||
Map<String, String> out = new HashMap<>();
|
||||
if(markedColors != null && !markedColors.isColorless())
|
||||
out.put("markedColors", markedColors.toString());
|
||||
if(noSellValue)
|
||||
out.put("noSellValue", "true");
|
||||
asMap = out;
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.toMap().entrySet().stream()
|
||||
.map((e) -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining("\t"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof PaperCardFlags that)) return false;
|
||||
return noSellValue == that.noSellValue && Objects.equals(markedColors, that.markedColors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(markedColors, noSellValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String name;
|
||||
private String collectorNumber;
|
||||
private String artist;
|
||||
private transient CardEdition edition;
|
||||
private ArrayList<String> imageFileName = new ArrayList<>();
|
||||
private transient CardRules cardRules;
|
||||
@@ -54,75 +55,31 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
return makeTokenFileName(fileName);
|
||||
}
|
||||
|
||||
public static String makeTokenFileName(final CardRules rules, CardEdition edition) {
|
||||
ArrayList<String> build = new ArrayList<>();
|
||||
|
||||
String subtypes = StringUtils.join(rules.getType().getSubtypes(), " ");
|
||||
if (!rules.getName().equals(subtypes)) {
|
||||
return makeTokenFileName(rules.getName());
|
||||
}
|
||||
|
||||
ColorSet colors = rules.getColor();
|
||||
|
||||
if (colors.isColorless()) {
|
||||
build.add("C");
|
||||
} else {
|
||||
String color = "";
|
||||
if (colors.hasWhite()) color += "W";
|
||||
if (colors.hasBlue()) color += "U";
|
||||
if (colors.hasBlack()) color += "B";
|
||||
if (colors.hasRed()) color += "R";
|
||||
if (colors.hasGreen()) color += "G";
|
||||
|
||||
build.add(color);
|
||||
}
|
||||
|
||||
if (rules.getPower() != null && rules.getToughness() != null) {
|
||||
build.add(rules.getPower());
|
||||
build.add(rules.getToughness());
|
||||
}
|
||||
|
||||
String cardTypes = "";
|
||||
if (rules.getType().isArtifact()) cardTypes += "A";
|
||||
if (rules.getType().isEnchantment()) cardTypes += "E";
|
||||
|
||||
if (!cardTypes.isEmpty()) {
|
||||
build.add(cardTypes);
|
||||
}
|
||||
|
||||
build.add(subtypes);
|
||||
|
||||
// Are these keywords sorted?
|
||||
for (String keyword : rules.getMainPart().getKeywords()) {
|
||||
build.add(keyword);
|
||||
}
|
||||
|
||||
if (edition != null) {
|
||||
build.add(edition.getCode());
|
||||
}
|
||||
|
||||
return StringUtils.join(build, "_").replace('*', 'x').toLowerCase();
|
||||
}
|
||||
|
||||
public PaperToken(final CardRules c, CardEdition edition0, String imageFileName) {
|
||||
public PaperToken(final CardRules c, CardEdition edition0, String imageFileName, String collectorNumber, String artist) {
|
||||
this.cardRules = c;
|
||||
this.name = c.getName();
|
||||
this.edition = edition0;
|
||||
this.collectorNumber = collectorNumber;
|
||||
this.artist = artist;
|
||||
|
||||
if (edition != null && edition.getTokens().containsKey(imageFileName)) {
|
||||
this.artIndex = edition.getTokens().get(imageFileName);
|
||||
if (collectorNumber != null && !collectorNumber.isEmpty() && edition != null && edition.getTokens().containsKey(imageFileName)) {
|
||||
int idx = 0;
|
||||
// count the one with the same collectorNumber
|
||||
for (CardEdition.EditionEntry t : edition.getTokens().get(imageFileName)) {
|
||||
++idx;
|
||||
if (!t.collectorNumber().equals(collectorNumber)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (imageFileName == null) {
|
||||
// This shouldn't really happen. We can just use the normalized name again for the base image name
|
||||
this.imageFileName.add(makeTokenFileName(c, edition0));
|
||||
// TODO make better image file names when collector number is known
|
||||
// for the right index, we need to count the ones with wrong collector number too
|
||||
this.imageFileName.add(String.format("%s|%s|%s|%d", imageFileName, edition.getCode(), collectorNumber, idx));
|
||||
}
|
||||
this.artIndex = this.imageFileName.size();
|
||||
} else if (null == edition || CardEdition.UNKNOWN == edition) {
|
||||
this.imageFileName.add(imageFileName);
|
||||
} else {
|
||||
String formatEdition = null == edition || CardEdition.UNKNOWN == edition ? "" : "_" + edition.getCode().toLowerCase();
|
||||
|
||||
this.imageFileName.add(String.format("%s%s", imageFileName, formatEdition));
|
||||
for (int idx = 2; idx <= this.artIndex; idx++) {
|
||||
this.imageFileName.add(String.format("%s%d%s", imageFileName, idx, formatEdition));
|
||||
}
|
||||
// Fallback if CollectorNumber is not used
|
||||
this.imageFileName.add(String.format("%s|%s", imageFileName, edition.getCode()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,12 +95,14 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
|
||||
@Override
|
||||
public String getEdition() {
|
||||
return edition != null ? edition.getCode() : "???";
|
||||
return edition != null ? edition.getCode() : CardEdition.UNKNOWN_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCollectorNumber() {
|
||||
if (collectorNumber.isEmpty())
|
||||
return IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
return collectorNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,7 +112,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getColorID() {
|
||||
public ColorSet getMarkedColors() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -178,13 +137,8 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArtist() { /*TODO*/
|
||||
return "";
|
||||
}
|
||||
|
||||
// Unfortunately this is a property of token, cannot move it outside of class
|
||||
public String getImageFilename() {
|
||||
return getImageFilename(1);
|
||||
public String getArtist() {
|
||||
return artist;
|
||||
}
|
||||
|
||||
public String getImageFilename(int idx) {
|
||||
@@ -259,24 +213,21 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
// InventoryItem
|
||||
@Override
|
||||
public String getImageKey(boolean altState) {
|
||||
if (hasBackFace()) {
|
||||
String edCode = edition != null ? "_" + edition.getCode().toLowerCase() : "";
|
||||
if (altState) {
|
||||
String name = ImageKeys.TOKEN_PREFIX + cardRules.getOtherPart().getName().toLowerCase().replace(" token", "");
|
||||
name.replace(" ", "_");
|
||||
return name + edCode;
|
||||
String suffix = "";
|
||||
if (hasBackFace() && altState) {
|
||||
if (collectorNumber != null && !collectorNumber.isEmpty() && edition != null) {
|
||||
String name = cardRules.getOtherPart().getName().toLowerCase().replace(" token", "").replace(" ", "_");
|
||||
return ImageKeys.getTokenKey(String.format("%s|%s|%s%s", name, edition.getCode(), collectorNumber, ImageKeys.BACKFACE_POSTFIX));
|
||||
} else {
|
||||
String name = ImageKeys.TOKEN_PREFIX + cardRules.getMainPart().getName().toLowerCase().replace(" token", "");
|
||||
name.replace(" ", "_");
|
||||
return name + edCode;
|
||||
suffix = ImageKeys.BACKFACE_POSTFIX;
|
||||
}
|
||||
}
|
||||
int idx = MyRandom.getRandom().nextInt(artIndex);
|
||||
return getImageKey(idx);
|
||||
return getImageKey(idx) + suffix;
|
||||
}
|
||||
|
||||
public String getImageKey(int artIndex) {
|
||||
return ImageKeys.TOKEN_PREFIX + imageFileName.get(artIndex).replace(" ", "_");
|
||||
return ImageKeys.getTokenKey(imageFileName.get(artIndex).replace(" ", "_"));
|
||||
}
|
||||
|
||||
public boolean isRebalanced() {
|
||||
|
||||
@@ -65,8 +65,8 @@ public class BoosterGenerator {
|
||||
}
|
||||
|
||||
public static List<PaperCard> getBoosterPack(SealedTemplate template) {
|
||||
if (template instanceof SealedTemplateWithSlots) {
|
||||
return BoosterGenerator.getBoosterPack((SealedTemplateWithSlots) template);
|
||||
if (template instanceof SealedTemplateWithSlots slots) {
|
||||
return BoosterGenerator.getBoosterPack(slots);
|
||||
}
|
||||
|
||||
List<PaperCard> result = new ArrayList<>();
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package forge.token;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import forge.card.CardDb;
|
||||
import forge.card.CardEdition;
|
||||
import forge.card.CardRules;
|
||||
import forge.item.IPaperCard;
|
||||
import forge.item.PaperToken;
|
||||
import forge.util.Aggregates;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
@@ -23,8 +28,8 @@ public class TokenDb implements ITokenDatabase {
|
||||
|
||||
// The image names should be the same as the script name + _set
|
||||
// If that isn't found, consider falling back to the original token
|
||||
|
||||
private final Map<String, PaperToken> tokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
|
||||
private final Multimap<String, PaperToken> allTokenByName = HashMultimap.create();
|
||||
private final Map<String, PaperToken> extraTokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
|
||||
|
||||
private final CardEdition.Collection editions;
|
||||
private final Map<String, CardRules> rulesByName;
|
||||
@@ -38,38 +43,87 @@ public class TokenDb implements ITokenDatabase {
|
||||
return this.rulesByName.containsKey(rule);
|
||||
|
||||
}
|
||||
@Override
|
||||
public PaperToken getToken(String tokenName) {
|
||||
return getToken(tokenName, CardEdition.UNKNOWN.getName());
|
||||
}
|
||||
|
||||
public void preloadTokens() {
|
||||
for (CardEdition edition : this.editions) {
|
||||
for (String name : edition.getTokens().keySet()) {
|
||||
try {
|
||||
getToken(name, edition.getCode());
|
||||
} catch(Exception e) {
|
||||
System.out.println(name + "_" + edition.getCode() + " defined in Edition file, but not defined as a token script.");
|
||||
for (Map.Entry<String, Collection<CardEdition.EditionEntry>> inSet : edition.getTokens().asMap().entrySet()) {
|
||||
String name = inSet.getKey();
|
||||
String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
|
||||
for (CardEdition.EditionEntry t : inSet.getValue()) {
|
||||
allTokenByName.put(fullName, addTokenInSet(edition, name, t));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean loadTokenFromSet(CardEdition edition, String name) {
|
||||
String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
|
||||
if (allTokenByName.containsKey(fullName)) {
|
||||
return true;
|
||||
}
|
||||
if (!edition.getTokens().containsKey(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (CardEdition.EditionEntry t : edition.getTokens().get(name)) {
|
||||
allTokenByName.put(fullName, addTokenInSet(edition, name, t));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected PaperToken addTokenInSet(CardEdition edition, String name, CardEdition.EditionEntry t) {
|
||||
CardRules rules;
|
||||
if (rulesByName.containsKey(name)) {
|
||||
rules = rulesByName.get(name);
|
||||
} else if ("w_2_2_spirit".equals(name) || "w_3_3_spirit".equals(name)) { // Hotfix for Endure Token
|
||||
rules = rulesByName.get("w_x_x_spirit");
|
||||
} else {
|
||||
throw new RuntimeException("wrong token name:" + name);
|
||||
}
|
||||
return new PaperToken(rules, edition, name, t.collectorNumber(), t.artistName());
|
||||
}
|
||||
|
||||
// try all editions to find token
|
||||
protected PaperToken fallbackToken(String name) {
|
||||
for (CardEdition edition : this.editions) {
|
||||
String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
|
||||
if (loadTokenFromSet(edition, name)) {
|
||||
return Aggregates.random(allTokenByName.get(fullName));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperToken getToken(String tokenName) {
|
||||
return getToken(tokenName, CardEdition.UNKNOWN.getCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperToken getToken(String tokenName, String edition) {
|
||||
String fullName = String.format("%s_%s", tokenName, edition.toLowerCase());
|
||||
CardEdition realEdition = editions.getEditionByCodeOrThrow(edition);
|
||||
String fullName = String.format("%s_%s", tokenName, realEdition.getCode().toLowerCase());
|
||||
|
||||
if (!tokensByName.containsKey(fullName)) {
|
||||
// token exist in Set, return one at random
|
||||
if (loadTokenFromSet(realEdition, tokenName)) {
|
||||
return Aggregates.random(allTokenByName.get(fullName));
|
||||
}
|
||||
PaperToken fallback = this.fallbackToken(tokenName);
|
||||
if (fallback != null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (!extraTokensByName.containsKey(fullName)) {
|
||||
try {
|
||||
PaperToken pt = new PaperToken(rulesByName.get(tokenName), editions.get(edition), tokenName);
|
||||
tokensByName.put(fullName, pt);
|
||||
PaperToken pt = new PaperToken(rulesByName.get(tokenName), realEdition, tokenName, "", IPaperCard.NO_ARTIST_NAME);
|
||||
extraTokensByName.put(fullName, pt);
|
||||
return pt;
|
||||
} catch(Exception e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return tokensByName.get(fullName);
|
||||
return extraTokensByName.get(fullName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -119,7 +173,7 @@ public class TokenDb implements ITokenDatabase {
|
||||
|
||||
@Override
|
||||
public List<PaperToken> getAllTokens() {
|
||||
return new ArrayList<>(tokensByName.values());
|
||||
return new ArrayList<>(allTokenByName.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,6 +193,6 @@ public class TokenDb implements ITokenDatabase {
|
||||
|
||||
@Override
|
||||
public Iterator<PaperToken> iterator() {
|
||||
return tokensByName.values().iterator();
|
||||
return allTokenByName.values().iterator();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import forge.StaticData;
|
||||
import forge.card.CardDb;
|
||||
import forge.card.CardRules;
|
||||
import forge.card.CardSplitType;
|
||||
import forge.card.CardStateName;
|
||||
import forge.item.IPaperCard;
|
||||
import forge.item.PaperCard;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -24,20 +25,17 @@ public class ImageUtil {
|
||||
key = imageKey.substring(ImageKeys.CARD_PREFIX.length());
|
||||
else
|
||||
return null;
|
||||
|
||||
if (key.endsWith(ImageKeys.BACKFACE_POSTFIX)) {
|
||||
key = key.substring(0, key.length() - ImageKeys.BACKFACE_POSTFIX.length());
|
||||
}
|
||||
|
||||
if (key.isEmpty())
|
||||
return null;
|
||||
|
||||
CardDb db = StaticData.instance().getCommonCards();
|
||||
PaperCard cp = null;
|
||||
//db shouldn't be null
|
||||
if (db != null) {
|
||||
cp = db.getCard(key);
|
||||
if (cp == null) {
|
||||
db = StaticData.instance().getVariantCards();
|
||||
if (db != null)
|
||||
cp = db.getCard(key);
|
||||
}
|
||||
}
|
||||
String[] tempdata = key.split("\\|");
|
||||
PaperCard cp = StaticData.instance().fetchCard(tempdata[0], tempdata[1], tempdata[2]);
|
||||
|
||||
if (cp == null)
|
||||
System.err.println("Can't find PaperCard from key: " + key);
|
||||
// return cp regardless if it's null
|
||||
@@ -54,6 +52,21 @@ public class ImageUtil {
|
||||
return key;
|
||||
}
|
||||
|
||||
public static String getImageRelativePath(String name, String set, String collectorNumber, boolean artChop) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append(set).append("/");
|
||||
if (!collectorNumber.isEmpty() && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)) {
|
||||
sb.append(collectorNumber).append("_");
|
||||
}
|
||||
sb.append(StringUtils.stripAccents(name));
|
||||
|
||||
sb.append(artChop ? ".artcrop" : ".fullborder");
|
||||
sb.append(".jpg");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
public static String getImageRelativePath(PaperCard cp, String face, boolean includeSet, boolean isDownloadUrl) {
|
||||
final String nameToUse = cp == null ? null : getNameToUse(cp, face);
|
||||
if (nameToUse == null) {
|
||||
@@ -123,25 +136,15 @@ public class ImageUtil {
|
||||
else
|
||||
return null;
|
||||
} else if (face.equals("white")) {
|
||||
if (card.getWSpecialize() != null) {
|
||||
return card.getWSpecialize().getName();
|
||||
}
|
||||
return card.getImageName(CardStateName.SpecializeW);
|
||||
} else if (face.equals("blue")) {
|
||||
if (card.getUSpecialize() != null) {
|
||||
return card.getUSpecialize().getName();
|
||||
}
|
||||
return card.getImageName(CardStateName.SpecializeU);
|
||||
} else if (face.equals("black")) {
|
||||
if (card.getBSpecialize() != null) {
|
||||
return card.getBSpecialize().getName();
|
||||
}
|
||||
return card.getImageName(CardStateName.SpecializeB);
|
||||
} else if (face.equals("red")) {
|
||||
if (card.getRSpecialize() != null) {
|
||||
return card.getRSpecialize().getName();
|
||||
}
|
||||
return card.getImageName(CardStateName.SpecializeR);
|
||||
} else if (face.equals("green")) {
|
||||
if (card.getGSpecialize() != null) {
|
||||
return card.getGSpecialize().getName();
|
||||
}
|
||||
return card.getImageName(CardStateName.SpecializeG);
|
||||
} else if (CardSplitType.Split == cp.getRules().getSplitType()) {
|
||||
return card.getMainPart().getName() + card.getOtherPart().getName();
|
||||
} else if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(cp.getFunctionalVariant())) {
|
||||
@@ -150,50 +153,95 @@ public class ImageUtil {
|
||||
return cp.getName();
|
||||
}
|
||||
|
||||
public static String getNameToUse(PaperCard cp, CardStateName face) {
|
||||
if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(cp.getFunctionalVariant())) {
|
||||
return cp.getFunctionalVariant();
|
||||
}
|
||||
final CardRules card = cp.getRules();
|
||||
return card.getImageName(face);
|
||||
}
|
||||
|
||||
public static String getImageKey(PaperCard cp, String face, boolean includeSet) {
|
||||
return getImageRelativePath(cp, face, includeSet, false);
|
||||
}
|
||||
|
||||
public static String getImageKey(PaperCard cp, CardStateName face) {
|
||||
String name = getNameToUse(cp, face);
|
||||
String number = cp.getCollectorNumber();
|
||||
String suffix = "";
|
||||
switch (face) {
|
||||
case SpecializeB:
|
||||
number += "b";
|
||||
break;
|
||||
case SpecializeG:
|
||||
number += "g";
|
||||
break;
|
||||
case SpecializeR:
|
||||
number += "r";
|
||||
break;
|
||||
case SpecializeU:
|
||||
number += "u";
|
||||
break;
|
||||
case SpecializeW:
|
||||
number += "w";
|
||||
break;
|
||||
case Meld:
|
||||
case Modal:
|
||||
case Secondary:
|
||||
case Transformed:
|
||||
suffix = ImageKeys.BACKFACE_POSTFIX;
|
||||
break;
|
||||
case Flipped:
|
||||
break; // add info to rotate the image?
|
||||
default:
|
||||
break;
|
||||
};
|
||||
return ImageKeys.CARD_PREFIX + name + CardDb.NameSetSeparator + cp.getEdition()
|
||||
+ CardDb.NameSetSeparator + number + CardDb.NameSetSeparator + cp.getArtIndex() + suffix;
|
||||
}
|
||||
|
||||
public static String getDownloadUrl(PaperCard cp, String face) {
|
||||
return getImageRelativePath(cp, face, true, true);
|
||||
}
|
||||
|
||||
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){
|
||||
return getScryfallDownloadUrl(cp, face, setCode, langCode, useArtCrop, false);
|
||||
public static String getScryfallDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam, boolean useArtCrop){
|
||||
return getScryfallDownloadUrl(collectorNumber, setCode, langCode, faceParam, useArtCrop, false);
|
||||
}
|
||||
|
||||
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){
|
||||
String editionCode;
|
||||
if ((setCode != null) && (setCode.length() > 0))
|
||||
editionCode = setCode;
|
||||
else
|
||||
editionCode = cp.getEdition().toLowerCase();
|
||||
String cardCollectorNumber = cp.getCollectorNumber();
|
||||
public static String getScryfallDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam, boolean useArtCrop, boolean hyphenateAlchemy){
|
||||
// Hack to account for variations in Arabian Nights
|
||||
cardCollectorNumber = cardCollectorNumber.replace("+", "†");
|
||||
collectorNumber = collectorNumber.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";
|
||||
cardCollectorNumber = cardCollectorNumber.substring("OHOP".length());
|
||||
} else if (cardCollectorNumber.startsWith("OPCA")) {
|
||||
editionCode = "opca";
|
||||
cardCollectorNumber = cardCollectorNumber.substring("OPCA".length());
|
||||
} else if (cardCollectorNumber.startsWith("OPC2")) {
|
||||
editionCode = "opc2";
|
||||
cardCollectorNumber = cardCollectorNumber.substring("OPC2".length());
|
||||
if (collectorNumber.startsWith("OHOP")) {
|
||||
setCode = "ohop";
|
||||
collectorNumber = collectorNumber.substring("OHOP".length());
|
||||
} else if (collectorNumber.startsWith("OPCA")) {
|
||||
setCode = "opca";
|
||||
collectorNumber = collectorNumber.substring("OPCA".length());
|
||||
} else if (collectorNumber.startsWith("OPC2")) {
|
||||
setCode = "opc2";
|
||||
collectorNumber = collectorNumber.substring("OPC2".length());
|
||||
} else if (hyphenateAlchemy) {
|
||||
if (!cardCollectorNumber.startsWith("A")) {
|
||||
if (!collectorNumber.startsWith("A")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
cardCollectorNumber = cardCollectorNumber.replace("A", "A-");
|
||||
collectorNumber = collectorNumber.replace("A", "A-");
|
||||
}
|
||||
String versionParam = useArtCrop ? "art_crop" : "normal";
|
||||
String faceParam = "";
|
||||
if (cp.getRules().getOtherPart() != null) {
|
||||
faceParam = (face.equals("back") ? "&face=back" : "&face=front");
|
||||
if (!faceParam.isEmpty()) {
|
||||
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
|
||||
}
|
||||
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, cardCollectorNumber,
|
||||
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, collectorNumber,
|
||||
langCode, versionParam, faceParam);
|
||||
}
|
||||
|
||||
public static String getScryfallTokenDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam) {
|
||||
String versionParam = "normal";
|
||||
if (!faceParam.isEmpty()) {
|
||||
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
|
||||
}
|
||||
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, collectorNumber,
|
||||
langCode, versionParam, faceParam);
|
||||
}
|
||||
|
||||
|
||||
@@ -269,13 +269,20 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
|
||||
// need not set out-of-sync: either remove did set, or nothing was removed
|
||||
}
|
||||
|
||||
public void removeIf(Predicate<T> test) {
|
||||
for (final T item : items.keySet()) {
|
||||
if (test.test(item))
|
||||
remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
items.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
return (obj instanceof ItemPool) &&
|
||||
(this.items.equals(((ItemPool)obj).items));
|
||||
return (obj instanceof ItemPool ip) &&
|
||||
(this.items.equals(ip.items));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ package forge.game;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.MagicColor;
|
||||
@@ -337,9 +339,6 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
if (params.containsKey("Blessing")) {
|
||||
if ("True".equalsIgnoreCase(params.get("Blessing")) != hostController.hasBlessing()) return false;
|
||||
}
|
||||
if (params.containsKey("MaxSpeed")) {
|
||||
if ("True".equalsIgnoreCase(params.get("MaxSpeed")) != hostController.maxSpeed()) return false;
|
||||
}
|
||||
|
||||
if (params.containsKey("DayTime")) {
|
||||
if ("Day".equalsIgnoreCase(params.get("DayTime"))) {
|
||||
@@ -567,13 +566,23 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
return CardView.get(hostCard);
|
||||
}
|
||||
|
||||
protected IHasSVars getSVarFallback() {
|
||||
protected List<IHasSVars> getSVarFallback(final String name) {
|
||||
List<IHasSVars> result = Lists.newArrayList();
|
||||
|
||||
if (this.getKeyword() != null && this.getKeyword().getStatic() != null) {
|
||||
return this.getKeyword().getStatic();
|
||||
// only do when the keyword has part of the SVar in ins original string
|
||||
if (name == null || this.getKeyword().getOriginal().contains(name)) {
|
||||
// TODO try to add the keyword instead if possible?
|
||||
result.add(this.getKeyword().getStatic());
|
||||
}
|
||||
}
|
||||
if (getCardState() != null)
|
||||
return getCardState();
|
||||
return getHostCard();
|
||||
result.add(getCardState());
|
||||
result.add(getHostCard());
|
||||
return result;
|
||||
}
|
||||
protected Optional<IHasSVars> findSVar(final String name) {
|
||||
return getSVarFallback(name).stream().filter(f -> f.hasSVar(name)).findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -581,12 +590,12 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
if (sVars.containsKey(name)) {
|
||||
return sVars.get(name);
|
||||
}
|
||||
return getSVarFallback().getSVar(name);
|
||||
return findSVar(name).map(o -> o.getSVar(name)).orElse("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSVar(final String name) {
|
||||
return sVars.containsKey(name) || getSVarFallback().hasSVar(name);
|
||||
return sVars.containsKey(name) || findSVar(name).isPresent();
|
||||
}
|
||||
|
||||
public Integer getSVarInt(final String name) {
|
||||
@@ -601,22 +610,21 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setSVar(final String name, final String value) {
|
||||
public void setSVar(final String name, final String value) {
|
||||
sVars.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getSVars() {
|
||||
Map<String, String> res = Maps.newHashMap(getSVarFallback().getSVars());
|
||||
Map<String, String> res = Maps.newHashMap();
|
||||
// TODO reverse the order
|
||||
for (IHasSVars s : getSVarFallback(null)) {
|
||||
res.putAll(s.getSVars());
|
||||
}
|
||||
res.putAll(sVars);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDirectSVars() {
|
||||
return sVars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSVars(Map<String, String> newSVars) {
|
||||
sVars = Maps.newTreeMap();
|
||||
|
||||
@@ -122,23 +122,10 @@ public class ForgeScript {
|
||||
}
|
||||
}
|
||||
return found;
|
||||
} else if (property.equals("hasActivatedAbilityWithTapCost")) {
|
||||
} else if (property.startsWith("hasAbility")) {
|
||||
String valid = property.substring(11);
|
||||
for (final SpellAbility sa : cardState.getSpellAbilities()) {
|
||||
if (sa.isActivatedAbility() && sa.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (property.equals("hasActivatedAbility")) {
|
||||
for (final SpellAbility sa : cardState.getSpellAbilities()) {
|
||||
if (sa.isActivatedAbility()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (property.equals("hasOtherActivatedAbility")) {
|
||||
for (final SpellAbility sa : cardState.getSpellAbilities()) {
|
||||
if (sa.isActivatedAbility() && !sa.equals(spellAbility)) {
|
||||
if (sa.isValid(valid, sourceController, source, spellAbility)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -218,6 +205,8 @@ public class ForgeScript {
|
||||
return sa.isEternalize();
|
||||
} else if (property.equals("Flashback")) {
|
||||
return sa.isFlashback();
|
||||
} else if (property.equals("Harmonize")) {
|
||||
return sa.isHarmonize();
|
||||
} else if (property.equals("Jumpstart")) {
|
||||
return sa.isJumpstart();
|
||||
} else if (property.equals("Kicked")) {
|
||||
@@ -236,6 +225,8 @@ public class ForgeScript {
|
||||
return sa.isTurnFaceUp();
|
||||
} else if (property.equals("isCastFaceDown")) {
|
||||
return sa.isCastFaceDown();
|
||||
} else if (property.equals("Unearth")) {
|
||||
return sa.isKeyword(Keyword.UNEARTH);
|
||||
} else if (property.equals("Modular")) {
|
||||
return sa.isKeyword(Keyword.MODULAR);
|
||||
} else if (property.equals("Equip")) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.google.common.collect.HashBasedTable;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Table;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import forge.GameCommand;
|
||||
@@ -261,7 +262,6 @@ public class Game {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public void addPlayer(int id, Player player) {
|
||||
playerCache.put(id, player);
|
||||
}
|
||||
@@ -523,7 +523,7 @@ public class Game {
|
||||
* The Direction in which the turn order of this Game currently proceeds.
|
||||
*/
|
||||
public final Direction getTurnOrder() {
|
||||
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().getAmountOfKeyword("The turn order is reversed.") % 2 == 1) {
|
||||
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().isTurnOrderReversed()) {
|
||||
return turnOrder.getOtherDirection();
|
||||
}
|
||||
return turnOrder;
|
||||
@@ -958,9 +958,9 @@ public class Game {
|
||||
// if the player who lost was the Monarch, someone else will be the monarch
|
||||
// TODO need to check rules if it should try the next player if able
|
||||
if (p.equals(getPhaseHandler().getPlayerTurn())) {
|
||||
getAction().becomeMonarch(getNextPlayerAfter(p), null);
|
||||
getAction().becomeMonarch(getNextPlayerAfter(p), p.getMonarchSet());
|
||||
} else {
|
||||
getAction().becomeMonarch(getPhaseHandler().getPlayerTurn(), null);
|
||||
getAction().becomeMonarch(getPhaseHandler().getPlayerTurn(), p.getMonarchSet());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,9 +970,9 @@ public class Game {
|
||||
// If the player who has the initiative leaves the game on their own turn,
|
||||
// or the active player left the game at the same time, the next player in turn order takes the initiative.
|
||||
if (p.equals(getPhaseHandler().getPlayerTurn())) {
|
||||
getAction().takeInitiative(getNextPlayerAfter(p), null);
|
||||
getAction().takeInitiative(getNextPlayerAfter(p), p.getInitiativeSet());
|
||||
} else {
|
||||
getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), null);
|
||||
getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), p.getInitiativeSet());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1185,6 +1185,12 @@ public class Game {
|
||||
for (Player player : getRegisteredPlayers()) {
|
||||
player.onCleanupPhase();
|
||||
}
|
||||
for (final Card c : getCardsIncludePhasingIn(ZoneType.Battlefield)) {
|
||||
c.onCleanupPhase(getPhaseHandler().getPlayerTurn());
|
||||
}
|
||||
for (final Card card : getCardsInGame()) {
|
||||
card.resetActivationsPerTurn();
|
||||
}
|
||||
}
|
||||
|
||||
public void addCounterAddedThisTurn(Player putter, CounterType cType, Card card, Integer value) {
|
||||
@@ -1201,10 +1207,16 @@ public class Game {
|
||||
|
||||
public int getCounterAddedThisTurn(CounterType cType, String validPlayer, String validCard, Card source, Player sourceController, CardTraitBase ctb) {
|
||||
int result = 0;
|
||||
if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
Set<CounterType> types = null;
|
||||
if (cType == null) {
|
||||
types = countersAddedThisTurn.rowKeySet();
|
||||
} else if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
return result;
|
||||
} else {
|
||||
types = Sets.newHashSet(cType);
|
||||
}
|
||||
for (Map.Entry<Player, List<Pair<Card, Integer>>> e : countersAddedThisTurn.row(cType).entrySet()) {
|
||||
for (CounterType type : types) {
|
||||
for (Map.Entry<Player, List<Pair<Card, Integer>>> e : countersAddedThisTurn.row(type).entrySet()) {
|
||||
if (e.getKey().isValid(validPlayer.split(","), sourceController, source, ctb)) {
|
||||
for (Pair<Card, Integer> p : e.getValue()) {
|
||||
if (p.getKey().isValid(validCard.split(","), sourceController, source, ctb)) {
|
||||
@@ -1213,20 +1225,28 @@ public class Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public int getCounterAddedThisTurn(CounterType cType, Card card) {
|
||||
int result = 0;
|
||||
if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
Set<CounterType> types = null;
|
||||
if (cType == null) {
|
||||
types = countersAddedThisTurn.rowKeySet();
|
||||
} else if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
return result;
|
||||
} else {
|
||||
types = Sets.newHashSet(cType);
|
||||
}
|
||||
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(cType).values()) {
|
||||
for (CounterType type : types) {
|
||||
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(type).values()) {
|
||||
for (Pair<Card, Integer> p : l) {
|
||||
if (p.getKey().equalsWithGameTimestamp(card)) {
|
||||
result += p.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import forge.GameCommand;
|
||||
import forge.StaticData;
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.CardType.Supertype;
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.GamePieceType;
|
||||
import forge.card.MagicColor;
|
||||
import forge.deck.DeckSection;
|
||||
@@ -43,11 +44,10 @@ import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityPredicates;
|
||||
import forge.game.spellability.SpellPermanent;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityCantAttackBlock;
|
||||
import forge.game.staticability.StaticAbilityContinuous;
|
||||
import forge.game.staticability.StaticAbilityLayer;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.PlayerZone;
|
||||
import forge.game.zone.PlayerZoneBattlefield;
|
||||
@@ -82,12 +82,6 @@ public class GameAction {
|
||||
game = game0;
|
||||
}
|
||||
|
||||
public final void resetActivationsPerTurn() {
|
||||
for (final Card card : game.getCardsInGame()) {
|
||||
card.resetActivationsPerTurn();
|
||||
}
|
||||
}
|
||||
|
||||
public Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer position, SpellAbility cause) {
|
||||
return changeZone(zoneFrom, zoneTo, c, position, cause, null);
|
||||
}
|
||||
@@ -107,6 +101,8 @@ public class GameAction {
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// dev mode
|
||||
if (zoneFrom == null && !c.isToken()) {
|
||||
zoneTo.add(c, position, CardCopyService.getLKICopy(c));
|
||||
checkStaticAbilities();
|
||||
@@ -314,37 +310,34 @@ public class GameAction {
|
||||
c.getOwner().setCommanderReplacementSuppressed(true);
|
||||
}
|
||||
|
||||
// in addition to actual tokens, cards "made" by digital-only mechanics
|
||||
// are also added to inbound tokens so their etb replacements will work
|
||||
if (zoneFrom == null || zoneFrom.is(ZoneType.None)) {
|
||||
copied.getOwner().addInboundToken(copied);
|
||||
}
|
||||
|
||||
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(copied);
|
||||
repParams.put(AbilityKey.CardLKI, lastKnownInfo);
|
||||
repParams.put(AbilityKey.Cause, cause);
|
||||
repParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType() : null);
|
||||
repParams.put(AbilityKey.Destination, zoneTo.getZoneType());
|
||||
|
||||
if (toBattlefield) {
|
||||
repParams.put(AbilityKey.EffectOnly, true);
|
||||
repParams.put(AbilityKey.CounterTable, table);
|
||||
repParams.put(AbilityKey.CounterMap, table.column(copied));
|
||||
}
|
||||
|
||||
if (params != null) {
|
||||
repParams.putAll(params);
|
||||
}
|
||||
|
||||
// in addition to actual tokens, cards "made" by digital-only mechanics
|
||||
// are also added to inbound tokens so their etb replacements will work
|
||||
if (zoneFrom == null || zoneFrom.is(ZoneType.None)) {
|
||||
copied.getOwner().addInboundToken(copied);
|
||||
}
|
||||
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams);
|
||||
copied.getOwner().removeInboundToken(copied);
|
||||
|
||||
if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) {
|
||||
// reset failed manifested Cards back to original
|
||||
if ((c.isManifested() || c.isCloaked()) && !c.isInPlay()) {
|
||||
c.forceTurnFaceUp();
|
||||
}
|
||||
|
||||
copied.getOwner().removeInboundToken(copied);
|
||||
|
||||
if (repres == ReplacementResult.Prevented) {
|
||||
c.clearControllers();
|
||||
cleanStaticEffect(staticEff, copied);
|
||||
@@ -359,10 +352,6 @@ public class GameAction {
|
||||
if (c.isInZone(ZoneType.Stack) && !zoneTo.is(ZoneType.Graveyard)) {
|
||||
return moveToGraveyard(c, cause, params);
|
||||
}
|
||||
|
||||
copied.clearDevoured();
|
||||
copied.clearDelved();
|
||||
copied.clearExploited();
|
||||
} else if (toBattlefield && !c.isInPlay()) {
|
||||
// was replaced with another Zone Change
|
||||
if (c.removeChangedState()) {
|
||||
@@ -379,8 +368,6 @@ public class GameAction {
|
||||
copied.setGameTimestamp(game.getNextTimestamp());
|
||||
}
|
||||
|
||||
copied.getOwner().removeInboundToken(copied);
|
||||
|
||||
// Aura entering as Copy from stack
|
||||
// without targets it is sent to graveyard
|
||||
if (copied.isAura() && !copied.isAttachedToEntity() && toBattlefield) {
|
||||
@@ -399,7 +386,7 @@ public class GameAction {
|
||||
return moveToGraveyard(copied, cause, params);
|
||||
}
|
||||
}
|
||||
attachAuraOnIndirectEnterBattlefield(copied, params);
|
||||
attachAuraOnIndirectETB(copied, params);
|
||||
}
|
||||
|
||||
// Handle merged permanent here so all replacement effects are already applied.
|
||||
@@ -432,10 +419,6 @@ public class GameAction {
|
||||
}
|
||||
}
|
||||
|
||||
if (suppress) {
|
||||
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
|
||||
}
|
||||
|
||||
if (zoneFrom != null) {
|
||||
if (fromBattlefield && game.getCombat() != null) {
|
||||
if (!toBattlefield) {
|
||||
@@ -549,29 +532,25 @@ public class GameAction {
|
||||
// order here is important so it doesn't unattach cards that might have returned from UntilHostLeavesPlay
|
||||
unattachCardLeavingBattlefield(copied, c);
|
||||
c.runLeavesPlayCommands();
|
||||
|
||||
if (copied.isTapped()) {
|
||||
copied.setTapped(false); //untap card after it leaves the battlefield if needed
|
||||
game.fireEvent(new GameEventCardTapped(c, false));
|
||||
}
|
||||
}
|
||||
if (fromGraveyard) {
|
||||
game.addLeftGraveyardThisTurn(lastKnownInfo);
|
||||
}
|
||||
|
||||
// do ETB counters after zone add
|
||||
if (!suppress && toBattlefield && !table.isEmpty()) {
|
||||
game.getTriggerHandler().registerActiveTrigger(copied, false);
|
||||
}
|
||||
|
||||
if (c.hasChosenColorSpire()) {
|
||||
copied.setChosenColorID(ImmutableSet.copyOf(c.getChosenColorID()));
|
||||
if (c.hasMarkedColor()) {
|
||||
copied.setMarkedColors(c.getMarkedColors());
|
||||
}
|
||||
|
||||
copied.updateStateForView();
|
||||
|
||||
if (fromBattlefield) {
|
||||
copied.setDamage(0); //clear damage after a card leaves the battlefield
|
||||
copied.setHasBeenDealtDeathtouchDamage(false);
|
||||
if (copied.isTapped()) {
|
||||
copied.setTapped(false); //untap card after it leaves the battlefield if needed
|
||||
game.fireEvent(new GameEventCardTapped(c, false));
|
||||
}
|
||||
// needed for counters + ascend
|
||||
if (!suppress && toBattlefield) {
|
||||
game.getTriggerHandler().registerActiveTrigger(copied, false);
|
||||
}
|
||||
|
||||
if (!table.isEmpty()) {
|
||||
@@ -579,12 +558,12 @@ public class GameAction {
|
||||
game.getTriggerHandler().suppressMode(TriggerType.Always);
|
||||
// Need to apply any static effects to produce correct triggers
|
||||
checkStaticAbilities();
|
||||
// do ETB counters after zone add
|
||||
table.replaceCounterEffect(game, null, true, true, params);
|
||||
game.getTriggerHandler().clearSuppression(TriggerType.Always);
|
||||
}
|
||||
|
||||
table.replaceCounterEffect(game, null, true, true, params);
|
||||
|
||||
// update static abilities after etb counters have been placed
|
||||
game.getTriggerHandler().clearSuppression(TriggerType.Always);
|
||||
checkStaticAbilities();
|
||||
|
||||
// 400.7g try adding keyword back into card if it doesn't already have it
|
||||
@@ -607,12 +586,13 @@ public class GameAction {
|
||||
c.cleanupExiledWith();
|
||||
}
|
||||
|
||||
game.getTriggerHandler().clearActiveTriggers(copied, null);
|
||||
game.getTriggerHandler().registerActiveTrigger(copied, false);
|
||||
|
||||
// play the change zone sound
|
||||
game.fireEvent(new GameEventCardChangeZone(c, zoneFrom, zoneTo));
|
||||
|
||||
game.getTriggerHandler().clearActiveTriggers(copied, null);
|
||||
game.getTriggerHandler().registerActiveTrigger(copied, false);
|
||||
|
||||
if (!suppress) {
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(copied);
|
||||
runParams.put(AbilityKey.CardLKI, lastKnownInfo);
|
||||
runParams.put(AbilityKey.Cause, cause);
|
||||
@@ -620,12 +600,12 @@ public class GameAction {
|
||||
runParams.put(AbilityKey.Destination, zoneTo.getZoneType().name());
|
||||
runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek());
|
||||
runParams.put(AbilityKey.MergedCards, mergedCards);
|
||||
|
||||
if (params != null) {
|
||||
runParams.putAll(params);
|
||||
}
|
||||
|
||||
game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, true);
|
||||
}
|
||||
|
||||
if (fromBattlefield && !zoneFrom.getPlayer().equals(zoneTo.getPlayer())) {
|
||||
final Map<AbilityKey, Object> runParams2 = AbilityKey.mapFromCard(lastKnownInfo);
|
||||
runParams2.put(AbilityKey.OriginalController, zoneFrom.getPlayer());
|
||||
@@ -635,31 +615,18 @@ public class GameAction {
|
||||
game.getTriggerHandler().runTrigger(TriggerType.ChangesController, runParams2, false);
|
||||
}
|
||||
|
||||
if (suppress) {
|
||||
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
|
||||
}
|
||||
|
||||
if (zoneFrom == null) {
|
||||
return copied;
|
||||
}
|
||||
|
||||
if (!c.isRealToken() && !toBattlefield) {
|
||||
copied.clearDevoured();
|
||||
copied.clearDelved();
|
||||
copied.clearExploited();
|
||||
}
|
||||
|
||||
// rule 504.6: reveal a face-down card leaving the stack
|
||||
if (zoneFrom != null && zoneTo != null && zoneFrom.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield) && wasFacedown) {
|
||||
// CR 708.9 reveal face-down card leaving
|
||||
if (wasFacedown && (fromBattlefield || (zoneFrom.is(ZoneType.Stack) && !toBattlefield))) {
|
||||
Card revealLKI = CardCopyService.getLKICopy(c);
|
||||
revealLKI.forceTurnFaceUp();
|
||||
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card moves from the stack: ");
|
||||
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card leaves the " + zoneFrom.toString() + ": ");
|
||||
}
|
||||
|
||||
if (fromBattlefield) {
|
||||
if (!c.isRealToken() && !c.isSpecialized()) {
|
||||
copied.setState(CardStateName.Original, true);
|
||||
}
|
||||
// Soulbond unpairing
|
||||
if (c.isPaired()) {
|
||||
c.getPairedWith().setPairedWith(null);
|
||||
@@ -680,27 +647,12 @@ public class GameAction {
|
||||
}
|
||||
changeZone(null, zoneTo, unmeld, position, cause, params);
|
||||
}
|
||||
// Reveal if face-down
|
||||
if (wasFacedown) {
|
||||
Card revealLKI = CardCopyService.getLKICopy(c);
|
||||
revealLKI.forceTurnFaceUp();
|
||||
reveal(new CardCollection(revealLKI), revealLKI.getOwner(), true, "Face-down card leaves the battlefield: ");
|
||||
|
||||
copied.setState(CardStateName.Original, true);
|
||||
}
|
||||
} else if (toBattlefield) {
|
||||
for (Player p : game.getPlayers()) {
|
||||
copied.getDamageHistory().setNotAttackedSinceLastUpkeepOf(p);
|
||||
copied.getDamageHistory().setNotBlockedSinceLastUpkeepOf(p);
|
||||
copied.getDamageHistory().setNotBeenBlockedSinceLastUpkeepOf(p);
|
||||
}
|
||||
} else if (zoneTo.is(ZoneType.Graveyard)
|
||||
|| zoneTo.is(ZoneType.Hand)
|
||||
|| zoneTo.is(ZoneType.Library)
|
||||
|| zoneTo.is(ZoneType.Exile)) {
|
||||
if (copied.isFaceDown()) {
|
||||
copied.setState(CardStateName.Original, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Cards not on the battlefield / stack should not have controller
|
||||
@@ -748,14 +700,14 @@ public class GameAction {
|
||||
eff.setLayerTimestamp(timestamp);
|
||||
} else {
|
||||
// otherwise create effect first
|
||||
eff = SpellAbilityEffect.createEffect(cause, cause.getActivatingPlayer(), name, source.getImageKey(), timestamp);
|
||||
eff = SpellAbilityEffect.createEffect(cause, cause.getHostCard(), cause.getActivatingPlayer(), name, source.getImageKey(), timestamp);
|
||||
eff.setRenderForUI(false);
|
||||
StaticAbility stAb = eff.addStaticAbility(AbilityUtils.getSVar(cause, cause.getParam("StaticEffect")));
|
||||
stAb.setActiveZone(EnumSet.of(ZoneType.Command));
|
||||
// needed for ETB lookahead like Bronzehide Lion
|
||||
stAb.putParam("AffectedZone", "Battlefield,Hand,Graveyard,Exile,Stack,Library,Command");
|
||||
stAb.putParam("AffectedZone", "All");
|
||||
SpellAbilityEffect.addForgetOnMovedTrigger(eff, "Battlefield");
|
||||
game.getAction().moveToCommand(eff, cause);
|
||||
eff.getOwner().getZone(ZoneType.Command).add(eff);
|
||||
}
|
||||
|
||||
eff.addRemembered(copied);
|
||||
@@ -772,7 +724,6 @@ public class GameAction {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void storeChangesZoneAll(Card c, Zone zoneFrom, Zone zoneTo, Map<AbilityKey, Object> params) {
|
||||
if (params != null && params.containsKey(AbilityKey.InternalTriggerTable)) {
|
||||
((CardZoneTable) params.get(AbilityKey.InternalTriggerTable)).put(zoneFrom != null ? zoneFrom.getZoneType() : null, zoneTo.getZoneType(), c);
|
||||
@@ -846,7 +797,7 @@ public class GameAction {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stAb.checkMode("CantBlockBy")) {
|
||||
if (stAb.checkMode(StaticAbilityMode.CantBlockBy)) {
|
||||
if (!stAb.hasParam("ValidAttacker") || (stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
|
||||
continue;
|
||||
}
|
||||
@@ -856,7 +807,7 @@ public class GameAction {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stAb.checkMode(StaticAbilityCantAttackBlock.MinMaxBlockerMode)) {
|
||||
if (stAb.checkMode(StaticAbilityMode.MinMaxBlocker)) {
|
||||
for (Card creature : IterableUtil.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.CREATURES)) {
|
||||
if (stAb.matchesValidParam("ValidCard", creature)) {
|
||||
creature.updateAbilityTextForView();
|
||||
@@ -982,6 +933,10 @@ public class GameAction {
|
||||
final PlayerZone removed = c.getOwner().getZone(ZoneType.Exile);
|
||||
final Card copied = moveTo(removed, c, cause, params);
|
||||
|
||||
if (c.isImmutable()) {
|
||||
return copied;
|
||||
}
|
||||
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c);
|
||||
runParams.put(AbilityKey.Cause, cause);
|
||||
if (origin != null) { // is generally null when adding via dev mode
|
||||
@@ -1038,7 +993,8 @@ public class GameAction {
|
||||
lki = CardCopyService.getLKICopy(c);
|
||||
}
|
||||
game.addChangeZoneLKIInfo(lki);
|
||||
if (lki.isInPlay()) {
|
||||
// CR 702.26k
|
||||
if (lki.isInPlay() && !lki.isPhasedOut()) {
|
||||
if (game.getCombat() != null) {
|
||||
game.getCombat().saveLKI(lki);
|
||||
game.getCombat().removeFromCombat(c);
|
||||
@@ -1117,7 +1073,7 @@ public class GameAction {
|
||||
public boolean hasStaticAbilityAffectingZone(ZoneType zone, StaticAbilityLayer layer) {
|
||||
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
|
||||
for (final StaticAbility stAb : ca.getStaticAbilities()) {
|
||||
if (!stAb.checkConditions("Continuous")) {
|
||||
if (!stAb.checkConditions(StaticAbilityMode.Continuous)) {
|
||||
continue;
|
||||
}
|
||||
if (layer != null && !stAb.getLayers().contains(layer)) {
|
||||
@@ -1149,13 +1105,13 @@ public class GameAction {
|
||||
// remove old effects
|
||||
game.getStaticEffects().clearStaticEffects(affectedCards);
|
||||
|
||||
for (final Player p : game.getPlayers()) {
|
||||
p.clearStaticAbilities();
|
||||
}
|
||||
|
||||
// search for cards with static abilities
|
||||
final FCollection<StaticAbility> staticAbilities = new FCollection<>();
|
||||
final CardCollection staticList = new CardCollection();
|
||||
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies = null;
|
||||
if (preList.isEmpty()) {
|
||||
dependencies = HashBasedTable.create();
|
||||
}
|
||||
|
||||
game.forEachCardInGame(new Visitor<Card>() {
|
||||
@Override
|
||||
@@ -1163,7 +1119,7 @@ public class GameAction {
|
||||
// need to get Card from preList if able
|
||||
final Card co = preList.get(c);
|
||||
for (StaticAbility stAb : co.getStaticAbilities()) {
|
||||
if (stAb.checkMode("Continuous")) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.zonesCheck()) {
|
||||
staticAbilities.add(stAb);
|
||||
}
|
||||
}
|
||||
@@ -1190,7 +1146,7 @@ public class GameAction {
|
||||
StaticAbility stAb = staticsForLayer.get(0);
|
||||
// dependency with CDA seems unlikely
|
||||
if (!stAb.isCharacteristicDefining()) {
|
||||
stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility);
|
||||
stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility, dependencies);
|
||||
}
|
||||
staticsForLayer.remove(stAb);
|
||||
final CardCollectionView previouslyAffected = affectedPerAbility.get(stAb);
|
||||
@@ -1210,7 +1166,7 @@ public class GameAction {
|
||||
if (affectedHere != null) {
|
||||
for (final Card c : affectedHere) {
|
||||
for (final StaticAbility st2 : c.getStaticAbilities()) {
|
||||
if (!staticAbilities.contains(st2)) {
|
||||
if (!staticAbilities.contains(st2) && st2.checkMode(StaticAbilityMode.Continuous) && st2.zonesCheck()) {
|
||||
toAdd.add(st2);
|
||||
st2.applyContinuousAbilityBefore(layer, preList);
|
||||
}
|
||||
@@ -1277,14 +1233,16 @@ public class GameAction {
|
||||
game.getTriggerHandler().runTrigger(TriggerType.Always, runParams, false);
|
||||
|
||||
game.getTriggerHandler().runTrigger(TriggerType.Immediate, runParams, false);
|
||||
|
||||
game.getView().setDependencies(dependencies);
|
||||
}
|
||||
|
||||
// Update P/T and type in the view only once after all the cards have been processed, to avoid flickering
|
||||
for (Card c : affectedCards) {
|
||||
c.updateNameforView();
|
||||
c.updatePowerToughnessForView();
|
||||
c.updatePTforView();
|
||||
c.updateTypesForView();
|
||||
c.updateAbilityTextForView(); // only update keywords and text for view to avoid flickering
|
||||
c.updateKeywords();
|
||||
}
|
||||
|
||||
// TODO filter out old copies from zone change
|
||||
@@ -1295,7 +1253,8 @@ public class GameAction {
|
||||
game.getTracker().unfreeze();
|
||||
}
|
||||
|
||||
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility) {
|
||||
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility,
|
||||
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
|
||||
if (staticsForLayer.size() == 1) {
|
||||
return staticsForLayer.get(0);
|
||||
}
|
||||
@@ -1309,12 +1268,11 @@ public class GameAction {
|
||||
dependencyGraph.addVertex(stAb);
|
||||
|
||||
boolean exists = stAb.getHostCard().getStaticAbilities().contains(stAb);
|
||||
boolean compareAffected = true;
|
||||
boolean compareAffected = false;
|
||||
CardCollectionView affectedHere = affectedPerAbility.get(stAb);
|
||||
if (affectedHere == null) {
|
||||
affectedHere = StaticAbilityContinuous.getAffectedCards(stAb, preList);
|
||||
} else {
|
||||
compareAffected = false;
|
||||
compareAffected = true;
|
||||
}
|
||||
List<Object> effectResults = generateStaticAbilityResult(layer, stAb);
|
||||
|
||||
@@ -1342,21 +1300,24 @@ public class GameAction {
|
||||
// ...what it applies to...
|
||||
if (!dependency && compareAffected) {
|
||||
CardCollectionView affectedAfterOther = StaticAbilityContinuous.getAffectedCards(stAb, preList);
|
||||
if (!Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator())) {
|
||||
dependency = true;
|
||||
}
|
||||
dependency = !Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator());
|
||||
}
|
||||
// ...or what it does to any of the things it applies to
|
||||
if (!dependency) {
|
||||
List<Object> effectResultsAfterOther = generateStaticAbilityResult(layer, stAb);
|
||||
if (!effectResults.equals(effectResultsAfterOther)) {
|
||||
dependency = true;
|
||||
}
|
||||
dependency = !effectResults.equals(effectResultsAfterOther);
|
||||
}
|
||||
|
||||
if (dependency) {
|
||||
dependencyGraph.addVertex(otherStAb);
|
||||
dependencyGraph.addEdge(stAb, otherStAb);
|
||||
if (dependencies != null) {
|
||||
if (dependencies.contains(stAb, otherStAb)) {
|
||||
dependencies.get(stAb, otherStAb).add(layer);
|
||||
} else {
|
||||
dependencies.put(stAb, otherStAb, EnumSet.of(layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// undo changes and check next pair
|
||||
@@ -1389,7 +1350,7 @@ public class GameAction {
|
||||
}
|
||||
dependencyGraph.removeAllVertices(toRemove);
|
||||
|
||||
// now the earlist one left is the correct choice
|
||||
// now the earliest one left is the correct choice
|
||||
List<StaticAbility> statics = Lists.newArrayList(dependencyGraph.vertexSet());
|
||||
statics.sort(Comparator.comparing(s -> s.getHostCard().getLayerTimestamp()));
|
||||
|
||||
@@ -1502,17 +1463,12 @@ public class GameAction {
|
||||
checkAgainCard |= stateBasedAction_Saga(c, sacrificeList);
|
||||
checkAgainCard |= stateBasedAction_Battle(c, noRegCreats);
|
||||
checkAgainCard |= stateBasedAction_Role(c, unAttachList);
|
||||
checkAgainCard |= stateBasedAction704_attach(c, unAttachList); // Attachment
|
||||
checkAgainCard |= stateBasedAction704_attach(c, unAttachList);
|
||||
checkAgainCard |= stateBasedAction_Contraption(c, noRegCreats);
|
||||
|
||||
checkAgainCard |= stateBasedAction704_5r(c); // annihilate +1/+1 counters with -1/-1 ones
|
||||
checkAgainCard |= stateBasedAction704_5q(c); // annihilate +1/+1 counters with -1/-1 ones
|
||||
|
||||
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
|
||||
|
||||
if (c.getCounters(dreamType) > 7 && c.hasKeyword("CARDNAME can't have more than seven dream counters on it.")) {
|
||||
c.subtractCounter(dreamType, c.getCounters(dreamType) - 7, null);
|
||||
checkAgainCard = true;
|
||||
}
|
||||
checkAgainCard |= stateBasedAction704_5r(c);
|
||||
|
||||
if (c.hasKeyword("The number of loyalty counters on CARDNAME is equal to the number of Beebles you control.")) {
|
||||
int beeble = CardLists.getValidCardCount(game.getCardsIn(ZoneType.Battlefield), "Beeble.YouCtrl", c.getController(), c, null);
|
||||
@@ -1554,9 +1510,7 @@ public class GameAction {
|
||||
if (!spaceSculptors.isEmpty() && !spaceSculptors.contains(p)) {
|
||||
checkAgain |= stateBasedAction704_5u(p);
|
||||
}
|
||||
if (handleLegendRule(p, noRegCreats)) {
|
||||
checkAgain = true;
|
||||
}
|
||||
checkAgain |= handleLegendRule(p, noRegCreats);
|
||||
|
||||
if ((game.getRules().hasAppliedVariant(GameType.Commander)
|
||||
|| game.getRules().hasAppliedVariant(GameType.Brawl)
|
||||
@@ -1569,15 +1523,21 @@ public class GameAction {
|
||||
}
|
||||
}
|
||||
|
||||
if (handlePlaneswalkerRule(p, noRegCreats)) {
|
||||
// 704.5z If a player controls a permanent with start your engines! and that player has no speed, that player’s speed becomes 1.
|
||||
if (p.getSpeed() == 0 && p.getCardsIn(ZoneType.Battlefield).anyMatch(c -> c.hasKeyword(Keyword.START_YOUR_ENGINES))) {
|
||||
p.increaseSpeed();
|
||||
checkAgain = true;
|
||||
}
|
||||
|
||||
checkAgain |= handlePlaneswalkerRule(p, noRegCreats);
|
||||
}
|
||||
for (Player p : spaceSculptors) {
|
||||
checkAgain |= stateBasedAction704_5u(p);
|
||||
}
|
||||
|
||||
// 704.5m World rule
|
||||
checkAgain |= handleWorldRule(noRegCreats);
|
||||
|
||||
// only check static abilities once after destroying all the creatures
|
||||
// (e.g. helpful for Erebos's Titan and another creature dealing lethal damage to each other simultaneously)
|
||||
setHoldCheckingStaticAbilities(true);
|
||||
@@ -1609,6 +1569,7 @@ public class GameAction {
|
||||
orderedSacrificeList = true;
|
||||
}
|
||||
sacrifice(sacrificeList, null, true, mapParams);
|
||||
|
||||
setHoldCheckingStaticAbilities(false);
|
||||
|
||||
table.triggerChangesZoneAll(game, null);
|
||||
@@ -1667,7 +1628,7 @@ public class GameAction {
|
||||
|
||||
private boolean stateBasedAction_Saga(Card c, CardCollection sacrificeList) {
|
||||
boolean checkAgain = false;
|
||||
if (!c.isSaga()) {
|
||||
if (!c.isSaga() || !c.hasChapter()) {
|
||||
return false;
|
||||
}
|
||||
// needs to be effect, because otherwise it might be a cost?
|
||||
@@ -1686,11 +1647,40 @@ public class GameAction {
|
||||
|
||||
private boolean stateBasedAction_Battle(Card c, CardCollection removeList) {
|
||||
boolean checkAgain = false;
|
||||
if (!c.getType().isBattle()) {
|
||||
return false;
|
||||
if (!c.isBattle()) {
|
||||
return checkAgain;
|
||||
}
|
||||
Player battleController = c.getController();
|
||||
Player battleProtector = c.getProtectingPlayer();
|
||||
/*
|
||||
704.5w If a battle has no player in the game designated as its protector and no attacking creatures are currently
|
||||
attacking that battle, that battle’s controller chooses an appropriate player to be its protector based on its
|
||||
battle type. If no player can be chosen this way, the battle is put into its owner’s graveyard.
|
||||
|
||||
704.5x If a Siege’s controller is also its designated protector, that player chooses an opponent to become its
|
||||
protector. If no player can be chosen this way, the battle is put into its owner’s graveyard.
|
||||
*/
|
||||
if (((battleProtector == null || !battleProtector.isInGame()) &&
|
||||
(game.getCombat() == null || game.getCombat().getAttackersOf(c).isEmpty())) ||
|
||||
(c.getType().hasStringType("Siege") && battleController.equals(battleProtector))) {
|
||||
Player newProtector;
|
||||
if (c.getType().getBattleTypes().contains("Siege"))
|
||||
newProtector = battleController.getController().chooseSingleEntityForEffect(battleController.getOpponents(), new SpellAbility.EmptySa(ApiType.ChoosePlayer, c), "Choose an opponent to protect this battle", null);
|
||||
else {
|
||||
// Fall back to the controller. Technically should fall back to null per the above rules, but no official
|
||||
// cards should use this branch. For now this better supports custom cards. May need to revise this later.
|
||||
newProtector = battleController;
|
||||
}
|
||||
// seems unlikely unless range of influence gets implemented
|
||||
if (newProtector == null) {
|
||||
removeList.add(c);
|
||||
} else {
|
||||
c.setProtectingPlayer(newProtector);
|
||||
}
|
||||
checkAgain = true;
|
||||
}
|
||||
if (c.getCounters(CounterEnumType.DEFENSE) > 0) {
|
||||
return false;
|
||||
return checkAgain;
|
||||
}
|
||||
// 704.5v If a battle has defense 0 and it isn't the source of an ability that has triggered but not yet left the stack,
|
||||
// it’s put into its owner’s graveyard.
|
||||
@@ -1766,12 +1756,12 @@ public class GameAction {
|
||||
}
|
||||
|
||||
private boolean stateBasedAction_Contraption(Card c, CardCollection removeList) {
|
||||
if(!c.isContraption())
|
||||
if (!c.isContraption())
|
||||
return false;
|
||||
int currentSprocket = c.getSprocket();
|
||||
|
||||
//A contraption that is in the battlefield without being assembled is put into the graveyard or junkyard.
|
||||
if(currentSprocket == 0) {
|
||||
if (currentSprocket == 0) {
|
||||
removeList.add(c);
|
||||
return true;
|
||||
}
|
||||
@@ -1780,7 +1770,7 @@ public class GameAction {
|
||||
//A reassemble effect can handle that on its own. But if it changed controller due to some other effect,
|
||||
//we assign it here. A contraption uses sprocket -1 to signify it has been assembled previously but now needs
|
||||
//a new sprocket.
|
||||
if(currentSprocket > 0 && currentSprocket <= 3)
|
||||
if (currentSprocket > 0 && currentSprocket <= 3)
|
||||
return false;
|
||||
|
||||
int sprocket = c.getController().getController().chooseSprocket(c);
|
||||
@@ -1832,13 +1822,17 @@ public class GameAction {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean stateBasedAction704_5r(Card c) {
|
||||
private boolean stateBasedAction704_5q(Card c) {
|
||||
boolean checkAgain = false;
|
||||
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
|
||||
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
|
||||
int plusOneCounters = c.getCounters(p1p1);
|
||||
int minusOneCounters = c.getCounters(m1m1);
|
||||
if (plusOneCounters > 0 && minusOneCounters > 0) {
|
||||
if (!c.canRemoveCounters(p1p1) || !c.canRemoveCounters(m1m1)) {
|
||||
return checkAgain;
|
||||
}
|
||||
|
||||
int remove = Math.min(plusOneCounters, minusOneCounters);
|
||||
// If a permanent has both a +1/+1 counter and a -1/-1 counter on it,
|
||||
// N +1/+1 and N -1/-1 counters are removed from it, where N is the
|
||||
@@ -1850,6 +1844,26 @@ public class GameAction {
|
||||
}
|
||||
return checkAgain;
|
||||
}
|
||||
private boolean stateBasedAction704_5r(Card c) {
|
||||
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
|
||||
|
||||
int old = c.getCounters(dreamType);
|
||||
if (old <= 0) {
|
||||
return false;
|
||||
}
|
||||
Integer max = c.getCounterMax(dreamType);
|
||||
if (max == null) {
|
||||
return false;
|
||||
}
|
||||
if (old > max) {
|
||||
if (!c.canRemoveCounters(dreamType)) {
|
||||
return false;
|
||||
}
|
||||
c.subtractCounter(dreamType, old - max, null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a token is in a zone other than the battlefield, it ceases to exist.
|
||||
private boolean stateBasedAction704_5d(Card c) {
|
||||
@@ -1872,19 +1886,10 @@ public class GameAction {
|
||||
|
||||
public void checkGameOverCondition() {
|
||||
// award loses as SBE
|
||||
List<Player> losers = null;
|
||||
|
||||
FCollectionView<Player> allPlayers = game.getPlayers();
|
||||
for (Player p : allPlayers) {
|
||||
if (p.checkLoseCondition()) { // this will set appropriate outcomes
|
||||
if (losers == null) {
|
||||
losers = Lists.newArrayListWithCapacity(3);
|
||||
}
|
||||
losers.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
GameEndReason reason = null;
|
||||
List<Player> losers = null;
|
||||
FCollectionView<Player> allPlayers = game.getPlayers();
|
||||
|
||||
// Has anyone won by spelleffect?
|
||||
for (Player p : allPlayers) {
|
||||
if (!p.hasWon()) {
|
||||
@@ -1910,24 +1915,17 @@ public class GameAction {
|
||||
break;
|
||||
}
|
||||
|
||||
// loop through all the non-losing players that can't win
|
||||
// see if all of their opponents are in that "about to lose" collection
|
||||
if (losers != null) {
|
||||
if (reason == null) {
|
||||
for (Player p : allPlayers) {
|
||||
if (losers.contains(p)) {
|
||||
continue;
|
||||
if (p.checkLoseCondition()) { // this will set appropriate outcomes
|
||||
if (losers == null) {
|
||||
losers = Lists.newArrayListWithCapacity(3);
|
||||
}
|
||||
losers.add(p);
|
||||
}
|
||||
if (p.cantWin()) {
|
||||
if (losers.containsAll(p.getOpponents())) {
|
||||
// what to do here?!?!?!
|
||||
System.err.println(p.toString() + " is about to win, but can't!");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// need a separate loop here, otherwise ConcurrentModificationException is raised
|
||||
if (losers != null) {
|
||||
for (Player p : losers) {
|
||||
game.onPlayerLost(p);
|
||||
@@ -2040,7 +2038,7 @@ public class GameAction {
|
||||
}
|
||||
|
||||
private boolean handleWorldRule(CardCollection noRegCreats) {
|
||||
final List<Card> worlds = CardLists.getType(game.getCardsIn(ZoneType.Battlefield), "World");
|
||||
final List<Card> worlds = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), c -> c.getType().hasSupertype(Supertype.World));
|
||||
if (worlds.size() <= 1) {
|
||||
return false;
|
||||
}
|
||||
@@ -2049,7 +2047,7 @@ public class GameAction {
|
||||
long ts = 0;
|
||||
|
||||
for (final Card crd : worlds) {
|
||||
long crdTs = crd.getGameTimestamp();
|
||||
long crdTs = crd.getWorldTimestamp();
|
||||
if (crdTs > ts) {
|
||||
ts = crdTs;
|
||||
toKeep.clear();
|
||||
@@ -2430,15 +2428,14 @@ public class GameAction {
|
||||
for (Card c : spires) {
|
||||
// TODO: only do this for the AI, for the player part, get the encoded color from the deck file and pass
|
||||
// it to either player or the papercard object so it feels like rule based for the player side..
|
||||
if (!c.hasChosenColorSpire()) {
|
||||
if (!c.hasMarkedColor()) {
|
||||
if (takesAction.isAI()) {
|
||||
List<String> colorChoices = new ArrayList<>(MagicColor.Constant.ONLY_COLORS);
|
||||
String prompt = CardTranslation.getTranslatedName(c.getName()) + ": " +
|
||||
Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(2));
|
||||
SpellAbility sa = new SpellAbility.EmptySa(ApiType.ChooseColor, c, takesAction);
|
||||
sa.putParam("AILogic", "MostProminentInComputerDeck");
|
||||
Set<String> chosenColors = new HashSet<>(takesAction.getController().chooseColors(prompt, sa, 2, 2, colorChoices));
|
||||
c.setChosenColorID(chosenColors);
|
||||
ColorSet chosenColors = ColorSet.fromNames(takesAction.getController().chooseColors(prompt, sa, 2, 2, MagicColor.Constant.ONLY_COLORS));
|
||||
c.setMarkedColors(chosenColors);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2684,8 +2681,8 @@ public class GameAction {
|
||||
if (isCombat) {
|
||||
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {
|
||||
final GameEntity ge = et.getKey();
|
||||
if (ge instanceof Card) {
|
||||
((Card) ge).clearAssignedDamage();
|
||||
if (ge instanceof Card c) {
|
||||
c.clearAssignedDamage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2705,8 +2702,7 @@ public class GameAction {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.getKey() instanceof Card && !lethalDamage.containsKey(e.getKey())) {
|
||||
Card c = (Card) e.getKey();
|
||||
if (e.getKey() instanceof Card c && !lethalDamage.containsKey(c)) {
|
||||
lethalDamage.put(c, c.getExcessDamageValue(false));
|
||||
}
|
||||
|
||||
@@ -2788,21 +2784,35 @@ public class GameAction {
|
||||
* the source
|
||||
* @return true, if successful
|
||||
*/
|
||||
public static boolean attachAuraOnIndirectEnterBattlefield(final Card source, Map<AbilityKey, Object> params) {
|
||||
// When an Aura ETB without being cast you can choose a valid card to
|
||||
// attach it to
|
||||
final SpellAbility aura = source.getFirstAttachSpell();
|
||||
private boolean attachAuraOnIndirectETB(final Card source, Map<AbilityKey, Object> params) {
|
||||
// When an Aura ETB without being cast you can choose a valid card to attach it to
|
||||
if (!source.hasKeyword(Keyword.ENCHANT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SpellAbility aura = source.getCurrentState().getAuraSpell();
|
||||
if (aura == null) {
|
||||
return false;
|
||||
}
|
||||
aura.setActivatingPlayer(source.getController());
|
||||
final Game game = source.getGame();
|
||||
final TargetRestrictions tgt = aura.getTargetRestrictions();
|
||||
|
||||
Set<ZoneType> zones = EnumSet.noneOf(ZoneType.class);
|
||||
boolean canTargetPlayer = false;
|
||||
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
|
||||
String o = ki.getOriginal();
|
||||
String m[] = o.split(":");
|
||||
String v = m[1];
|
||||
if (v.contains("inZone")) { // currently the only other zone is Graveyard
|
||||
zones.add(ZoneType.Graveyard);
|
||||
} else {
|
||||
zones.add(ZoneType.Battlefield);
|
||||
}
|
||||
if (v.startsWith("Player") || v.startsWith("Opponent")) {
|
||||
canTargetPlayer = true;
|
||||
}
|
||||
}
|
||||
Player p = source.getController();
|
||||
if (tgt.canTgtPlayer()) {
|
||||
final FCollection<Player> players = game.getPlayers().filter(PlayerPredicates.canBeAttached(source, aura));
|
||||
if (canTargetPlayer) {
|
||||
final FCollection<Player> players = game.getPlayers().filter(PlayerPredicates.canBeAttached(source, null));
|
||||
|
||||
final Player pa = p.getController().chooseSingleEntityForEffect(players, aura,
|
||||
Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", CardTranslation.getTranslatedName(source.getName())), null);
|
||||
@@ -2811,9 +2821,7 @@ public class GameAction {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
List<ZoneType> zones = Lists.newArrayList(tgt.getZone());
|
||||
CardCollection list = new CardCollection();
|
||||
|
||||
if (params != null) {
|
||||
if (zones.contains(ZoneType.Battlefield)) {
|
||||
list.addAll((CardCollectionView) params.get(AbilityKey.LastStateBattlefield));
|
||||
@@ -2826,7 +2834,7 @@ public class GameAction {
|
||||
}
|
||||
list.addAll(game.getCardsIn(zones));
|
||||
|
||||
list = CardLists.filter(list, CardPredicates.canBeAttached(source, aura));
|
||||
list = CardLists.filter(list, CardPredicates.canBeAttached(source, null));
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import forge.game.spellability.*;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityAlternativeCost;
|
||||
import forge.game.staticability.StaticAbilityLayer;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.zone.Zone;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
@@ -183,6 +184,34 @@ public final class GameActionUtil {
|
||||
flashback.setKeyword(inst);
|
||||
flashback.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(flashback);
|
||||
} else if (keyword.startsWith("Harmonize")) {
|
||||
if (!source.isInZone(ZoneType.Graveyard)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.equals("Harmonize") && source.getManaCost().isNoCost()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SpellAbility harmonize = null;
|
||||
|
||||
if (keyword.contains(":")) {
|
||||
final String[] k = keyword.split(":");
|
||||
harmonize = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
|
||||
String extraParams = k.length > 2 ? k[2] : "";
|
||||
if (!extraParams.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
|
||||
harmonize.putParam(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
harmonize = sa.copy(activator);
|
||||
}
|
||||
harmonize.setAlternativeCost(AlternativeCost.Harmonize);
|
||||
harmonize.getRestrictions().setZone(ZoneType.Graveyard);
|
||||
harmonize.setKeyword(inst);
|
||||
harmonize.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(harmonize);
|
||||
} else if (keyword.startsWith("Foretell")) {
|
||||
// Foretell cast only from Exile
|
||||
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() ||
|
||||
@@ -390,7 +419,7 @@ public final class GameActionUtil {
|
||||
costSources.addAll(game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES));
|
||||
for (final Card ca : costSources) {
|
||||
for (final StaticAbility stAb : ca.getStaticAbilities()) {
|
||||
if (!stAb.checkConditions("OptionalCost")) {
|
||||
if (!stAb.checkConditions(StaticAbilityMode.OptionalCost)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -582,9 +611,8 @@ public final class GameActionUtil {
|
||||
" or greater>";
|
||||
final Cost cost = new Cost(casualtyCost, false);
|
||||
String str = "Pay for Casualty? " + cost.toSimpleString();
|
||||
boolean v = pc.addKeywordCost(sa, cost, ki, str);
|
||||
|
||||
if (v) {
|
||||
if (pc.addKeywordCost(sa, cost, ki, str)) {
|
||||
if (result == null) {
|
||||
result = sa.copy();
|
||||
}
|
||||
@@ -630,9 +658,7 @@ public final class GameActionUtil {
|
||||
final Cost cost = new Cost(k[1], false);
|
||||
String str = "Pay for Offspring? " + cost.toSimpleString();
|
||||
|
||||
boolean v = pc.addKeywordCost(sa, cost, ki, str);
|
||||
|
||||
if (v) {
|
||||
if (pc.addKeywordCost(sa, cost, ki, str)) {
|
||||
if (result == null) {
|
||||
result = sa.copy();
|
||||
}
|
||||
@@ -679,6 +705,25 @@ public final class GameActionUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isHarmonize()) {
|
||||
CardCollectionView creatures = activator.getCreaturesInPlay();
|
||||
if (!creatures.isEmpty()) {
|
||||
int max = Aggregates.max(creatures, Card::getNetPower);
|
||||
int n = pc.chooseNumber(sa, "Choose power of creature to tap", 0, max);
|
||||
final String harmonizeCost = "tapXType<1/Creature.powerEQ" + n + "/creature for Harmonize>";
|
||||
final Cost cost = new Cost(harmonizeCost, false);
|
||||
|
||||
if (pc.addKeywordCost(sa, cost, sa.getKeyword(), "Tap creature?")) {
|
||||
if (result == null) {
|
||||
result = sa.copy();
|
||||
}
|
||||
result.getPayCosts().add(cost);
|
||||
reset = true;
|
||||
result.setOptionalKeywordAmount(sa.getKeyword(), n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (host.isCreature()) {
|
||||
String kw = "As an additional cost to cast creature spells," +
|
||||
" you may pay any amount of mana. If you do, that creature enters " +
|
||||
@@ -902,14 +947,16 @@ public final class GameActionUtil {
|
||||
}
|
||||
|
||||
if (fromZone != null && !fromZone.is(ZoneType.None)) { // and not a copy
|
||||
// add back to where it came from, hopefully old state
|
||||
// skip GameAction
|
||||
oldCard.getZone().remove(oldCard);
|
||||
|
||||
// might have been an alternative lki host
|
||||
oldCard = ability.getCardState().getCard();
|
||||
|
||||
oldCard.setCastSA(null);
|
||||
oldCard.setCastFrom(null);
|
||||
// add back to where it came from, hopefully old state
|
||||
// skip GameAction
|
||||
oldCard.getZone().remove(oldCard);
|
||||
|
||||
// in some rare cases the old position no longer exists (Panglacial Wurm + Selvala)
|
||||
Integer newPosition = zonePosition >= 0 ? Math.min(zonePosition, fromZone.size()) : null;
|
||||
fromZone.add(oldCard, newPosition, null, true);
|
||||
|
||||
@@ -23,15 +23,12 @@ package forge.game;
|
||||
public enum GameEndReason {
|
||||
/** The All opponents lost. */
|
||||
AllOpponentsLost,
|
||||
// Noone won
|
||||
/** The Draw. */
|
||||
Draw, // Having little idea how they can reach a draw, so I didn't enumerate
|
||||
// possible reasons here
|
||||
// Special conditions, they force one player to win and thus end the game
|
||||
|
||||
/** The Wins game spell effect. */
|
||||
WinsGameSpellEffect, // ones that could be both hardcoded (felidar) and
|
||||
// scripted ( such as Mayael's Aria )
|
||||
/** Noone won */
|
||||
Draw,
|
||||
|
||||
/** Special conditions, they force one player to win and thus end the game */
|
||||
WinsGameSpellEffect,
|
||||
|
||||
/** Used to end multiplayer games where the all humans have lost or conceded while AIs cannot end match by themselves.*/
|
||||
AllHumansLost,
|
||||
|
||||
@@ -37,11 +37,11 @@ import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.event.GameEventCardAttachment;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityCantAttach;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
@@ -267,15 +267,18 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
}
|
||||
|
||||
protected boolean canBeEnchantedBy(final Card aura) {
|
||||
// TODO need to check for multiple Enchant Keywords
|
||||
|
||||
SpellAbility sa = aura.getFirstAttachSpell();
|
||||
TargetRestrictions tgt = null;
|
||||
if (sa != null) {
|
||||
tgt = sa.getTargetRestrictions();
|
||||
if (!aura.hasKeyword(Keyword.ENCHANT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return tgt != null && isValid(tgt.getValidTgts(), aura.getController(), aura, sa);
|
||||
for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) {
|
||||
String k = ki.getOriginal();
|
||||
String m[] = k.split(":");
|
||||
String v = m[1];
|
||||
if (!isValid(v.split(","), aura.getController(), aura, null)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean hasCounters() {
|
||||
@@ -318,11 +321,20 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
return canReceiveCounters(CounterType.get(type));
|
||||
}
|
||||
|
||||
public final void addCounter(final CounterType counterType, final int n, final Player source, GameEntityCounterTable table) {
|
||||
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
|
||||
if (n <= 0 || !canReceiveCounters(counterType)) {
|
||||
// As per rule 107.1b
|
||||
return;
|
||||
}
|
||||
|
||||
Integer max = getCounterMax(counterType);
|
||||
if (max != null) {
|
||||
n = Math.min(n, max - getCounters(counterType));
|
||||
if (n <= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// doesn't really add counters, but is just a helper to add them to the Table
|
||||
// so the Table can handle the Replacement Effect
|
||||
table.put(source, this, counterType, n);
|
||||
@@ -340,6 +352,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
|
||||
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params);
|
||||
}
|
||||
public Integer getCounterMax(final CounterType counterType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Pair<Integer, Boolean>> getDamageReceivedThisTurn() {
|
||||
return damageReceivedThisTurn;
|
||||
|
||||
@@ -159,12 +159,17 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
|
||||
}
|
||||
|
||||
// Add ETB flag
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
runParams.put(AbilityKey.Cause, cause);
|
||||
if (params != null) {
|
||||
runParams.putAll(params);
|
||||
}
|
||||
|
||||
boolean firstTime = false;
|
||||
if (gm.getKey() instanceof Card c) {
|
||||
firstTime = game.getCounterAddedThisTurn(null, c) == 0;
|
||||
}
|
||||
|
||||
// Apply counter after replacement effect
|
||||
for (Map.Entry<Optional<Player>, Map<CounterType, Integer>> e : values.entrySet()) {
|
||||
boolean remember = cause != null && cause.hasParam("RememberPut");
|
||||
@@ -182,6 +187,13 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.containsColumn(gm.getKey())) {
|
||||
runParams = AbilityKey.newMap();
|
||||
runParams.put(AbilityKey.Object, gm.getKey());
|
||||
runParams.put(AbilityKey.FirstTime, firstTime);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.CounterTypeAddedAll, runParams, false);
|
||||
}
|
||||
}
|
||||
|
||||
int totalAdded = totalValues();
|
||||
|
||||
@@ -21,7 +21,7 @@ import com.google.common.collect.Lists;
|
||||
import forge.StaticData;
|
||||
import forge.card.CardDb;
|
||||
import forge.card.CardEdition;
|
||||
import forge.card.CardEdition.CardInSet;
|
||||
import forge.card.CardEdition.EditionEntry;
|
||||
import forge.card.CardRarity;
|
||||
import forge.deck.CardPool;
|
||||
import forge.deck.Deck;
|
||||
@@ -156,7 +156,7 @@ public class GameFormat implements Comparable<GameFormat> {
|
||||
for (CardRarity cr: this.getAllowedRarities()) {
|
||||
crp.add(StaticData.instance().getCommonCards().wasPrintedAtRarity(cr));
|
||||
}
|
||||
p = p.and(IterableUtil.or(crp));
|
||||
p = p.and(IterableUtil.<PaperCard>or(crp));
|
||||
}
|
||||
if (!this.getAdditionalCards().isEmpty()) {
|
||||
p = p.or(PaperCardPredicates.names(this.getAdditionalCards()));
|
||||
@@ -226,9 +226,9 @@ public class GameFormat implements Comparable<GameFormat> {
|
||||
for (String setCode : allowedSetCodes_ro) {
|
||||
CardEdition edition = StaticData.instance().getEditions().get(setCode);
|
||||
if (edition != null) {
|
||||
for (CardInSet card : edition.getAllCardsInSet()) {
|
||||
if (!bannedCardNames_ro.contains(card.name)) {
|
||||
PaperCard pc = commonCards.getCard(card.name, setCode, card.collectorNumber);
|
||||
for (EditionEntry card : edition.getAllCardsInSet()) {
|
||||
if (!bannedCardNames_ro.contains(card.name())) {
|
||||
PaperCard pc = commonCards.getCard(card.name(), setCode, card.collectorNumber());
|
||||
if (pc != null) {
|
||||
cards.add(pc);
|
||||
}
|
||||
|
||||
@@ -273,8 +273,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
}
|
||||
|
||||
String controllerName;
|
||||
if (defender instanceof Card) {
|
||||
Card c = ((Card)defender);
|
||||
if (defender instanceof Card c) {
|
||||
controllerName = c.isBattle() ? c.getProtectingPlayer().getName() : c.getController().getName();
|
||||
} else {
|
||||
controllerName = defender.getName();
|
||||
@@ -305,8 +304,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventCardForetold ev) {
|
||||
String sb = TextUtil.concatWithSpace(ev.activatingPlayer.toString(), "has foretold.");
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, sb);
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -314,6 +312,11 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventDoorChanged ev) {
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void recieve(GameEvent ev) {
|
||||
GameLogEntry le = ev.visit(this);
|
||||
|
||||
@@ -170,7 +170,7 @@ public class GameSnapshot {
|
||||
newPlayer.setDamageReceivedThisTurn(origPlayer.getDamageReceivedThisTurn());
|
||||
newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn());
|
||||
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
|
||||
newPlayer.setBlessing(origPlayer.hasBlessing());
|
||||
newPlayer.setBlessing(origPlayer.hasBlessing(), null);
|
||||
newPlayer.setRevolt(origPlayer.hasRevolt());
|
||||
newPlayer.setLibrarySearched(origPlayer.getLibrarySearched());
|
||||
newPlayer.setSpellsCastLastTurn(origPlayer.getSpellsCastLastTurn());
|
||||
@@ -322,7 +322,7 @@ public class GameSnapshot {
|
||||
newCard.setLayerTimestamp(fromCard.getLayerTimestamp());
|
||||
newCard.setTapped(fromCard.isTapped());
|
||||
newCard.setFaceDown(fromCard.isFaceDown());
|
||||
newCard.setManifested(fromCard.isManifested());
|
||||
newCard.setManifested(fromCard.getManifestedSA());
|
||||
newCard.setSickness(fromCard.hasSickness());
|
||||
newCard.setState(fromCard.getCurrentStateName(), false);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public enum GameType {
|
||||
Winston (DeckFormat.Limited, true, true, true, "lblWinston", ""),
|
||||
Gauntlet (DeckFormat.Constructed, false, true, true, "lblGauntlet", ""),
|
||||
Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""),
|
||||
CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommander", "lblCommanderDesc"),
|
||||
CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommanderGauntlet", "lblCommanderDesc"),
|
||||
Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""),
|
||||
QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""),
|
||||
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""),
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package forge.game;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Table;
|
||||
import com.google.common.collect.Table.Cell;
|
||||
|
||||
import forge.LobbyPlayer;
|
||||
import forge.deck.Deck;
|
||||
import forge.game.GameOutcome.AnteResult;
|
||||
@@ -16,6 +20,8 @@ import forge.game.phase.PhaseType;
|
||||
import forge.game.player.PlayerView;
|
||||
import forge.game.player.RegisteredPlayer;
|
||||
import forge.game.spellability.StackItemView;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityLayer;
|
||||
import forge.game.zone.MagicStack;
|
||||
import forge.trackable.TrackableCollection;
|
||||
import forge.trackable.TrackableObject;
|
||||
@@ -200,15 +206,36 @@ public class GameView extends TrackableObject {
|
||||
public TrackableCollection<CardView> getRevealedCollection() {
|
||||
return get(TrackableProperty.RevealedCardsCollection);
|
||||
}
|
||||
|
||||
public void updateRevealedCards(TrackableCollection<CardView> collection) {
|
||||
set(TrackableProperty.RevealedCardsCollection, collection);
|
||||
}
|
||||
|
||||
public String getDependencies() {
|
||||
return get(TrackableProperty.Dependencies);
|
||||
}
|
||||
public void setDependencies(Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
|
||||
if (dependencies.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
StaticAbilityLayer layer = null;
|
||||
for (StaticAbilityLayer sal : StaticAbilityLayer.CONTINUOUS_LAYERS_WITH_DEPENDENCY) {
|
||||
for (Cell<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dep : dependencies.cellSet()) {
|
||||
if (dep.getValue().contains(sal)) {
|
||||
if (layer != sal) {
|
||||
layer = sal;
|
||||
sb.append("Layer " + layer.num).append(": ");
|
||||
}
|
||||
sb.append(dep.getColumnKey().getHostCard().toString()).append(" <- ").append(dep.getRowKey().getHostCard().toString()).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
set(TrackableProperty.Dependencies, sb.toString());
|
||||
}
|
||||
|
||||
public CombatView getCombat() {
|
||||
return get(TrackableProperty.CombatView);
|
||||
}
|
||||
|
||||
public void updateCombatView(CombatView combatView) {
|
||||
set(TrackableProperty.CombatView, combatView);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ public interface IHasSVars {
|
||||
//public Set<String> getSVars();
|
||||
|
||||
public Map<String, String> getSVars();
|
||||
public Map<String, String> getDirectSVars();
|
||||
|
||||
public void removeSVar(final String var);
|
||||
}
|
||||
|
||||
@@ -206,7 +206,6 @@ public class StaticEffect {
|
||||
if (layers.contains(StaticAbilityLayer.ABILITIES)) {
|
||||
p.removeChangedKeywords(getTimestamp(), ability.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// modify the affected card
|
||||
@@ -219,7 +218,9 @@ public class StaticEffect {
|
||||
|
||||
if (layers.contains(StaticAbilityLayer.TEXT)) {
|
||||
// Revert changed color words
|
||||
if (hasParam("ChangeColorWordsTo")) {
|
||||
affectedCard.removeChangedTextColorWord(getTimestamp(), ability.getId());
|
||||
}
|
||||
|
||||
// remove changed name
|
||||
if (hasParam("SetName") || hasParam("AddNames")) {
|
||||
@@ -265,7 +266,7 @@ public class StaticEffect {
|
||||
if (hasParam("AddAbility") || hasParam("GainsAbilitiesOf")
|
||||
|| hasParam("GainsAbilitiesOfDefined") || hasParam("GainsTriggerAbsOf")
|
||||
|| hasParam("AddTrigger") || hasParam("AddStaticAbility")
|
||||
|| hasParam("AddReplacementEffects") || hasParam("RemoveAllAbilities")
|
||||
|| hasParam("AddReplacementEffect") || hasParam("RemoveAllAbilities")
|
||||
|| hasParam("RemoveLandTypes")) {
|
||||
affectedCard.removeChangedCardTraits(getTimestamp(), ability.getId());
|
||||
}
|
||||
@@ -275,11 +276,14 @@ public class StaticEffect {
|
||||
}
|
||||
|
||||
affectedCard.removeChangedSVars(getTimestamp(), ability.getId());
|
||||
|
||||
// need update for clean reapply
|
||||
affectedCard.updateKeywordsCache(affectedCard.getCurrentState());
|
||||
}
|
||||
|
||||
if (layers.contains(StaticAbilityLayer.SETPT)) {
|
||||
if (layers.contains(StaticAbilityLayer.CHARACTERISTIC) || layers.contains(StaticAbilityLayer.SETPT)) {
|
||||
if (hasParam("SetPower") || hasParam("SetToughness")) {
|
||||
affectedCard.removeNewPT(getTimestamp(), ability.getId());
|
||||
affectedCard.removeNewPT(getTimestamp(), ability.getId(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,8 +315,6 @@ public class StaticEffect {
|
||||
affectedCard.removeCanBlockAdditional(getTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
affectedCard.updateAbilityTextForView(); // need to update keyword cache for clean reapply
|
||||
}
|
||||
return affectedCards;
|
||||
}
|
||||
|
||||
@@ -180,26 +180,19 @@ public final class AbilityFactory {
|
||||
}
|
||||
|
||||
public static Cost parseAbilityCost(final CardState state, Map<String, String> mapParams, AbilityRecordType type) {
|
||||
Cost abCost = null;
|
||||
if (type != AbilityRecordType.SubAbility) {
|
||||
String cost = mapParams.get("Cost");
|
||||
if (cost == null) {
|
||||
if (type == AbilityRecordType.Spell) {
|
||||
SpellAbility firstAbility = state.getFirstAbility();
|
||||
if (firstAbility != null && firstAbility.isSpell()) {
|
||||
// TODO might remove when Enchant Keyword is refactored
|
||||
System.err.println(state.getName() + " already has Spell using mana cost");
|
||||
if (type == AbilityRecordType.SubAbility) {
|
||||
return null;
|
||||
}
|
||||
String cost = mapParams.get("Cost");
|
||||
if (cost != null) {
|
||||
return new Cost(cost, type == AbilityRecordType.Ability);
|
||||
}
|
||||
if (type == AbilityRecordType.Spell) {
|
||||
// for a Spell if no Cost is used, use the card states ManaCost
|
||||
abCost = new Cost(state.getManaCost(), false);
|
||||
return new Cost(state.getManaCost(), false);
|
||||
} else {
|
||||
throw new RuntimeException("AbilityFactory : getAbility -- no Cost in " + state.getName());
|
||||
}
|
||||
} else {
|
||||
abCost = new Cost(cost, type == AbilityRecordType.Ability);
|
||||
}
|
||||
}
|
||||
return abCost;
|
||||
}
|
||||
|
||||
public static SpellAbility getAbility(AbilityRecordType type, ApiType api, Map<String, String> mapParams,
|
||||
@@ -216,15 +209,6 @@ public final class AbilityFactory {
|
||||
}
|
||||
}
|
||||
|
||||
else if (api == ApiType.PermanentCreature || api == ApiType.PermanentNoncreature) {
|
||||
// If API is a permanent type, and creating AF Spell
|
||||
// Clear out the auto created SpellPermanent spell
|
||||
if (type == AbilityRecordType.Spell
|
||||
&& !mapParams.containsKey("SubAbility") && !mapParams.containsKey("NonBasicSpell")) {
|
||||
hostCard.clearFirstSpell();
|
||||
}
|
||||
}
|
||||
|
||||
if (abCost == null) {
|
||||
abCost = parseAbilityCost(state, mapParams, type);
|
||||
}
|
||||
@@ -510,8 +494,9 @@ public final class AbilityFactory {
|
||||
AbilityRecordType leftType = AbilityRecordType.getRecordType(leftMap);
|
||||
ApiType leftApi = leftType.getApiTypeOf(leftMap);
|
||||
leftMap.put("StackDescription", leftMap.get("SpellDescription"));
|
||||
leftMap.put("SpellDescription", "Fuse (you may cast both halves of this card from your hand).");
|
||||
leftMap.put("SpellDescription", "Fuse (You may cast one or both halves of this card from your hand.)");
|
||||
leftMap.put("ActivationZone", "Hand");
|
||||
leftMap.put("Secondary", "True");
|
||||
|
||||
CardState rightState = card.getState(CardStateName.RightSplit);
|
||||
SpellAbility rightAbility = rightState.getFirstAbility();
|
||||
@@ -526,8 +511,10 @@ public final class AbilityFactory {
|
||||
totalCost.add(parseAbilityCost(rightState, rightMap, rightType));
|
||||
|
||||
final SpellAbility left = getAbility(leftType, leftApi, leftMap, totalCost, leftState, leftState);
|
||||
left.setOriginalAbility(leftAbility);
|
||||
left.setCardState(card.getState(CardStateName.Original));
|
||||
final AbilitySub right = (AbilitySub) getAbility(AbilityRecordType.SubAbility, rightApi, rightMap, null, rightState, rightState);
|
||||
right.setOriginalAbility(rightAbility);
|
||||
left.appendSubAbility(right);
|
||||
return left;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ public enum AbilityKey {
|
||||
Causer("Causer"),
|
||||
Championed("Championed"),
|
||||
ClassLevel("ClassLevel"),
|
||||
Cost("Cost"),
|
||||
CostStack("CostStack"),
|
||||
CounterAmount("CounterAmount"),
|
||||
CounteredSA("CounteredSA"),
|
||||
@@ -62,6 +61,7 @@ public enum AbilityKey {
|
||||
DefendingPlayer("DefendingPlayer"),
|
||||
Destination("Destination"),
|
||||
Devoured("Devoured"),
|
||||
DicePTExchanges("DicePTExchanges"),
|
||||
Discard("Discard"),
|
||||
DiscardedBefore("DiscardedBefore"),
|
||||
DividedShieldAmount("DividedShieldAmount"),
|
||||
@@ -72,7 +72,6 @@ public enum AbilityKey {
|
||||
Explored("Explored"),
|
||||
Explorer("Explorer"),
|
||||
ExtraTurn("ExtraTurn"),
|
||||
Event("Event"),
|
||||
ETB("ETB"),
|
||||
Fighter("Fighter"),
|
||||
Fighters("Fighters"),
|
||||
@@ -94,8 +93,8 @@ public enum AbilityKey {
|
||||
Mana("Mana"),
|
||||
MergedCards("MergedCards"),
|
||||
Mode("Mode"),
|
||||
Modifier("Modifier"),
|
||||
MonstrosityAmount("MonstrosityAmount"),
|
||||
NaturalResult("NaturalResult"),
|
||||
NewCard("NewCard"),
|
||||
NewCounterAmount("NewCounterAmount"),
|
||||
NoPreventDamage("NoPreventDamage"),
|
||||
|
||||
@@ -14,6 +14,7 @@ import forge.game.*;
|
||||
import forge.game.ability.AbilityFactory.AbilityRecordType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.IndividualCostPaymentInstance;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.mana.Mana;
|
||||
@@ -537,6 +538,8 @@ public class AbilityUtils {
|
||||
val = handlePaid(card.getEmerged(), calcX[1], card, ability);
|
||||
} else if (calcX[0].startsWith("Crewed")) {
|
||||
val = handlePaid(card.getCrewedByThisTurn(), calcX[1], card, ability);
|
||||
} else if (calcX[0].startsWith("ChosenCard")) {
|
||||
val = handlePaid(card.getChosenCards(), calcX[1], card, ability);
|
||||
} else if (calcX[0].startsWith("Remembered")) {
|
||||
// Add whole Remembered list to handlePaid
|
||||
final CardCollection list = new CardCollection();
|
||||
@@ -1362,10 +1365,8 @@ public class AbilityUtils {
|
||||
}
|
||||
|
||||
// do blessing there before condition checks
|
||||
if (source.hasKeyword(Keyword.ASCEND)) {
|
||||
if (controller.getZone(ZoneType.Battlefield).size() >= 10) {
|
||||
controller.setBlessing(true);
|
||||
}
|
||||
if (source.hasKeyword(Keyword.ASCEND) && controller.getZone(ZoneType.Battlefield).size() >= 10) {
|
||||
controller.setBlessing(true, source.getSetCode());
|
||||
}
|
||||
|
||||
if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) {
|
||||
@@ -1620,7 +1621,8 @@ public class AbilityUtils {
|
||||
|
||||
final String[] sq;
|
||||
sq = l[0].split("\\.");
|
||||
|
||||
String[] paidparts = l[0].split("\\$", 2);
|
||||
Iterable<Card> someCards = null;
|
||||
final Game game = c.getGame();
|
||||
|
||||
if (ctb != null) {
|
||||
@@ -1785,11 +1787,10 @@ public class AbilityUtils {
|
||||
}
|
||||
// Count$NumTimesChoseMode
|
||||
if (sq[0].startsWith("NumTimesChoseMode")) {
|
||||
SpellAbility sub = sa.getRootAbility();
|
||||
int amount = 0;
|
||||
while (sub != null) {
|
||||
if (sub.getDirectSVars().containsKey("CharmOrder")) amount++;
|
||||
sub = sub.getSubAbility();
|
||||
SpellAbility tail = sa.getTailAbility();
|
||||
if (tail.hasSVar("CharmOrder")) {
|
||||
amount = tail.getSVarInt("CharmOrder");
|
||||
}
|
||||
return doXMath(amount, expr, c, ctb);
|
||||
}
|
||||
@@ -1808,27 +1809,25 @@ public class AbilityUtils {
|
||||
}
|
||||
|
||||
if (sq[0].startsWith("LastStateBattlefield")) {
|
||||
final String[] k = l[0].split(" ");
|
||||
CardCollectionView list;
|
||||
final String[] k = paidparts[0].split(" ");
|
||||
// this is only for spells that were cast
|
||||
if (sq[0].contains("WithFallback")) {
|
||||
if (!sa.getHostCard().wasCast()) {
|
||||
return doXMath(0, expr, c, ctb);
|
||||
}
|
||||
list = sa.getHostCard().getCastSA().getLastStateBattlefield();
|
||||
someCards = sa.getHostCard().getCastSA().getLastStateBattlefield();
|
||||
} else {
|
||||
list = sa.getLastStateBattlefield();
|
||||
someCards = sa.getLastStateBattlefield();
|
||||
}
|
||||
if (list == null || list.isEmpty()) {
|
||||
if (someCards == null || Iterables.isEmpty(someCards)) {
|
||||
// LastState is Empty
|
||||
if (sq[0].contains("WithFallback")) {
|
||||
list = game.getCardsIn(ZoneType.Battlefield);
|
||||
someCards = game.getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
return doXMath(0, expr, c, ctb);
|
||||
}
|
||||
}
|
||||
list = CardLists.getValidCards(list, k[1], player, c, sa);
|
||||
return doXMath(list.size(), expr, c, ctb);
|
||||
someCards = CardLists.getValidCards(someCards, k[1], player, c, sa);
|
||||
}
|
||||
|
||||
if (sq[0].startsWith("LastStateGraveyard")) {
|
||||
@@ -1855,6 +1854,10 @@ public class AbilityUtils {
|
||||
return doXMath(list.size(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].equals("ActivatedThisGame")) {
|
||||
return doXMath(sa.getActivationsThisGame(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].equals("ResolvedThisTurn")) {
|
||||
return doXMath(sa.getResolvedThisTurn(), expr, c, ctb);
|
||||
}
|
||||
@@ -1955,9 +1958,6 @@ public class AbilityUtils {
|
||||
return doXMath(sum, expr, c, ctb);
|
||||
}
|
||||
|
||||
String[] paidparts = l[0].split("\\$", 2);
|
||||
Iterable<Card> someCards = null;
|
||||
|
||||
// count valid cards in any specified zone/s
|
||||
if (sq[0].startsWith("Valid")) {
|
||||
String[] lparts = paidparts[0].split(" ", 2);
|
||||
@@ -2216,7 +2216,7 @@ public class AbilityUtils {
|
||||
// Count$IfCastInOwnMainPhase.<numMain>.<numNotMain> // 7/10
|
||||
if (sq[0].contains("IfCastInOwnMainPhase")) {
|
||||
final PhaseHandler cPhase = game.getPhaseHandler();
|
||||
final boolean isMyMain = cPhase.getPhase().isMain() && cPhase.isPlayerTurn(player) && c.getCastFrom() != null;
|
||||
final boolean isMyMain = cPhase.getPhase().isMain() && cPhase.isPlayerTurn(player) && c.wasCast();
|
||||
return doXMath(Integer.parseInt(sq[isMyMain ? 1 : 2]), expr, c, ctb);
|
||||
}
|
||||
|
||||
@@ -2225,6 +2225,11 @@ public class AbilityUtils {
|
||||
return doXMath(game.getPhaseHandler().getNumUpkeep() - (game.getPhaseHandler().is(PhaseType.UPKEEP) ? 1 : 0), expr, c, ctb);
|
||||
}
|
||||
|
||||
// Count$FinishedEndOfTurnsThisTurn
|
||||
if (sq[0].startsWith("FinishedEndOfTurnsThisTurn")) {
|
||||
return doXMath(game.getPhaseHandler().getNumEndOfTurn() - (game.getPhaseHandler().is(PhaseType.END_OF_TURN) ? 1 : 0), expr, c, ctb);
|
||||
}
|
||||
|
||||
// Count$AttachedTo <restriction>
|
||||
if (sq[0].startsWith("AttachedTo")) {
|
||||
final String[] k = l[0].split(" ");
|
||||
@@ -2267,6 +2272,9 @@ public class AbilityUtils {
|
||||
if (sq[0].equals("Delirium")) {
|
||||
return doXMath(calculateAmount(c, sq[player.hasDelirium() ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
if (sq[0].equals("MaxSpeed")) {
|
||||
return doXMath(calculateAmount(c, sq[player.maxSpeed() ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
if (sq[0].equals("FatefulHour")) {
|
||||
return doXMath(calculateAmount(c, sq[player.getLife() <= 5 ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
@@ -2316,6 +2324,10 @@ public class AbilityUtils {
|
||||
return doXMath(player.getNumDrawnLastTurn(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].equals("YouFlipThisTurn")) {
|
||||
return doXMath(player.getNumFlipsThisTurn(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].equals("YouRollThisTurn")) {
|
||||
return doXMath(player.getNumRollsThisTurn(), expr, c, ctb);
|
||||
}
|
||||
@@ -2401,6 +2413,10 @@ public class AbilityUtils {
|
||||
return doXMath(player.getMaxOpponentAssignedDamage(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].equals("MaxCombatDamageThisTurn")) {
|
||||
return doXMath(player.getMaxAssignedCombatDamage(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].contains("TotalDamageThisTurn")) {
|
||||
String[] props = l[0].split(" ");
|
||||
int sum = 0;
|
||||
@@ -2468,7 +2484,6 @@ public class AbilityUtils {
|
||||
// But these aren't really things you count so they'll show up in properties most likely
|
||||
}
|
||||
|
||||
|
||||
//Count$TypesSharedWith [defined]
|
||||
if (sq[0].startsWith("TypesSharedWith")) {
|
||||
Set<CardType.CoreType> thisTypes = Sets.newHashSet(c.getType().getCoreTypes());
|
||||
@@ -2698,24 +2713,6 @@ public class AbilityUtils {
|
||||
return doXMath(calculateAmount(c, sq[res.size() > 0 ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].startsWith("CreatureType")) {
|
||||
String[] sqparts = l[0].split(" ", 2);
|
||||
final String[] rest = sqparts[1].split(",");
|
||||
|
||||
final CardCollectionView cardsInZones = sqparts[0].length() > 12
|
||||
? game.getCardsIn(ZoneType.listValueOf(sqparts[0].substring(12)))
|
||||
: game.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
CardCollection cards = CardLists.getValidCards(cardsInZones, rest, player, c, ctb);
|
||||
final Set<String> creatTypes = Sets.newHashSet();
|
||||
|
||||
for (Card card : cards) {
|
||||
creatTypes.addAll(card.getType().getCreatureTypes());
|
||||
}
|
||||
// filter out fun types?
|
||||
return doXMath(creatTypes.size(), expr, c, ctb);
|
||||
}
|
||||
|
||||
// Count$Chroma.<color name>
|
||||
if (sq[0].startsWith("Chroma")) {
|
||||
final CardCollectionView cards;
|
||||
@@ -2774,16 +2771,6 @@ public class AbilityUtils {
|
||||
return game.getPhaseHandler().getPlanarDiceSpecialActionThisTurn();
|
||||
}
|
||||
|
||||
if (sq[0].equals("AllTypes")) {
|
||||
List<Card> cards = getDefinedCards(c, sq[1], ctb);
|
||||
|
||||
int amount = countCardTypesFromList(cards, false) +
|
||||
countSuperTypesFromList(cards) +
|
||||
countSubTypesFromList(cards);
|
||||
|
||||
return doXMath(amount, expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].equals("TotalTurns")) {
|
||||
return doXMath(game.getPhaseHandler().getTurn(), expr, c, ctb);
|
||||
}
|
||||
@@ -2839,7 +2826,13 @@ public class AbilityUtils {
|
||||
final String[] workingCopy = paidparts[0].split("_");
|
||||
final String validFilter = workingCopy[1];
|
||||
// use objectXCount ?
|
||||
return CardUtil.getThisTurnActivated(validFilter, c, ctb, player).size();
|
||||
int activated = CardUtil.getThisTurnActivated(validFilter, c, ctb, player).size();
|
||||
for (IndividualCostPaymentInstance i : game.costPaymentStack) {
|
||||
if (i.getPayment().getAbility().isValid(validFilter, player, c, ctb)) {
|
||||
activated++;
|
||||
}
|
||||
}
|
||||
return activated;
|
||||
}
|
||||
|
||||
// Count$ThisTurnEntered <ZoneDestination> [from <ZoneOrigin>] <Valid>
|
||||
@@ -2923,18 +2916,6 @@ public class AbilityUtils {
|
||||
return doXMath(colorSize[colorSize.length - 2], expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].startsWith("ColorsCtrl")) {
|
||||
final String restriction = l[0].substring(11);
|
||||
final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
|
||||
return doXMath(CardUtil.getColorsFromCards(list).countColors(), expr, c, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].startsWith("ColorsDefined")) {
|
||||
final String restriction = l[0].substring(14);
|
||||
final CardCollection list = getDefinedCards(c, restriction, ctb);
|
||||
return doXMath(CardUtil.getColorsFromCards(list).countColors(), expr, c, ctb);
|
||||
}
|
||||
|
||||
// TODO move below to handlePaid
|
||||
if (sq[0].startsWith("SumPower")) {
|
||||
final String[] restrictions = l[0].split("_");
|
||||
@@ -3419,17 +3400,14 @@ public class AbilityUtils {
|
||||
return doXMath(numTied, m, source, ctb);
|
||||
}
|
||||
|
||||
final String[] sq;
|
||||
sq = l[0].split("\\.");
|
||||
|
||||
// the number of players passed in
|
||||
if (sq[0].equals("Amount")) {
|
||||
if (l[0].equals("Amount")) {
|
||||
return doXMath(players.size(), m, source, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].startsWith("HasProperty")) {
|
||||
if (l[0].startsWith("HasProperty")) {
|
||||
int totPlayer = 0;
|
||||
String property = sq[0].substring(11);
|
||||
String property = l[0].substring(11);
|
||||
for (Player p : players) {
|
||||
if (p.hasProperty(property, controller, source, ctb)) {
|
||||
totPlayer++;
|
||||
@@ -3453,7 +3431,7 @@ public class AbilityUtils {
|
||||
return doXMath(totPlayer, m, source, ctb);
|
||||
}
|
||||
|
||||
if (sq[0].contains("DamageThisTurn")) {
|
||||
if (l[0].contains("DamageThisTurn")) {
|
||||
int totDmg = 0;
|
||||
for (Player p : players) {
|
||||
totDmg += p.getAssignedDamage();
|
||||
@@ -3699,6 +3677,10 @@ public class AbilityUtils {
|
||||
return doXMath(amount, m, source, ctb);
|
||||
}
|
||||
|
||||
if (value.equals("AttractionsVisitedThisTurn")) {
|
||||
return doXMath(player.getAttractionsVisitedThisTurn(), m, source, ctb);
|
||||
}
|
||||
|
||||
if (value.startsWith("PlaneswalkedToThisTurn")) {
|
||||
int found = 0;
|
||||
String name = value.split(" ")[1];
|
||||
@@ -3777,6 +3759,10 @@ public class AbilityUtils {
|
||||
return Aggregates.max(paidList, Card::getCMC);
|
||||
}
|
||||
|
||||
if (string.equals("Colors")) {
|
||||
return CardUtil.getColorsFromCards(paidList).countColors();
|
||||
}
|
||||
|
||||
if (string.equals("DifferentColorPair")) {
|
||||
final Set<ColorSet> diffPair = new HashSet<>();
|
||||
for (final Card card : paidList) {
|
||||
@@ -3806,10 +3792,25 @@ public class AbilityUtils {
|
||||
return doXMath(num, splitString.length > 1 ? splitString[1] : null, source, ctb);
|
||||
}
|
||||
|
||||
if (string.startsWith("AllTypes")) {
|
||||
return countCardTypesFromList(paidList, false) +
|
||||
countSuperTypesFromList(paidList) +
|
||||
countSubTypesFromList(paidList);
|
||||
}
|
||||
|
||||
if (string.startsWith("CardTypes")) {
|
||||
return doXMath(countCardTypesFromList(paidList, string.startsWith("CardTypesPermanent")), CardFactoryUtil.extractOperators(string), source, ctb);
|
||||
}
|
||||
|
||||
if (string.startsWith("CreatureType")) {
|
||||
final Set<String> creatTypes = Sets.newHashSet();
|
||||
for (Card card : paidList) {
|
||||
creatTypes.addAll(card.getType().getCreatureTypes());
|
||||
}
|
||||
// filter out fun types?
|
||||
return doXMath(creatTypes.size(), CardFactoryUtil.extractOperators(string), source, ctb);
|
||||
}
|
||||
|
||||
String filteredString = string;
|
||||
Iterable<Card> filteredList = paidList;
|
||||
final String[] filter = filteredString.split("_");
|
||||
|
||||
@@ -59,7 +59,6 @@ public enum ApiType {
|
||||
Cleanup (CleanUpEffect.class),
|
||||
Cloak (CloakEffect.class),
|
||||
Clone (CloneEffect.class),
|
||||
CompanionChoose (ChooseCompanionEffect.class),
|
||||
Connive (ConniveEffect.class),
|
||||
CopyPermanent (CopyPermanentEffect.class),
|
||||
CopySpellAbility (CopySpellAbilityEffect.class),
|
||||
@@ -86,12 +85,14 @@ public enum ApiType {
|
||||
Encode (EncodeEffect.class),
|
||||
EndCombatPhase (EndCombatPhaseEffect.class),
|
||||
EndTurn (EndTurnEffect.class),
|
||||
Endure (EndureEffect.class),
|
||||
ExchangeLife (LifeExchangeEffect.class),
|
||||
ExchangeLifeVariant (LifeExchangeVariantEffect.class),
|
||||
ExchangeControl (ControlExchangeEffect.class),
|
||||
ExchangeControlVariant (ControlExchangeVariantEffect.class),
|
||||
ExchangePower (PowerExchangeEffect.class),
|
||||
ExchangeZone (ZoneExchangeEffect.class),
|
||||
ExchangeTextBox (TextBoxExchangeEffect.class),
|
||||
Explore (ExploreEffect.class),
|
||||
Fight (FightEffect.class),
|
||||
FlipACoin (FlipCoinEffect.class),
|
||||
@@ -207,6 +208,7 @@ public enum ApiType {
|
||||
BlankLine (BlankLineEffect.class),
|
||||
DamageResolve (DamageResolveEffect.class),
|
||||
ChangeZoneResolve (ChangeZoneResolveEffect.class),
|
||||
CompanionChoose (CharmEffect.class),
|
||||
InternalLegendaryRule (CharmEffect.class),
|
||||
InternalIgnoreEffect (CharmEffect.class),
|
||||
InternalRadiation (InternalRadiationEffect.class),
|
||||
|
||||
@@ -83,8 +83,8 @@ public abstract class SpellAbilityEffect {
|
||||
if ("SpellDescription".equalsIgnoreCase(stackDesc)) {
|
||||
if (params.containsKey("SpellDescription")) {
|
||||
String rawSDesc = params.get("SpellDescription");
|
||||
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replaceAll(",,,,,,", " ");
|
||||
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replaceAll(",,,", " ");
|
||||
if (rawSDesc.contains(",,,,,,")) rawSDesc = rawSDesc.replace(",,,,,,", " ");
|
||||
if (rawSDesc.contains(",,,")) rawSDesc = rawSDesc.replace(",,,", " ");
|
||||
String spellDesc = CardTranslation.translateSingleDescriptionText(rawSDesc, sa.getHostCard());
|
||||
|
||||
//trim reminder text from StackDesc
|
||||
@@ -356,6 +356,7 @@ public abstract class SpellAbilityEffect {
|
||||
boolean intrinsic = sa.isIntrinsic();
|
||||
boolean your = location.startsWith("Your");
|
||||
boolean combat = location.endsWith("Combat");
|
||||
boolean upkeep = location.endsWith("Upkeep");
|
||||
|
||||
String desc = sa.getParamOrDefault("AtEOTDesc", "");
|
||||
|
||||
@@ -365,11 +366,16 @@ public abstract class SpellAbilityEffect {
|
||||
if (combat) {
|
||||
location = location.substring(0, location.length() - "Combat".length());
|
||||
}
|
||||
if (upkeep) {
|
||||
location = location.substring(0, location.length() - "Upkeep".length());
|
||||
}
|
||||
|
||||
if (desc.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (location.equals("Hand")) {
|
||||
sb.append("Return ");
|
||||
} else if (location.equals("Library")) {
|
||||
sb.append("Shuffle ");
|
||||
} else if (location.equals("SacrificeCtrl")) {
|
||||
sb.append("Its controller sacrifices ");
|
||||
} else {
|
||||
@@ -378,6 +384,8 @@ public abstract class SpellAbilityEffect {
|
||||
sb.append(Lang.joinHomogenous(crds));
|
||||
if (location.equals("Hand")) {
|
||||
sb.append(" to your hand");
|
||||
} else if (location.equals("Library")) {
|
||||
sb.append(" into your library");
|
||||
}
|
||||
sb.append(" at the ");
|
||||
if (combat) {
|
||||
@@ -385,14 +393,18 @@ public abstract class SpellAbilityEffect {
|
||||
} else {
|
||||
sb.append("beginning of ");
|
||||
sb.append(your ? "your" : "the");
|
||||
if (upkeep) {
|
||||
sb.append(" next upkeep.");
|
||||
} else {
|
||||
sb.append(" next end step.");
|
||||
}
|
||||
}
|
||||
desc = sb.toString();
|
||||
}
|
||||
|
||||
StringBuilder delTrig = new StringBuilder();
|
||||
delTrig.append("Mode$ Phase | Phase$ ");
|
||||
delTrig.append(combat ? "EndCombat " : "End Of Turn ");
|
||||
delTrig.append(combat ? "EndCombat " : upkeep ? "Upkeep" : "End Of Turn ");
|
||||
|
||||
if (your) {
|
||||
delTrig.append("| ValidPlayer$ You ");
|
||||
@@ -410,6 +422,8 @@ public abstract class SpellAbilityEffect {
|
||||
String trigSA = "";
|
||||
if (location.equals("Hand")) {
|
||||
trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRememberedLKI | Origin$ Battlefield | Destination$ Hand";
|
||||
} else if (location.equals("Library")) {
|
||||
trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRememberedLKI | Origin$ Battlefield | Destination$ Library | Shuffle$ True";
|
||||
} else if (location.equals("SacrificeCtrl")) {
|
||||
trigSA = "DB$ SacrificeAll | Defined$ DelayTriggerRememberedLKI";
|
||||
} else if (location.equals("Sacrifice")) {
|
||||
@@ -588,11 +602,10 @@ public abstract class SpellAbilityEffect {
|
||||
|
||||
// create a basic template for Effect to be used somewhere els
|
||||
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image) {
|
||||
return createEffect(sa, controller, name, image, controller.getGame().getNextTimestamp());
|
||||
return createEffect(sa, sa.getHostCard(), controller, name, image, controller.getGame().getNextTimestamp());
|
||||
}
|
||||
public static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image, final long timestamp) {
|
||||
final Card hostCard = sa.getHostCard();
|
||||
final Game game = hostCard.getGame();
|
||||
public static Card createEffect(final SpellAbility sa, final Card hostCard, final Player controller, final String name, final String image, final long timestamp) {
|
||||
final Game game = controller.getGame();
|
||||
final Card eff = new Card(game.nextCardId(), game);
|
||||
|
||||
eff.setGameTimestamp(timestamp);
|
||||
@@ -608,12 +621,7 @@ public abstract class SpellAbilityEffect {
|
||||
eff.setRarity(hostCard.getRarity());
|
||||
}
|
||||
|
||||
if (sa.hasParam("Boon")) {
|
||||
eff.setBoon(true);
|
||||
}
|
||||
|
||||
eff.setOwner(controller);
|
||||
eff.setSVars(sa.getSVars());
|
||||
|
||||
eff.setSetCode(hostCard.getSetCode());
|
||||
if (image != null) {
|
||||
@@ -621,7 +629,12 @@ public abstract class SpellAbilityEffect {
|
||||
}
|
||||
|
||||
eff.setGamePieceType(GamePieceType.EFFECT);
|
||||
if (sa != null) {
|
||||
eff.setEffectSource(sa);
|
||||
eff.setSVars(sa.getSVars());
|
||||
} else {
|
||||
eff.setEffectSource(hostCard);
|
||||
}
|
||||
|
||||
return eff;
|
||||
}
|
||||
@@ -768,7 +781,11 @@ public abstract class SpellAbilityEffect {
|
||||
return combatChanged;
|
||||
}
|
||||
|
||||
protected static GameCommand untilHostLeavesPlayCommand(final CardZoneTable triggerList, final SpellAbility sa) {
|
||||
protected static void changeZoneUntilCommand(final CardZoneTable triggerList, final SpellAbility sa) {
|
||||
if (!sa.hasParam("Duration")) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Card hostCard = sa.getHostCard();
|
||||
final Game game = hostCard.getGame();
|
||||
hostCard.addUntilLeavesBattlefield(triggerList.allCards());
|
||||
@@ -783,7 +800,7 @@ public abstract class SpellAbilityEffect {
|
||||
lki = null;
|
||||
}
|
||||
|
||||
return new GameCommand() {
|
||||
GameCommand gc = new GameCommand() {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -838,6 +855,13 @@ public abstract class SpellAbilityEffect {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// corner case can lead to host exiling itself during the effect
|
||||
if (sa.getParam("Duration").contains("UntilHostLeavesPlay") && !hostCard.isInPlay()) {
|
||||
gc.run();
|
||||
} else {
|
||||
addUntilCommand(sa, gc);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void discard(SpellAbility sa, final boolean effect, Map<Player, CardCollectionView> discardedMap, Map<AbilityKey, Object> params) {
|
||||
@@ -898,6 +922,8 @@ public abstract class SpellAbilityEffect {
|
||||
} else {
|
||||
game.getUpkeep().addUntilEnd(controller, until);
|
||||
}
|
||||
} else if ("UntilTheEndOfYourNextUntap".equals(duration)) {
|
||||
game.getUntap().addUntilEnd(controller, until);
|
||||
} else if ("UntilNextEndStep".equals(duration)) {
|
||||
game.getEndOfTurn().addAt(until);
|
||||
} else if ("UntilYourNextEndStep".equals(duration)) {
|
||||
@@ -998,8 +1024,9 @@ public abstract class SpellAbilityEffect {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Player getNewChooser(final SpellAbility sa, final Player activator, final Player loser) {
|
||||
public static Player getNewChooser(final SpellAbility sa, final Player loser) {
|
||||
// CR 800.4g
|
||||
final Player activator = sa.getActivatingPlayer();
|
||||
final PlayerCollection options;
|
||||
if (loser.isOpponentOf(activator)) {
|
||||
options = activator.getOpponents();
|
||||
@@ -1041,7 +1068,9 @@ public abstract class SpellAbilityEffect {
|
||||
exilingSource = cause.getOriginalHost();
|
||||
}
|
||||
movedCard.setExiledWith(exilingSource);
|
||||
movedCard.setExiledBy(cause.getActivatingPlayer());
|
||||
Player exiler = cause.hasParam("DefinedExiler") ?
|
||||
getDefinedPlayersOrTargeted(cause, "DefinedExiler").get(0) : cause.getActivatingPlayer();
|
||||
movedCard.setExiledBy(exiler);
|
||||
}
|
||||
|
||||
public static GameCommand exileEffectCommand(final Game game, final Card effect) {
|
||||
|
||||
@@ -25,7 +25,7 @@ public class AddPhaseEffect extends SpellAbilityEffect {
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
final Player activator = sa.getActivatingPlayer();
|
||||
boolean isTopsy = activator.getAmountOfKeyword("The phases of your turn are reversed.") % 2 == 1;
|
||||
boolean isTopsy = activator.isPhasesReversed();
|
||||
PhaseHandler phaseHandler = activator.getGame().getPhaseHandler();
|
||||
|
||||
PhaseType currentPhase = phaseHandler.getPhase();
|
||||
|
||||
@@ -7,6 +7,8 @@ import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.phase.ExtraTurn;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementHandler;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerHandler;
|
||||
@@ -72,11 +74,12 @@ public class AddTurnEffect extends SpellAbilityEffect {
|
||||
|
||||
final Card eff = createEffect(sa, sa.getActivatingPlayer(), name, image);
|
||||
|
||||
String stEffect = "Mode$ CantSetSchemesInMotion | EffectZone$ Command | Description$ Schemes can't be set in Motion";
|
||||
|
||||
eff.addStaticAbility(stEffect);
|
||||
String strRe = "Event$ SetInMotion | EffectZone$ Command | Layer$ CantHappen | Description$ Schemes can't be set in Motion";
|
||||
ReplacementEffect re = ReplacementHandler.parseReplacement(strRe, eff, true);
|
||||
eff.addReplacementEffect(re);
|
||||
|
||||
game.getAction().moveToCommand(eff, sa);
|
||||
game.getEndOfTurn().addUntil(exileEffectCommand(game, eff));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.event.GameEventCardPlotted;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.util.Lang;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
|
||||
public class AlterAttributeEffect extends SpellAbilityEffect {
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
@@ -48,6 +48,8 @@ public class AlterAttributeEffect extends SpellAbilityEffect {
|
||||
switch (attr.trim()) {
|
||||
case "Plotted":
|
||||
altered = gameCard.setPlotted(activate);
|
||||
|
||||
c.getGame().fireEvent(new GameEventCardPlotted(c, sa.getActivatingPlayer()));
|
||||
break;
|
||||
case "Solve":
|
||||
case "Solved":
|
||||
|
||||
@@ -202,7 +202,7 @@ public class AnimateEffect extends AnimateEffectBase {
|
||||
|
||||
if (sa.isCrew()) {
|
||||
gameCard.becomesCrewed(sa);
|
||||
gameCard.updatePowerToughnessForView();
|
||||
gameCard.updatePTforView();
|
||||
}
|
||||
|
||||
game.fireEvent(new GameEventCardStatsChanged(gameCard));
|
||||
|
||||
@@ -121,6 +121,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
|
||||
if (perpetual) {
|
||||
Map <String, Object> params = new HashMap<>();
|
||||
params.put("AddKeywords", keywords);
|
||||
params.put("RemoveKeywords", removeKeywords);
|
||||
params.put("RemoveAll", removeAll);
|
||||
params.put("Timestamp", timestamp);
|
||||
params.put("Category", "Keywords");
|
||||
@@ -164,14 +165,8 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
|
||||
|
||||
// remove abilities
|
||||
final List<SpellAbility> removedAbilities = Lists.newArrayList();
|
||||
boolean clearSpells = sa.hasParam("OverwriteSpells");
|
||||
|
||||
if (clearSpells) {
|
||||
removedAbilities.addAll(Lists.newArrayList(c.getSpells()));
|
||||
}
|
||||
|
||||
if (sa.hasParam("RemoveThisAbility") && !removedAbilities.contains(sa)) {
|
||||
removedAbilities.add(sa);
|
||||
if (sa.hasParam("RemoveThisAbility")) {
|
||||
removedAbilities.add(sa.getOriginalAbility());
|
||||
}
|
||||
|
||||
// give abilities
|
||||
@@ -251,9 +246,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
|
||||
}
|
||||
|
||||
if (!"Permanent".equals(duration) && !perpetual) {
|
||||
if ("UntilControllerNextUntap".equals(duration)) {
|
||||
game.getUntap().addUntil(c.getController(), unanimate);
|
||||
} else if ("UntilAnimatedFaceup".equals(duration)) {
|
||||
if ("UntilAnimatedFaceup".equals(duration)) {
|
||||
c.addFaceupCommand(unanimate);
|
||||
} else {
|
||||
addUntilCommand(sa, unanimate);
|
||||
|
||||
@@ -39,7 +39,7 @@ public class AscendEffect extends SpellAbilityEffect {
|
||||
}
|
||||
// Player need 10+ permanents on the battlefield
|
||||
if (p.getZone(ZoneType.Battlefield).size() >= 10) {
|
||||
p.setBlessing(true);
|
||||
p.setBlessing(true, sa.getOriginalHost().getSetCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user