mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-14 01:38:13 +00:00
Compare commits
735 Commits
daily-snap
...
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 |
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 }}
|
||||
|
||||
@@ -122,3 +122,11 @@ jobs:
|
||||
prerelease: true
|
||||
artifacts: izpack/*
|
||||
allowUpdates: true
|
||||
removeArtifacts: true
|
||||
|
||||
- name: Send failure notification to Discord
|
||||
if: failure() # This step runs only if the job fails
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d "{\"content\": \"🔴 Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
|
||||
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
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;
|
||||
@@ -1458,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;
|
||||
@@ -1519,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;
|
||||
}
|
||||
@@ -2429,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;
|
||||
}
|
||||
@@ -2891,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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
|
||||
@@ -608,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,24 +642,28 @@ public class ComputerUtilMana {
|
||||
List<SpellAbility> paymentList = Lists.newArrayList();
|
||||
final ManaPool manapool = ai.getManaPool();
|
||||
|
||||
// Apply the color/type conversion matrix if necessary
|
||||
manapool.restoreColorReplacements();
|
||||
CardPlayOption mayPlay = sa.getMayPlayOption();
|
||||
if (!effect) {
|
||||
if (sa.isSpell() && mayPlay != null) {
|
||||
mayPlay.applyManaConvert(manapool);
|
||||
} else if (sa.isActivatedAbility() && sa.getGrantorStatic() != null && sa.getGrantorStatic().hasParam("ManaConversion")) {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getGrantorStatic().getParam("ManaConversion"));
|
||||
// Apply color/type conversion matrix if necessary (already done via autopay)
|
||||
if (ai.getControllingPlayer() == null) {
|
||||
manapool.restoreColorReplacements();
|
||||
CardPlayOption mayPlay = sa.getMayPlayOption();
|
||||
if (!effect) {
|
||||
if (sa.isSpell() && mayPlay != null) {
|
||||
mayPlay.applyManaConvert(manapool);
|
||||
} else if (sa.isActivatedAbility() && sa.getGrantorStatic() != null && sa.getGrantorStatic().hasParam("ManaConversion")) {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getGrantorStatic().getParam("ManaConversion"));
|
||||
}
|
||||
}
|
||||
if (sa.hasParam("ManaConversion")) {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
|
||||
}
|
||||
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
|
||||
}
|
||||
if (sa.hasParam("ManaConversion")) {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
|
||||
}
|
||||
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
|
||||
|
||||
// not worth checking if it makes sense to not spend floating first
|
||||
if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) {
|
||||
CostPayment.handleOfferings(sa, test, cost.isPaid());
|
||||
return true; // paid all from floating mana
|
||||
// paid all from floating mana
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
|
||||
|
||||
@@ -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)) {
|
||||
@@ -1389,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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -72,7 +75,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attach spells always have a target
|
||||
@@ -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,20 +420,9 @@ 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
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// already affected
|
||||
if (!c.canUntap(c.getController(), true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -486,29 +507,29 @@ public class AttachAi extends SpellAbilityAi {
|
||||
*/
|
||||
private static Card attachAIAnimatePreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
|
||||
final Card attachSource) {
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Card card = null;
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Card card = null;
|
||||
// AI For choosing a Card to Animate.
|
||||
List<Card> betterList = CardLists.getNotType(list, "Creature");
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Animate Artifact")) {
|
||||
betterList = CardLists.filter(betterList, c -> c.getCMC() > 0);
|
||||
card = ComputerUtilCard.getMostExpensivePermanentAI(betterList);
|
||||
} else {
|
||||
List<Card> evenBetterList = CardLists.filter(betterList, c -> c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.hasKeyword(Keyword.HEXPROOF));
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
evenBetterList = CardLists.filter(betterList, CardPredicates.UNTAPPED);
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
evenBetterList = CardLists.filter(betterList, c -> c.getTurnInZone() != c.getGame().getPhaseHandler().getTurn());
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
evenBetterList = CardLists.filter(betterList, c -> {
|
||||
List<Card> evenBetterList = CardLists.filter(betterList, c -> c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.hasKeyword(Keyword.HEXPROOF));
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
evenBetterList = CardLists.filter(betterList, CardPredicates.UNTAPPED);
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
evenBetterList = CardLists.filter(betterList, c -> c.getTurnInZone() != c.getGame().getPhaseHandler().getTurn());
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
evenBetterList = CardLists.filter(betterList, c -> {
|
||||
for (final SpellAbility sa1 : c.getSpellAbilities()) {
|
||||
if (sa1.isAbility() && sa1.getPayCosts().hasTapCost()) {
|
||||
return false;
|
||||
@@ -516,10 +537,10 @@ public class AttachAi extends SpellAbilityAi {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
card = ComputerUtilCard.getWorstAI(betterList);
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
card = ComputerUtilCard.getWorstAI(betterList);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -1044,17 +1126,17 @@ public class AttachAi extends SpellAbilityAi {
|
||||
// Probably want to "weight" the list by amount of Enchantments and
|
||||
// choose the "lightest"
|
||||
|
||||
List<Card> betterList = CardLists.filter(magnetList, c -> CombatUtil.canAttack(c, ai.getWeakestOpponent()));
|
||||
if (!betterList.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(betterList);
|
||||
}
|
||||
List<Card> betterList = CardLists.filter(magnetList, c -> CombatUtil.canAttack(c, ai.getWeakestOpponent()));
|
||||
if (!betterList.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(betterList);
|
||||
}
|
||||
|
||||
// Magnet List should not be attached when they are useless
|
||||
betterList = CardLists.filter(magnetList, c -> !ComputerUtilCard.isUselessCreature(ai, c));
|
||||
// Magnet List should not be attached when they are useless
|
||||
betterList = CardLists.filter(magnetList, c -> !ComputerUtilCard.isUselessCreature(ai, c));
|
||||
|
||||
if (!betterList.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(betterList);
|
||||
}
|
||||
if (!betterList.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(betterList);
|
||||
}
|
||||
|
||||
//return ComputerUtilCard.getBestAI(magnetList);
|
||||
}
|
||||
@@ -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(" & ")));
|
||||
}
|
||||
@@ -1143,13 +1223,13 @@ public class AttachAi extends SpellAbilityAi {
|
||||
prefList = ComputerUtil.getSafeTargets(ai, sa, prefList);
|
||||
|
||||
if (attachSource.isAura()) {
|
||||
if (!attachSource.getName().equals("Daybreak Coronet")) {
|
||||
// TODO For Auras like Rancor, that aren't as likely to lead to
|
||||
// card disadvantage, this check should be skipped
|
||||
prefList = CardLists.filter(prefList, c -> !c.isEnchanted() || c.hasKeyword(Keyword.HEXPROOF));
|
||||
}
|
||||
if (!attachSource.getName().equals("Daybreak Coronet")) {
|
||||
// TODO For Auras like Rancor, that aren't as likely to lead to
|
||||
// card disadvantage, this check should be skipped
|
||||
prefList = CardLists.filter(prefList, c -> !c.isEnchanted() || c.hasKeyword(Keyword.HEXPROOF));
|
||||
}
|
||||
|
||||
// should not attach Auras to creatures that does leave the play
|
||||
// should not attach Auras to creatures that does leave the play
|
||||
prefList = CardLists.filter(prefList, c -> !c.hasSVar("EndOfTurnLeavePlay"));
|
||||
}
|
||||
|
||||
@@ -1158,10 +1238,15 @@ 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")) {
|
||||
canOnlyTargetCreatures = false;
|
||||
break;
|
||||
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())) {
|
||||
@@ -1172,7 +1257,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
// Probably prefer to Enchant Creatures that Can Attack
|
||||
// Filter out creatures that can't Attack or have Defender
|
||||
if (keywords.isEmpty()) {
|
||||
final int powerBonus = totPower;
|
||||
final int powerBonus = totPower;
|
||||
prefList = CardLists.filter(prefList, c -> {
|
||||
if (!c.isCreature()) {
|
||||
return true;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,7 +1285,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
sa.getTargets().add(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.
|
||||
@@ -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()
|
||||
|
||||
@@ -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,7 +287,9 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
if (sa.canTarget(choice)) {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
} else if (sa.hasParam("Defined")) {
|
||||
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
@@ -361,7 +366,10 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
Card c = ComputerUtilCard.getBestAI(preferred);
|
||||
sa.getTargets().add(c);
|
||||
|
||||
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);
|
||||
}
|
||||
sa.getTargets().add(c);
|
||||
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());
|
||||
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;
|
||||
});
|
||||
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)
|
||||
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,20 +166,49 @@ 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;
|
||||
private static final Map<String, String> sortableCollNumberLookup = new HashMap<>();
|
||||
/**
|
||||
* This method implements the main strategy to allow for natural ordering of collectorNumber
|
||||
* (i.e. "1" < "10"), overloading the default lexicographic order (i.e. "10" < "1").
|
||||
* Any non-numerical parts in the input collectorNumber will be also accounted for, and attached to the
|
||||
* resulting sorting key, accordingly.
|
||||
*
|
||||
* @param collectorNumber: Input collectorNumber tro transform in a Sorting Key
|
||||
* @return A 5-digits zero-padded collector number + any non-numerical parts attached.
|
||||
*/
|
||||
public static String getSortableCollectorNumber(final String collectorNumber){
|
||||
String inputCollNumber = collectorNumber;
|
||||
if (collectorNumber == null || collectorNumber.isEmpty())
|
||||
inputCollNumber = "50000"; // very big number of 5 digits to have them in last positions
|
||||
|
||||
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;
|
||||
String matchedCollNr = sortableCollNumberLookup.getOrDefault(inputCollNumber, null);
|
||||
if (matchedCollNr != null)
|
||||
return matchedCollNr;
|
||||
|
||||
// Now, for proper sorting, let's zero-pad the collector number (if integer)
|
||||
int collNr;
|
||||
String sortableCollNr;
|
||||
try {
|
||||
collNr = Integer.parseInt(inputCollNumber);
|
||||
sortableCollNr = String.format("%05d", collNr);
|
||||
} catch (NumberFormatException ex) {
|
||||
String nonNumSub = inputCollNumber.replaceAll("[0-9]", "");
|
||||
String onlyNumSub = inputCollNumber.replaceAll("[^0-9]", "");
|
||||
try {
|
||||
collNr = Integer.parseInt(onlyNumSub);
|
||||
} catch (NumberFormatException exon) {
|
||||
collNr = 0; // this is the case of ONLY-letters collector numbers
|
||||
}
|
||||
if ((collNr > 0) && (inputCollNumber.startsWith(onlyNumSub))) // e.g. 12a, 37+, 2018f,
|
||||
sortableCollNr = String.format("%05d", collNr) + nonNumSub;
|
||||
else // e.g. WS6, S1
|
||||
sortableCollNr = nonNumSub + String.format("%05d", collNr);
|
||||
}
|
||||
sortableCollNumberLookup.put(inputCollNumber, sortableCollNr);
|
||||
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();
|
||||
@@ -186,7 +216,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
sb.append(collectorNumber);
|
||||
sb.append(' ');
|
||||
}
|
||||
if (rarity != CardRarity.Unknown) {
|
||||
if (rarity != CardRarity.Unknown && rarity != CardRarity.Token) {
|
||||
sb.append(rarity);
|
||||
sb.append(' ');
|
||||
}
|
||||
@@ -202,50 +232,8 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
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
|
||||
* (i.e. "1" < "10"), overloading the default lexicographic order (i.e. "10" < "1").
|
||||
* Any non-numerical parts in the input collectorNumber will be also accounted for, and attached to the
|
||||
* resulting sorting key, accordingly.
|
||||
*
|
||||
* @param collectorNumber: Input collectorNumber tro transform in a Sorting Key
|
||||
* @return A 5-digits zero-padded collector number + any non-numerical parts attached.
|
||||
*/
|
||||
public static String getSortableCollectorNumber(final String collectorNumber){
|
||||
String inputCollNumber = collectorNumber;
|
||||
if (collectorNumber == null || collectorNumber.isEmpty())
|
||||
inputCollNumber = "50000"; // very big number of 5 digits to have them in last positions
|
||||
|
||||
String matchedCollNr = sortableCollNumberLookup.getOrDefault(inputCollNumber, null);
|
||||
if (matchedCollNr != null)
|
||||
return matchedCollNr;
|
||||
|
||||
// Now, for proper sorting, let's zero-pad the collector number (if integer)
|
||||
int collNr;
|
||||
String sortableCollNr;
|
||||
try {
|
||||
collNr = Integer.parseInt(inputCollNumber);
|
||||
sortableCollNr = String.format("%05d", collNr);
|
||||
} catch (NumberFormatException ex) {
|
||||
String nonNumSub = inputCollNumber.replaceAll("[0-9]", "");
|
||||
String onlyNumSub = inputCollNumber.replaceAll("[^0-9]", "");
|
||||
try {
|
||||
collNr = Integer.parseInt(onlyNumSub);
|
||||
} catch (NumberFormatException exon) {
|
||||
collNr = 0; // this is the case of ONLY-letters collector numbers
|
||||
}
|
||||
if ((collNr > 0) && (inputCollNumber.startsWith(onlyNumSub))) // e.g. 12a, 37+, 2018f,
|
||||
sortableCollNr = String.format("%05d", collNr) + nonNumSub;
|
||||
else // e.g. WS6, S1
|
||||
sortableCollNr = nonNumSub + String.format("%05d", collNr);
|
||||
}
|
||||
sortableCollNumberLookup.put(inputCollNumber, sortableCollNr);
|
||||
return sortableCollNr;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ICardFace getUSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeU);
|
||||
}
|
||||
public ICardFace getBSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeB);
|
||||
}
|
||||
public ICardFace getRSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeR);
|
||||
}
|
||||
public ICardFace getGSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeG);
|
||||
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();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
/**
|
||||
* Magic Cards Database.
|
||||
* --------------------
|
||||
* This interface defines the general API for Database Access and Cards' Lookup.
|
||||
*
|
||||
* 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)
|
||||
*
|
||||
* 2. [getCardFromSet]: Card Lookup from a single Expansion set.
|
||||
* Particularly useful in Deck Editors when a specific Set is specified.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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)
|
||||
*
|
||||
* 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)
|
||||
* */
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -671,11 +671,9 @@ public class DeckRecognizer {
|
||||
// ok so the card has been found - let's see if there's any restriction on the set
|
||||
return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine,
|
||||
currentDeckSection, true);
|
||||
// On the off chance we accidentally interpreted part of the card's name as a set code, e.g. "Tyrranax Rex"
|
||||
if (data.isMTGCard(cardName + " " + setCode))
|
||||
continue;
|
||||
// UNKNOWN card as in the Counterspell|FEM case
|
||||
return Token.UnknownCard(cardName, setCode, cardCount);
|
||||
unknownCardToken = Token.UnknownCard(cardName, setCode, cardCount);
|
||||
continue;
|
||||
}
|
||||
// ok so we can simply ignore everything but card name - as set code does not exist
|
||||
// At this stage, we know the card name exists in the DB so a Card MUST be found
|
||||
@@ -989,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>() {{
|
||||
@@ -1008,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);
|
||||
}
|
||||
return flaglessVersion;
|
||||
}
|
||||
public PaperCard getColorIDVersion(Set<String> colors) {
|
||||
if (colors == null && this.colorID == null)
|
||||
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 (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));
|
||||
} 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));
|
||||
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;
|
||||
}
|
||||
// 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 {
|
||||
// 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() {
|
||||
return IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
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() {
|
||||
|
||||
@@ -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,6 +269,13 @@ 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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -564,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
|
||||
@@ -578,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) {
|
||||
@@ -598,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1207,29 +1207,43 @@ 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()) {
|
||||
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)) {
|
||||
result += p.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
result += p.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 (Pair<Card, Integer> p : l) {
|
||||
if (p.getKey().equalsWithGameTimestamp(card)) {
|
||||
result += p.getValue();
|
||||
for (CounterType type : types) {
|
||||
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(type).values()) {
|
||||
for (Pair<Card, Integer> p : l) {
|
||||
if (p.getKey().equalsWithGameTimestamp(card)) {
|
||||
result += p.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -386,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.
|
||||
@@ -542,8 +542,8 @@ public class GameAction {
|
||||
game.addLeftGraveyardThisTurn(lastKnownInfo);
|
||||
}
|
||||
|
||||
if (c.hasChosenColorSpire()) {
|
||||
copied.setChosenColorID(ImmutableSet.copyOf(c.getChosenColorID()));
|
||||
if (c.hasMarkedColor()) {
|
||||
copied.setMarkedColors(c.getMarkedColors());
|
||||
}
|
||||
|
||||
copied.updateStateForView();
|
||||
@@ -797,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;
|
||||
}
|
||||
@@ -807,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();
|
||||
@@ -1073,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)) {
|
||||
@@ -1105,10 +1105,6 @@ 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();
|
||||
@@ -1123,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") && stAb.zonesCheck()) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.zonesCheck()) {
|
||||
staticAbilities.add(stAb);
|
||||
}
|
||||
}
|
||||
@@ -1170,7 +1166,7 @@ public class GameAction {
|
||||
if (affectedHere != null) {
|
||||
for (final Card c : affectedHere) {
|
||||
for (final StaticAbility st2 : c.getStaticAbilities()) {
|
||||
if (!staticAbilities.contains(st2) && st2.checkMode("Continuous") && st2.zonesCheck()) {
|
||||
if (!staticAbilities.contains(st2) && st2.checkMode(StaticAbilityMode.Continuous) && st2.zonesCheck()) {
|
||||
toAdd.add(st2);
|
||||
st2.applyContinuousAbilityBefore(layer, preList);
|
||||
}
|
||||
@@ -1272,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);
|
||||
|
||||
@@ -1355,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()));
|
||||
|
||||
@@ -1468,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);
|
||||
@@ -1520,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)
|
||||
@@ -1535,21 +1523,21 @@ public class GameAction {
|
||||
}
|
||||
}
|
||||
|
||||
// 704.5z If a player controls a permanent with start your engines! and that player has no speed, that player’s speed becomes 1. See rule 702.179, “Start Your Engines!”
|
||||
// 704.5z If a player controls a permanent with start your engines! and that player has no speed, that 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;
|
||||
}
|
||||
|
||||
if (handlePlaneswalkerRule(p, noRegCreats)) {
|
||||
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);
|
||||
@@ -1581,6 +1569,7 @@ public class GameAction {
|
||||
orderedSacrificeList = true;
|
||||
}
|
||||
sacrifice(sacrificeList, null, true, mapParams);
|
||||
|
||||
setHoldCheckingStaticAbilities(false);
|
||||
|
||||
table.triggerChangesZoneAll(game, null);
|
||||
@@ -1639,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?
|
||||
@@ -1658,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.
|
||||
@@ -1738,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;
|
||||
}
|
||||
@@ -1752,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);
|
||||
@@ -1804,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
|
||||
@@ -1822,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) {
|
||||
@@ -1996,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;
|
||||
}
|
||||
@@ -2005,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();
|
||||
@@ -2386,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2743,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);
|
||||
@@ -2766,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));
|
||||
@@ -2781,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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -180,26 +180,19 @@ public final class AbilityFactory {
|
||||
}
|
||||
|
||||
public static Cost parseAbilityCost(final CardState state, Map<String, String> mapParams, AbilityRecordType type) {
|
||||
Cost abCost = null;
|
||||
if (type != AbilityRecordType.SubAbility) {
|
||||
String cost = mapParams.get("Cost");
|
||||
if (cost == null) {
|
||||
if (type == AbilityRecordType.Spell) {
|
||||
SpellAbility firstAbility = state.getFirstAbility();
|
||||
if (firstAbility != null && firstAbility.isSpell()) {
|
||||
// TODO might remove when Enchant Keyword is refactored
|
||||
System.err.println(state.getName() + " already has Spell using mana cost");
|
||||
}
|
||||
// for a Spell if no Cost is used, use the card states ManaCost
|
||||
abCost = new Cost(state.getManaCost(), false);
|
||||
} else {
|
||||
throw new RuntimeException("AbilityFactory : getAbility -- no Cost in " + state.getName());
|
||||
}
|
||||
} else {
|
||||
abCost = new Cost(cost, type == AbilityRecordType.Ability);
|
||||
}
|
||||
if (type == AbilityRecordType.SubAbility) {
|
||||
return null;
|
||||
}
|
||||
String cost = mapParams.get("Cost");
|
||||
if (cost != null) {
|
||||
return new Cost(cost, type == AbilityRecordType.Ability);
|
||||
}
|
||||
if (type == AbilityRecordType.Spell) {
|
||||
// for a Spell if no Cost is used, use the card states ManaCost
|
||||
return new Cost(state.getManaCost(), false);
|
||||
} else {
|
||||
throw new RuntimeException("AbilityFactory : getAbility -- no Cost in " + state.getName());
|
||||
}
|
||||
return abCost;
|
||||
}
|
||||
|
||||
public static SpellAbility getAbility(AbilityRecordType type, ApiType api, Map<String, String> mapParams,
|
||||
@@ -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"),
|
||||
|
||||
@@ -1366,7 +1366,7 @@ public class AbilityUtils {
|
||||
|
||||
// do blessing there before condition checks
|
||||
if (source.hasKeyword(Keyword.ASCEND) && controller.getZone(ZoneType.Battlefield).size() >= 10) {
|
||||
controller.setBlessing(true);
|
||||
controller.setBlessing(true, source.getSetCode());
|
||||
}
|
||||
|
||||
if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) {
|
||||
@@ -1621,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) {
|
||||
@@ -1786,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);
|
||||
}
|
||||
@@ -1809,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")) {
|
||||
@@ -1856,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);
|
||||
}
|
||||
@@ -1956,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);
|
||||
@@ -2217,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);
|
||||
}
|
||||
|
||||
@@ -2226,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(" ");
|
||||
@@ -2268,6 +2272,9 @@ public class AbilityUtils {
|
||||
if (sq[0].equals("Delirium")) {
|
||||
return doXMath(calculateAmount(c, sq[player.hasDelirium() ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
if (sq[0].equals("MaxSpeed")) {
|
||||
return doXMath(calculateAmount(c, sq[player.maxSpeed() ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
if (sq[0].equals("FatefulHour")) {
|
||||
return doXMath(calculateAmount(c, sq[player.getLife() <= 5 ? 1 : 2], ctb), expr, c, ctb);
|
||||
}
|
||||
@@ -2317,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);
|
||||
}
|
||||
@@ -2402,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;
|
||||
@@ -2469,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());
|
||||
@@ -2699,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;
|
||||
@@ -2775,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);
|
||||
}
|
||||
@@ -2930,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("_");
|
||||
@@ -3426,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++;
|
||||
@@ -3460,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();
|
||||
@@ -3788,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) {
|
||||
@@ -3817,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");
|
||||
sb.append(" next end step.");
|
||||
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")) {
|
||||
@@ -767,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());
|
||||
@@ -782,7 +800,7 @@ public abstract class SpellAbilityEffect {
|
||||
lki = null;
|
||||
}
|
||||
|
||||
return new GameCommand() {
|
||||
GameCommand gc = new GameCommand() {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -837,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) {
|
||||
@@ -897,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)) {
|
||||
@@ -997,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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -165,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
|
||||
@@ -252,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class BecomeMonarchEffect extends SpellAbilityEffect {
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
// TODO: improve ai and fix corner cases
|
||||
final String set = sa.getHostCard().getSetCode();
|
||||
final String set = sa.getOriginalHost().getSetCode();
|
||||
|
||||
for (final Player p : getTargetPlayers(sa)) {
|
||||
if (!p.isInGame()) {
|
||||
|
||||
@@ -186,10 +186,12 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
|
||||
|
||||
triggerList.triggerChangesZoneAll(game, sa);
|
||||
|
||||
if (sa.hasParam("Duration")) {
|
||||
addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, sa));
|
||||
if (sa.hasParam("AtEOT") && !triggerList.isEmpty()) {
|
||||
registerDelayedTrigger(sa, sa.getParam("AtEOT"), triggerList.allCards());
|
||||
}
|
||||
|
||||
changeZoneUntilCommand(triggerList, sa);
|
||||
|
||||
// CR 701.20d If an effect would cause a player to shuffle a set of objects into a library,
|
||||
// that library is shuffled even if there are no objects in that set.
|
||||
if (sa.hasParam("Shuffle")) {
|
||||
|
||||
@@ -663,9 +663,24 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
}
|
||||
if (sa.isKeyword(Keyword.UNEARTH) && movedCard.isInPlay()) {
|
||||
movedCard.setUnearthed(true);
|
||||
movedCard.addChangedCardKeywords(Lists.newArrayList("Haste"), null, false, game.getNextTimestamp(), null);
|
||||
|
||||
final Card eff = createEffect(sa, sa.getActivatingPlayer(), "Unearth Effect", hostCard.getImageKey());
|
||||
|
||||
// It gains haste.
|
||||
String s = "Mode$ Continuous | Affected$ Card.IsRemembered | EffectZone$ Command | AddKeyword$ Haste";
|
||||
eff.addStaticAbility(s);
|
||||
|
||||
// If it would leave the battlefield, exile it instead of putting it anywhere else.
|
||||
addLeaveBattlefieldReplacement(eff, "Exile");
|
||||
|
||||
eff.addRemembered(movedCard);
|
||||
|
||||
movedCard.addLeavesPlayCommand(exileEffectCommand(game, eff));
|
||||
|
||||
game.getAction().moveToCommand(eff, sa);
|
||||
|
||||
// Exile it at the beginning of the next end step.
|
||||
registerDelayedTrigger(sa, "Exile", Lists.newArrayList(movedCard));
|
||||
addLeaveBattlefieldReplacement(movedCard, sa, "Exile");
|
||||
}
|
||||
if (sa.hasParam("LeaveBattlefield")) {
|
||||
addLeaveBattlefieldReplacement(movedCard, sa, sa.getParam("LeaveBattlefield"));
|
||||
@@ -732,8 +747,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
if (sa.hasParam("ForetoldCost")) {
|
||||
movedCard.setForetoldCostByEffect(true);
|
||||
}
|
||||
// look at the exiled card
|
||||
movedCard.addMayLookTemp(activator);
|
||||
}
|
||||
// look at the exiled card
|
||||
if (sa.hasParam("WithMayLook") || sa.hasParam("Foretold")) {
|
||||
movedCard.addMayLookFaceDownExile(activator);
|
||||
}
|
||||
|
||||
// CR 400.7k
|
||||
@@ -817,9 +834,8 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
if (sa.hasParam("AtEOT") && !triggerList.isEmpty()) {
|
||||
registerDelayedTrigger(sa, sa.getParam("AtEOT"), triggerList.allCards());
|
||||
}
|
||||
if ("UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
|
||||
addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, sa));
|
||||
}
|
||||
|
||||
changeZoneUntilCommand(triggerList, sa);
|
||||
|
||||
// might set after card is moved again if something has changed
|
||||
if (destination.equals(ZoneType.Exile)) {
|
||||
@@ -1024,6 +1040,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
|
||||
handleCastWhileSearching(fetchList, decider);
|
||||
}
|
||||
if (sa.hasParam("RememberSearched")) {
|
||||
source.addRemembered(player);
|
||||
}
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(decider);
|
||||
runParams.put(AbilityKey.Target, player);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.SearchedLibrary, runParams, false);
|
||||
@@ -1129,7 +1148,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we're choosing multiple cards, only need to show the reveal dialog the first time through.
|
||||
boolean shouldReveal = (i == 0);
|
||||
Card c = null;
|
||||
@@ -1364,8 +1382,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
if (sa.hasParam("ForetoldCost")) {
|
||||
movedCard.setForetoldCostByEffect(true);
|
||||
}
|
||||
// look at the exiled card
|
||||
movedCard.addMayLookTemp(sa.getActivatingPlayer());
|
||||
}
|
||||
|
||||
// look at the exiled card
|
||||
if (sa.hasParam("WithMayLook") || sa.hasParam("Foretold")) {
|
||||
movedCard.addMayLookFaceDownExile(sa.getActivatingPlayer());
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -1461,9 +1482,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
}
|
||||
triggerList.triggerChangesZoneAll(game, sa);
|
||||
|
||||
if ("UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
|
||||
addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, sa));
|
||||
}
|
||||
changeZoneUntilCommand(triggerList, sa);
|
||||
}
|
||||
|
||||
private void handleCastWhileSearching(final CardCollection fetchList, final Player decider) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -45,7 +46,7 @@ public class CharmEffect extends SpellAbilityEffect {
|
||||
choices.removeAll(toRemove);
|
||||
}
|
||||
|
||||
int indx = 0;
|
||||
int indx = 1;
|
||||
// set CharmOrder
|
||||
for (AbilitySub sub : choices) {
|
||||
sub.setSVar("CharmOrder", Integer.toString(indx));
|
||||
@@ -89,12 +90,13 @@ public class CharmEffect extends SpellAbilityEffect {
|
||||
boolean limit = sa.hasParam("ActivationLimit");
|
||||
boolean gameLimit = sa.hasParam("GameActivationLimit");
|
||||
boolean oppChooses = "Opponent".equals(sa.getParam("Chooser"));
|
||||
boolean spree = sa.hasParam("Spree");
|
||||
boolean spree = source.hasKeyword(Keyword.SPREE);
|
||||
boolean tiered = source.hasKeyword(Keyword.TIERED);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(sa.getCostDescription());
|
||||
|
||||
if (!spree) {
|
||||
if (!spree && !tiered) {
|
||||
sb.append(oppChooses ? "An opponent chooses " : "Choose ");
|
||||
if (isX) {
|
||||
sb.append(sa.hasParam("MinCharmNum") && min == 0 ? "up to " : "").append("X");
|
||||
@@ -163,7 +165,7 @@ public class CharmEffect extends SpellAbilityEffect {
|
||||
if (!includeChosen) {
|
||||
sb.append(num == 1 ? " mode." : " modes.");
|
||||
} else if (!list.isEmpty()) {
|
||||
if (!spree) {
|
||||
if (!spree && !tiered) {
|
||||
if (!repeat && !additionalDesc && !limit && !gameLimit) {
|
||||
sb.append(" \u2014");
|
||||
}
|
||||
@@ -171,7 +173,10 @@ public class CharmEffect extends SpellAbilityEffect {
|
||||
}
|
||||
for (AbilitySub sub : list) {
|
||||
if (spree) {
|
||||
sb.append("+ " + new Cost(sub.getParam("SpreeCost"), false).toSimpleString() + " \u2014 ");
|
||||
sb.append("+ " + new Cost(sub.getParam("ModeCost"), false).toSimpleString() + " \u2014 ");
|
||||
} else if (tiered) {
|
||||
sb.append("\u2022 ").append(sub.getParam("PrecostDesc")).append(" \u2014 ");
|
||||
sb.append(new Cost(sub.getParam("ModeCost"), false).toSimpleString() + " \u2014 ");
|
||||
} else if (sub.hasParam("Pawprint")) {
|
||||
sb.append(StringUtils.repeat("{P}", Integer.parseInt(sub.getParam("Pawprint"))) + " \u2014 ");
|
||||
} else {
|
||||
@@ -271,10 +276,14 @@ public class CharmEffect extends SpellAbilityEffect {
|
||||
// Sort Chosen by SA order
|
||||
chosen.sort(Comparator.comparingInt(o -> o.getSVarInt("CharmOrder")));
|
||||
|
||||
int indx = 1;
|
||||
for (AbilitySub sub : chosen) {
|
||||
// Clone the chosen, just in case the same subAb gets chosen multiple times
|
||||
AbilitySub clone = (AbilitySub)sub.copy(sa.getActivatingPlayer());
|
||||
|
||||
clone.setSVar("CharmOrder", Integer.toString(indx));
|
||||
indx++;
|
||||
|
||||
// make StackDescription be the SpellDescription if it doesn't already have one
|
||||
if (!clone.hasParam("StackDescription")) {
|
||||
clone.putParam("StackDescription", "SpellDescription");
|
||||
|
||||
@@ -103,7 +103,7 @@ public class ChooseCardEffect extends SpellAbilityEffect {
|
||||
CardCollectionView pChoices = choices;
|
||||
CardCollection chosen = new CardCollection();
|
||||
if (!p.isInGame()) {
|
||||
p = getNewChooser(sa, activator, p);
|
||||
p = getNewChooser(sa, p);
|
||||
}
|
||||
if (sa.hasParam("ControlledByPlayer")) {
|
||||
final String param = sa.getParam("ControlledByPlayer");
|
||||
@@ -118,16 +118,9 @@ public class ChooseCardEffect extends SpellAbilityEffect {
|
||||
}
|
||||
boolean dontRevealToOwner = true;
|
||||
if (sa.hasParam("EachBasicType")) {
|
||||
// Get all lands,
|
||||
List<Card> land = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS);
|
||||
String eachBasic = sa.getParam("EachBasicType");
|
||||
if (eachBasic.equals("Controlled")) {
|
||||
land = CardLists.filterControlledBy(land, p);
|
||||
}
|
||||
|
||||
// Choose one of each BasicLand given special place
|
||||
for (final String type : CardType.getBasicTypes()) {
|
||||
final CardCollectionView cl = CardLists.getType(land, type);
|
||||
final CardCollectionView cl = CardLists.getType(pChoices, type);
|
||||
if (!cl.isEmpty()) {
|
||||
final String prompt = Localizer.getInstance().getMessage("lblChoose") + " " + Lang.nounWithAmount(1, type);
|
||||
Card c = p.getController().chooseSingleEntityForEffect(cl, sa, prompt, false, null);
|
||||
@@ -138,7 +131,7 @@ public class ChooseCardEffect extends SpellAbilityEffect {
|
||||
}
|
||||
} else if (sa.hasParam("ChooseEach")) {
|
||||
final String s = sa.getParam("ChooseEach");
|
||||
final String[] types = s.equals("Party") ? new String[]{"Cleric","Thief","Warrior","Wizard"}
|
||||
final String[] types = s.equals("Party") ? new String[]{"Cleric","Rogue","Warrior","Wizard"}
|
||||
: s.split(" & ");
|
||||
for (final String type : types) {
|
||||
CardCollection valids = CardLists.filter(pChoices, CardPredicates.isType(type));
|
||||
@@ -291,11 +284,9 @@ public class ChooseCardEffect extends SpellAbilityEffect {
|
||||
allChosen.addAll(chosen);
|
||||
}
|
||||
if (sa.hasParam("Reveal") && sa.hasParam("Secretly")) {
|
||||
for (final Player p : tgtPlayers) {
|
||||
game.getAction().reveal(allChosen, p, true, revealTitle ?
|
||||
sa.getParam("RevealTitle") : Localizer.getInstance().getMessage("lblChosenCards") + " ",
|
||||
!revealTitle);
|
||||
}
|
||||
game.getAction().revealTo(allChosen, game.getPlayers(), revealTitle ?
|
||||
sa.getParam("RevealTitle") : Localizer.getInstance().getMessage("lblChosenCards") + " ",
|
||||
!revealTitle);
|
||||
}
|
||||
host.setChosenCards(allChosen);
|
||||
if (sa.hasParam("ForgetOtherRemembered")) {
|
||||
|
||||
@@ -90,7 +90,7 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
|
||||
} else {
|
||||
chosen = p.getController().chooseCardName(sa, faces, message);
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
// use CardFace because you might name a alternate names
|
||||
Predicate<ICardFace> cpp = x -> true;
|
||||
if (sa.hasParam("ValidCards")) {
|
||||
@@ -112,8 +112,7 @@ public class ChooseCardNameEffect extends SpellAbilityEffect {
|
||||
}
|
||||
if (randomChoice) {
|
||||
chosen = StaticData.instance().getCommonCards().streamAllFaces()
|
||||
.filter(cpp).collect(StreamUtil.random()).get()
|
||||
.getName();
|
||||
.filter(cpp).collect(StreamUtil.random()).map(ICardFace::getName).orElse("");
|
||||
} else {
|
||||
chosen = p.getController().chooseCardName(sa, cpp, valid, message);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.MagicColor;
|
||||
import forge.deck.DeckRecognizer;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.Aggregates;
|
||||
@@ -41,6 +44,13 @@ public class ChooseColorEffect extends SpellAbilityEffect {
|
||||
String[] restrictedChoices = sa.getParam("Choices").split(",");
|
||||
colorChoices = Arrays.asList(restrictedChoices);
|
||||
}
|
||||
if (sa.hasParam("ColorsFrom")) {
|
||||
ColorSet cs = CardUtil.getColorsFromCards(AbilityUtils.getDefinedCards(card, sa.getParam("ColorsFrom"), sa));
|
||||
if (cs.isColorless()) {
|
||||
return;
|
||||
}
|
||||
colorChoices = cs.stream().map(Object::toString).collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
if (sa.hasParam("Exclude")) {
|
||||
for (String s : sa.getParam("Exclude").split(",")) {
|
||||
colorChoices.remove(s);
|
||||
@@ -49,24 +59,22 @@ public class ChooseColorEffect extends SpellAbilityEffect {
|
||||
|
||||
for (Player p : getTargetPlayers(sa)) {
|
||||
if (!p.isInGame()) {
|
||||
p = getNewChooser(sa, sa.getActivatingPlayer(), p);
|
||||
p = getNewChooser(sa, p);
|
||||
}
|
||||
List<String> chosenColors = new ArrayList<>();
|
||||
int cntMin = sa.hasParam("TwoColors") ? 2 : 1;
|
||||
int cntMin = sa.hasParam("UpTo") ? 0 : sa.hasParam("TwoColors") ? 2 : 1;
|
||||
int cntMax = sa.hasParam("TwoColors") ? 2 : sa.hasParam("OrColors") ? colorChoices.size() : 1;
|
||||
String prompt = null;
|
||||
if (cntMax == 1) {
|
||||
prompt = Localizer.getInstance().getMessage("lblChooseAColor");
|
||||
} else {
|
||||
if (cntMax > cntMin) {
|
||||
if (cntMax >= MagicColor.NUMBER_OR_COLORS) {
|
||||
prompt = Localizer.getInstance().getMessage("lblAtLastChooseNumColors", Lang.getNumeral(cntMin));
|
||||
} else {
|
||||
prompt = Localizer.getInstance().getMessage("lblChooseSpecifiedRangeColors", Lang.getNumeral(cntMin), Lang.getNumeral(cntMax));
|
||||
}
|
||||
} else if (cntMax > cntMin) {
|
||||
if (cntMax >= MagicColor.NUMBER_OR_COLORS) {
|
||||
prompt = Localizer.getInstance().getMessage("lblAtLastChooseNumColors", Lang.getNumeral(cntMin));
|
||||
} else {
|
||||
prompt = Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(cntMax));
|
||||
prompt = Localizer.getInstance().getMessage("lblChooseSpecifiedRangeColors", Lang.getNumeral(cntMin), Lang.getNumeral(cntMax));
|
||||
}
|
||||
} else {
|
||||
prompt = Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(cntMax));
|
||||
}
|
||||
Player noNotify = p;
|
||||
if (sa.hasParam("Random")) {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
public class ChooseCompanionEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
// This isn't a real effect. Just need it for AI choosing.
|
||||
}
|
||||
}
|
||||
@@ -62,23 +62,22 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
|
||||
|
||||
for (Player p : getDefinedPlayersOrTargeted(sa)) {
|
||||
if (!p.isInGame()) {
|
||||
p = getNewChooser(sa, sa.getActivatingPlayer(), p);
|
||||
p = getNewChooser(sa, p);
|
||||
}
|
||||
// determine if any of the choices are not valid
|
||||
List<SpellAbility> saToRemove = Lists.newArrayList();
|
||||
|
||||
// determine if any of the choices are not valid
|
||||
List<SpellAbility> availableSA = Lists.newArrayList(abilities);
|
||||
for (SpellAbility saChoice : abilities) {
|
||||
if (saChoice.getRestrictions() != null && !saChoice.getRestrictions().checkOtherRestrictions(host, saChoice, sa.getActivatingPlayer())) {
|
||||
saToRemove.add(saChoice);
|
||||
availableSA.remove(saChoice);
|
||||
} else if (saChoice.hasParam("UnlessCost")) {
|
||||
// generic check for if the cost can be paid
|
||||
Cost unlessCost = new Cost(saChoice.getParam("UnlessCost"), false);
|
||||
if (!unlessCost.canPay(sa, p, true)) {
|
||||
saToRemove.add(saChoice);
|
||||
availableSA.remove(saChoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
abilities.removeAll(saToRemove);
|
||||
|
||||
List<SpellAbility> chosenSAs = Lists.newArrayList();
|
||||
String prompt = sa.getParamOrDefault("ChoicePrompt", "Choose");
|
||||
@@ -86,7 +85,7 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
|
||||
|
||||
if (sa.hasParam("AtRandom")) {
|
||||
random = true;
|
||||
chosenSAs = Aggregates.random(abilities, amount);
|
||||
chosenSAs = Aggregates.random(availableSA, amount);
|
||||
|
||||
int i = 0;
|
||||
while (sa.getParam("AtRandom").equals("Urza") && i < chosenSAs.size()) {
|
||||
@@ -99,8 +98,8 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
|
||||
chosenSAs.set(i, Aggregates.random(abilities));
|
||||
}
|
||||
}
|
||||
} else if (!abilities.isEmpty()) {
|
||||
chosenSAs = p.getController().chooseSpellAbilitiesForEffect(abilities, sa, prompt, amount, ImmutableMap.of());
|
||||
} else if (!availableSA.isEmpty()) {
|
||||
chosenSAs = p.getController().chooseSpellAbilitiesForEffect(availableSA, sa, prompt, amount, ImmutableMap.of());
|
||||
}
|
||||
|
||||
List<Object> oldRem = Lists.newArrayList(IterableUtil.filter(host.getRemembered(), Player.class));
|
||||
@@ -117,7 +116,6 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
|
||||
} else if (secretly) {
|
||||
if (record.length() > 0) record.append("\r\n");
|
||||
record.append(Localizer.getInstance().getMessage("lblPlayerChooseValue", p, chosenValue));
|
||||
|
||||
}
|
||||
if (sa.hasParam("SetChosenMode")) {
|
||||
sa.getHostCard().setChosenMode(chosenValue);
|
||||
|
||||
@@ -122,7 +122,7 @@ public class ChooseTypeEffect extends SpellAbilityEffect {
|
||||
}
|
||||
}
|
||||
|
||||
if (validTypes.isEmpty() && sa.hasParam("Note")) {
|
||||
if (validTypes.isEmpty() && sa.hasParam("TypesFromDefined")) {
|
||||
// OK to end up with no choices/have nothing new to note
|
||||
} else if (!validTypes.isEmpty()) {
|
||||
for (final Player p : tgtPlayers) {
|
||||
|
||||
@@ -129,6 +129,8 @@ public class CloneEffect extends SpellAbilityEffect {
|
||||
cloneTargets.remove(cardToCopy);
|
||||
}
|
||||
|
||||
final long ts = game.getNextTimestamp();
|
||||
|
||||
for (Card tgtCard : cloneTargets) {
|
||||
if (sa.hasParam("CloneZone") &&
|
||||
!tgtCard.isInZone(ZoneType.smartValueOf(sa.getParam("CloneZone")))) {
|
||||
@@ -141,7 +143,6 @@ public class CloneEffect extends SpellAbilityEffect {
|
||||
|
||||
game.getTriggerHandler().clearActiveTriggers(tgtCard, null);
|
||||
|
||||
final long ts = game.getNextTimestamp();
|
||||
tgtCard.addCloneState(CardFactory.getCloneStates(cardToCopy, tgtCard, sa), ts);
|
||||
tgtCard.updateRooms();
|
||||
|
||||
@@ -199,7 +200,7 @@ public class CloneEffect extends SpellAbilityEffect {
|
||||
tgtCard.addRemembered(cardToCopy);
|
||||
}
|
||||
// spire
|
||||
tgtCard.setChosenColorID(cardToCopy.getChosenColorID());
|
||||
tgtCard.setMarkedColors(cardToCopy.getMarkedColors());
|
||||
|
||||
game.fireEvent(new GameEventCardStatsChanged(tgtCard));
|
||||
}
|
||||
|
||||
@@ -132,10 +132,6 @@ public class ControlGainEffect extends SpellAbilityEffect {
|
||||
tgtCards = getDefinedCards(sa);
|
||||
}
|
||||
|
||||
if (tgtCards != null & sa.hasParam("ControlledByTarget")) {
|
||||
tgtCards = CardLists.filterControlledBy(tgtCards, getTargetPlayers(sa));
|
||||
}
|
||||
|
||||
// check for lose control criteria right away
|
||||
if (lose != null && lose.contains("LeavesPlay") && !source.isInPlay()) {
|
||||
return;
|
||||
@@ -170,7 +166,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
|
||||
tgtC.addTempController(newController, tStamp);
|
||||
|
||||
if (bUntap) {
|
||||
if (tgtC.untap(true)) untapped.add(tgtC);
|
||||
if (tgtC.untap()) untapped.add(tgtC);
|
||||
}
|
||||
|
||||
if (keywords != null) {
|
||||
|
||||
@@ -26,10 +26,8 @@ public class ControlPlayerEffect extends SpellAbilityEffect {
|
||||
@SuppressWarnings("serial")
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Player activator = sa.getActivatingPlayer();
|
||||
final Game game = activator.getGame();
|
||||
final Player controller = sa.hasParam("Controller") ? AbilityUtils.getDefinedPlayers(
|
||||
sa.getHostCard(), sa.getParam("Controller"), sa).get(0) : activator;
|
||||
final Player controller = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Controller"), sa).get(0);
|
||||
final Game game = controller.getGame();
|
||||
|
||||
for (final Player pTarget: getTargetPlayers(sa)) {
|
||||
// before next untap gain control
|
||||
|
||||
@@ -32,7 +32,6 @@ import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.item.PaperCard;
|
||||
import forge.util.PredicateString.StringOp;
|
||||
|
||||
public class CopyPermanentEffect extends TokenEffectBase {
|
||||
|
||||
@@ -185,19 +184,15 @@ public class CopyPermanentEffect extends TokenEffectBase {
|
||||
System.err.println("Copying random permanent(s): " + tgtCards.toString());
|
||||
}
|
||||
} else if (sa.hasParam("DefinedName")) {
|
||||
List<PaperCard> cards = Lists.newArrayList(StaticData.instance().getCommonCards().getUniqueCards());
|
||||
String name = sa.getParam("DefinedName");
|
||||
if (name.equals("NamedCard")) {
|
||||
if (!host.getNamedCard().isEmpty()) {
|
||||
name = host.getNamedCard();
|
||||
}
|
||||
}
|
||||
|
||||
Predicate<PaperCard> cpp = PaperCardPredicates.fromRules(CardRulesPredicates.name(StringOp.EQUALS, name));
|
||||
cards = Lists.newArrayList(IterableUtil.filter(cards, cpp));
|
||||
|
||||
if (!cards.isEmpty()) {
|
||||
tgtCards.add(Card.fromPaperCard(cards.get(0), controller));
|
||||
PaperCard pc = StaticData.instance().getCommonCards().getUniqueByName(name);
|
||||
if (pc != null) {
|
||||
tgtCards.add(Card.fromPaperCard(pc, controller));
|
||||
}
|
||||
} else if (sa.hasParam("Choices")) {
|
||||
Player chooser = activator;
|
||||
@@ -314,7 +309,7 @@ public class CopyPermanentEffect extends TokenEffectBase {
|
||||
}
|
||||
}
|
||||
// spire
|
||||
copy.setChosenColorID(original.getChosenColorID());
|
||||
copy.setMarkedColors(original.getMarkedColors());
|
||||
|
||||
copy.setTokenSpawningAbility(sa);
|
||||
copy.setGamePieceType(GamePieceType.TOKEN);
|
||||
|
||||
@@ -13,7 +13,6 @@ import forge.game.card.CardFactory;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbilityCantBeCopied;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.*;
|
||||
import forge.util.collect.FCollection;
|
||||
@@ -66,7 +65,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
|
||||
|
||||
List<SpellAbility> tgtSpells = getTargetSpells(sa);
|
||||
|
||||
tgtSpells.removeIf(tgtSA -> StaticAbilityCantBeCopied.cantBeCopied(tgtSA.getHostCard()));
|
||||
tgtSpells.removeIf(SpellAbility::cantBeCopied);
|
||||
|
||||
if (tgtSpells.isEmpty() || amount == 0) {
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import forge.card.MagicColor;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.GameEntityCounterTable;
|
||||
@@ -631,7 +632,19 @@ public class CountersPutEffect extends SpellAbilityEffect {
|
||||
return;
|
||||
}
|
||||
}
|
||||
resolvePerType(sa, placer, counterType, counterAmount, table, true);
|
||||
if (sa.hasParam("ForColor")) {
|
||||
Iterable<String> oldColors = card.getChosenColors();
|
||||
for (String color : MagicColor.Constant.ONLY_COLORS) {
|
||||
card.setChosenColors(Lists.newArrayList(color));
|
||||
if (sa.getOriginalParam("ChoiceTitle") != null) {
|
||||
sa.getMapParams().put("ChoiceTitle", sa.getOriginalParam("ChoiceTitle").replace("chosenColor", color));
|
||||
}
|
||||
resolvePerType(sa, placer, counterType, counterAmount, table, true);
|
||||
}
|
||||
card.setChosenColors(Lists.newArrayList(oldColors));
|
||||
} else {
|
||||
resolvePerType(sa, placer, counterType, counterAmount, table, true);
|
||||
}
|
||||
}
|
||||
|
||||
table.replaceCounterEffect(game, sa, true);
|
||||
|
||||
@@ -65,8 +65,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
|
||||
ctype = CounterType.getType(sa.getParam("CounterType"));
|
||||
}
|
||||
|
||||
final Player pl = !sa.hasParam("DefinedPlayer") ? sa.getActivatingPlayer() :
|
||||
AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
|
||||
final Player pl = AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
|
||||
final boolean eachExisting = sa.hasParam("EachExistingCounter");
|
||||
|
||||
GameEntityCounterTable table = new GameEntityCounterTable();
|
||||
@@ -79,7 +78,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
|
||||
if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) {
|
||||
continue;
|
||||
}
|
||||
if (!eachExisting && sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null,
|
||||
if (sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null,
|
||||
Localizer.getInstance().getMessage("lblWouldYouLikePutRemoveCounters", ctype.getName(),
|
||||
CardTranslation.getTranslatedName(gameCard.getName())), null)) {
|
||||
continue;
|
||||
@@ -114,8 +113,6 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
|
||||
String prompt = Localizer.getInstance().getMessage("lblSelectCounterTypeToAddOrRemove");
|
||||
CounterType chosenType = pc.chooseCounterType(list, sa, prompt, params);
|
||||
|
||||
params.put("CounterType", chosenType);
|
||||
prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " ";
|
||||
boolean putCounter;
|
||||
if (sa.hasParam("RemoveConditionSVar")) {
|
||||
final Card host = sa.getHostCard();
|
||||
@@ -137,6 +134,8 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
|
||||
} else if (!canReceive && canRemove) {
|
||||
putCounter = false;
|
||||
} else {
|
||||
params.put("CounterType", chosenType);
|
||||
prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " ";
|
||||
putCounter = pc.chooseBinary(sa, prompt, BinaryChoiceType.AddOrRemove, params);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user