Compare commits
1417 Commits
forge-2.0.
...
01da2df6f0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01da2df6f0 | ||
|
|
ec84a3f137 | ||
|
|
02e2e713ed | ||
|
|
a21666040c | ||
|
|
fe2bd016b6 | ||
|
|
0bb67ec8d0 | ||
|
|
6d188c09ca | ||
|
|
f25d8c58d9 | ||
|
|
f9c5fc7317 | ||
|
|
02031e159d | ||
|
|
dd35f97035 | ||
|
|
ee045d854d | ||
|
|
65e4fa7fd7 | ||
|
|
be77cc1147 | ||
|
|
53683f2496 | ||
|
|
f0b937a5f0 | ||
|
|
def3fa5d23 | ||
|
|
e33ddee5bf | ||
|
|
15fca4243d | ||
|
|
6bf288af38 | ||
|
|
586e30af09 | ||
|
|
ab3c66fd0a | ||
|
|
ab4c91dd2d | ||
|
|
54b8094211 | ||
|
|
1445fc2621 | ||
|
|
d53d9a4ded | ||
|
|
650bf09720 | ||
|
|
759c592b00 | ||
|
|
7d53fa88cf | ||
|
|
0de568ce71 | ||
|
|
9f3120e2a9 | ||
|
|
fc89ca7093 | ||
|
|
4c44b6b337 | ||
|
|
3a1c3bf632 | ||
|
|
e5483f03c5 | ||
|
|
45e9bee6e5 | ||
|
|
107aa63c07 | ||
|
|
106afcc200 | ||
|
|
2fc1c120f3 | ||
|
|
8a7f28858f | ||
|
|
149c14eea1 | ||
|
|
f6c29f54b3 | ||
|
|
61f197b93c | ||
|
|
124c595af5 | ||
|
|
1cf6633b13 | ||
|
|
c8fd36682f | ||
|
|
4673ec23bd | ||
|
|
b1ba531c64 | ||
|
|
2fa753d583 | ||
|
|
50c55a1893 | ||
|
|
fdc65ffd85 | ||
|
|
954031cdb2 | ||
|
|
364845ab4e | ||
|
|
5fc1d4214b | ||
|
|
2ee356461c | ||
|
|
bfb0c9d41f | ||
|
|
b272612c41 | ||
|
|
a6e8ac5ccf | ||
|
|
136940a16c | ||
|
|
76b9da7e22 | ||
|
|
f291e9ea59 | ||
|
|
42a14cfad0 | ||
|
|
ed3bc665e2 | ||
|
|
0eb3062c64 | ||
|
|
4063ac55e6 | ||
|
|
4cb581152e | ||
|
|
43525ce657 | ||
|
|
8c2c32a3d8 | ||
|
|
7ea1c51990 | ||
|
|
b0ee850d0f | ||
|
|
21a293a458 | ||
|
|
e67365e0d6 | ||
|
|
8f12828617 | ||
|
|
1cb873a1da | ||
|
|
f9b6652c2a | ||
|
|
1188b6889a | ||
|
|
ea590f8574 | ||
|
|
6cc2dcf3f0 | ||
|
|
42357d5d6f | ||
|
|
21977e7dcd | ||
|
|
608a1c11ed | ||
|
|
79d1f6f1da | ||
|
|
2fc6935563 | ||
|
|
5efd7e6e7c | ||
|
|
a8d494d2d8 | ||
|
|
7367e93140 | ||
|
|
6f38479190 | ||
|
|
80a4a42575 | ||
|
|
24ef895cb7 | ||
|
|
472f94af66 | ||
|
|
a99c899eec | ||
|
|
d94fe1e958 | ||
|
|
27a638a32b | ||
|
|
a0a3546691 | ||
|
|
bbf5319815 | ||
|
|
5588268c74 | ||
|
|
b9008b7f12 | ||
|
|
32fb982d0d | ||
|
|
efbba093e0 | ||
|
|
4a616c8ac6 | ||
|
|
f64701d039 | ||
|
|
77dae2418f | ||
|
|
7e47a6ee2c | ||
|
|
1093984bec | ||
|
|
8ae1bcd2c5 | ||
|
|
f9150bea34 | ||
|
|
04186de397 | ||
|
|
4827e10bd4 | ||
|
|
da92a93d1d | ||
|
|
6b17b65084 | ||
|
|
fac5b6c253 | ||
|
|
6617e2627c | ||
|
|
17d361d62d | ||
|
|
05a81d00fd | ||
|
|
808ecde0c0 | ||
|
|
7fb7c62fbe | ||
|
|
6ddb3124ac | ||
|
|
c454253d48 | ||
|
|
fdbdffd874 | ||
|
|
010f917550 | ||
|
|
43cefed048 | ||
|
|
3d15f743c3 | ||
|
|
f05170c1a4 | ||
|
|
9147836f1a | ||
|
|
098c98570d | ||
|
|
8bb15f6bbc | ||
|
|
63002aba97 | ||
|
|
52adb2dc2b | ||
|
|
8478d3ce47 | ||
|
|
02c865765b | ||
|
|
327d60ed84 | ||
|
|
f8d883d91f | ||
|
|
1962a8d28b | ||
|
|
f3ecd51253 | ||
|
|
bceb3c4503 | ||
|
|
87977e836d | ||
|
|
7b8f69a022 | ||
|
|
a0442e7c3e | ||
|
|
845969bc34 | ||
|
|
4256d9ae1f | ||
|
|
3a956c4e20 | ||
|
|
590abbe8e6 | ||
|
|
29fe1266d7 | ||
|
|
6152933766 | ||
|
|
af947c9af5 | ||
|
|
7deb7b1ecc | ||
|
|
5a19c9ed7e | ||
|
|
bbd9fb7c37 | ||
|
|
4e17b4c9d2 | ||
|
|
caf8b0535f | ||
|
|
0ee3dc4e67 | ||
|
|
3847080221 | ||
|
|
f7c965de02 | ||
|
|
b844181c0b | ||
|
|
7f02c73090 | ||
|
|
f0f1f5a1d4 | ||
|
|
5a59113d78 | ||
|
|
6935f962d3 | ||
|
|
34002a1ed7 | ||
|
|
a22f28ef50 | ||
|
|
f6d33ed483 | ||
|
|
5fae32e048 | ||
|
|
ab1d423e28 | ||
|
|
1b8c87e578 | ||
|
|
bbf7a25979 | ||
|
|
31c2686307 | ||
|
|
04f454ae83 | ||
|
|
5b2a56bd4d | ||
|
|
5bbc86c1a1 | ||
|
|
a5765905d8 | ||
|
|
c4a3b5c545 | ||
|
|
81e1306cd7 | ||
|
|
39f6377f33 | ||
|
|
0af4be42c4 | ||
|
|
da8b77acf7 | ||
|
|
4cef049a1d | ||
|
|
8cd386b654 | ||
|
|
c1e87334cd | ||
|
|
3ae3e1b218 | ||
|
|
7acfe6c240 | ||
|
|
66a8e1f04f | ||
|
|
091c61829b | ||
|
|
a2b324c9ce | ||
|
|
4ab8c1414c | ||
|
|
46df4cdb0d | ||
|
|
183100fbdc | ||
|
|
50c7188e04 | ||
|
|
d46d7300b4 | ||
|
|
27a0a8f791 | ||
|
|
5a63250735 | ||
|
|
04fbe9fac3 | ||
|
|
31f8da5687 | ||
|
|
c0fe93ee30 | ||
|
|
a1042e0a1d | ||
|
|
cdbf10f1f7 | ||
|
|
bc6eed5c7d | ||
|
|
b90c14660c | ||
|
|
eeac0272ab | ||
|
|
dcdf2b6e7a | ||
|
|
dab01c9b86 | ||
|
|
74dc09b2e5 | ||
|
|
8dc9a18c4f | ||
|
|
a390284b8e | ||
|
|
1d97a4ec7e | ||
|
|
a21a61cb05 | ||
|
|
cd84663bed | ||
|
|
4d2b634e4f | ||
|
|
4eccfa8177 | ||
|
|
90ea109a17 | ||
|
|
a9c42fb340 | ||
|
|
f1a095fb56 | ||
|
|
4ed06fae87 | ||
|
|
0d020070a2 | ||
|
|
5d39efd9fc | ||
|
|
fd5555fdd6 | ||
|
|
6dd10be044 | ||
|
|
1b419a13e8 | ||
|
|
c0f5615885 | ||
|
|
c10b5706f1 | ||
|
|
9599d01d2c | ||
|
|
f11c2ad8a4 | ||
|
|
20479518da | ||
|
|
1ded8fe5da | ||
|
|
2d1fe88d32 | ||
|
|
7ca0c2b87b | ||
|
|
e4a83de6cf | ||
|
|
7321aac496 | ||
|
|
28dcdf6ddf | ||
|
|
5999909d32 | ||
|
|
0036c27470 | ||
|
|
7325443da5 | ||
|
|
c4c1388f8b | ||
|
|
f4e90e1d38 | ||
|
|
ab3b00cb65 | ||
|
|
a6cbbeef84 | ||
|
|
40629a72a9 | ||
|
|
f6bc6d6742 | ||
|
|
da52bc17e8 | ||
|
|
50b7543933 | ||
|
|
a4378a20e5 | ||
|
|
a6395373f7 | ||
|
|
54f41a28cd | ||
|
|
a2c798ba86 | ||
|
|
8f7a77354a | ||
|
|
b50cdea5df | ||
|
|
a7568b0845 | ||
|
|
e158b5966e | ||
|
|
bea5ffb0a8 | ||
|
|
ac492fac35 | ||
|
|
35e6268a95 | ||
|
|
90bd0c73d0 | ||
|
|
ea293a46b1 | ||
|
|
6ec6d64cb2 | ||
|
|
9bc6ab747d | ||
|
|
a72df6ad16 | ||
|
|
1105b3fcbd | ||
|
|
08239def86 | ||
|
|
cb7fc3df4e | ||
|
|
9b8020bb30 | ||
|
|
398917d010 | ||
|
|
10a68c27d5 | ||
|
|
ba4ee98c3b | ||
|
|
c2c3add52b | ||
|
|
1c7bc1c0d3 | ||
|
|
14f146f8d0 | ||
|
|
0cab03b96c | ||
|
|
8f5f2059a2 | ||
|
|
34323107c9 | ||
|
|
dbf9568866 | ||
|
|
7933893dcb | ||
|
|
415e2c12e1 | ||
|
|
930019db40 | ||
|
|
72139b523c | ||
|
|
6cf2f20cdc | ||
|
|
cebddb7f4b | ||
|
|
4a6275d282 | ||
|
|
bdeaf8cef1 | ||
|
|
40f6a5b472 | ||
|
|
f4bff30680 | ||
|
|
5f32c23dc5 | ||
|
|
ae7159297b | ||
|
|
cedfa68c16 | ||
|
|
1f235c5e71 | ||
|
|
9f8bb8ae4d | ||
|
|
cd3a26e434 | ||
|
|
d2d6b7bc53 | ||
|
|
052e72d8ea | ||
|
|
b902d516a7 | ||
|
|
79fd4a3f8d | ||
|
|
10d359e7d7 | ||
|
|
1cb9e78b21 | ||
|
|
d999c5e213 | ||
|
|
1f7862c970 | ||
|
|
cdc696c506 | ||
|
|
64897f1ab4 | ||
|
|
905cf52c5e | ||
|
|
3139d593a3 | ||
|
|
8f71a5b06e | ||
|
|
e9742a2292 | ||
|
|
a6c893693d | ||
|
|
327ee6853f | ||
|
|
3e7f98db6a | ||
|
|
e49021ffdd | ||
|
|
0c5ff17e94 | ||
|
|
2925e94cec | ||
|
|
02b7e408dc | ||
|
|
b61044abb5 | ||
|
|
f1114eeada | ||
|
|
b131e4eb04 | ||
|
|
8ba2ae4a24 | ||
|
|
ac93233e19 | ||
|
|
6b29e18a58 | ||
|
|
fd2d3c7bb9 | ||
|
|
a1d67cf844 | ||
|
|
2b64f82f98 | ||
|
|
43a1570601 | ||
|
|
db8e084332 | ||
|
|
c2fc877201 | ||
|
|
50f34442f6 | ||
|
|
1dbd358c19 | ||
|
|
bc3faa88c4 | ||
|
|
10618d4f31 | ||
|
|
47e873b4e9 | ||
|
|
acc9ead5b2 | ||
|
|
6125022c9e | ||
|
|
a0be5e25cb | ||
|
|
f2e3cdc111 | ||
|
|
7384aada40 | ||
|
|
1a8a4f63ae | ||
|
|
530d1efcd8 | ||
|
|
d8b1d76f42 | ||
|
|
830b145e88 | ||
|
|
51ed06d60a | ||
|
|
ac9b73935f | ||
|
|
fe2f49fbcc | ||
|
|
3d31494a1b | ||
|
|
ff42e730a2 | ||
|
|
a66fb8791c | ||
|
|
2ad5d20e83 | ||
|
|
4790394698 | ||
|
|
8b1c427809 | ||
|
|
c0b667c373 | ||
|
|
ca61627f5b | ||
|
|
c3e2a5b5ea | ||
|
|
0d26b499d3 | ||
|
|
bbbc5e0ee6 | ||
|
|
714d9442f1 | ||
|
|
d199765e6d | ||
|
|
e5156d8999 | ||
|
|
c0d5397541 | ||
|
|
444897d0f9 | ||
|
|
f2cb7956d3 | ||
|
|
e0c2a49c6b | ||
|
|
d64e5ebc13 | ||
|
|
9bea1bc717 | ||
|
|
bdf9573467 | ||
|
|
042eb4bf79 | ||
|
|
4b0337a836 | ||
|
|
9284775921 | ||
|
|
1671a3298e | ||
|
|
64e3193a80 | ||
|
|
3e6cd92f14 | ||
|
|
b7d4e0129e | ||
|
|
f76127dcea | ||
|
|
c7c0938936 | ||
|
|
9788a815a7 | ||
|
|
cce25a37c4 | ||
|
|
85812d819f | ||
|
|
a974352908 | ||
|
|
45e04cfef7 | ||
|
|
6a83cb89f7 | ||
|
|
494bd75572 | ||
|
|
b065e468b0 | ||
|
|
86aff59e40 | ||
|
|
748c4e1dd7 | ||
|
|
dfb776f526 | ||
|
|
ca5d2bb109 | ||
|
|
b42050a40f | ||
|
|
14220ae37f | ||
|
|
46900c3f49 | ||
|
|
cb2bf996d0 | ||
|
|
edbf6692f2 | ||
|
|
b5f16b83a0 | ||
|
|
ee4902a5ae | ||
|
|
0835817d8a | ||
|
|
15d4e1ac20 | ||
|
|
ecdce50f12 | ||
|
|
247fd7810e | ||
|
|
055cd40fb1 | ||
|
|
f1d448509e | ||
|
|
f2581855a4 | ||
|
|
1e4c7d4b00 | ||
|
|
2d80e44f70 | ||
|
|
a207bdbb51 | ||
|
|
c09ec2f410 | ||
|
|
c579df4c82 | ||
|
|
d39ab505df | ||
|
|
7756dfa5be | ||
|
|
c1b6f3126e | ||
|
|
11d45e0fa3 | ||
|
|
0504b41dbd | ||
|
|
b670cc3736 | ||
|
|
885300ba49 | ||
|
|
ff8fd51bf1 | ||
|
|
80475c6b62 | ||
|
|
88ea36523b | ||
|
|
3386d79cb1 | ||
|
|
f97429d1d4 | ||
|
|
3628d4e22a | ||
|
|
fffc9a4282 | ||
|
|
7cb32b0b25 | ||
|
|
cb13682737 | ||
|
|
9656a9fb4c | ||
|
|
2fd9d3ae14 | ||
|
|
1ea90f6655 | ||
|
|
37342910fa | ||
|
|
3a00727602 | ||
|
|
589d592997 | ||
|
|
83101f7f41 | ||
|
|
c7d24f1c01 | ||
|
|
2a678aa7e9 | ||
|
|
6a758ffc0a | ||
|
|
83421bac56 | ||
|
|
5234365cc1 | ||
|
|
76558a841c | ||
|
|
1338735000 | ||
|
|
44c33e2955 | ||
|
|
b8a5668db6 | ||
|
|
6a84a7a6f0 | ||
|
|
4811c9d726 | ||
|
|
7cea6fce1e | ||
|
|
46ba289329 | ||
|
|
2c9e5ee814 | ||
|
|
230eba687d | ||
|
|
5faf317aaf | ||
|
|
388448c94f | ||
|
|
230282b525 | ||
|
|
0f92bc18d4 | ||
|
|
dc4cee62dc | ||
|
|
ea4c8fbc72 | ||
|
|
068ea955d9 | ||
|
|
f898263bac | ||
|
|
a3147f5e26 | ||
|
|
a309eaee02 | ||
|
|
94b058ec07 | ||
|
|
46bf3348be | ||
|
|
7c4c820e92 | ||
|
|
ed3ea99e1a | ||
|
|
ec21a0c789 | ||
|
|
bfe0a78765 | ||
|
|
0abc224e1b | ||
|
|
fa3ce1c7f7 | ||
|
|
48a5046d93 | ||
|
|
9f9e68f34d | ||
|
|
d7c7aaa51e | ||
|
|
ffe883e7d9 | ||
|
|
b07a9944aa | ||
|
|
997d489a7e | ||
|
|
d181746679 | ||
|
|
693d6cd9e3 | ||
|
|
01d5bf3c21 | ||
|
|
29a630386f | ||
|
|
75c7938f1e | ||
|
|
110e885c67 | ||
|
|
548b448f0d | ||
|
|
a2e80ac0f3 | ||
|
|
238d426202 | ||
|
|
d6f585a80c | ||
|
|
223aeb1bff | ||
|
|
4bf02ea61f | ||
|
|
ea186ae4af | ||
|
|
3e4cd9e189 | ||
|
|
e35c193f92 | ||
|
|
e5443fc394 | ||
|
|
9998092c70 | ||
|
|
9f81e0cd34 | ||
|
|
5569f18053 | ||
|
|
40190b5442 | ||
|
|
a33905241d | ||
|
|
3a40b560f6 | ||
|
|
d79fb96770 | ||
|
|
f50fe2f89c | ||
|
|
a36deab334 | ||
|
|
28feff99d7 | ||
|
|
bab8791012 | ||
|
|
db20b27c9d | ||
|
|
087db3f757 | ||
|
|
db194fab97 | ||
|
|
0585ece2c1 | ||
|
|
1611559909 | ||
|
|
d6dee9575b | ||
|
|
6dd9a731fa | ||
|
|
cf5a8508d6 | ||
|
|
0fa7df090e | ||
|
|
86838d94f7 | ||
|
|
83c4db07ac | ||
|
|
b544f2dd00 | ||
|
|
079e6f04ac | ||
|
|
d04c541578 | ||
|
|
7cc2cf530d | ||
|
|
61a2c7cadb | ||
|
|
efbf2e1a9c | ||
|
|
9b8441d45b | ||
|
|
f8b7a0fb9a | ||
|
|
ab49e97797 | ||
|
|
c27a9d136f | ||
|
|
91241ee53c | ||
|
|
31e537f42b | ||
|
|
e550c307c2 | ||
|
|
b4f01b7ebb | ||
|
|
7744474f39 | ||
|
|
1365b82968 | ||
|
|
5bb532bf9d | ||
|
|
258b8c18a9 | ||
|
|
a66349d8a1 | ||
|
|
1933cf1863 | ||
|
|
0d342c778f | ||
|
|
f452b94cb8 | ||
|
|
9325794e2f | ||
|
|
192a64bbc3 | ||
|
|
deb8369f11 | ||
|
|
b24f536190 | ||
|
|
28ec24069c | ||
|
|
f27472d9bd | ||
|
|
780cc8ddbf | ||
|
|
4abc96c045 | ||
|
|
bee208f1d5 | ||
|
|
999e76e598 | ||
|
|
23555b9564 | ||
|
|
431827be35 | ||
|
|
b0dba74c6c | ||
|
|
ad0a690764 | ||
|
|
977f2c75b9 | ||
|
|
8313414f78 | ||
|
|
89955bf201 | ||
|
|
99d191901a | ||
|
|
1a306b3da3 | ||
|
|
9ff03cbd41 | ||
|
|
07a1dbc099 | ||
|
|
f24b3ea3b3 | ||
|
|
770cc72fcd | ||
|
|
8426a74900 | ||
|
|
8d98eda18d | ||
|
|
6e6509eaff | ||
|
|
c56ddee47d | ||
|
|
07985ac487 | ||
|
|
6c93491d7f | ||
|
|
c9b012c88a | ||
|
|
9b53144976 | ||
|
|
31020296d8 | ||
|
|
65fb3414d8 | ||
|
|
98fee0d86b | ||
|
|
becdadb279 | ||
|
|
26da0ab0d4 | ||
|
|
e1f4d755e0 | ||
|
|
efe7d67d9f | ||
|
|
3a5e11504a | ||
|
|
6657602b80 | ||
|
|
eeca33855d | ||
|
|
cdc63f35bb | ||
|
|
5a9ea8d260 | ||
|
|
5a363922bd | ||
|
|
1ff16ca509 | ||
|
|
c878401197 | ||
|
|
8f518b7b1f | ||
|
|
fb624458f0 | ||
|
|
ee3220f33b | ||
|
|
7bc591fa78 | ||
|
|
c02d942fc3 | ||
|
|
3b9ded8270 | ||
|
|
fdf0c13a7f | ||
|
|
80b1cac394 | ||
|
|
8c80c32113 | ||
|
|
a74b033c26 | ||
|
|
16fcbc0ebf | ||
|
|
62ffada6fe | ||
|
|
e5a0d335af | ||
|
|
ba8a30ebdd | ||
|
|
e25dbe5196 | ||
|
|
3533ad8b74 | ||
|
|
2d20ce07e3 | ||
|
|
e5e0a9240d | ||
|
|
5e83fff859 | ||
|
|
2d0acc734e | ||
|
|
b6bc3a6e96 | ||
|
|
7ec7a65f33 | ||
|
|
2a97b67f72 | ||
|
|
06be01d81c | ||
|
|
386f6aaac6 | ||
|
|
82d11bfb45 | ||
|
|
f39d900fbf | ||
|
|
335edec357 | ||
|
|
e58f035b19 | ||
|
|
f85d610393 | ||
|
|
fce0095af1 | ||
|
|
82bea111df | ||
|
|
0aded8f1f4 | ||
|
|
84130f0586 | ||
|
|
ed4d1059dd | ||
|
|
c73be86211 | ||
|
|
6cb2e7a91a | ||
|
|
58713e65ed | ||
|
|
676c26fae7 | ||
|
|
f2e4e67021 | ||
|
|
18ba476fc9 | ||
|
|
d9ed82972f | ||
|
|
c9affaa1a1 | ||
|
|
8ef89adaef | ||
|
|
f25db898ae | ||
|
|
cddc41b353 | ||
|
|
d4e918660b | ||
|
|
3f16a3e27f | ||
|
|
53d27f1437 | ||
|
|
408dd310d3 | ||
|
|
0050446ff0 | ||
|
|
efb47d949f | ||
|
|
a67303866e | ||
|
|
756e80595c | ||
|
|
089a021d02 | ||
|
|
d89a94dcd2 | ||
|
|
b298f66348 | ||
|
|
fa277c7b7c | ||
|
|
76ec449d33 | ||
|
|
d481ade524 | ||
|
|
1b468efff1 | ||
|
|
9be9a4795b | ||
|
|
49bcf7ded4 | ||
|
|
28989b9c49 | ||
|
|
9e40b1f6cf | ||
|
|
a9e95caa42 | ||
|
|
eb39c97661 | ||
|
|
52f19272d0 | ||
|
|
4cf37379c7 | ||
|
|
746d0476c9 | ||
|
|
cf93d61d0c | ||
|
|
5d9197446b | ||
|
|
753d5560d6 | ||
|
|
1b68d30ff1 | ||
|
|
278eed7af9 | ||
|
|
6eb4e32225 | ||
|
|
e160a7d517 | ||
|
|
476cdda0c2 | ||
|
|
efdabb8bf6 | ||
|
|
ca648df852 | ||
|
|
04afb61351 | ||
|
|
869632361c | ||
|
|
d994abb559 | ||
|
|
d082c2c250 | ||
|
|
586a474168 | ||
|
|
b7268dd6fd | ||
|
|
c3e4ea228a | ||
|
|
237946f569 | ||
|
|
cae32e5b2a | ||
|
|
715ba9803f | ||
|
|
283198f0a4 | ||
|
|
bab889c406 | ||
|
|
92c6f5369d | ||
|
|
6042948aed | ||
|
|
136577fec0 | ||
|
|
180fda53df | ||
|
|
ca6b175fb8 | ||
|
|
dd25dc4b06 | ||
|
|
9893fc3cf9 | ||
|
|
67b6c2c03f | ||
|
|
bb073b8682 | ||
|
|
9bbfad5be3 | ||
|
|
2c7e7d5b63 | ||
|
|
94e7258a03 | ||
|
|
95eb90eac2 | ||
|
|
0f0a4c54e7 | ||
|
|
399923bfa4 | ||
|
|
39f0ab9eae | ||
|
|
8b1cd54417 | ||
|
|
ff2192fa7a | ||
|
|
08443a307c | ||
|
|
bd90e1bccc | ||
|
|
9cb0bd301e | ||
|
|
0923960215 | ||
|
|
0f68dc1ab6 | ||
|
|
09b88b8575 | ||
|
|
fae2f25b69 | ||
|
|
d3e0696ecc | ||
|
|
6a7723eba9 | ||
|
|
10d6fd157e | ||
|
|
048133df30 | ||
|
|
2fe7fec14f | ||
|
|
73e7b27c09 | ||
|
|
2fdbd5a85c | ||
|
|
78e27e0073 | ||
|
|
bad585c8c9 | ||
|
|
11d10a8129 | ||
|
|
0b89e9d137 | ||
|
|
70df1ff0aa | ||
|
|
24d3169592 | ||
|
|
b8db0fea5e | ||
|
|
514519b45b | ||
|
|
70f6bcb63c | ||
|
|
5a3e55f704 | ||
|
|
4452e07443 | ||
|
|
a125e6c0ea | ||
|
|
fa4688e113 | ||
|
|
0236609558 | ||
|
|
3f15fb1b98 | ||
|
|
e22718c72a | ||
|
|
e94985576c | ||
|
|
71e132a9e0 | ||
|
|
8d5d56bed8 | ||
|
|
386c9799c2 | ||
|
|
a1be657460 | ||
|
|
d4994e8fa0 | ||
|
|
fb054d9f64 | ||
|
|
8790843b3b | ||
|
|
38383dff02 | ||
|
|
a0f9923c21 | ||
|
|
812c0ac5b1 | ||
|
|
2e09146f44 | ||
|
|
25080411d5 | ||
|
|
55e20e96f9 | ||
|
|
981ab1c6ed | ||
|
|
a1089f8073 | ||
|
|
fe747a3908 | ||
|
|
a16c9480e7 | ||
|
|
b40e9fc817 | ||
|
|
6eec0e8988 | ||
|
|
d6320caadf | ||
|
|
c4f125525a | ||
|
|
56209261d0 | ||
|
|
5df4d64345 | ||
|
|
d6d6065104 | ||
|
|
333aea0641 | ||
|
|
c7bb3d49a6 | ||
|
|
7c982498dd | ||
|
|
0e2c95afed | ||
|
|
df6eb23341 | ||
|
|
1204de13c3 | ||
|
|
9cb21de11d | ||
|
|
2f16b9e110 | ||
|
|
cf28139b02 | ||
|
|
87e8e7d5e5 | ||
|
|
dcc3a681d9 | ||
|
|
cc7f30da88 | ||
|
|
ed9eabed38 | ||
|
|
99ac95bfca | ||
|
|
eb178fc9d1 | ||
|
|
7567e29cf1 | ||
|
|
b2a456140c | ||
|
|
9c3ff6b570 | ||
|
|
e4d238ba6c | ||
|
|
42363204dd | ||
|
|
de89d557dd | ||
|
|
c5a53b21e7 | ||
|
|
02f43b1ef0 | ||
|
|
d7cb0b7ac1 | ||
|
|
2efeb573e6 | ||
|
|
370ff638ab | ||
|
|
108564efe3 | ||
|
|
43f96657e0 | ||
|
|
cb004bfba2 | ||
|
|
f276597a82 | ||
|
|
4fd9855daa | ||
|
|
ebdce74f92 | ||
|
|
98e9b8f28b | ||
|
|
a31c0358a4 | ||
|
|
8640522fff | ||
|
|
a5fa350e7e | ||
|
|
c4a0cff1ca | ||
|
|
30d02ebbb6 | ||
|
|
9a90359283 | ||
|
|
edbcab544e | ||
|
|
f55bf4691d | ||
|
|
16e44309a6 | ||
|
|
c4d58e3dba | ||
|
|
16155d3670 | ||
|
|
bd929bcc72 | ||
|
|
e2cc52fd02 | ||
|
|
be5c7cfd04 | ||
|
|
9d14949138 | ||
|
|
6fe5dbad3c | ||
|
|
f4f8ee9cb6 | ||
|
|
0767eb03e0 | ||
|
|
feb6062f2f | ||
|
|
113dc4d5e8 | ||
|
|
ca20d98a2c | ||
|
|
599a629068 | ||
|
|
4c05ad655d | ||
|
|
203233f0d2 | ||
|
|
ac8f7357db | ||
|
|
4c396065f5 | ||
|
|
cc4dac38fe | ||
|
|
394818f533 | ||
|
|
bfd4a68e23 | ||
|
|
9d54e82214 | ||
|
|
18c2278066 | ||
|
|
b3d549a712 | ||
|
|
0887e78e90 | ||
|
|
df3f86dd73 | ||
|
|
89af1d0cf1 | ||
|
|
10a68d63a1 | ||
|
|
8ccebbc13c | ||
|
|
4c663cd943 | ||
|
|
ca16bf74f5 | ||
|
|
a1968ef9fd | ||
|
|
de1a999611 | ||
|
|
d0e8bc5de0 | ||
|
|
38fb647cd7 | ||
|
|
90bb83c5d4 | ||
|
|
467fff3651 | ||
|
|
3ab50a5fb5 | ||
|
|
e4d58a0a88 | ||
|
|
10e592c741 | ||
|
|
b3241c85eb | ||
|
|
abd1702c96 | ||
|
|
d91e86c608 | ||
|
|
f0ebac019d | ||
|
|
c18995915a | ||
|
|
55d2c23aa0 | ||
|
|
28c9f719c5 | ||
|
|
bb5a276324 | ||
|
|
e99df5b267 | ||
|
|
54e67b3d57 | ||
|
|
b1d30ab9dd | ||
|
|
f2d65b2515 | ||
|
|
286fcd3fbe | ||
|
|
00332ff3fe | ||
|
|
000fa14c17 | ||
|
|
c5b4be6c97 | ||
|
|
d9b2a8f4e0 | ||
|
|
2898b00300 | ||
|
|
a4671b62d4 | ||
|
|
0c87ed7381 | ||
|
|
4104a0b2a8 | ||
|
|
4d62f436d5 | ||
|
|
506f88bd6f | ||
|
|
a1ad355e25 | ||
|
|
91d6571cff | ||
|
|
e43c5534b3 | ||
|
|
77e3453d35 | ||
|
|
2568df1b91 | ||
|
|
dc830a8432 | ||
|
|
a9cffbd313 | ||
|
|
65775f5c20 | ||
|
|
c6f9056e2c | ||
|
|
967807d441 | ||
|
|
cc7b3505d7 | ||
|
|
3cd965a24f | ||
|
|
3502bf6de2 | ||
|
|
458e05635c | ||
|
|
f4cd9e0f4f | ||
|
|
1f3871d5dc | ||
|
|
f4d304b2c1 | ||
|
|
24937fb32d | ||
|
|
5d222577be | ||
|
|
0e010cece2 | ||
|
|
21080d65f5 | ||
|
|
83475b9063 | ||
|
|
15a6494d9c | ||
|
|
58ad1f3103 | ||
|
|
b2bff7762f | ||
|
|
cd4a673862 | ||
|
|
1f61c6bd00 | ||
|
|
cc2f46795f | ||
|
|
db5ea3ec9c | ||
|
|
21f2f6e2c3 | ||
|
|
65b5f4fac3 | ||
|
|
2291cf9d0c | ||
|
|
3f8b3fa2a7 | ||
|
|
e6fa1268d9 | ||
|
|
eda8205acd | ||
|
|
019131447a | ||
|
|
41cd90bc2a | ||
|
|
0fe1677ced | ||
|
|
b1a2a93f75 | ||
|
|
50a2c283f1 | ||
|
|
83188daba4 | ||
|
|
3312e409fd | ||
|
|
5420a90f05 | ||
|
|
4d340d7bf3 | ||
|
|
5342175a97 | ||
|
|
e5a1c86b1f | ||
|
|
1aca32005b | ||
|
|
6d7853428f | ||
|
|
683d92399d | ||
|
|
9b9bf6cf53 | ||
|
|
c9b826d9b4 | ||
|
|
81ff72f1f2 | ||
|
|
dfda04f7ba | ||
|
|
3bc0e0c386 | ||
|
|
5829192446 | ||
|
|
cbae45be87 | ||
|
|
2b4ec73318 | ||
|
|
7ff426e413 | ||
|
|
25e3ad8432 | ||
|
|
051edff18e | ||
|
|
dde58be5a6 | ||
|
|
1ad4fda3e6 | ||
|
|
b78ef7b010 | ||
|
|
d1776ce37a | ||
|
|
100ae7b164 | ||
|
|
415e247f6c | ||
|
|
ca4a934e0f | ||
|
|
3b4c417549 | ||
|
|
f5e96bc756 | ||
|
|
7428b7420a | ||
|
|
761d7848ef | ||
|
|
e9fc7899cd | ||
|
|
df40fd5cb3 | ||
|
|
dce3b4b142 | ||
|
|
b20110edb4 | ||
|
|
4a05a66bc0 | ||
|
|
39acc78fad | ||
|
|
a0c58e2a1d | ||
|
|
c46ceca140 | ||
|
|
4db0b6a67f | ||
|
|
38f6b699c6 | ||
|
|
9d80f4e78f | ||
|
|
7dc95f8540 | ||
|
|
723ad5fe7c | ||
|
|
94aac494ea | ||
|
|
3e6b7af61f | ||
|
|
1574a88255 | ||
|
|
2e2fea9182 | ||
|
|
55549f1e70 | ||
|
|
71e97146ed | ||
|
|
35e4e580fa | ||
|
|
baab31d2e2 | ||
|
|
3f1295d9e4 | ||
|
|
d759cc4e94 | ||
|
|
4c941df647 | ||
|
|
1f39ad4e91 | ||
|
|
194e1d3356 | ||
|
|
0aa8c933d8 | ||
|
|
cf11802fbe | ||
|
|
2703cbc4c9 | ||
|
|
b87f3bd00b | ||
|
|
a1227167ea | ||
|
|
7ccb925d76 | ||
|
|
beedd17cfe | ||
|
|
332fa94dab | ||
|
|
ae4b5be29f | ||
|
|
cc4a417c0a | ||
|
|
8a6e03e172 | ||
|
|
3ff0c740c4 | ||
|
|
82f8a8f30b | ||
|
|
2f7c315822 | ||
|
|
89b9c050a8 | ||
|
|
eb70e21f3d | ||
|
|
ab2b06500b | ||
|
|
79845eff1d | ||
|
|
9d1b935643 | ||
|
|
ad0c81e984 | ||
|
|
e86f4e80cb | ||
|
|
af4a6de809 | ||
|
|
0d0fdd3c0b | ||
|
|
de4dcb19cf | ||
|
|
cfcbbb5c21 | ||
|
|
6d11361725 | ||
|
|
924d9f57a6 | ||
|
|
8c5dfb61e7 | ||
|
|
895c5c1e65 | ||
|
|
e64faa3cf2 | ||
|
|
11815a4cf2 | ||
|
|
d903e4d80d | ||
|
|
6530515e98 | ||
|
|
28711aa669 | ||
|
|
f3b2192dd2 | ||
|
|
d6451ae486 | ||
|
|
a78c648e77 | ||
|
|
b1afd28556 | ||
|
|
82f5e17705 | ||
|
|
15e955576f | ||
|
|
23b0fa09d9 | ||
|
|
34e31e7e29 | ||
|
|
3fcb9e7a3c | ||
|
|
f69ff10b7c | ||
|
|
bd6ea12cc6 | ||
|
|
10fa3f7f22 | ||
|
|
462b183548 | ||
|
|
4b5bf6fef4 | ||
|
|
1f28e46e06 | ||
|
|
990e515cb9 | ||
|
|
6ee86f9762 | ||
|
|
41dfb3488c | ||
|
|
f61e1cd435 | ||
|
|
1627503248 | ||
|
|
4c87c8a1ff | ||
|
|
8036d4a553 | ||
|
|
a9f18bbf48 | ||
|
|
64738f58d6 | ||
|
|
f426e9f236 | ||
|
|
234237b43a | ||
|
|
854d6640d1 | ||
|
|
c6514e0183 | ||
|
|
9164e76f44 | ||
|
|
5cf155fb94 | ||
|
|
11be569f35 | ||
|
|
00412f6418 | ||
|
|
c66e4a08e6 | ||
|
|
c22934f4af | ||
|
|
2792c57405 | ||
|
|
e3637586d6 | ||
|
|
5787dfbca5 | ||
|
|
9b80408bc6 | ||
|
|
073f32c1c9 | ||
|
|
6564e76aad | ||
|
|
35563fe834 | ||
|
|
9b46581289 | ||
|
|
dbde27bc88 | ||
|
|
b37c280bf8 | ||
|
|
643fc2106b | ||
|
|
48496cfbbc | ||
|
|
3022129fb5 | ||
|
|
5d0a7b5ef9 | ||
|
|
fa1557c775 | ||
|
|
cb47a9c4be | ||
|
|
9e2ab76be3 | ||
|
|
90c8f8e332 | ||
|
|
63397dee07 | ||
|
|
a602b1bdf2 | ||
|
|
8653a26d38 | ||
|
|
8ea5ac97a2 | ||
|
|
d72d36a8b9 | ||
|
|
664ce69993 | ||
|
|
93bca202bf | ||
|
|
72641e000b | ||
|
|
36696f9b1b | ||
|
|
83bfe2a524 | ||
|
|
827a02563b | ||
|
|
f06acf88f9 | ||
|
|
0f78751ce3 | ||
|
|
c9fbd579b6 | ||
|
|
4e12f73444 | ||
|
|
22f4a250e4 | ||
|
|
ed6966b570 | ||
|
|
14c6794ee3 | ||
|
|
9494321965 | ||
|
|
34d009a458 | ||
|
|
ac5128d133 | ||
|
|
12c487f7b8 | ||
|
|
285417275d | ||
|
|
f2db114cb6 | ||
|
|
87226d9bd9 | ||
|
|
a644a4c7bc | ||
|
|
462bdf19b9 | ||
|
|
3d9bb1f437 | ||
|
|
485d2dbaf8 | ||
|
|
8f8abdbab9 | ||
|
|
7ebe74e57c | ||
|
|
b45153bee8 | ||
|
|
55c4d4240f | ||
|
|
0d2d01060d | ||
|
|
f84237f25b | ||
|
|
07cff6f9a9 | ||
|
|
9f7852e76f | ||
|
|
943d6edabd | ||
|
|
800b88911a | ||
|
|
dccbd9912d | ||
|
|
b5f696756e | ||
|
|
9ff528029a | ||
|
|
44046b475b | ||
|
|
65959f9089 | ||
|
|
94d87039e3 | ||
|
|
d3a13d4515 | ||
|
|
b63bee045f | ||
|
|
977d46bca5 | ||
|
|
8293d4de63 | ||
|
|
94feb1b63b | ||
|
|
e37dac6cbe | ||
|
|
c7b98bc578 | ||
|
|
7d4369fdca | ||
|
|
28cb9f9d7d | ||
|
|
94e3c71809 | ||
|
|
5dd6a5b45a | ||
|
|
e486f846d8 | ||
|
|
e7793764a6 | ||
|
|
5aea2e5bbe | ||
|
|
f52410f2f8 | ||
|
|
53e2f199cc | ||
|
|
f86e9a56d1 | ||
|
|
31ebe48bf3 | ||
|
|
ff963b2cb9 | ||
|
|
9a6ebce85a | ||
|
|
eb5e37e3de | ||
|
|
92b96aea75 | ||
|
|
0f4c0488d5 | ||
|
|
6879089149 | ||
|
|
3e2eb86bd4 | ||
|
|
84ff75c9cf | ||
|
|
9d3605ee96 | ||
|
|
83cd6b7291 | ||
|
|
514d40aefd | ||
|
|
31cca02d8d | ||
|
|
5fbd2f9552 | ||
|
|
7ee0431a2f | ||
|
|
0786403fff | ||
|
|
4f7dbfbc80 | ||
|
|
6caf02fc77 | ||
|
|
9dedc9ecfa | ||
|
|
b62434616a | ||
|
|
ad8f3ea06f | ||
|
|
dbed505282 | ||
|
|
1468746045 | ||
|
|
6841b31849 | ||
|
|
a151dff3c5 | ||
|
|
90f6e2b17f | ||
|
|
7b1e96410f | ||
|
|
7ecf619b94 | ||
|
|
b1d8b6fd5c | ||
|
|
a15a990730 | ||
|
|
7402aab2a8 | ||
|
|
2a68427c82 | ||
|
|
14dd9721e3 | ||
|
|
6115eaf6da | ||
|
|
b4922ce353 | ||
|
|
0fb37bf2d4 | ||
|
|
8ce5272e95 | ||
|
|
08135545da | ||
|
|
3cdbb8f1dd | ||
|
|
4f662a5651 | ||
|
|
0e0811a1ac | ||
|
|
71882fe83c | ||
|
|
fce67b847c | ||
|
|
2ff0c6b92d | ||
|
|
b27b8b7ba5 | ||
|
|
f889abd57a | ||
|
|
1d2852fff6 | ||
|
|
48247f7d3b | ||
|
|
d3e6932a23 | ||
|
|
e4d3e061d1 | ||
|
|
e57708543a | ||
|
|
69e2f90c8d | ||
|
|
655d01114c | ||
|
|
0d40508e69 | ||
|
|
b785a84dca | ||
|
|
14af90cd27 | ||
|
|
0278918886 | ||
|
|
b44738cda3 | ||
|
|
ddbe2d6051 | ||
|
|
a15ebe86db | ||
|
|
16225d2894 | ||
|
|
b4c87a198a | ||
|
|
38da204607 | ||
|
|
dbe410f8af | ||
|
|
b05eb4eed3 | ||
|
|
02201c4527 | ||
|
|
6536d39969 | ||
|
|
c9b41c2b81 | ||
|
|
74e587a8a0 | ||
|
|
a751ad186e | ||
|
|
bae41478d9 | ||
|
|
10dbafbe8c | ||
|
|
504946f72a | ||
|
|
e45ae205ac | ||
|
|
4a5969157d | ||
|
|
bdda4d50f7 | ||
|
|
f565943d8d | ||
|
|
3158c0b1d9 | ||
|
|
4346d83582 | ||
|
|
7e016f4ca5 | ||
|
|
38242d0226 | ||
|
|
b565095136 | ||
|
|
93b5fd5f3e | ||
|
|
8ca03c1f50 | ||
|
|
f5204b89fb | ||
|
|
b9dc63b3df | ||
|
|
86910302dc | ||
|
|
a0cca65075 | ||
|
|
0cc2c3b275 | ||
|
|
2cb4856dc7 | ||
|
|
88a53226a2 | ||
|
|
1d42ab50c7 | ||
|
|
cf10851c78 | ||
|
|
c1727ffb3f | ||
|
|
43d79c024e | ||
|
|
3fcf200a86 | ||
|
|
8bc5588342 | ||
|
|
ab27b613e3 | ||
|
|
c6816dbe74 | ||
|
|
41e43a8441 | ||
|
|
ff91114f39 | ||
|
|
b3951555a1 | ||
|
|
f3a68e940e | ||
|
|
1d646753f2 | ||
|
|
b293700b13 | ||
|
|
ea2e0fa3e6 | ||
|
|
bad55a173d | ||
|
|
69538f248d | ||
|
|
c2f4736f08 | ||
|
|
9aa142ba5a | ||
|
|
f4aba94a18 | ||
|
|
910c06b4d2 | ||
|
|
490a9b1693 | ||
|
|
f6a8705849 | ||
|
|
df2f1105c1 | ||
|
|
acaf683166 | ||
|
|
1c6b256268 | ||
|
|
d59ec1527d | ||
|
|
4b6e46ab7d | ||
|
|
68b949bdbf | ||
|
|
a49cc1db97 | ||
|
|
701cc31e1c | ||
|
|
8832920a4d | ||
|
|
d044d0dc65 | ||
|
|
e7427bb180 | ||
|
|
989acaa7d4 | ||
|
|
6351ea2593 | ||
|
|
1a586f07d1 | ||
|
|
69ec802299 | ||
|
|
817b72f67f | ||
|
|
cf1398606b | ||
|
|
31d733aacb | ||
|
|
77544f855b | ||
|
|
1be4880776 | ||
|
|
ff39f82099 | ||
|
|
200e509cda | ||
|
|
6a70c6705d | ||
|
|
6749e5de7e | ||
|
|
9b83c7eeec | ||
|
|
1c8f7eccbe | ||
|
|
13c98c68e9 | ||
|
|
d492cff553 | ||
|
|
875bd54c21 | ||
|
|
9825bebb75 | ||
|
|
f2ab574a3b | ||
|
|
94babc2c53 | ||
|
|
4e42963033 | ||
|
|
6fec40522e | ||
|
|
eea9890f9d | ||
|
|
89d1cffc12 | ||
|
|
40f168bf1e | ||
|
|
891a098d2a | ||
|
|
b6c0d86945 | ||
|
|
2cb69c2d6e | ||
|
|
9a96760061 | ||
|
|
d4a484522e | ||
|
|
7a24b5cd0c | ||
|
|
c0e34799c8 | ||
|
|
964d500414 | ||
|
|
e2949b66fc | ||
|
|
06fcb531cd | ||
|
|
923b12caa0 | ||
|
|
69d75489e7 | ||
|
|
9e985ff471 | ||
|
|
47390abf3b | ||
|
|
59484f30b5 | ||
|
|
e36701a9f3 | ||
|
|
03b6567993 | ||
|
|
56ef1a1df5 | ||
|
|
6d4af69c19 | ||
|
|
b8bd9d92a7 | ||
|
|
6a4018f206 | ||
|
|
986b44af43 | ||
|
|
64e6dbad13 | ||
|
|
2e559effcb | ||
|
|
d02f755fa7 | ||
|
|
17ffda684f | ||
|
|
d6cd2f35ff | ||
|
|
81bba70548 | ||
|
|
831a2d23e4 | ||
|
|
a0f15cdcb1 | ||
|
|
e882bd8264 | ||
|
|
7101d638c6 | ||
|
|
865e3dd783 | ||
|
|
b1c992db26 | ||
|
|
58f79ae6c0 | ||
|
|
fd5af3c93e | ||
|
|
127d0fced6 | ||
|
|
6526d736bf | ||
|
|
cc4a43f0a2 | ||
|
|
286ade792f | ||
|
|
e681775f47 | ||
|
|
67a9a2c34a | ||
|
|
88c0cc00bc | ||
|
|
f16f153448 | ||
|
|
1f93dcf5db | ||
|
|
783fff300d | ||
|
|
935e4a00d5 | ||
|
|
6986b5a53d | ||
|
|
e6ed31e9d9 | ||
|
|
288c845116 | ||
|
|
597222dbf5 | ||
|
|
cea6eeabba | ||
|
|
8475456c0b | ||
|
|
651caccd71 | ||
|
|
8e9fb8570e | ||
|
|
5a183a6042 | ||
|
|
67e6a7aa1a | ||
|
|
b76c67f309 | ||
|
|
99c6b6d815 | ||
|
|
cadc39699d | ||
|
|
8c06aab7b3 | ||
|
|
25c59cd5dd | ||
|
|
9825239e43 | ||
|
|
fb69f245da | ||
|
|
7ef8dddc2a | ||
|
|
2abcae84b1 | ||
|
|
b4beb6c182 | ||
|
|
786d14663b | ||
|
|
f8269e69c4 | ||
|
|
29274d0acf | ||
|
|
d6b925f171 | ||
|
|
f1f18d1823 | ||
|
|
03ff2147db | ||
|
|
c66a72806d | ||
|
|
e89f2cbac0 | ||
|
|
d831530c50 | ||
|
|
9fe18c2af8 | ||
|
|
fe4ff7ac0f | ||
|
|
a8ca3d8188 | ||
|
|
3b9867c537 | ||
|
|
01d22c26f4 | ||
|
|
e8f7fe5a95 | ||
|
|
bd0b8fbc65 | ||
|
|
05e20b4e92 | ||
|
|
716d58ad4b | ||
|
|
ca12ad529a | ||
|
|
6eea82706d | ||
|
|
d69d005ce0 | ||
|
|
b98b322dfe | ||
|
|
e73e72d150 | ||
|
|
8cabc244f7 | ||
|
|
4f04e5cc13 | ||
|
|
bcf9d2585d | ||
|
|
e03b07f940 | ||
|
|
a45c6aa37e | ||
|
|
dd672945f2 | ||
|
|
e634be4273 | ||
|
|
9acf5bee41 | ||
|
|
1e3297ab64 | ||
|
|
58df290c8e | ||
|
|
e2075886b1 | ||
|
|
ad53abc75a | ||
|
|
a599c318dd | ||
|
|
14e2a0c5e2 | ||
|
|
2ae3efc12e | ||
|
|
fb18134c12 | ||
|
|
2215a101a3 | ||
|
|
4df2fe7ac5 | ||
|
|
c24369d1ec | ||
|
|
41eb86029a | ||
|
|
357026ce66 | ||
|
|
9b558cb069 | ||
|
|
303020ca75 | ||
|
|
b900abcc71 | ||
|
|
c4ba6df6e9 | ||
|
|
cc4a507799 | ||
|
|
d8de8ec696 | ||
|
|
e6563814e8 | ||
|
|
016d51669f | ||
|
|
f19828b2ec | ||
|
|
36fc87c2a1 | ||
|
|
4322bddabf | ||
|
|
c128a9d4ba | ||
|
|
2feeffc95c | ||
|
|
53de238a7e | ||
|
|
c207e74369 | ||
|
|
04ca02a77a | ||
|
|
6ff89c71b6 | ||
|
|
d7c6a8e53e | ||
|
|
150741a443 | ||
|
|
460f322c44 | ||
|
|
ef5d35fe38 | ||
|
|
16044556b5 | ||
|
|
81d5079468 | ||
|
|
edc4b22cbc | ||
|
|
c01dfd4740 | ||
|
|
53a4a46d1f | ||
|
|
d61e7e7102 | ||
|
|
17f4df9293 | ||
|
|
24071582bb | ||
|
|
5754466a0d | ||
|
|
a18eda92b7 | ||
|
|
11d0a8f2fe | ||
|
|
2b8b386882 | ||
|
|
45a5027451 | ||
|
|
2cb72f55f3 | ||
|
|
fa27fbab46 | ||
|
|
87821fe287 | ||
|
|
cee4bcd867 | ||
|
|
b9cc4c18a8 | ||
|
|
4577e61940 | ||
|
|
9ac6147a98 | ||
|
|
74d4ea8a78 | ||
|
|
e2f4c7f872 | ||
|
|
07db8e5a94 | ||
|
|
ed7fb03b9c | ||
|
|
f0ebc3e6a0 | ||
|
|
f2570b4cef | ||
|
|
97e1939021 | ||
|
|
bebb894f3e | ||
|
|
288c533c91 | ||
|
|
6891f9a5d5 | ||
|
|
37a62eed6b | ||
|
|
3ee6b0e58d | ||
|
|
7e6f772345 | ||
|
|
ef28173b65 | ||
|
|
a26614c6b6 | ||
|
|
ee00293e48 | ||
|
|
ce977387e6 | ||
|
|
8cc45dbbd8 | ||
|
|
851a4c104c | ||
| 45ddf3fded | |||
|
|
cb538f4026 | ||
|
|
6bd6db3c16 | ||
|
|
6af3940ffb | ||
|
|
66c864c8d7 | ||
|
|
15354dd8e8 | ||
|
|
215070fe3f | ||
|
|
ed73883e80 | ||
|
|
df85ebc0aa | ||
|
|
885a6af943 | ||
|
|
94820d4782 | ||
|
|
7f044e5ac3 | ||
|
|
1284f23620 | ||
|
|
bdcc8acdc4 | ||
|
|
73a9dfcf43 | ||
|
|
7a277ce283 | ||
|
|
f8b3f9dd30 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -4,6 +4,7 @@ about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: 'Bug'
|
||||
|
||||
---
|
||||
|
||||
@@ -31,7 +32,6 @@ If applicable, add screenshots to help explain your problem.
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -4,6 +4,7 @@ about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: 'Feature'
|
||||
|
||||
---
|
||||
|
||||
|
||||
4
.github/workflows/maven-publish.yml
vendored
@@ -129,7 +129,9 @@ jobs:
|
||||
makeLatest: true
|
||||
|
||||
- name: 🔧 Install XML tools
|
||||
run: sudo apt-get install -y libxml2-utils
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: 🔼 Bump versionCode in root POM
|
||||
id: bump_version
|
||||
|
||||
28
.github/workflows/sync-wiki.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Publish wiki
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- docs/**
|
||||
- .github/workflows/sync-wiki.yml
|
||||
workflow_dispatch:
|
||||
concurrency:
|
||||
group: publish-wiki
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
publish-wiki:
|
||||
if: github.repository_owner == 'Card-Forge'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: fix md links for Gollum
|
||||
run: find ${{ github.workspace }}/docs/ -type f -name "*.md" -exec sed -i -E 's|(\[[^]]+]\()([^)]+\/)*([^).]+).md\)|\1\3)|g' '{}' \;
|
||||
- name: fix image links for Gollum
|
||||
run: find ${{ github.workspace }}/docs/ -type f -name "*.png" -exec mv '{}' ${{ github.workspace }}/docs/ \;
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@v5
|
||||
with:
|
||||
path: docs
|
||||
preprocess: false
|
||||
strategy: init
|
||||
2
.github/workflows/test-build.yaml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ '17' ]
|
||||
java: ['17', '21']
|
||||
name: Test with Java ${{ matrix.Java }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
7
.gitignore
vendored
@@ -66,6 +66,9 @@ forge-gui-mobile-dev/testAssets
|
||||
|
||||
forge-gui/res/cardsfolder/*.bat
|
||||
|
||||
# Generated changelog file
|
||||
forge-gui/release-files/CHANGES.txt
|
||||
|
||||
forge-gui/res/PerSetTrackingResults
|
||||
forge-gui/res/decks
|
||||
forge-gui/res/layouts
|
||||
@@ -87,3 +90,7 @@ forge-gui/tools/PerSetTrackingResults
|
||||
*.tiled-session
|
||||
/forge-gui/res/adventure/*.tiled-project
|
||||
/forge-gui/res/adventure/*.tiled-session
|
||||
|
||||
# Ignore python temporaries
|
||||
__pycache__
|
||||
*.pyc
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
Summary
|
||||
|
||||
(Summarize the bug encountered concisely)
|
||||
|
||||
|
||||
Steps to reproduce
|
||||
|
||||
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
|
||||
|
||||
|
||||
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
|
||||
|
||||
|
||||
What is the current bug behavior?
|
||||
|
||||
(What actually happens)
|
||||
|
||||
|
||||
What is the expected correct behavior?
|
||||
|
||||
(What you should see instead)
|
||||
|
||||
|
||||
Relevant logs and/or screenshots
|
||||
|
||||
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
|
||||
|
||||
|
||||
Possible fixes
|
||||
|
||||
(If you can, link to the line of code that might be responsible for the problem)
|
||||
|
||||
/label ~needs-investigation
|
||||
@@ -1,15 +0,0 @@
|
||||
Summary
|
||||
|
||||
(Summarize the feature you wish concisely)
|
||||
|
||||
|
||||
Example screenshots
|
||||
|
||||
(If this is a UI change, please provide an example screenshot of how this feature might work)
|
||||
|
||||
|
||||
Feature type
|
||||
|
||||
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
|
||||
|
||||
/label ~feature request
|
||||
@@ -6,7 +6,7 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
|
||||
|
||||
## Requirements / Tools
|
||||
|
||||
- you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
||||
- your favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
||||
- Java JDK 17 or later
|
||||
- Git
|
||||
- Git client (optional)
|
||||
@@ -22,42 +22,41 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
|
||||
|
||||
- Clone your forked project to your local machine
|
||||
|
||||
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
|
||||
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
|
||||
|
||||
## IntelliJ
|
||||
|
||||
IntelliJ is the recommended IDE for Forge development. Quick start guide for [setting up the Forge project within IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup).
|
||||
|
||||
|
||||
## Eclipse
|
||||
|
||||
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
|
||||
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
|
||||
At this time, Eclipse is not the recommended IDE for Forge development.
|
||||
|
||||
### Project Setup
|
||||
|
||||
- Follow the instructions for cloning from GitHub. You'll need to setup an account and your SSH key.
|
||||
- Follow the instructions for cloning from GitHub. You'll need to setup an account and your SSH key.
|
||||
|
||||
If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
|
||||
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your GitHub profile under
|
||||
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing GitHub.
|
||||
If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
|
||||
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your GitHub profile under
|
||||
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing GitHub.
|
||||
|
||||
- Fork the Forge git repo to your GitHub account.
|
||||
|
||||
- Clone your forked repo to your local machine.
|
||||
|
||||
- Make sure the Java SDK is installed -- not just the JRE. Java 17 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 17 or later.
|
||||
- Make sure the Java SDK is installed -- not just the JRE. Java 17 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 17 or later.
|
||||
|
||||
- Install Eclipse 2021-12 or later for Java. Launch it.
|
||||
- Install Eclipse 2021-12 or later for Java. Launch it.
|
||||
|
||||
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
|
||||
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
|
||||
ensure everything is checked > Finish.
|
||||
|
||||
- Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
|
||||
be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
|
||||
- Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
|
||||
be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
|
||||
for this first time through.
|
||||
|
||||
- Once everything builds, all errors should disappear. You can now advance to Project launch.
|
||||
- Once everything builds, all errors should disappear. You can now advance to Project launch.
|
||||
|
||||
### Project Launch
|
||||
|
||||
@@ -67,15 +66,15 @@ This is the standard configuration used for releasing to Windows / Linux / MacOS
|
||||
|
||||
- Right-click on forge-gui-desktop > Run As... > Java Application > "Main - forge.view" > Ok
|
||||
|
||||
- The familiar Forge splash screen, etc. should appear. Enjoy!
|
||||
- The familiar Forge splash screen, etc. should appear. Enjoy!
|
||||
|
||||
#### Mobile (Desktop dev)
|
||||
|
||||
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
|
||||
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
|
||||
|
||||
- Right-click on forge-gui-mobile-dev > Run As... > Java Application > "Main - forge.app" > Ok.
|
||||
|
||||
- A view similar to a mobile phone should appear. Enjoy!
|
||||
- A view similar to a mobile phone should appear. Enjoy!
|
||||
|
||||
### Eclipse / Android SDK Integration
|
||||
|
||||
@@ -99,7 +98,7 @@ TBD
|
||||
|
||||
#### Android Platform
|
||||
|
||||
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
|
||||
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
|
||||
|
||||
- Android SDK Build-tools 35.0.0
|
||||
- Android 15 (API 35) SDK Platform
|
||||
@@ -124,10 +123,11 @@ TBD
|
||||
|
||||
SNAPSHOT builds can be built via the Maven integration in Eclipse.
|
||||
|
||||
1) Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
|
||||
1. Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
|
||||
|
||||
- On the Main tab, set Goals: clean install, set Profiles: windows-linux
|
||||
|
||||
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
|
||||
2. Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
|
||||
|
||||
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
|
||||
|
||||
@@ -141,7 +141,7 @@ Card scripting resources are found in the forge-gui/res/ path.
|
||||
|
||||
### Project Hierarchy
|
||||
|
||||
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
|
||||
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
|
||||
|
||||
- forge-ai
|
||||
- forge-core
|
||||
@@ -158,32 +158,38 @@ The platform-specific projects are:
|
||||
|
||||
#### forge-ai
|
||||
|
||||
The forge-ai project contains the computer opponent logic for gameplay. It includes decision-making algorithms for specific abilities, cards and turn phases.
|
||||
|
||||
#### forge-core
|
||||
|
||||
The forge-core project contains the core game engine, card mechanics, rules engine, and fundamental game logic. It includes the implementation of Magic: The Gathering rules, card interactions, and the game state management system.
|
||||
|
||||
#### forge-game
|
||||
|
||||
The forge-game project handles the game session management, player interactions, and game flow control. It includes implementations for multiplayer support, game modes, matchmaking, and game state persistence. This module bridges the core game engine with the user interface and networking components.
|
||||
|
||||
#### forge-gui
|
||||
|
||||
The forge-gui project includes the scripting resource definitions in the res/ path.
|
||||
The forge-gui project contains the user interface components and rendering logic for the game. It includes the main game window, card displays, player interactions, and the scripting resource definitions in the res/ path.
|
||||
|
||||
#### forge-gui-android
|
||||
|
||||
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
|
||||
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
#### forge-gui-desktop
|
||||
|
||||
Java Swing based GUI targeting desktop machines.
|
||||
|
||||
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
|
||||
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
|
||||
|
||||
#### forge-gui-ios
|
||||
|
||||
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
|
||||
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
#### forge-gui-mobile
|
||||
|
||||
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
|
||||
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
|
||||
|
||||
#### forge-gui-mobile-dev
|
||||
|
||||
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.
|
||||
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ⚔️ Forge: The Magic: The Gathering Rules Engine
|
||||
# ⚔️ Forge: The Magic: The Gathering Rules Engine
|
||||
|
||||
Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
||||
|
||||
@@ -32,6 +32,7 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
||||
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
|
||||
|
||||
### 📱 Android Installation
|
||||
- _(Note: **Android 11** is the minimum requirements with at least **6GB RAM** to run smoothly. You need to enable **"Install unknown apps"** for Forge to initialize and update itself)_
|
||||
- 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.
|
||||
|
||||
---
|
||||
@@ -46,11 +47,13 @@ Embark on a thrilling single-player journey where you can:
|
||||
- Challenge diverse AI opponents.
|
||||
- Collect cards and items to boost your abilities.
|
||||
|
||||

|
||||
<img width="1282" height="752" alt="Shandalar World" src="https://github.com/user-attachments/assets/9af31471-d688-442f-9418-9807d8635b72" />
|
||||
|
||||
### 🔍 Quest Modes
|
||||
Engage in focused gameplay without the overworld exploration—perfect for quick sessions!
|
||||
|
||||
<img width="1282" height="752" alt="Quest Duels" src="https://github.com/user-attachments/assets/b9613b1c-e8c3-4320-8044-6922c519aad4" />
|
||||
|
||||
### 🤖 AI Formats
|
||||
Test your skills against AI in multiple formats:
|
||||
- **Sealed**
|
||||
@@ -60,6 +63,8 @@ Test your skills against AI in multiple formats:
|
||||
|
||||
For comprehensive gameplay instructions, visit our [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide).
|
||||
|
||||
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" />
|
||||
|
||||
---
|
||||
|
||||
## 💬 Support & Community
|
||||
|
||||
@@ -15,7 +15,7 @@ public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
|
||||
GuiBase.setDeviceInfo("", "", 0, 0);
|
||||
GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/");
|
||||
new EditorMainWindow(Config.instance());
|
||||
}
|
||||
}
|
||||
|
||||
68
docs/AI.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# About Forge's Artificial Intelligence
|
||||
|
||||
The AI is *not* "trained". It uses basic rules and can be easy to overcome knowing it's weaknesses.
|
||||
|
||||
The AI is:
|
||||
* Best with Aggro and midrange decks
|
||||
* Poor to Ok in control decks
|
||||
* Pretty bad for most combo decks
|
||||
|
||||
If you want to train a model for the AI, please do. We would love to see something like that implemented in Forge.
|
||||
|
||||
# AI Matches from Command Line
|
||||
|
||||
The AI can battle itself in the command line, allowing the tests to be performed on headless servers or on computers that have poor graphic performance, and when you just don't need to see the match. This can be useful if you want to script testing of decks, test a large tournament, or just bash 100's of games out to see how well a deck performs.
|
||||
|
||||
Please understand, the AI is still the AI, and it's limitations exist even against itself. Games can lag and become almost unbearably long when the AI has a lot to think about, and you can't see what's on the table for it to play against. It's best if you set up the tournament and walk away, you can analyze logs later, results are printed at the end.
|
||||
|
||||
## Syntax
|
||||
|
||||
`sim -d <deck1[.dck]> ... <deckX[.dck]> -D [path] -n [N] -f [F] -t [T] -p [P] -q`
|
||||
|
||||
In linux and mac, command line arguments are not currently passed through the sh script, please call `java -jar` manually, instead of the exe.
|
||||
- `sim` - "Simulation Mode" forces Forge to not start the GUI and automatically runs the AI matches in command line. Enables all other switches for simulation mode.
|
||||
- `-d <deck1[.dck]> ... <deckX[.dck]>` - Space separated list of deck files, in `-f` game type path. (For example; If `-f` is set to Commander, decks from `<userdata>/decks/commander/` will be searched. If `-f` is not set then default is `<userdata>/decks/constructed/`.) Names must use quote marks when they contain spaces.
|
||||
- `deck1.dck` - Literal deck file name, when the value has ".dck" extension.
|
||||
- `deck` - A meta deck name of a deck file.
|
||||
- `-D [path]` - [path] is absolute directory path to load decks from. (Overrides path for `-d`.)
|
||||
- `-n [N]` - [N] number of games, just flat test the AI multiple times. Default is 1.
|
||||
- `-m [M]` - [M] number of matches, best of [M] matches. (Overrides -n) Recommended 1, 3, or 5. Default is 1.
|
||||
- `-f [F]` - Runs [F] format of game. Default is "constructed" (other options may not work, list extracted from code)
|
||||
- `Commander`
|
||||
- `Oathbreaker`
|
||||
- `TinyLeaders`
|
||||
- `Brawl`
|
||||
- `MomirBasic`
|
||||
- `Vanguard`
|
||||
- `MoJhoSto`
|
||||
- `-t [T]` - for Tournament Mode, [T] for type of tournament.
|
||||
- `Bracket` - See wikipedia for [Bracket Tournament](https://en.wikipedia.org/wiki/Bracket_(tournament))
|
||||
- `RoundRobin` - See wikipedia for [Round Robin Tournaments](https://en.wikipedia.org/wiki/Round-robin_tournament)
|
||||
- `Swiss` - See wikipedia for [Swiss Pairing Tournaments](https://en.wikipedia.org/wiki/Swiss-system_tournament)
|
||||
- `-p [P]` - [P] number of players paired, only used in tournament mode. Default is 2.
|
||||
- `-q` - Quiet Mode, only prints the result not the entire log.
|
||||
|
||||
## Examples
|
||||
In linux and macos you must run forge by evoking java and calling the jar, currently command line parameters are not passed through the script. The forge jar filename is truncated in these examples from `forge-whatever-version-youre-on.jar` to `forge.jar`.
|
||||
|
||||
In Windows, if you use the EXE file as described below, the simulation runs in the background and output is sent to the forge log file only. If you want to have output to the console, please use the `java -jar` evocation of forge.
|
||||
|
||||
To simulate a basic three games of two decks (deck1 and deck2 must be meta deck names of decks in `<userdata>\decks\constructed\`):
|
||||
- Windows/Linux/MacOS: `java -jar forge.jar sim -d deck1 deck2 -n 3`
|
||||
- Windows: `.\forge.exe sim -d deck1 deck2 -n 3`
|
||||
|
||||
To simulate a single 3-player Commander game (deck1, deck2, and deck3 must be meta deck names of decks in `<userdata>\decks\commander\`):
|
||||
- Windows/Linux/MacOS: `java -jar forge.jar sim -d deck1 deck2 deck3 -f commander`
|
||||
- Windows: `.\forge.exe sim -d deck1 deck2 deck3 -f commander`
|
||||
|
||||
To simulate a round robin tournament; best of three, with all decks in a directory:
|
||||
- Windows/Linux/MacOS: `java -jar forge.jar sim -D /path/to/DecksFolder/ -m 3 -t RoundRobin`
|
||||
- Windows: `.\forge.exe sim -D C:\DecksFolder\ -m 3 -t RoundRobin`
|
||||
|
||||
To simulate a swiss tournament; best of three, all decks in a directory, 3 player pairings:
|
||||
- Windows/Linux/MacOS: `java -jar forge.jar sim -D /path/to/DecksFolder/ -m 3 -t Swiss -p 3`
|
||||
- Windows: `.\forge.exe sim -D C:\DecksFolder\ -m 3 -t Swiss -p 3`
|
||||
|
||||
***
|
||||
|
||||
Each game ends with an announcement of the winner, and the current status of the match.
|
||||
12
docs/Adventure-Mode.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# What is Adventure Mode?
|
||||
|
||||
Adventure mode is a work-in-progress game mode where you explore the ever-changing landscape of Shandalar, duel creatures to earn gold and new cards to battle the various bosses. You can visit towns to buy equipment and cards, and crawl through dungeons to find artifacts and loot to help you on your journey. Adventure mode is an awesome reimagining of the original "Shandalar" 1997 PC Game in Forge, even though the scope of adventure is much broader and we don't constrain ourselves to the lore of the plane of Shandalar. You can play Shandalar on Desktop (Linux, Windows, IOS) or on Android devices.
|
||||
|
||||
# How to run?
|
||||
|
||||
1. Step 1: Download the latest version of Forge https://downloads.cardforge.org/dailysnapshots/
|
||||
2. Step 2: Make sure you have the required version of the JDK (https://www.oracle.com/be/java/technologies/downloads/)
|
||||
3. Step 3: Launch the adventure mode from the "adventure.exe" file included in the forge version (on android, the adventure version should be embedded into the main Forge program)
|
||||
4. Step 4: In order to have pictures show up make sure you enable "automatically download missing card images" in the settings. This should automatically download the picture of the card as you encounter them in game.
|
||||
|
||||
|
||||
109
docs/Card-Images.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# About
|
||||
|
||||
Card images are assets used to represent the real cards in game. You DO NOT need images to play forge, however representing real cards is a nice ability forge provides. These images can be any image set you like, or custom images too.
|
||||
|
||||
Primarily there are two types of images you'll care about; cards, and tokens.
|
||||
|
||||
**Cards** - are the primary card image files, and can be generic (as all cards of the same name have the same rules) or per set since there may be different art work in different editions. Typically these are scans of the original cards and are provided by forge's FTP, or scryfall. You can customize a full generic set, or any edition if you desire... (you could for example, blur every card image and play a literal "blind" or "farsighted" game.)
|
||||
|
||||
**Tokens** - are the images for the cards replacing a generic "1/1 zombie" for example. These are less frequently updated, and are typically the bulk of what is missing when doing an audit. However, these are probably where the more true "custom" replacements are available, with either custom artwork, or modified of other existing.
|
||||
|
||||
# Downloading
|
||||
|
||||
Due to charges in Forges hosting and scryfall terms you can no longer predownload card images. Turn on auto download in forge to download card images when first viewed.
|
||||
|
||||
## In Forge Auto Download:
|
||||
|
||||
**Download Missing Images - Setting**
|
||||
- This will download the images from the sources as the game requests the image in situ.
|
||||
- This can be useful if you don't want to have copies of every card... You can do small pre-caching by loading your decks in the deck editor prior to playing to download just those images.
|
||||
|
||||
## Bulk Download Sites: (Not in game)
|
||||
|
||||
- [http://download.austeregrim.net](http://download.austeregrim.net)
|
||||
- Note from user AustereGrim;
|
||||
> I provide my site for free for bulk downloading the entire image catalog. So you don't need to give those spam sites more advertising spots. If the server is loaded bandwidth is shared, right now it's not heavily used so please feel free to download the 4+gb zips, or the individual zips if you need sets. They are the images Kev has uploaded to my site, and the Zips are updated nightly automatically.
|
||||
|
||||
**(I'm not gatekeeping, please if you have a private location for bulk downloads or for alternate or custom arts, you can update this wiki too or let us know in the discord. I'll be happy to update the wiki page with additional sources.)**
|
||||
|
||||
# Storage
|
||||
|
||||
Card images are stored in `pics/cards`, and tokens in `pics/tokens`, in the Cache folder for forge:
|
||||
|
||||
- **Windows** - `C:\Users\<username>\appdata\local\forge\Cache\`
|
||||
- You'll need to enable hidden folders.
|
||||
- **Android 11+** - `Internal Storage/Android/obb/forge.app/Forge/cache/`
|
||||
- *_NOTE: You need a third party File Manager to access the obb folder and allow storage access permission_*
|
||||
- **Android 8 to 10** - `Internal Storage/Forge/cache/`
|
||||
- **Linux** - `/home/<username>/.cache/forge/`
|
||||
- **MacOS** - `/Users/<username>/Library/caches/forge/`
|
||||
- Use `Command + Shift + .` to show hidden files.
|
||||
|
||||
|
||||
# Subfolders
|
||||
|
||||
If you don't care about the edition's version of cards, images can be stored in the root directory of the cards folder.
|
||||
|
||||
`/cache/pics/cards/`
|
||||
|
||||
If you want the edition's versions of the cards, they need to go under the edition's code subfolder.
|
||||
|
||||
`/cache/pics/cards/AFR` for example for Adventures in the Forgotten Realms.
|
||||
|
||||
# File Naming
|
||||
|
||||
**File Names:**
|
||||
- Cards file names follow a simple principle: `Card Name#.border.ext`
|
||||
- `Card Name` - Card Name with spaces.
|
||||
- `#` - Alternate Art number; if more than one art exists for the card.
|
||||
- `border` - Border Type; fullborder, crop. (I don't know all of them.)
|
||||
- `ext` - Extension, jpg or png are supported.
|
||||
|
||||
**Alternate images:**
|
||||
|
||||
Alternate images are defined as cards with the same name in the set's edition file, if the edition file does not have the alternate listed forge will not see the alternate there!
|
||||
|
||||
**Standard Alternate Arts:**
|
||||
|
||||
So for example the AFR set (as most sets) shows these 4 versions of swamp;
|
||||
```
|
||||
270 L Swamp @Piotr Dura
|
||||
271 L Swamp @Sarah Finnigan
|
||||
272 L Swamp @Titus Lunter
|
||||
273 L Swamp @Adam Paquette
|
||||
```
|
||||
The file naming would be represented by a number after the name:
|
||||
```
|
||||
Swamp1.fullborder.jpg
|
||||
Swamp2.fullborder.jpg
|
||||
Swamp3.fullborder.jpg
|
||||
Swamp4.fullborder.jpg
|
||||
```
|
||||
|
||||
**Additional Alternate Arts:**
|
||||
|
||||
They may also be listed separately as "extended arts", "showcase", or "borderless" in the same editions file:
|
||||
```
|
||||
[cards]
|
||||
90 U Black Dragon @Mark Zug
|
||||
```
|
||||
and
|
||||
```
|
||||
[borderless]
|
||||
291 U Black Dragon @Jason A. Engle
|
||||
```
|
||||
Where the files are:
|
||||
```
|
||||
black dragon1.fullborder.jpg
|
||||
black dragon2.fullborder.jpg
|
||||
```
|
||||
|
||||
**Forcing an Alternate:**
|
||||
|
||||
Renaming and creating a second of an existing card **will not work**, for example creating two "Burning hands" which does not have alternate art;
|
||||
```
|
||||
burning hands1.fullborder.jpg
|
||||
burning hands2.fullborder.jpg
|
||||
```
|
||||
Forge will not see either of those, and will probably download the missing `burning hands.fullborder.jpg` for you. Similarly adding a 3rd black dragon `black dragon3.fullborder.jpg` will **not** work either.
|
||||
|
||||
900
docs/Card-scripting-API/AbilityFactory.md
Normal file
@@ -0,0 +1,900 @@
|
||||
AbilityFactory parses differently from the Keyword parser. Your Ability line will look more like this:
|
||||
|
||||
`A:{AB/SP/DB/ST}$ <AFSubclass> | {Necessary$ Parameters} | {Separated$ By} | {Pipes$ Here} | [Optional$ Values]`
|
||||
|
||||
In most cases, each AF subclass implements both the Spell and Ability.
|
||||
Much of the code is shared, so creating the data object will look very similar.
|
||||
|
||||
- **AB** is for Activated Abilities
|
||||
- **SP** is for Spell
|
||||
- **DB** is for Drawback and many abilities that are subsidiary to other things, like replacements. They are only used to chain AFs together, and will never be the root AF
|
||||
- **ST** is for Static, this gets used in case the API should resolve without using the stack<br> (e.g. the unique *Circling Vultures* special action is directly implemented in the script this way)
|
||||
|
||||
>*NOTE:*
|
||||
> - these factories are refactored from time to time (often to adapt to new sets), so while some entries could be slightly outdated, the base information should still be correct
|
||||
> - a few factories also have _*All_ variants, these are slowly being phased out
|
||||
> - some parameters are only added for very exotic cards, these won't be included here to keep the focus on understanding the general concepts of scripting
|
||||
> - when in doubt you can always cross check with the [available APIs](https://github.com/Card-Forge/forge/tree/master/forge-game/src/main/java/forge/game/ability/effects) code
|
||||
|
||||
# Common Parameters
|
||||
|
||||
## Cost / UnlessCost
|
||||
|
||||
`Cost$ <AbilityCost>` is the appropriate way to set the cost of the ability. Currently for spells, any additional costs including the original Mana cost need to appear in the Cost parameter in the AbilityFactory. For each card that uses it, the order in which the cost is paid will always be the same.
|
||||
|
||||
Secondary abilities such as the DB executed by triggers or replacements (usually) don't need costs. (This is one reason to use DB over AB in these cases.)
|
||||
|
||||
Read more about it in [Costs](Costs)
|
||||
|
||||
## ValidTgts / Defined
|
||||
|
||||
Most effects need to know (at least implicitly) which players or objects they're trying to affect. There are two different ways for that:
|
||||
- `ValidTgts` will need to be used for abilities that target
|
||||
- if your ability instead describes on what it's applied use `Defined`
|
||||
|
||||
Read more about it in [Affected / Targets](Targeting)
|
||||
|
||||
## Restrictions / Conditions
|
||||
|
||||
Restrictions limit when abilities can be put on the stack and Conditions apply during resolving. Common examples are Putrid Leech's only activate this once per turn or different cards that can activate from Zones like the Hand or the Graveyard.
|
||||
|
||||
Read more about it in [Restriction](Restrictions)
|
||||
|
||||
## SpellDescription
|
||||
|
||||
SpellDescription is how the text of the ability will display on the card and in the option dialog for cards with multiple abilities.
|
||||
|
||||
The SpellDescription for secondary abilities (both AB and DB) is now displayed when (and if) the ability prompts for user input in the prompt pane so it is useful to put some nice text there.
|
||||
|
||||
## StackDescription
|
||||
|
||||
*(Optional)* StackDescription is the description the ability will have on the stack. This is automatically generated by the effect, but may be overridden using this parameter. This is sometimes needed with complex effects, when the generated text can't handle some details. Properties of the spell can be accessed like this: {c:Targeted}. You can reuse the spell text by just putting `SpellDescription` or `None` to leave it empty.
|
||||
|
||||
## Remember*
|
||||
|
||||
Remembering is often needed when a card becomes a new object, which is then further affected by the ability. Typical example: [Flicker](https://github.com/Card-Forge/forge/blob/master/forge-gui/res/cardsfolder/f/flicker.txt)<br>
|
||||
Because cards keep their remembered parts when changing zones manual [cleanup](#Cleanup) is usually required.
|
||||
|
||||
## AI params
|
||||
|
||||
`IsCurse$ True` - For effects that are normally treated positive e.g. Pump
|
||||
|
||||
`AITgts$ BetterThanEvalRating.130`
|
||||
|
||||
# Factories (in Alphabetical Order)
|
||||
|
||||
## AlterLife
|
||||
|
||||
AlterLife is for Abilities that Alter a player's life total.
|
||||
|
||||
### GainLife
|
||||
|
||||
Have a player gain the specified amount of life.
|
||||
|
||||
`A:AB$ GainLife | Cost$ T | LifeAmount$ 1 | SpellDescription$ You gain 1 life.`
|
||||
|
||||
LifeAmount$ is required. This is how much life you will gain.
|
||||
|
||||
Defined is optional, if it appears the defined player(s) gain life.
|
||||
Target is optional, if it appears and Defined doesn't then targeted player(s) gain life.
|
||||
|
||||
### LoseLife
|
||||
|
||||
Have a player lose the specified amount of life.
|
||||
|
||||
`A:AB$ LoseLife | Cost$ Sac<1/CARDNAME> | ValidTgts$ Player | TgtPrompt$ Target a player to lose a life | LifeAmount$ 1 | SpellDescription$ Target player loses 1 life.`
|
||||
`A:SP$ LoseLife | Cost$ 2 B | Defined$ Opponent | LifeAmount$ 2 | SpellDescription$ Each opponent loses 2 life.`
|
||||
|
||||
LifeAmount$ is required. This is how much life will be lost.
|
||||
|
||||
Target is optional. If Target doesn't appear then Defined will be used.
|
||||
Remember, if Defined is missing, the default for Players is "You"
|
||||
|
||||
Part of resolving sets the **SVar AFLifeLost** to the amount of life lost by all players.
|
||||
|
||||
### SetLife
|
||||
|
||||
SetLife sets one or both player's life total to a specified value (i.e.
|
||||
"your life total becomes 20" or "Target player's life total is equal to
|
||||
the number of cards in your graveyard").
|
||||
|
||||
`A:SP$ SetLife | Cost$ 7 W W | ValidTgts$ Player | TgtPrompt$ Select target player | LifeAmount$ 20 | SpellDescription$ Target player's life total becomes 20.`
|
||||
|
||||
Parameters:
|
||||
LifeAmount (required) - the value to set the life total(s) to
|
||||
|
||||
Defined is optional. If it exists, it will be used. Target is optional.
|
||||
If it exists and defined doesn't it will be used. Default player is "You".
|
||||
|
||||
### ExchangeLife
|
||||
|
||||
ExchangeLife switches the Life total of two players.
|
||||
|
||||
`A:AB$ ExchangeLife | Cost$ 6 T | ValidTgts$ Player | TargetMin$ 2 | TargetMax$ 2 | TgtPrompt$ Select target player | SpellDescription$ Two target players exchange life totals.`
|
||||
|
||||
One of Defined or Target is required, since there needs to be two
|
||||
Players exchanging life If Defined it will be used. If Target exists and
|
||||
defined doesn't it will be used.
|
||||
|
||||
If there aren't two determined players by the SA, the activating player is added as the second player.
|
||||
|
||||
## Animate
|
||||
|
||||
Animate handles animation effects like "This card becomes a 5/5
|
||||
green creature with flying until end of turn." It is designed to handle
|
||||
color changing, type changing, P/T setting, and granting/removing abilities.
|
||||
|
||||
`A:SP$Animate | Cost$ G | ValidTgts$ Land | TgtPrompt$ Select target land | Power$ 3 | Toughness$ 3 | Types$ Creature | SpellDescription$ Until end of turn, target land becomes a 3/3 creature that's still a land.`
|
||||
|
||||
`A:AB$Animate | Cost$ 1 B | Defined$ Self | Power$ 1 | Toughness$ 1 | Types$ Creature,Skeleton | Colors$ Black | Abilities$ ABRegen | SpellDescription$ CARDNAME becomes a 1/1 black Skeleton creature with "B: Regenerate this creature" until end of turn. It's still a land. (If it regenerates, the next time it would be destroyed this turn, it isn't. Instead tap it, remove all damage from it, and remove it from combat.)`
|
||||
`SVar:ABRegen:AB$Regenerate | Cost$ B | SpellDescription$ Regenerate CARDNAME.`
|
||||
|
||||
`A:AB$Animate | Cost$ 2 R G | Defined$ Self | Power$ 3 | Toughness$ 3 | Types$ Creature,Elemental | Colors$ Red,Green | Triggers$ TrigAttack | SpellDescription$ Until end of turn, CARDNAME becomes a 3/3 red and green Elemental creature with "Whenever this creature attacks, put a +1/+1 counter on it." It's still a land.`
|
||||
`SVar:TrigAttack:Mode$ Attacks | ValidCard$ Creature.Self | Execute$ TrigPutCounter | TriggerDescription$ Whenever CARDNAME attacks, put a +1/+1 counter on it.`
|
||||
`SVar:TrigPutCounter:AB$PutCounter | Cost$ 0 | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1`
|
||||
|
||||
Parameters:
|
||||
|
||||
- Power (required) - the power to assign to the animated card
|
||||
- Toughness (required) - the toughness to assign to the animated card
|
||||
- Types (optional) - the additional types to give the animated card;
|
||||
comma delimited
|
||||
- OverwriteTypes (optional) - set to True if the animated being should
|
||||
have these types **instead** as opposed to **in addition to**
|
||||
- RemoveTypes (optional) - a list of types to Remove from the animated
|
||||
card
|
||||
- ChosenType (optional) - overrides types before it and just will add
|
||||
the ChosenType
|
||||
- Keywords (optional) - a " & " delimited list of keywords to give the
|
||||
animated being (just like AB$Pump)
|
||||
- HiddenKeywords (optional) - a " & " delimited list of hidden
|
||||
keywords to give the animated being (just like AB$Pump)
|
||||
- RemoveKeywords (optional) - a " & " delimited list of keywords to
|
||||
remove from the animated being (just like AB$Debuff)
|
||||
- Colors (optional) - a comma-delimited list of Colors to give to the
|
||||
animated being (capitalized and spelled out) (ChosenColor accepted)
|
||||
- Abilities (optional) - a comma-delimited list of SVar names which
|
||||
contain abilities that should be granted to the animated being
|
||||
- OverwriteAbilities - Remove Abilities from animated being
|
||||
- Triggers (optional) - a comma-delimited list of SVar names which
|
||||
contain triggers that should be granted to the animated being
|
||||
- OverwriteTriggers - Remove/suppress triggers from animated being
|
||||
- staticAbilities (optional) - a comma-delimited list of SVar names
|
||||
which contain static abilities that should be granted to the
|
||||
animated being
|
||||
- OverwriteStatics- Remove static abilities from animated being
|
||||
- OverwriteReplacements - Remove replacement effects from animated
|
||||
being
|
||||
- RemoveAllAbilities - Remove all Abilities, Triggers, Statics, and
|
||||
Replacement effects
|
||||
- sVars(optional) - a comma-delimited list of SVars that should be
|
||||
granted to the animated being
|
||||
- Duration (Default is end of turn)
|
||||
- Permanent
|
||||
- UntilEndOfCombat - if the effect should last only until End of Combat instead of End of Turn
|
||||
- UntilHostLeavesPlay - if the effect should last as long as the host is still in play
|
||||
- UntilYourNextUpkeep
|
||||
- UntilControllerNextUntap
|
||||
- UntilYourNextTurn
|
||||
|
||||
Target is optional, will be used if possible. Defined is optional, will be used if no Targets (Self by default)
|
||||
|
||||
## Attach
|
||||
|
||||
Attach is being used directly only for Auras, primarily for Aura Spells, but also for Auras entering the battlefield by some effect.
|
||||
|
||||
`AB$ Attach | Cost$ R R | ValidTgts$ Creature | AILogic$ Pump`
|
||||
|
||||
Parameters:
|
||||
|
||||
- Object (optional) - This is the Object that will be Attached
|
||||
(generally the Source Card for Auras)
|
||||
- AILogic - AI Logic tells the AI which base AI code it should use for
|
||||
Attaching
|
||||
- GainControl - Gains Control of the Attached Permanent (Control
|
||||
Magic)
|
||||
- Curse - A Generic Curse the AI has a handful of checks to see
|
||||
what the most appropriate Target is.
|
||||
- Pump - A Generic Pump. The AI has a handful of checks to see
|
||||
what the most appropriate Target is.
|
||||
- ChangeType - For Attachments that change types. Evil Presence is
|
||||
a good example. This logic should be expanded.
|
||||
- KeepTapped - For Attachments that keep a Permanent tapped. The
|
||||
AI will also check for a few things like Vigilance, and another
|
||||
KeepTapped Aura. Paralyzing Grasp is a good example.
|
||||
|
||||
Attach separates the actually granting of abilities from the attaching to permanents to streamline how things work.
|
||||
|
||||
## BecomeMonarch
|
||||
|
||||
## Bond
|
||||
|
||||
Soulbonding two creatures. Only used internally by the engine.
|
||||
|
||||
## Branch
|
||||
Sometimes, an ability might do certain things when a specific condition is true, and other things if not. This can be implemented by using `Branch`.
|
||||
The branch evaluates the SVar specified by the property `BranchConditionSVar`, using the comparison defined with `BranchConditionSVarCompare` (such as `GTY`, `LT1`, etc). Depending on whether the condition evaluated to true or false, the subability defined by `TrueSubAbility` or `FalseSubAbility` is executed.
|
||||
|
||||
The example below is for "Composer of Spring", which allows either a "land" or a "land or creature" to be put on the battlefield, depending on the number of enchantments in play under your control.
|
||||
|
||||
```
|
||||
SVar:TrigBranch:DB$ Branch | BranchConditionSVar$ X | BranchConditionSVarCompare$ GE6 | TrueSubAbility$ PutLandCreature | FalseSubAbility$ PutLand
|
||||
SVar:PutLand:DB$ ChangeZone | Origin$ Hand | Destination$ Battlefield | Tapped$ True | ChangeType$ Land.YouOwn
|
||||
SVar:PutLandCreature:DB$ ChangeZone | Origin$ Hand | Destination$ Battlefield | Tapped$ True | ChangeType$ Creature.YouOwn,Land.YouOwn
|
||||
SVar:X:Count$Valid Enchantment.YouCtrl
|
||||
```
|
||||
|
||||
## Charm
|
||||
|
||||
This allows cards that have a mode to be chosen to occur after a trigger.
|
||||
|
||||
Parameters
|
||||
|
||||
- CharmNum - Number of Modes to Choose
|
||||
- Choices - A Comma delimited list of SVars containing the Modes
|
||||
|
||||
## Choose*
|
||||
|
||||
These can be used to chain effects together. However for common cases many effects already support this directly, e.g. `PutCounter | Choices$``.<br>
|
||||
Besides making the script shorter using such shortcuts usually also helps the AI making better use of the effect.
|
||||
|
||||
### ChooseType
|
||||
|
||||
This can be used when you are asked to choose a card type or creature type.
|
||||
|
||||
- Type - Required - Can be Card or Creature
|
||||
- InvalidTypes - Optional - Use to specify any type that cannot be chosen (ex: "Choose any creature type except Wall")
|
||||
|
||||
The Defined is for target players.
|
||||
|
||||
## Clash
|
||||
|
||||
This AF handles clashing. It takes two special parameters: WinSubAbility and
|
||||
OtherwiseSubAbility. They are both optional and work the same way,
|
||||
namely that it contains the name of an SVar that in turn contains a
|
||||
drawback to be executed. The example below is for Release the Ants.
|
||||
|
||||
`A:SP$ DealDamage | Cost$ 1 R | Tgt$ TgtCP | NumDmg$ 1 | SubAbility$ DBClash | SpellDescription$ Release the Ants deals 1 damage to target creature or player. Clash with an opponent. If you win, return CARDNAME to its owner's hand.`
|
||||
`SVar:DBClash:DB$ Clash | WinSubAbility$ DBReturn`
|
||||
`SVar:DBReturn:DB$ ChangeZone | Defined$ Self | Origin$ Stack | Destination$ Hand`
|
||||
|
||||
## Cleanup
|
||||
|
||||
A non-functional, maintenance AF used for Cleaning up certain Variables before a Spell finishes Resolving.
|
||||
|
||||
Parameters
|
||||
|
||||
- ClearRemembered$ (optional) Set to True to clear this card's
|
||||
remembered list. Generally useful for Cards that Remember a card, do
|
||||
something to it, then need to forget it once it's done.
|
||||
- ClearImprinted$ (optional) Set to True to clear the list of
|
||||
imprinted cards.
|
||||
- ClearChosenX$ (optional) Set to True to clear the chosen X value.
|
||||
- ClearTriggered$ (optional) Set to True to clear any delayed triggers
|
||||
produced by this card.
|
||||
- ClearCoinFlips$ (optional) Set to True to clear the remembered coin
|
||||
flip result.
|
||||
- ClearChosenCard$ (optional) Set to True to clear the chosen cards.
|
||||
- ForgetDefined$ (optional) If present, remove the specified cards
|
||||
from this card's remembered list.
|
||||
|
||||
## Control
|
||||
|
||||
### GainControl
|
||||
|
||||
Example: Act of Aggression
|
||||
|
||||
`A:SP$ GainControl | Cost$ 3 PR PR | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls. | LoseControl$ EOT | Untap$ True | AddKWs$ Haste | SpellDescription$ Gain control of target creature an opponent controls until end of turn. Untap that creature. It gains haste until end of turn.`
|
||||
|
||||
Parameters:
|
||||
|
||||
- NewController(Targeted player, if there is no target player, the
|
||||
default is the ActivatingPlayer)
|
||||
- AllValid(all valid types, no targets)
|
||||
- LoseControl(LeavesPlay, Untap, LoseControl, EOT(end of turn))
|
||||
|
||||
### ControlExchange
|
||||
|
||||
### ControlSpell
|
||||
|
||||
## Copy*
|
||||
|
||||
### CopyPermanent
|
||||
|
||||
Copies a permanent on the battlefield.
|
||||
|
||||
Parameters:
|
||||
|
||||
- NumCopies - optional - the number of copies to put onto the
|
||||
battlefield. Supports SVar:X:????.
|
||||
- Keywords - optional - a list of keywords to add to the copies
|
||||
- AtEOT - optional
|
||||
- Sacrifice - set to this is copy should be sacrificed at End of
|
||||
Turn
|
||||
- Exile - set to this is copy should be exiled at End of Turn
|
||||
|
||||
### CopySpellAbility
|
||||
|
||||
Copies a spell on the stack (Twincast, etc.).
|
||||
|
||||
## Counter
|
||||
|
||||
Countering Spells or Abilities.
|
||||
|
||||
`A:SP$ Counter | Cost$ 1 U | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | UnlessCost$ 3 | SpellDescription$ Counter target spell unless its controller pays 3.`
|
||||
`A:SP$ Counter | Cost$ U | TgtPrompt$ Select target Activated or Triggered Ability | ValidTgts$ Card | TargetType$ Activated,Triggered | SpellDescription$ Counter target activated or triggered ability.`
|
||||
`A:SP$ Counter | Cost$ G | TargetType$ Spell | ValidTgts$ Instant,Aura | TargetValidTargeting$ Permanent.YouCtrl | SpellDescription$ Counter target instant or Aura spell that targets a permanent you control. `
|
||||
|
||||
Parameters
|
||||
|
||||
- TargetType - Can be Spell,Activated,Triggered. If more than one,
|
||||
just put a comma in between.
|
||||
- ValidTgts - a "valid" expression for types of source card (if you
|
||||
don't know what it is it's just "Card")
|
||||
- TargetValidTargeting- a "valid" expression for targets of this
|
||||
spell's target
|
||||
- Destination - send countered spell to: (only applies to Spells; ignored for Abilities)
|
||||
- Graveyard (Default)
|
||||
- Exile
|
||||
- TopDeck
|
||||
- Hand
|
||||
- BottomDeck
|
||||
- Shuffle
|
||||
|
||||
## Counters*
|
||||
|
||||
Factories to handle counters on cards.
|
||||
|
||||
### Poison
|
||||
|
||||
Poison gives a player the specified number of poison counters.
|
||||
|
||||
`A:AB$ Poison | Cost$ B T | ValidTgts$ Player | TgtPrompt$ Select target player | Num$ 2 | SpellDescription$ Target player gets 2 poison counters.`
|
||||
|
||||
Parameters:
|
||||
|
||||
- Num (required) - the number of poison counters to give
|
||||
|
||||
Target is optional and used if available. Defined is optional and used if no Target (defaults to "You").
|
||||
|
||||
### PutCounter
|
||||
|
||||
Put any type of counter on a game object.
|
||||
|
||||
`A:AB$ PutCounter | Cost$ T | CounterType$ CHARGE | CounterNum$1 | SpellDescription$ Put a charge counter on CARDNAME.`
|
||||
`A:SP$ PutCounter | Cost$ G | Tgt$ TgtC | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME.`
|
||||
|
||||
Target is optional. If no Target is provided, the permanent will put counters on itself.
|
||||
|
||||
- CounterType (required) specifies the type of counter and should
|
||||
appear in all caps. It should be one of the values in the Counters
|
||||
enum.
|
||||
- CounterNum (required) specifies how many counters will be put on
|
||||
the chosen card.
|
||||
|
||||
### PutCounterAll
|
||||
|
||||
Put any type of counter on all valid cards.
|
||||
|
||||
- CounterType (required) specifies the type of counter and should
|
||||
appear in all caps. It should be one of the values in the Counters
|
||||
enum.
|
||||
- CounterNum (required) specifies how many counters will be put on
|
||||
the chosen cards.
|
||||
- ValidCards (required) specifies the cards to add counters to.
|
||||
|
||||
### RemoveCounter
|
||||
|
||||
Remove any type of counter from a card.
|
||||
|
||||
Target is optional. If no Target is provided, the permanent will remove
|
||||
counters from itself.
|
||||
|
||||
- CounterType (required) specifies the type of counter and should
|
||||
appear in all caps. It should be one of the values in the Counters
|
||||
enum.
|
||||
- CounterNum (required) specifies how many counters will be removed
|
||||
from the chosen card.
|
||||
- UpTo is optional. If an effect states you may remove "up to X
|
||||
counters", set this to True.
|
||||
|
||||
### RemoveCounterAll
|
||||
|
||||
Remove any type of counter from all valid cards.
|
||||
|
||||
- CounterType$ (required) specifies the type of counter and should
|
||||
appear in all caps. It should be one of the values in the Counters
|
||||
enum.
|
||||
- CounterNum$ (required) specifies how many counters will be removed
|
||||
from the chosen cards.
|
||||
- ValidCards$ (required) specifies the card to remove counters from.
|
||||
|
||||
### Proliferate
|
||||
|
||||
No own parameters.
|
||||
|
||||
### MoveCounters
|
||||
|
||||
Used for cards that Move Counters on Resolution, requiring the Host card
|
||||
to have Counters for the Move to occur.
|
||||
|
||||
Parameters
|
||||
|
||||
- Source - The Source of the Moving Counters
|
||||
- Defined - The Destination of the Moving Counters
|
||||
- CounterType - The type of counter to move.
|
||||
- CounterNum - The number of counters to move.
|
||||
|
||||
## Damage
|
||||
|
||||
### DealDamage
|
||||
|
||||
Deal damage to a specified player or permanent.
|
||||
|
||||
`A:AB$ DealDamage | Cost$ T | Tgt$ TgtCP | NumDmg$ 1 | SpellDescription$ CARDNAME deals 1 damage to target creature or player.`
|
||||
|
||||
NumDmg is required. This is the amount of damage dealt.
|
||||
|
||||
### EachDamage
|
||||
|
||||
## Debuff
|
||||
|
||||
Parameters
|
||||
|
||||
- Keywords
|
||||
- Duration
|
||||
|
||||
An AbilityFactory for Removing Keywords, either temporarily or for longer durations.
|
||||
|
||||
## Destroy
|
||||
|
||||
These APIs handles destruction of cards on the battlefield.
|
||||
|
||||
`A:SP$Destroy | Cost$ 1 W W | ValidTgts$ Artifact,Enchantment | TgtPrompt$ Select target artifact or enchantment | SpellDescription$ Destroy target artifact or enchantment.`
|
||||
|
||||
## Effect
|
||||
|
||||
Effect is an oddball of the AF family. Where usually AFs have similarities to each other to help with AI use, Effect doesn't fall under that jurisdiction. In general, an effect is some type of SA that
|
||||
lasts longer than its resolution.
|
||||
|
||||
A good example is High Tide. For the rest of the turn, High Tide makes
|
||||
all Islands produce an extra mana. It doesn't matter if the Island was
|
||||
in play, if it turned into an Island after High Tide was cast, any of that.
|
||||
|
||||
`A:SP$ Effect | Cost$ U | Name$ High Tide Effect | Triggers$ IslandTrigger | SVars$ TrigMana | SpellDescription$ Until end of turn, whenever a player taps an Island for mana, that player adds U to his or her mana pool (in addition to the mana the land produces).`
|
||||
`SVar:IslandTrigger:Mode$ TapsForMana | ValidCard$ Island | Execute$ TrigMana | TriggerDescription$ Whenever a player taps an Island for mana, that player adds U to his or her mana pool (in addition to the mana the land produces).`
|
||||
`SVar:TrigMana:AB$Mana | Cost$ 0 | Produced$ U | Amount$ 1`
|
||||
|
||||
Effect is most similar to Token as it creates a pseudo-permanent, except
|
||||
Effect creates in the command zone rather than the battlefield. It stays
|
||||
active there for a set Duration.
|
||||
|
||||
Parameters
|
||||
|
||||
- Abilities,Triggers,SVars are comma separated lists which contain
|
||||
SVars that point to the appropriate type that the Effect will gain.
|
||||
- Duration is how long the Effect lasts. Right now, most effects will
|
||||
last Until End of Turn. In the future, they may have other
|
||||
conditions.
|
||||
|
||||
Duration$ Permanent for effects that have no specific Duration.
|
||||
|
||||
- Stackable$ False - Most Effects are assumed to be Stackable. By
|
||||
setting the Stackable Flag to False, the AI will know having a
|
||||
second one in play is useless, so will save it's Resource for
|
||||
something else.
|
||||
- Image - a file\_name\_without\_extension (image needs to reside in
|
||||
the tokens directory)
|
||||
|
||||
## Explore
|
||||
|
||||
## Fight
|
||||
|
||||
## Fog
|
||||
|
||||
Fog is an ability based on the original Fog spell. "Prevent all combat
|
||||
damage that would be dealt this turn." While this could be done with an
|
||||
effect, the specialized nature of the AI gives it its own AF.
|
||||
|
||||
## Game outcome
|
||||
|
||||
### GameDraw
|
||||
|
||||
### GameLoss
|
||||
|
||||
### GameWin
|
||||
|
||||
### RestartGame
|
||||
|
||||
Used in the script of *Karn Liberated*
|
||||
|
||||
## Goad
|
||||
|
||||
## Investigate
|
||||
|
||||
## Mana
|
||||
|
||||
For lands or other permanent to produce mana.
|
||||
|
||||
`A:AB$ Mana | Cost$ T | Produced$ <ManaType> | SpellDescription$ Add W to your mana pool.`
|
||||
|
||||
In this example ManaType would be W.
|
||||
|
||||
## Manifest
|
||||
|
||||
## PermanentState
|
||||
|
||||
API for things that alter a permanent's state.
|
||||
|
||||
### Phases
|
||||
|
||||
### SetState
|
||||
|
||||
Changing a cards State. This is mostly for Flip Cards or the Transform mechanic.
|
||||
|
||||
### Tap
|
||||
|
||||
`A:AB$ Tap | Cost$ R | ValidTgts$ Wall | TgtPrompt$ Select target wall | SpellDescription$ Tap target wall.`
|
||||
|
||||
### TapOrUntap
|
||||
|
||||
### Untap
|
||||
|
||||
`A:AB$ Untap | Cost$ G | ActivationLimit$ 1| SpellDescription$ Untap CARDNAME. Activate this ability only once each turn.`
|
||||
`A:SP$ Untap | Cost$ W | ValidTgts$ Permanent | TgtPrompt$ Select target permanent | SpellDescription$ Untap target permanent.`
|
||||
|
||||
Target is optional. If not provided, will untap the permanent with this ability.
|
||||
|
||||
## Play
|
||||
|
||||
Playing a card or cards as part of SA. The player may have to make a
|
||||
choice about which card to play if there are more choices than the
|
||||
number of cards to play.
|
||||
|
||||
Sunbird's Invocation uses many of the parameters.
|
||||
|
||||
Amount - How many cards can be played (a non-negative integer or "All").
|
||||
|
||||
Valid - Selection criteria for valid cards from the zone to cast.
|
||||
|
||||
ValidSA - Applied after Valid, this will filter based on all spells of the cards.
|
||||
|
||||
ValidZone - The zone to look in to determine the valid cards.
|
||||
|
||||
ShowCards - Other cards in the zone to show when selecting valid cards.
|
||||
|
||||
Optional - Casting the card is optional.
|
||||
|
||||
RememberPlayed - Remember the card played.
|
||||
|
||||
ForgetRemembered - Remove all remembered cards from the source (but only
|
||||
if a card was successfully played)
|
||||
|
||||
ForgetTargetRemembered - Remove the played card from remembered cards
|
||||
(but only if it was successfully played)
|
||||
|
||||
WithoutManaCost - The card can be cast without mana payment.
|
||||
|
||||
## PreventDamage
|
||||
|
||||
AF damage prevention effects.
|
||||
|
||||
- A:SP$ PreventDamage | Cost$ W | ValidTgts$ Creature | Amount$ 3
|
||||
| TgtPrompt$ Select target creature | SpellDescription$ Prevent
|
||||
the next 3 damage that would be dealt to target creature this
|
||||
turn.
|
||||
|
||||
## Protection
|
||||
|
||||
Protection grants protection from colors or cards types, or creature
|
||||
types. Anything that has "Protection from ..."
|
||||
|
||||
Gains - required - the thing to gain protection from (green, artifacts,
|
||||
Demons, etc.) or "Choice" if you can choose one of...
|
||||
|
||||
Choices - optional
|
||||
- if Gains$ Choice then this is a comma-delimited list of choices
|
||||
|
||||
## Pump
|
||||
|
||||
This factory handles pumping creatures power/toughness or granting abilities to permanents (usually creatures).
|
||||
|
||||
- A:AB$ Pump | Cost$ R | NumAtt$ +1 | SpellDescription$ CARDNAME
|
||||
gets +1/+0 until end of turn.
|
||||
- A:SP$ Pump | Cost$ 1 U | ValidTgts$ Creature | KW$ Shroud|
|
||||
SpellDescription$ Target creature gains shroud until end of
|
||||
turn. | TgtPrompt$ Select target creature.
|
||||
|
||||
Target is optional. If it's not provided, the activating permanent will be pumped.
|
||||
|
||||
NumAtt$ is optional, will pump Power.
|
||||
|
||||
NumDef$ is optional, will pump Toughness.
|
||||
|
||||
KW$ is optional, will give temporary keywords.
|
||||
|
||||
## Regenerate
|
||||
|
||||
Regenerate is similar to abilities like Pump. But for creating
|
||||
Regeneration shields.
|
||||
|
||||
- A:AB$ Regenerate | Cost$ B | SpellDescription$ Regenerate
|
||||
CARDNAME
|
||||
- A:SP$ Regenerate | Cost$ W | ValidTgts$ Creature | TgtPrompt$
|
||||
Select target creature | SpellDescription$ Regenerate target
|
||||
creature.
|
||||
|
||||
Target is optional. If not provided, will regenerate the permanent with this ability.
|
||||
|
||||
## Repeat
|
||||
|
||||
Repeat the specified ability.
|
||||
|
||||
### Repeat
|
||||
|
||||
`A:SP$ Repeat | Cost$ 3 B B | RepeatSubAbility$ DBDig | RepeatOptional$ True`
|
||||
|
||||
- MaxRepeat - optional - the maxium times to repeat, execute repeat
|
||||
ability at least once
|
||||
- RepeatSubAbility - required - setup subability to repeat
|
||||
- RepeatOptional - optional - you make the choice whether to repeat
|
||||
the process
|
||||
- RepeatPresent, RepeatCompare, RepeatDefined, RepeatCheckSVar,
|
||||
RepeatSVarCompare - optional - condition check
|
||||
|
||||
### RepeatEach
|
||||
|
||||
- RepeatSubAbility - required - to set up repeat subability
|
||||
- RepeatCards - to repeat for each valid card (zone: present zone of
|
||||
the valid repeat cards, default: battlefield)
|
||||
- DefinedCards
|
||||
- RepeatPlayers - to repeat for each valid player
|
||||
- RepeatCounters - to repeat for each valid counters
|
||||
|
||||
## Reveal
|
||||
|
||||
### RevealHand
|
||||
|
||||
Look at a player's hand.
|
||||
|
||||
Target or Defined is required.
|
||||
|
||||
`A:AB$ RevealHand | Cost$ T | ValidTgts$ Player | TgtPrompt$ Select target player | SpellDescription$ Look at target player's hand.`
|
||||
|
||||
### Reveal
|
||||
|
||||
`A:AB$ Reveal | Cost$ 2 U T | Defined$ You | RevealValid$ Card.Blue | AnyNumber$ True | RememberRevealed$ True`
|
||||
|
||||
Parameters:
|
||||
|
||||
- RevealValid: to limit the valid cards.
|
||||
- AnyNumber
|
||||
- Random
|
||||
- RememberRevealed: to remember the cards revealed
|
||||
|
||||
### PeekAndReveal
|
||||
|
||||
This AF is very similar to things that Dig can do, but handle a much
|
||||
simpler form, with less complex coding underneath. Similar to how
|
||||
RearrangeTopOfLibrary could be handled with Dig.
|
||||
|
||||
Primarily used with cards that allow you to Peek at the top card of your
|
||||
library, and allow you to reveal it if it's of a certain type. The
|
||||
Kinship cards fit this bill perfectly, so they are used to simplify the
|
||||
complex popups that would be required if using multiple Dig
|
||||
SubAbilities.
|
||||
|
||||
RevealOptional - Whether or not the Reveal is optional.
|
||||
|
||||
RememberRevealed - Whether to remember the revealed cards (after
|
||||
filtering by Valid)
|
||||
|
||||
RememberPeeked - Whether to remember the peeked cards (only if they are
|
||||
not revealed\!)
|
||||
|
||||
RevealValid - defaults to Card, but allows you to set a specific
|
||||
ValidType if you can only have certain things
|
||||
|
||||
PeekAmount - defaults to 1, but allows you to peek at multiple cards if
|
||||
possible
|
||||
|
||||
## RollDice
|
||||
|
||||
## Sacrifice
|
||||
|
||||
Usually you choose a player and that player has to sacrifice something
|
||||
|
||||
`A:SP$ Sacrifice | Cost$ 1 B | ValidTgts$ Player | SacValid$ Creature | SacMessage$ Creature | Amount$ 2 | SpellDescription$ Target player sacrifices a creature.`
|
||||
|
||||
Destroy$ True - An optional parameter for destroying permanents target
|
||||
player chooses (eg: Burning of Xinye, or Imperial Edict).
|
||||
|
||||
`A:SP$ Sacrifice | Cost$ 1 B | ValidTgts$ Opponent | SacValid$ Creature | SacMessage$ Creature | Destroy$ True | SpellDescription$ Target opponent chooses a creature he or she controls. Destroy it.`
|
||||
|
||||
## Scry
|
||||
|
||||
`A:AB$ Scry | Cost$ 1 T | ScryNum$ 2`
|
||||
|
||||
## StoreSVar
|
||||
|
||||
## Token
|
||||
|
||||
Token simply lets you create tokens of any type.
|
||||
|
||||
`A:SP$ Token | Cost$ 3 W U | TokenImage$ W 1 1 Bird Flying | TokenAmount$ X | TokenName$ Bird | TokenTypes$ Creature,Bird | TokenOwner$ You | TokenColors$ Blue | TokenPower$ 1 | TokenToughness$ 1 | TokenKeywords$ Flying`
|
||||
|
||||
This ability factory does not take a target. All the parameters are
|
||||
mandatory except for TokenKeywords. If you provide a non-integer for
|
||||
TokenAmount, TokenPower or TokenToughness the AF will attempt to look for
|
||||
an SVar of that name and interpret it's contents as a Count$ line. Worth
|
||||
noting is that TokenTypes and TokenColors are simple commaseparated
|
||||
lists while TokenKeywords is a list where the items are separated by
|
||||
"\<\>". If TokenImage is not provided, the factory will attempt to
|
||||
construct a filename on it's own. TokenOwner can use Defined-like
|
||||
parameters, such as "You" "Opponent" or the new Triggered-Variables.
|
||||
|
||||
You can also use the parameters TokenAbilities$, TokenTriggers$ and
|
||||
TokenSVars$ to give the created tokens any number of either. For
|
||||
example, here's how Growth Spasm creates an Eldrazi Spawn token complete
|
||||
with ability.
|
||||
|
||||
SVar:DBToken:DB$Token | TokenAmount$ 1 | TokenName$ Eldrazi Spawn | TokenTypes$ Creature,Eldrazi,Spawn | TokenOwner$ You | TokenColors$ Colorless | TokenPower$ 0 | TokenToughness$ 1 | TokenAbilities$ ABMana SVar:ABMana:AB$Mana | Cost$ Sac<1/CARDNAME> | Produced$ 1 | Amount$ 1 | SpellDescription$ Add 1 to your mana pool.
|
||||
|
||||
As another example, here's Mitotic Slimes' use of TokenTriggers$:
|
||||
|
||||
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigTokenSenior | TriggerDescription$ When CARDNAME is put into a graveyard from the battlefield, put two 2/2 green Ooze creature tokens onto the battlefield. They have "When this creature is put into a graveyard, put two 1/1 green Ooze creature tokens onto the battlefield."
|
||||
SVar:TrigTokenSenior:AB$ Token | Cost$ 0 | TokenImage$ g 2 2 ooze | TokenName$ Ooze | TokenTypes$ Creature,Ooze | TokenColors$ Green | TokenOwner$ You | TokenPower$ 2 | TokenToughness$ 2 | TokenAmount$ 2 | TokenTriggers$ TriggerJunior | TokenSVars$ TrigTokenJunior
|
||||
SVar:TriggerJunior:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigTokenJunior | TriggerDescription$ When this creature is put into a graveyard, put two 1/1 green Ooze creature tokens onto the battlefield. SVar:TrigTokenJunior:AB$Token | Cost$ 0 | TokenImage$ g 1 1 ooze | TokenName$ Ooze | TokenTypes$ Creature,Ooze | TokenColors$ Green | TokenOwner$ You | TokenPower$ 1 | TokenToughness$ 1 | TokenAmount$ 2
|
||||
|
||||
## Trigger
|
||||
|
||||
If possible split the SpellDescription$ of the the effect so the part for the trigger can become the StackDescription directly.
|
||||
|
||||
### DelayedTrigger
|
||||
|
||||
### ImmediateTrigger
|
||||
|
||||
## Turn structure
|
||||
|
||||
### AddPhase
|
||||
|
||||
### AddTurn
|
||||
|
||||
`A:SP$ AddTurn | Cost$ 1 U | NumTurns$ 1 | SpellDescription$ Take an extra turn after this one.`
|
||||
|
||||
### EndTurn
|
||||
|
||||
### ReverseTurnOrder
|
||||
|
||||
### SkipPhase
|
||||
|
||||
### SkipTurn
|
||||
|
||||
## ZoneAffecting
|
||||
|
||||
For specific effects that handle zones in a specific manner
|
||||
|
||||
### ChangeZone
|
||||
|
||||
ChangeZone is a united front of any card that changes zone. This does
|
||||
not include: drawing, discarding, destroying, or milling, as these
|
||||
represent specific words on which triggers and replacements can react.
|
||||
There are two primary forms, but the distinction is handled mostly in
|
||||
the codebase. The only thing that is required is to set appropriate parameters.
|
||||
|
||||
Origin and Destination are both required.
|
||||
|
||||
Origin is where the card is coming from.
|
||||
|
||||
Destination is where the card is going to. If Destination is Library, a
|
||||
LibraryPosition is recommended, but not required. **Default value of the
|
||||
LibraryPosition is 0.** 0 represents the top of the library, -1 represents the bottom.
|
||||
|
||||
There are two primary versions of ChangeZone.
|
||||
|
||||
#### Hidden Origin
|
||||
|
||||
The first is hidden, generally used for Origin zones that are not known
|
||||
information, like the Library or the Hand. The choice of "What card is
|
||||
changing zones?" happens during resolution.
|
||||
|
||||
`A:SP$ ChangeZone | Cost$ W | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Artifact,Enchantment | ChangeNum$ 1 | SpellDescription$ Search your library for an artifact or enchantment card and reveal that card. Shuffle your library, then put the card on top of it.`
|
||||
`A:AB$ ChangeZone | Cost$ T | Origin$ Hand | Destination$ Battlefield | ChangeType$ Land | ChangeNum$ 1 | Optional$ True | SpellDescription$ You may put a land card from your hand onto the battlefield.`
|
||||
|
||||
For Hidden, things like ChangeType and ChangeNum are used to restrict
|
||||
what can ChangeZone, and how many do. There are two parameters special
|
||||
to Hidden Origin:
|
||||
|
||||
"Chooser" defines which player has to decide which card changes zone
|
||||
(example You, Opponent).
|
||||
|
||||
"Mandatory" most of these abilities are not mandatory, but some are.
|
||||
|
||||
#### Known Origin
|
||||
|
||||
The second is known, generally used for Origin zones that are known
|
||||
information, like the Battlefield or the Graveyard. The choice of "What
|
||||
card is changing zones?" happens on activation, generally by targeting.
|
||||
|
||||
`A:AB$ ChangeZone | Cost$ 1 U T | TgtPrompt$ Choose target artifact card in your graveyard | ValidTgts$ Artifact.YouCtrl | Origin$ Graveyard | Destination$ Library | SpellDescription$ Put target artifact card from your graveyard on top of your library.`
|
||||
`A:SP$ ChangeZone | Cost$ U U | ValidTgts$ Permanent | TgtPrompt$ Select target permanent | Origin$ Battlefield | Destination$ Hand | SpellDescription$ Return target permanent to its owner's hand.`
|
||||
|
||||
For Known, since it just uses Target, normal target parameters are used in this Scenario.
|
||||
|
||||
### ChangeZoneResolve
|
||||
|
||||
This is a helper AF, for chained effects that create multiple permanents which should enter the battlefield at the same time.
|
||||
|
||||
To use it, you need to set the param "ChangeZoneTable" on the first effect and then call this at the end.
|
||||
|
||||
This is supported by the following effects:
|
||||
- Amass
|
||||
- CopyPermanent
|
||||
- RepeatEach (_NOTE: if you wrap the creation, you don't need to call this AF on its own_)
|
||||
- Token
|
||||
|
||||
### Dig
|
||||
|
||||
Dig is for an ability that does basically this: "You look at the X cards
|
||||
of your Library, put Y of them somewhere, then put the rest somewhere."
|
||||
Think of Impulse.
|
||||
|
||||
- DigNum - Required - look at the top number of cards of your library.
|
||||
- Reveal - Optional - for abilities that say "Reveal the top X cards
|
||||
of your library". Default is false.
|
||||
- SourceZone - Optional - the zone to dig in. Default is Library.
|
||||
- DestinationZone - Optional - the zone to put the Y cards in. Default
|
||||
is Hand.
|
||||
- LibraryPosition - Optional - if DestinationZone is Library, use this
|
||||
to specify position. Default is -1 (bottom of library).
|
||||
- ChangeNum - Optional - the number of cards to move to the
|
||||
DestinationZone (or "All" when it's for things like "put all lands
|
||||
revealed this way into your hand"). Default is 1.
|
||||
- ChangeValid - Optional - use this to specify if "you may move an
|
||||
artifact to DestinationZone". Default is any Card.
|
||||
- AnyNumber - Optional - use if you can move any number of Cards to
|
||||
DestinationZone. Default is false. (think of Lead the Stampede)
|
||||
- Optional - Optional - set this if you "may" move a card to
|
||||
DestinationZone. Default is false.
|
||||
- DestinationZone2 - Optional - the zone to put the rest of the cards
|
||||
in. If it is library, you are prompted for the order. Default is
|
||||
Library.
|
||||
- LibraryPosition2 - Optional - if DestinationZone2 is Library, use
|
||||
this to specify position. Default is -1 (bottom of library).
|
||||
|
||||
### DigUntil
|
||||
|
||||
### Discard
|
||||
|
||||
`A:AB$ Discard | Cost$ T | ValidTgts$ Opponent | NumCards$ 1 | Mode$ TgtChoose | SpellDescription$ Target opponent discards a card. `
|
||||
|
||||
- NumCards - the number of cards to be discarded (may be integer or X)
|
||||
- Mode - the mode of discard - should match spDiscard
|
||||
- Random
|
||||
- TgtChoose
|
||||
- RevealYouChoose
|
||||
- Hand
|
||||
- DiscardValid - a ValidCards syntax for acceptable cards to discard
|
||||
- UnlessType - a ValidCards expression for "discard X unless you
|
||||
discard <Type>"
|
||||
|
||||
### Draw
|
||||
|
||||
`A:AB$ Draw | Cost$ 1 Sac<1/CARDNAME> | NumCards$ 1 | SpellDescription$ Draw a card.`
|
||||
|
||||
### Mill
|
||||
|
||||
`A:AB$ Mill | Cost$ 2 T | NumCards$ 2 | ValidTgts$ Player | TgtPrompt$ Choose a player to mill | SpellDescription$ Target player puts the top two cards of his or her library into his or her graveyard.`
|
||||
|
||||
### RearrangeTopOfLibrary
|
||||
|
||||
### Shuffle
|
||||
|
||||
Used for shuffling a player's library
|
||||
|
||||
- Optional - Set this parameter if the user should be
|
||||
prompted Yes/No to shuffle the given library. Default is false.
|
||||
|
||||
### TwoPiles
|
||||
|
||||
## Vote
|
||||
356
docs/Card-scripting-API/Card-scripting-API.md
Normal file
@@ -0,0 +1,356 @@
|
||||
A reference guide for scripting cards using the API parsed by the Forge engine.
|
||||
|
||||
# Base Structure
|
||||
|
||||
By opening any file in the /res/cardsfolder folder you can see the basic structure of how the data is created.<br>
|
||||
Here's an example of a vanilla creature:
|
||||
|
||||
```
|
||||
Name:Vanilla Creature
|
||||
ManaCost:2 G
|
||||
Types:Creature Beast
|
||||
PT:2/2
|
||||
Oracle:
|
||||
```
|
||||
|
||||
The name of this card is Vanilla Creature.<br>
|
||||
It's casting cost is {2}{G}.<br>
|
||||
It has the types Creature and Beast.<br>
|
||||
It has a Power-Toughness of 2/2.<br>
|
||||
It will not display any additional text in the card's template.<br>
|
||||
|
||||
If a card has two faces, use AlternateMode:{CardStateName} in the front face and separate both by a new line with the text "ALTERNATE".
|
||||
|
||||
There are a few other properties that will appear in many cards. These are
|
||||
|
||||
| Property | Description
|
||||
| - | -
|
||||
|`A`|[Ability effect](AbilityFactory)
|
||||
|`AI`|RemoveDeck:<br />* `All`<br />This will prevent the card from appearing in random AI decks. It is applicable for cards the AI can't use at all like Dark Ritual and also for cards that the AI could use, but only ineffectively like Tortoise Formation. The AI won't draft these cards.<br />* `Random`<br /> This will prevent the card from appearing in random decks. It is only applicable for cards that are too narrow for random decks like Root Cage or Into the North. The AI won't draft these cards.<br />* `NonCommander`<br />
|
||||
|`Colors`|Color(s) of the card<br /><br />When a card's color is determined by a color indicator rather than shards in a mana cost, this property must be defined. If no identifier is needed, this property should be omitted.<br /><br />* `Colors:red` - This is used on Kobolds of Kher Keep, which has a casting cost of {0} and requires a red indicator to make it red.<br /><br />* `Colors:red,green` - Since Arlinn, Embraced by the Moon has no casting cost (it's the back of a double-faced card), the red and green indicator must be included.
|
||||
|`DeckHints`|AI-related hints for a deck including this card<br /><br />To improve synergy this will increase the rank of of all other cards that share some of its DeckHints types. This helps with smoothing the selection so cards without these Entries won't be at an unfair disadvantage.<br /><br />The relevant code can be found in the [CardRanker](https://git.cardforge.org/core-developers/forge/-/blob/master/forge-gui/src/main/java/forge/gamemodes/limited/CardRanker.java) class.
|
||||
|`DeckNeeds`|This can be considered a stronger variant when the AI should not put this card into its deck unless it has whatever other type is specified. The way this works is "inverted": it will directly decrease the rank of the card unless other cards are able to satisfy its types.<br />If a card demands more than one kind of type you can reuse it:<br />`DeckNeeds:Type$Human & Type$Warrior` will only find Human Warrior compared to `DeckNeeds:Type$Human\|Warrior` which is either
|
||||
|`DeckHas`|specifies that the deck now has a certain ability (like, token generation or counters) so that the drafting/deckbuilding AI knows that it now meets requirements for DeckHints/DeckNeeds. This is actually very useful since many of these (such as `Ability$Graveyard, Ability$Token, Ability$Counters`) are not deduced by parsing the abilities, so an explicit hint is necessary. Using the other types is also supported in case the implicit parsing wouldn't find it.<br />It doesn't require exact matching to have an effect but cards that care about multiple entries for a given type will be judged higher if a card seems to provide even "more" synergy for it.<br />Example:<br />Chishiro has two abilities so `DeckHas:Ability$Token & Ability$Counters` is used, therefore score for `DeckNeeds:Ability$Token\|Counters` is increased
|
||||
|`K`|Keyword (see below)
|
||||
|`Loyalty`|Number of starting loyalty counters
|
||||
|`ManaCost`|Cost to cast the card shown in mana shards<br /><br />This property is required. It has a single parameter that is a mana cost.<br /><br />* `ManaCost:no cost` for cards that cannot be cast<br />* `ManaCost:1 W W` sets the casting cost to {1}{W}{W}
|
||||
|`Name`|Name of the card<br /><br />A string of text that serves as the name of the card. Note that the registered trademark symbol cannot be included, and this property must have at least one character.<br /><br />Example:<br />* `Name:A Display of My Dark Power` sets the card's name to "A Display of My Dark Power"
|
||||
|`Oracle`|The current Oracle text used by the card.<br /><br />We actually have a Python Script that runs to be able to fill in this information, so don't worry about manually editing a lot of cards when Wizards decides to change the rules. <br /><br />This field is used by the Deck Editor to allow non-Legendary Creatures to be marked as potential commanders. Make sure "CARDNAME can be your commander." appears in the oracle text.
|
||||
|`PT`|Power and toughness
|
||||
|`R`|[Replacement effect](Replacements)
|
||||
|`S`|[Static ability](static-abilities)
|
||||
|`SVar`|String variable. Used throughout scripting in a handful of different ways.
|
||||
|`T`|[Triggered ability](Triggers)
|
||||
|`Text`|Additional text that needs to be displayed on the CardDetailPanel that doesn't have any spell/ability that generates a description for it, for example "CARDNAME can be your commander." or "X can't be 0.".
|
||||
|`Types`|Card types and subtypes<br /><br />Include all card types and subtypes, separated by spaces.<br /><br />Example:<br />* `Types:Enchantment Artifact Creature Golem` for a card that reads Enchantment Artifact Creature -- Golem
|
||||
|
||||
Rarity and Set info are now defined in edition definition files. These can be found at /res/reditions path.
|
||||
|
||||
## Conventions
|
||||
- filename: all lowercase, skip special characters, underscore for spaces
|
||||
- Unix(LF) line endings
|
||||
- use empty lines only when separating multiple faces on a card
|
||||
- try to avoid writing default params to keep scripts concise
|
||||
- e.g. just `SP$ Draw` instead of `SP$ Draw | Defined$ You | NumCards$ 1`
|
||||
|
||||
# Keywords
|
||||
|
||||
All keywords need to be prepended with "K:" to be parsed correctly. Each keyword must appear on a separate line.
|
||||
|
||||
## Keywords without Parameters
|
||||
|
||||
This section is for Keywords that require no additional parameters and are one or two words long. Most of these you would see exactly on cards in the game.<br>
|
||||
Examples:
|
||||
|
||||
- Cascade
|
||||
- Changeling
|
||||
- Cipher
|
||||
- Conspire
|
||||
- Convoke
|
||||
- Deathtouch
|
||||
- Defender
|
||||
- Delve
|
||||
- Devoid
|
||||
- Double Strike
|
||||
- Epic
|
||||
- Exalted
|
||||
- Fear
|
||||
- First Strike
|
||||
- Flanking
|
||||
- Flash
|
||||
- Flying
|
||||
- Forestwalk
|
||||
- Fuse
|
||||
- Haste
|
||||
- Hideaway
|
||||
- Horsemanship
|
||||
- Indestructible
|
||||
- Infect
|
||||
- Intimidate
|
||||
- Islandwalk
|
||||
- Landfall
|
||||
- Legendary landwalk
|
||||
- Lifelink
|
||||
- Living Weapon
|
||||
- Menace
|
||||
- Mentor
|
||||
- Mountainwalk
|
||||
- Nonbasic landwalk
|
||||
- Persist
|
||||
- Plainswalk
|
||||
- Prowess
|
||||
- Provoke
|
||||
- Reach
|
||||
- Rebound
|
||||
- Retrace
|
||||
- Riot
|
||||
- Shadow
|
||||
- Shroud
|
||||
- Snow forestwalk
|
||||
- Snow islandwalk
|
||||
- Snow landwalk
|
||||
- Snow mountainwalk
|
||||
- Snow plainswalk
|
||||
- Snow swamp walk
|
||||
- Soulbond
|
||||
- Split second
|
||||
- Storm
|
||||
- Sunburst
|
||||
- Swampwalk
|
||||
- Totem Armor
|
||||
- Trample
|
||||
- Unblockable
|
||||
- Undying
|
||||
- Vigilance
|
||||
- Wither
|
||||
|
||||
## Keywords with parameters
|
||||
|
||||
- Adapt:{cost}
|
||||
- AdjustLandPlays:{params}
|
||||
- Afterlife:{N}
|
||||
- AlternateAdditionalCost:{cost}
|
||||
- Amplify:{cost}:{validType(comma separated)}
|
||||
- Annihilator:{magnitude}
|
||||
- Bloodthirst:{magnitude}
|
||||
- Bestow:{cost}
|
||||
- Bushido:{magnitude}
|
||||
- CantBeBlockedByAmount {xMath}
|
||||
- Champion:{validType}
|
||||
- CostChange:{params}
|
||||
- Crew:{cost}
|
||||
- Cumulative upkeep:{cost}:{Description}
|
||||
- Cycling:{cost}
|
||||
- Dash:{cost}
|
||||
- Devour:{magnitude}
|
||||
- Dredge:{magnitude}
|
||||
- Echo:{cost}
|
||||
- Emerge:{cost}
|
||||
- Enchant {params} \[Curse\]
|
||||
- Enchant {Type}
|
||||
- Entwine:{cost}
|
||||
- Equip:{cost}
|
||||
- etbCounter:{CounterType}:{CounterAmount}
|
||||
- ETBReplacement:{Control/Copy/Other}:{AbilitySVar}\[:Optional\]
|
||||
- Evoke:{cost}
|
||||
- Fabricate:{cost}
|
||||
- Fading:{FadeCounters}
|
||||
- Flashback:{cost}
|
||||
- Foretell:{cost}
|
||||
- Fortify:{cost}
|
||||
- Graft:{value}
|
||||
- Haunt:{ability}:{Description}
|
||||
- Hexproof:{ValidCards}:{Description}
|
||||
- Kicker:{cost}
|
||||
- Level up:{cost}
|
||||
- Madness:{cost}
|
||||
- ManaConvert:
|
||||
- maxLevel:{magnitude}
|
||||
- MayEffectFromOpeningHand:{Effect}
|
||||
- Miracle:{cost}
|
||||
- Modular:{magnitude}
|
||||
- Monstrosity:{cost}
|
||||
- [Mega]Morph:{cost}
|
||||
- Multikicker:{magnitude}
|
||||
- Mutate:{cost}
|
||||
- Ninjutsu:{cost}
|
||||
- Outlast:{cost}
|
||||
- Partner:{CardName}
|
||||
- Poisonous {magnitude}
|
||||
- PreventAllDamageBy {ValidCards}
|
||||
- Protection:{ValidCards}:{Description}
|
||||
- Prowl:{cost}
|
||||
- Rampage:{magnitude}
|
||||
- Recover:{cost}
|
||||
- Renown:{N}
|
||||
- Replicate:{cost}
|
||||
- Ripple:{magnitude}
|
||||
- Soulshift:{magnitude}
|
||||
- Strive:{cost}
|
||||
- Suspend:{turns}:{cost}
|
||||
- Transmute:{cost}
|
||||
- Toxic:{poisonCounters}
|
||||
- TypeCycling:{Type}:{cost}
|
||||
- Unearth:{cost}
|
||||
- UpkeepCost:{cost}
|
||||
- Vanishing:{TimeCounters}
|
||||
|
||||
## Plaintext keywords
|
||||
|
||||
These are hardcoded but not truly keywords rules-wise and will eventually be turned into static abilities.
|
||||
Only listing the most common ones here so you can recognize them.
|
||||
CARDNAME is replaced by the card's name ingame.
|
||||
|
||||
- All creatures able to block CARDNAME do so.
|
||||
- CARDNAME assigns no combat damage
|
||||
- CARDNAME attacks each turn if able.
|
||||
- CARDNAME attacks each combat if able.
|
||||
- CARDNAME blocks each combat if able.
|
||||
- CARDNAME blocks each turn if able.
|
||||
- CARDNAME can attack as though it didn't have defender.
|
||||
- CARDNAME can attack as though it had haste.
|
||||
- CARDNAME can block as though it were untapped.
|
||||
- CARDNAME can block creatures with shadow as though they didn't have shadow.
|
||||
- CARDNAME can block creatures with landwalk abilities as though they didn't have those abilities.
|
||||
- CARDNAME can block only creatures with flying.
|
||||
- CARDNAME can only attack alone.
|
||||
- CARDNAME can't attack.
|
||||
- CARDNAME can't attack if you cast a spell this turn.
|
||||
- CARDNAME can't attack if defending player controls an untapped creature with power {rest of text string}
|
||||
- CARDNAME can't attack or block.
|
||||
- CARDNAME can't attack or block alone.
|
||||
- CARDNAME can't be countered.
|
||||
- CARDNAME can't be enchanted.
|
||||
- CARDNAME can't be equipped.
|
||||
- CARDNAME can't be regenerated.
|
||||
- CARDNAME can't be the target of Aura spells.
|
||||
- CARDNAME can't block.
|
||||
- CARDNAME can't block creatures with power {rest of text string}
|
||||
- CARDNAME can't block unless a creature with greater power also blocks.
|
||||
- CARDNAME can't block unless at least two other creatures block.
|
||||
- CARDNAME can't transform
|
||||
- CARDNAME doesn't untap during your untap step.
|
||||
- CARDNAME enters the battlefield tapped.
|
||||
- CARDNAME is {color}.
|
||||
- CARDNAME must be blocked if able.
|
||||
- CARDNAME must be blocked by exactly one creature if able.
|
||||
- CARDNAME must be blocked by two or more creatures if able.
|
||||
- CARDNAME can't be blocked unless all creatures defending player controls block it.
|
||||
- CARDNAME's power and toughness are switched
|
||||
- CARDNAME untaps during each other player's untap step.
|
||||
- CARDNAME's activated abilities can't be activated.
|
||||
- Creatures with power greater than CARDNAME's power can't block it.
|
||||
- Creatures can't attack unless their controller pays:{params}
|
||||
- Damage that would be dealt by CARDNAME can't be prevented.
|
||||
- Damage that would reduce your life total to less than 1 reduces it to 1 instead.
|
||||
- Enchant artifact
|
||||
- Enchant creature
|
||||
- Enchant creature with converted mana cost 2 or less
|
||||
- Enchant creature without flying
|
||||
- Enchant red or green creature
|
||||
- Enchant land
|
||||
- Enchant land you control
|
||||
- Enchant tapped creature
|
||||
- No more than one creature can attack each combat.
|
||||
- No more than one creature can block each combat.
|
||||
- No more than two creatures can attack you each combat.
|
||||
- No more than two creatures can block each combat.
|
||||
- Play with your hand revealed.
|
||||
- Prevent all combat damage that would be dealt to and dealt by CARDNAME.
|
||||
- Prevent all combat damage that would be dealt by CARDNAME.
|
||||
- Prevent all combat damage that would be dealt to CARDNAME.
|
||||
- Prevent all damage that would be dealt to and dealt by CARDNAME.
|
||||
- Prevent all damage that would be dealt by CARDNAME.
|
||||
- Prevent all damage that would be dealt to CARDNAME.
|
||||
- Protection from {type}
|
||||
- Remove CARDNAME from your deck before playing if you're not playing for ante.
|
||||
- Spells and abilities your opponents control can't cause you to sacrifice permanents.
|
||||
- You can't pay life to cast spells or activate abilities.
|
||||
- You can't sacrifice creatures to cast spells or activate abilities.
|
||||
- You can't draw cards.
|
||||
- You can't gain life.
|
||||
- You can't lose the game.
|
||||
- You can't win the game.
|
||||
- You don't lose the game for having 0 or less life.
|
||||
- You may choose not to untap CARDNAME during your untap step.
|
||||
- You may have CARDNAME assign its combat damage as though it weren't blocked.
|
||||
- Your life total can't change.
|
||||
|
||||
# General SVars
|
||||
|
||||
`SVar:SoundEffect:goblinpolkaband.mp3`
|
||||
|
||||
The sound system supports a special SVar that defines the sound that should be played when the spell is cast.
|
||||
|
||||
`SVar:X:Count$`
|
||||
|
||||
Count is our general value computation function. It's quite varied with a lot of different things it can calculate and is often being updated.
|
||||
|
||||
# AI specific SVars
|
||||
|
||||
`SVar:AIPreference:SacCost$Creature.token,Creature.cmcLE2`
|
||||
|
||||
`SVar:AntiBuffedBy:[ValidCards]`
|
||||
|
||||
If a permanent with this SVar is on the battlefield under human control
|
||||
the AI will play the specified cards in Main1. Applicable for cards like
|
||||
Heart Sliver or Timid Drake.
|
||||
|
||||
`SVar:BuffedBy:[ValidCards]`
|
||||
|
||||
If a permanent with this SVar is on the battlefield under its control
|
||||
the AI will play the specified cards in Main1. Applicable for creatures
|
||||
with a P/T setting static ability (Kithkin Rabble) or additional buffes
|
||||
(Radiant, Archangel).
|
||||
|
||||
`SVar:EnchantMe:[Multiple]/[Once]`
|
||||
|
||||
Creatures with "Multiple" in this SVar will always be prefered when the
|
||||
AI enchants (Rabid Wombat), creatures with "Once" only if they are not
|
||||
enchanted already (Gate Hound).
|
||||
|
||||
`SVar:EquipMe:[Multiple]/[Once]`
|
||||
|
||||
Creatures with "Multiple" in this SVar will always be prefered when the
|
||||
AI equippes (Myr Adapter), creatures with "Once" only if they are not
|
||||
equipped already (Kor Duelist).
|
||||
|
||||
`SVar:AIEvaluationModifier:[ValidAmount]`
|
||||
|
||||
`SVar:EndOfTurnLeavePlay:True`
|
||||
|
||||
`SVar:maxLevel:`
|
||||
|
||||
`SVar:HasCombatEffect:True`
|
||||
|
||||
`SVar:HasAttackEffect:True`
|
||||
|
||||
`SVar:HasBlockEffect:True`
|
||||
|
||||
`SVar:MustAttack:True`
|
||||
|
||||
`SVar:MustBeBlocked:True`
|
||||
|
||||
`SVar:NeedsToPlayVar:[ValidCards]`
|
||||
|
||||
`SVar:ManaNeededToAvoidNegativeEffect:`
|
||||
|
||||
`SVar:NonStackingEffect:True`
|
||||
|
||||
`SVar:PlayMain1:TRUE/ALWAYS/OPPONENTCREATURES`
|
||||
|
||||
The AI will play cards with this SVar in its first main phase. Without other AILogic, it will usually not play any permanents without this in Main1.
|
||||
|
||||
`SVar:SacMe:[number]`
|
||||
|
||||
The AI will sacrifice these cards to pay costs. The higher the number the higher the priority. Example: Hatching Plans has SVar:SacMe:5.
|
||||
|
||||
`SVar:Targeting:Dies`
|
||||
|
||||
`SVar:UntapMe:True`
|
||||
|
||||
The AI will prioritize untapping of this card.
|
||||
|
||||
`SVar:AIUntapPreference:`
|
||||
|
||||
`SVar:NoZeroToughnessAI:True`
|
||||
186
docs/Card-scripting-API/Costs.md
Normal file
@@ -0,0 +1,186 @@
|
||||
Cost is a class that attempts to streamline costs throughout all cards. It requires that each cost is separated by a space. I will use examples that could be found in Ability, although certain Keyworded abilities do use Cost too.
|
||||
|
||||
# Common
|
||||
|
||||
## Description
|
||||
|
||||
Description is an optional last parameter in the cost. This is to allow
|
||||
for complex Type definitions to have a nice Description that is readable.
|
||||
|
||||
## CostDesc / PrecostDesc
|
||||
|
||||
## UnlessCost
|
||||
|
||||
UnlessCost allows the player specified with UnlessPayer (same as
|
||||
Defined, defaults to TargetedController) to pay mana to prevent the
|
||||
resolving of the ability. If the script has the param "UnlessSwitched",
|
||||
then the player pays mana to resolve the ability (usually used to handle
|
||||
"any player may pay ..." ).
|
||||
|
||||
## XChoice
|
||||
|
||||
XChoice is the variable that basically means "You can choose whatever
|
||||
you want for this variable. But you need to decide what X is before you
|
||||
start paying." This would commonly appear as an SVar definition of X.
|
||||
|
||||
## xPaid
|
||||
|
||||
xPaid is the amount of Mana Paid for an X Cost. There are a few cards
|
||||
that will use the X Payment to determine other costs (like Abandon Hope)
|
||||
This would commonly appear as an SVar definition of X.
|
||||
|
||||
## CARDNAME
|
||||
|
||||
For Costs that do something to themselves (ex. Discard Self, Sacrifice
|
||||
Self)
|
||||
|
||||
# Types of Cost
|
||||
|
||||
## Discard
|
||||
|
||||
Discard has two required parameters and one optional in the form
|
||||
Discard<Num/Type/Description>
|
||||
|
||||
- The first is how many cards are being discarded.
|
||||
- The second is what card types can be discarded. (Hand for the whole
|
||||
hand, Random for chosen randomly)
|
||||
|
||||
## Draw
|
||||
|
||||
## Exert
|
||||
|
||||
## Exile
|
||||
|
||||
Exile has two required parameters and one option in the form of
|
||||
Exile<Num/Type/Description>
|
||||
|
||||
There are also a few sister abilities that all fit under the Exile
|
||||
umbrella.
|
||||
|
||||
- Exile (for cards on the Battlefield)
|
||||
- ExileFromGraveyard
|
||||
- ExileFromHand
|
||||
- ExileFromTop (for cards on top of your library, this doesn't default
|
||||
Type to Card, so make sure you add it)
|
||||
|
||||
Some Examples
|
||||
|
||||
- Exile<1/Creature>
|
||||
- Exile<1/CARDNAME>
|
||||
- ExileFromHand<1/CARDNAME>
|
||||
- ExileFromHand<2/Creature>
|
||||
- ExileFromGrave<1/CARDNAME>
|
||||
- ExileFromGrave<1/Treefolk>
|
||||
- ExileFromTop<10/Card>
|
||||
|
||||
## FlipCoin
|
||||
|
||||
Only used by "Karplusan Minotaur".
|
||||
|
||||
## Mana
|
||||
|
||||
- Cost$ 2
|
||||
- 2 colorless mana
|
||||
- Cost$ B R
|
||||
- 1 black and 1 red mana
|
||||
- Cost$ WG
|
||||
- Hybrid White/Green mana
|
||||
- Cost$ S
|
||||
- Snow Mana
|
||||
- Cost$ Mana<2\\Creature>
|
||||
- 2 colorless produced by a source with type 'creature'. Note the
|
||||
backslash - it was chosen because hybrid costs already use slash
|
||||
|
||||
Here's some examples:
|
||||
|
||||
- Discard<1/Card>
|
||||
- "Discard 1 Card"
|
||||
- Discard<0/Hand> (The number is ignored when Hand is used as a
|
||||
type.)
|
||||
- Discard your hand
|
||||
- Discard<2/Random>
|
||||
- Discard 2 Cards at Random
|
||||
- Discard<1/CARDNAME>
|
||||
- Discard Self (CARDNAME)
|
||||
- Discard<1/Creature.Black/Black Creature>
|
||||
- Discard 1 Black Creature
|
||||
|
||||
## Mill
|
||||
|
||||
## Subtract(Remove) Counter
|
||||
|
||||
SubCounter has two required parameters in the form of
|
||||
SubCounter<Num/CounterName>
|
||||
|
||||
- SubCounter<2/P1P1>
|
||||
- SubCounter<1/CHARGE>
|
||||
|
||||
Remember the token name should appear all in caps.
|
||||
|
||||
As third parameter you can use a ValidCard.
|
||||
|
||||
## Sacrifice
|
||||
|
||||
Sacrifice has two required parameters and one optional parameter in the
|
||||
form of Sac<Num/Type/Description>
|
||||
|
||||
- Sac<1/Artifact>
|
||||
- Sac<1/CARDNAME>
|
||||
|
||||
## Tap
|
||||
|
||||
- Cost$ T
|
||||
|
||||
## Untap
|
||||
|
||||
- Cost$ Untap
|
||||
|
||||
\- or -
|
||||
|
||||
- Cost$ Q
|
||||
|
||||
## Unattach
|
||||
|
||||
## PayEnergy
|
||||
|
||||
## PayLife
|
||||
|
||||
PayLife has one required parameter in the form of PayLife<Num>
|
||||
|
||||
- PayLife<2>
|
||||
|
||||
## GainLife
|
||||
|
||||
## TapXType
|
||||
|
||||
TapXType has two required parameters and one option in the form of
|
||||
tapXType<Num/Type/Description>
|
||||
|
||||
- tapXType<3/Creature.White>
|
||||
|
||||
## Return
|
||||
|
||||
Return has two required parameters and one optional in the form of
|
||||
Return<Num/Type/Description>
|
||||
|
||||
- Return<1/Land>
|
||||
- Return<1/CARDNAME>
|
||||
|
||||
## Reveal
|
||||
|
||||
# Putting it Together
|
||||
|
||||
Putting it together is pretty simple. If a card needs to pay mana and tap, it would look like this:
|
||||
|
||||
- Cost$1 W T
|
||||
|
||||
For a spell that has an additional cost of sacrificing a land, put the
|
||||
mana cost and the additional cost in the cost:
|
||||
|
||||
- Cost$2 G Sac<1/Land>
|
||||
|
||||
One of the features of Cost is you can have more than one of the same Cost type:
|
||||
|
||||
- Cost$ Sac<1/Swamp> Sac<1/Creature>
|
||||
|
||||
There are many examples, but they mostly fall into those categories.
|
||||
90
docs/Card-scripting-API/Replacements.md
Normal file
@@ -0,0 +1,90 @@
|
||||
Replacement create replacement effects, as you'd expect. Their script
|
||||
follows the format introduced by AbilityFactory; simply begin a line
|
||||
with "R:", to indicate that it's for a replacement effect, followed by a
|
||||
collection of name-value pairs (name and value are separated by $)
|
||||
separated by pipes (|). All Replacement effects expect an "Event$"
|
||||
parameter, which declares what event should be replaced. Most replacement effects will also have a "ReplaceWith$" parameter which points to an SVar which contains what should replace the event. They may have a "Prevent$True" parameter, instead though, which means that nothing happens instead of the event.
|
||||
|
||||
Similarly to triggers, the replacing code can access special variables
|
||||
pertaining to the event it replaced (like triggered-variables). These
|
||||
are specific to each event, and is listed below. Most replacement effects
|
||||
will also have a "Description$" parameter which is simply the card text
|
||||
for the ability.
|
||||
|
||||
## DamageDone
|
||||
|
||||
DamageDone events are checked when damage is about to be assigned to a
|
||||
card or player. There are 5 special parameters:
|
||||
|
||||
- ValidSource - The damage source must match this for the event to be
|
||||
replaced.
|
||||
- ValidTarget - The damage target must match this for the event to be
|
||||
replaced.
|
||||
- DamageAmount - The amount of damage must match this.
|
||||
- IsCombat - If true, the damage must be combat damage, if false, it
|
||||
can't be.
|
||||
- IsEquipping - If true, the host card must be equipping something.
|
||||
|
||||
There are 3 Replaced-variables:
|
||||
|
||||
- DamageAmount - The amount of damage to be assigned.
|
||||
- Target - The target of the damage.
|
||||
- Source - The source of the damage.
|
||||
|
||||
## Discard
|
||||
|
||||
Discard events are checked when a player is about to discard a card.
|
||||
There are 4 special parameters:
|
||||
|
||||
- ValidPlayer - The player who would discard must match this.
|
||||
- ValidCard - The the card that would be discarded must match this.
|
||||
- ValidSource - The card causing the discard must match this.
|
||||
- DiscardFromEffect - If true, only discards caused by spells/effects
|
||||
will be replaced. Cleanup/statebased discards will not.
|
||||
|
||||
There are 2 Replaced-variables:
|
||||
|
||||
- Card - The card that would be discarded.
|
||||
- Player - The player that would have discarded
|
||||
|
||||
## Draw
|
||||
|
||||
Draw events are checked when a player is about to draw a card. There is
|
||||
1 special parameter:
|
||||
|
||||
- ValidPlayer - The player who would draw must match this.
|
||||
|
||||
There are no Replaced-variables.
|
||||
|
||||
## GainLife
|
||||
|
||||
GainLife events are checked when a player would gain life. There is 1
|
||||
special parameter:
|
||||
|
||||
- ValidPlayer - The player who would gain life must match this.
|
||||
|
||||
There is 1 Replaced-variable:
|
||||
|
||||
- LifeGained - The amount of damage to be gained.
|
||||
|
||||
## GameLoss
|
||||
|
||||
GameLoss events are checked when a player would lose. There is 1 special
|
||||
parameter:
|
||||
|
||||
- ValidPlayer - The player who would lose must match this.
|
||||
|
||||
There are no Replaced-variables.
|
||||
|
||||
## Moved
|
||||
|
||||
Moved events are checked when a card would be moved between zones. There
|
||||
are 3 special parameters:
|
||||
|
||||
- ValidCard - The moving card must match this.
|
||||
- Origin - The card must be moving from this zone.
|
||||
- Destination - The card must be moving to this zone.
|
||||
|
||||
There is 1 Replaced-variable:
|
||||
|
||||
- Card - The moving card.
|
||||
103
docs/Card-scripting-API/Restrictions.md
Normal file
@@ -0,0 +1,103 @@
|
||||
### Activation
|
||||
|
||||
Activation$ <Option> can accept several different values:
|
||||
|
||||
- Metalcraft
|
||||
- Threshold
|
||||
- Hellbent
|
||||
|
||||
These basically follow the text of the card.
|
||||
|
||||
### ActivationZone
|
||||
|
||||
ActivationZone$ <Zone> For cards that have abilities that you can
|
||||
activate outside of the Battlefield
|
||||
|
||||
### Flashback
|
||||
|
||||
While Flashback isn't technically an SA\_Restriction it's listed here
|
||||
because it's handled very similarly in AFs. If sets the ActivationZone
|
||||
to Graveyard and sets the FlashbackAbility flag of a SpellAbility which
|
||||
will exile the card as it finishes resolving.
|
||||
|
||||
### SorcerySpeed
|
||||
|
||||
SorcerySpeed $True For cards that can only activate if you could cast a
|
||||
sorcery.
|
||||
|
||||
### PlayerTurn
|
||||
|
||||
PlayerTurn $True For cards that can only be activated during the
|
||||
activating player's turn.
|
||||
|
||||
### OpponentTurn
|
||||
|
||||
OpponentTurn $True For cards that can only be activated during the
|
||||
opponent's turn of the activating player.
|
||||
|
||||
### AnyPlayer
|
||||
|
||||
AnyPlayer $True For cards that can be activated by any player. Prophecy
|
||||
has lots of examples of these types of cards.
|
||||
|
||||
### ActivationLimit
|
||||
|
||||
ActivationLimit$<activationsPerTurn> For cards that have a limited
|
||||
amount of uses
|
||||
|
||||
### ActivationNumberSacrifice
|
||||
|
||||
ActivationNumberSacrifice$<activations> For cards that if they you
|
||||
activate them more than a certain amount per turn are sacrificed at end
|
||||
of the turn.
|
||||
|
||||
### ActivationPhases
|
||||
|
||||
ActivationPhases$\<Phases,Seperated,By,Commas\> For Abilities can only
|
||||
be activated during upkeep then it'll be ActivationPhases$Upkeep if it's
|
||||
before Declare Attackers than it's
|
||||
ActivationPhases$Upkeep,Draw,Main1,BeginCombat
|
||||
|
||||
This can also be handled in a range. ActivationPhases$
|
||||
BeginCombat-\>EndCombat for Spells that can only be cast during combat.
|
||||
|
||||
### ActivationCardsInHand
|
||||
|
||||
ActivationCardsInHand$<CardsInHand> For Abilities with Hellbent (for 0)
|
||||
or Library of Alexandria (for 7)
|
||||
|
||||
### Planeswalker
|
||||
|
||||
Planeswalker$ True For Planeswalker Abilities. These can only be
|
||||
activated at Sorcery Speed, and only if no Planeswalker Abilities
|
||||
(including this ability) on this card have been activated that turn.
|
||||
|
||||
Add Ultimate$ True when applicable for the AI and achievements.
|
||||
|
||||
### Present
|
||||
|
||||
Present is a Restriction that comes in two parts. IsPresent is required.
|
||||
And PresentCompare is optional.
|
||||
|
||||
#### IsPresent
|
||||
|
||||
IsPresent$ <ValidCards> gets all cards on the battlefield that is
|
||||
considered a ValidCard.
|
||||
|
||||
#### PresentCompare
|
||||
|
||||
PresentCompare$\<2 Letter Comparison\><Comparator>. The comparisons are
|
||||
LE,LT,EQ,NE,GE,GT and translated to \<=, \<, ==, \!=, \>=, \>. The
|
||||
comparator is on the right side of the equation.
|
||||
|
||||
If PresentCompare is **missing**, the Restriction defaults to GE1.
|
||||
|
||||
### Condition
|
||||
Condition is similar to a IsPresent restriction, except it's checked on Resolution of the spell and not on Activation.
|
||||
|
||||
### CheckSVar
|
||||
|
||||
CheckSVar specifies that the results computed from an SVar (Usually via
|
||||
xCount) must equal a certain value which you can specify in the
|
||||
accompanying SVarCompare parameter. SVarCompare is the same as
|
||||
PresentCompare above.
|
||||
219
docs/Card-scripting-API/Targeting.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# Affected
|
||||
|
||||
A defined parameter can be either for something that states what is
|
||||
receiving the action. Remember this is non-targeted!
|
||||
|
||||
## Defined Players
|
||||
|
||||
Defined Players are for SAs like Draw or Discard. They tell you who is
|
||||
receiving this action (but not in a targeted way).
|
||||
|
||||
### You
|
||||
|
||||
The most common of these is Defined$ You. It means exactly what you
|
||||
think "You draw a card." "You discard a card." Cards that don't seem to
|
||||
define anything that affect Players should default to "You"
|
||||
|
||||
A spell that lets you draw a card is:
|
||||
|
||||
SP$ Draw \| Defined$ You
|
||||
|
||||
It is important to include this in SAs that have an unclear "Default"
|
||||
value such as Damage.
|
||||
|
||||
### Opponent
|
||||
|
||||
Also, fairly common is Defined$ Opponent. It means "Deals damage to each
|
||||
opponent" "Each Opponent discards a card."
|
||||
|
||||
A spell that deals damage to each opponent is:
|
||||
|
||||
SP$DealDamage \| Defined$ Opponent \| NumDmg$ 5
|
||||
|
||||
### TargetedController
|
||||
|
||||
### TargetedOwner
|
||||
|
||||
### EnchantedController
|
||||
|
||||
It means, "the controller of the enchanted thing." This would be the
|
||||
"that player" part of things like "At the beginning of enchanted
|
||||
creature's controller's upkeep, that player loses 1 life."
|
||||
|
||||
### EnchantedOwner
|
||||
|
||||
### AttackingPlayer
|
||||
|
||||
Things like "Whenever Souls of the Faultless is dealt combat damage, you
|
||||
gain that much life and attacking player loses that much life."
|
||||
|
||||
### DefendingPlayer
|
||||
|
||||
Things like "Whenever CARDNAME deals combat damage, defending player
|
||||
discards a card."
|
||||
|
||||
### Player
|
||||
|
||||
Each player.
|
||||
|
||||
## Defined Cards
|
||||
|
||||
Defined Cards are for SAs like Pump or Regenerate. They tell you who is
|
||||
receiving this action (but not in a targeted way).
|
||||
|
||||
### Self
|
||||
|
||||
The most common of these is Defined$ Self. It means exactly what you
|
||||
think "I gain Flying" "Regenerate Me." Abilities that don't seem to
|
||||
define anything should affect Cards default to "Self"
|
||||
|
||||
AB$Regenerate\| Defined$ Self
|
||||
|
||||
It is important to include this in SAs that have an unclear "Default"
|
||||
value such as Damage.
|
||||
|
||||
### Enchanted
|
||||
|
||||
Enchanted is fairly common. It means "do this action to the card I'm
|
||||
Enchanted to"
|
||||
|
||||
A simple example is a card that Pumps the attack of the Enchanted creature
|
||||
|
||||
AB$Pump \| Defined$ Enchanted \| NumAtt$ +1
|
||||
|
||||
### Equipped
|
||||
|
||||
Equipped isn't very common, but is very similar. It means "do this
|
||||
action to the card I'm Equipped to"
|
||||
|
||||
AB$Pump \| Defined$ Equipped \| NumAtt$ +2 \| NumDef$ +2
|
||||
|
||||
### Remembered
|
||||
|
||||
### ThisTurnEntered
|
||||
|
||||
ThisTurnEntered lets you make the ability act on valid cards that were
|
||||
put in a certain zone,from a certain zone, this turn. The format is
|
||||
"ThisTurnEntered <Destination> \[from <Origin>\] <ValidExpression>".
|
||||
|
||||
For example: No Rest for the Wicked and the like would use:
|
||||
`AB$ ChangeZone | Cost$ Sac<1/CARDNAME> | Defined$ ThisTurnEntered Graveyard from Battlefield Creature.YouCtrl | SpellDescription$ Return to your hand all creature cards in your graveyard that were put there from the battlefield this turn.`
|
||||
|
||||
### Targeted
|
||||
|
||||
Targeted will only appear on a [SubAbility][]. It means "Do this action
|
||||
to whatever a parent Ability targeted"
|
||||
|
||||
That may sound confusing so here's an example.
|
||||
|
||||
If you had a spell that says "Untap target Creature. It gains +1/+1
|
||||
until end of turn" it would look like similar to this.
|
||||
|
||||
SP$Untap \| ValidTgts$ Creature \| SubAbility$ DBPump
|
||||
|
||||
SVar:DBPump:DB$Pump \| Defined$ Targeted \| NumAtt$ +1 \| NumDef$ +1
|
||||
|
||||
# Targets
|
||||
|
||||
Each element follows the form:
|
||||
|
||||
CardType{.restriction}{+furtherRestriction}
|
||||
|
||||
The restrictions are optional.
|
||||
|
||||
CardType may be any type, generally the supertypes like Creature,
|
||||
Artifact, Enchantment, Land, etc. However, it could also be Elf or
|
||||
Goblin... though that would also include Elf Enchantments, for example.
|
||||
To specify an Elf Creature, then it should be "Creature.Elf".
|
||||
"Permanent" represents any permanent, "Card" any card....
|
||||
|
||||
Restrictions other than type that are interpreted: (case sensitive)
|
||||
|
||||
` named{Name}`
|
||||
` notNamed{Name}`
|
||||
` sameName`
|
||||
` NamedCard`
|
||||
|
||||
` Color, nonColor (White, nonWhite, etc)`
|
||||
` Colorless, nonColorless`
|
||||
` Multicolor, nonMulticolor`
|
||||
` MonoColor, nonMonoColor`
|
||||
` ChosenColor`
|
||||
` SharesColorWith`
|
||||
|
||||
` YouCtrl, YouDontCtrl`
|
||||
` YouOwn, YouDontOwn`
|
||||
` EnchantedPlayerCtrl`
|
||||
` OwnerDoesntControl`
|
||||
` ControllerControls{type}`
|
||||
` RememberedPlayerCtrl (use if Remember will be set before resolution)`
|
||||
` TargetedPlayerCtrl (only use if restriction is needed wile initial targeting happens)`
|
||||
|
||||
` Other,Self`
|
||||
|
||||
` AttachedBy,Attached`
|
||||
` DamagedBy,Damaged`
|
||||
|
||||
` with{Keyword}, without{Keyword}`
|
||||
` tapped, untapped`
|
||||
` faceDown`
|
||||
` enchanted, unenchanted, enchanting, EnchantedBy`
|
||||
` equipped, unequipped, equipping, EquippedBy`
|
||||
` dealtDamageToYouThisTurn`
|
||||
` wasDealtDamageThisTurn`
|
||||
` wasDealtDamageByHostThisTurn`
|
||||
` wasDealtDamageByEquipeeThisTurn`
|
||||
` token, nonToken`
|
||||
` kicked, notkicked`
|
||||
` enteredBattlefieldThisTurn`
|
||||
|
||||
` power{cmp}{X or #}`
|
||||
` toughness{cmp}{X or #}`
|
||||
` cmc{cmp}{X or #}`
|
||||
` {cmp} is a comparator:`
|
||||
` LT - Less Than`
|
||||
` LE - Less than or Equal`
|
||||
` EQ - EQual`
|
||||
` GE - Greater than or Equal`
|
||||
` GT - Greater Than`
|
||||
` X is parsed from "SVar:X:Count$___"`
|
||||
|
||||
` greatestPower, leastPower`
|
||||
` greatestCMC`
|
||||
|
||||
` counters{cmp}{X or #}{Type}`
|
||||
|
||||
` attacking, notattacking`
|
||||
` blocking, notblocking`
|
||||
` blocked, unblocked, blockedBySource`
|
||||
` kicked, notkicked`
|
||||
` evoked`
|
||||
` hasDevoured, hasNotDevoured`
|
||||
` non{Type}`
|
||||
` ChosenType{Type} `
|
||||
` {Type}`
|
||||
|
||||
` CostsPhyrexianMana`
|
||||
|
||||
` Above, DirectlyAbove`
|
||||
` TopGraveyardCreature`
|
||||
` TopGraveyard`
|
||||
|
||||
` Cloned`
|
||||
|
||||
` isRemembered`
|
||||
|
||||
Examples:
|
||||
|
||||
"Artifact or Enchantment" would be represented as 2 elements "Artifact,Enchantment"
|
||||
|
||||
"non-black, non-artifact creature" would be represented as "Creature.nonBlack+nonArtifact"
|
||||
|
||||
"creature with power 2 or less" would be "Creature.powerLE2"
|
||||
|
||||
"nonbasic land" is "Land.nonBasic"
|
||||
|
||||
"creature with flying" is "Creature.withFlying"
|
||||
|
||||
"creature self with four or more level counters" is
|
||||
"Creature.countersGE4LEVEL+Self"
|
||||
918
docs/Card-scripting-API/Triggers.md
Normal file
@@ -0,0 +1,918 @@
|
||||
Triggers define when an ability should be automatically added to the
|
||||
stack. Their syntax in the card files are very similar to that used by
|
||||
the various AbilityFactorys, that is: every trigger line starts with
|
||||
"T:" (without the quotes) and is a collection of name-value pairs (name
|
||||
and value are separated by $) separated by pipes (|). Like with
|
||||
AbilityFactorys, there are a few that show up almost always if not
|
||||
always. There are two parameters that are always present on a trigger:
|
||||
|
||||
- Mode - Specifies what kind of situation the trigger should wait for.
|
||||
- TriggerDescription - Describes the trigger, much like AF's
|
||||
SpellDescription parameter.
|
||||
- (Execute - Specifies the name of the SVar that holds the ability to
|
||||
run when the trigger goes off.)
|
||||
|
||||
_NOTE: The Execute parameter may be absent, if you are writing a delayed triggered ability. See below._
|
||||
|
||||
Depending on which Mode is specified, other parameters may be expected.
|
||||
Below are the currently available modes.
|
||||
|
||||
The script has access to many things that were previously internal to
|
||||
triggers. These things are accessed via Triggered-variables. Triggered
|
||||
variables are always of the form "Triggered<Variable Name>\[Controller/Owner\]" and are specific to each trigger mode. You can use Triggered-variables that return a card or a player directly in Defined$ parameters or to grab extra info from (like you use "Targeted" for, for instance "SVar:X:TriggeredCard$CardPower").
|
||||
You can get the controller or owner of a card returned by a Triggered-variable by appending "Controller" or "Owner" to the variable. Triggered-variables that return an integer can only be accessed from Count$, i.e. "SVar:X:Count$TriggeredLifeAmount".
|
||||
|
||||
Other parameters that triggers can use are:
|
||||
|
||||
- Secondary - If a trigger has Secondary$ True set, it means that it's
|
||||
trigger description won't show up in a card's text box. This can be
|
||||
used if you need to use several triggers to emulate a single effect
|
||||
on a real card.
|
||||
- Static - This parameter is mainly used for "As CARDNAME enters the
|
||||
battlefield..." type things. It causes the triggered ability to
|
||||
resolve right away, instead of going on the stack.
|
||||
- ResolvingCheck
|
||||
- NoResolvingCheck - makes a trigger not recheck its condition to resolve
|
||||
|
||||
## Always
|
||||
|
||||
Always-triggers represent State triggers, a special kind of triggers. These triggers will not do any checks for intervening if-clauses and will not go on the stack if an instance of it has already been put there. They are checked every time state effects are checked.
|
||||
Examples: Emperor Crocodile.
|
||||
There are no special parameters and no triggered-variables.
|
||||
|
||||
## Attached
|
||||
|
||||
Attached-triggers go off when a card is attached, via enchanting or
|
||||
equipping, to another card.
|
||||
Examples: Bramble Elemental, Kithkin Armor.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidSource - The card that is being attached to another must match
|
||||
this for the trigger to go off.
|
||||
- ValidTarget - The card that is having another card attached to it
|
||||
must match this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Source - The card that is being attached.
|
||||
- Target - The card that is being attached to.
|
||||
|
||||
## AttackerBlocked
|
||||
|
||||
AttackerBlocked-triggers go off when a creature becomes blocked. It goes
|
||||
off only once (no matter how many blockers there are) right after the
|
||||
declare blockers step.
|
||||
Examples: AEther Membrane, Alley Grifters.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidCard - The attacking creature must match this for the trigger
|
||||
to go off.
|
||||
- ValidBlocker - The blocking creature must match this for the trigger
|
||||
to go off.
|
||||
|
||||
There are 3 Triggered-variables:
|
||||
|
||||
- Attacker - The card of the attacking creature.
|
||||
- Blocker - The card of the blocking creaure.
|
||||
- NumBlockers - The number of things blocking the attacker
|
||||
|
||||
## AttackerUnblocked
|
||||
|
||||
AttackerUnblocked-triggers go off when a creature attacks and is not
|
||||
blocked, right after the declare blockers step.
|
||||
Examples: Abyssal Nightstalker, Dauthi Mindripper
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The attacking creature must match this for the trigger
|
||||
to go off.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Attacker - The card of the attacking creature.
|
||||
|
||||
## AttackersDeclared
|
||||
|
||||
Goes off after attackers are declared, if any attackers were declared,
|
||||
once a combat only.
|
||||
Examples: Lightmine Field, Curse of Inertia. There are 2 special
|
||||
parameters:
|
||||
|
||||
- AttackingPlayer - The attacking player must match this.
|
||||
- AttackedTarget - One of the game entities in TriggeredAttackedTarget
|
||||
must match this.
|
||||
|
||||
There are 3 Triggered-variables:
|
||||
|
||||
- Attackers - Collection of attacking creatures.
|
||||
- AttackingPlayer - The targeted object.
|
||||
- AttackedTarget - Collection of game entities that each attacker is attacking.
|
||||
|
||||
## Attacks
|
||||
|
||||
Attacks-triggers go off when a creature attacks. That is, it goes off
|
||||
once for each creature that attacks during your each combat phase (Right
|
||||
after the declare attackers step).
|
||||
Examples: Accorder Paladin, Trepanation Blade.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidCard - The attacking creature must match this for the trigger
|
||||
to go off.
|
||||
- Alone - If this is True, the trigger will only go off if the
|
||||
creature attacks alone.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Attacker - The card of the attacking creature.
|
||||
|
||||
## BecomeMonstrous
|
||||
|
||||
BecomeMonstrous-triggers go off when a creature becomes Monstrous,
|
||||
naturally.
|
||||
Examples: Arbor Colossus, Ember Swallower.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The card that becomes monstrous must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that became monstrous.
|
||||
|
||||
## BecomesTarget
|
||||
|
||||
BecomesTarget-triggers go off when a spell or ability (or either) is put
|
||||
on the stack targeting something that matches a Valid-expression.
|
||||
Examples: Angelic Protector, Cephalid Illusionist.
|
||||
There are 3 special parameters:
|
||||
|
||||
- SourceType - Can be Spell or Ability.It is optional.
|
||||
- ValidSource - The card that targets something must match this.
|
||||
- ValidTarget - The targeted object must match this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Source - The targeting card.
|
||||
- Target - The targeted object.
|
||||
|
||||
## BlockersDeclared
|
||||
|
||||
Goes off after blockers are declared if there are any, once in a combat
|
||||
only. (unlike Blocks, which goes off once for each creature that blocks.)
|
||||
Examples: Tide of War
|
||||
There are no special parameters.
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Blockers - Collection of all blockers.
|
||||
- Attackers - Collection of all attackers.
|
||||
|
||||
## Blocks
|
||||
|
||||
Blocks-triggers go off when a creature blocks (No surprise there ;) ).
|
||||
Examples: Amphibious Kavu, Cinder Wall (See below).
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidCard - The blocking creature must match this.
|
||||
- ValidBlocked - The creature being blocked must match this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Attacker - The card of the attacking creature.
|
||||
- Blocker - The card of the blocking creaure.
|
||||
|
||||
## Championed
|
||||
|
||||
Goes off when a creature is championed.
|
||||
Examples: Mistbind Clique.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidCard - The card being exiled for championing must match this.
|
||||
- ValidSource - The champion card that is being played must match
|
||||
this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Championed - The champion-exiled card
|
||||
- Card - The championing card.
|
||||
|
||||
## ChangesController
|
||||
|
||||
Goes off when a card changes controller.
|
||||
Examples: Coffin Queen, Duplicity.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidCard - The card whose controller changes must match this.
|
||||
- ValidOriginalController - The player who originally controlled the
|
||||
card must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card whose controller changes.
|
||||
|
||||
## ChangesZone
|
||||
|
||||
ChangesZone-triggers go off whenever, you guessed it, a card changes
|
||||
zone.
|
||||
Examples: Contagion Clasp, see below.
|
||||
There are 3 special parameters:
|
||||
|
||||
- ValidCard - The card that was moved must match this for the trigger
|
||||
to go off.
|
||||
- Origin - The card must be moved from this zone for the trigger to go
|
||||
off. (Defaults to "Any")
|
||||
- Destination - The card must be moved to this zone for the trigger to
|
||||
go off. (Defaults to "Any")
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that was moved.
|
||||
|
||||
## Clashed
|
||||
|
||||
Clashed-triggers go off whenever a player has clashed, regardless of
|
||||
wether you won or not. They are always run after the clash is through.
|
||||
Examples: Entangling Trap, Rebellion of the Flamekin.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidPlayer - Who clashed.
|
||||
- Won - True if the player must have won, false otherwise.
|
||||
|
||||
There are no Triggered-variables.
|
||||
|
||||
## CombatDamageDoneOnce
|
||||
|
||||
Goes off once for every game entity that recieves damage in combat.
|
||||
Examples: Nature's Will, Pyrewild Shaman.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidSource - One or more of the game entities that dealt the damage
|
||||
must match this.
|
||||
- ValidTarget - The game entity that recieved damage must match this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Sources - A collection of the game entities that dealt the damage.
|
||||
- Target - The game entity that recieved damage.
|
||||
|
||||
## CounterAdded & CounterRemoved
|
||||
|
||||
These triggers go off when a counter is added to / removed from a
|
||||
card.
|
||||
Examples: Bloodcrazed Hoplite, Aeon Chronicler.
|
||||
There are 2 or 3 special parameters:
|
||||
|
||||
- ValidCard - The card getting the counter must match this for the
|
||||
trigger to go off.
|
||||
- CounterType - The counter must be of this type for the trigger to go
|
||||
off.
|
||||
- NewCounterAmount - The counter amount AFTER the trigger fires must
|
||||
be this. NOTE: only available to CounterRemoved at the moment.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - the card the counter was added to
|
||||
|
||||
## Countered
|
||||
|
||||
Goes off when a spell or ability is countered.
|
||||
Examples: Lullmage Mentor, Multani's Presence.
|
||||
There are 3 special parameters:
|
||||
|
||||
- ValidCard - The host card of the spell/ability that was countered
|
||||
must match this.
|
||||
- ValidPlayer - The player that cast/activated the spell/ability that
|
||||
was countered must match this.
|
||||
- ValidCause - The host card of the spell/ability that did the
|
||||
countering must match this.
|
||||
|
||||
There are 3 Triggered-variables:
|
||||
|
||||
- Card - The host card of the spell/ability that was countered.
|
||||
- Player - The player that cast/activated the spell/ability that was
|
||||
countered.
|
||||
- Cause - The host card of the spell/ability that did the countering.
|
||||
|
||||
## Cycled
|
||||
|
||||
Cycled-triggers simply go off whenever you cycle a card.
|
||||
Examples: Astral Slide, Bant Sojourners.
|
||||
There is 1 special parameters:
|
||||
|
||||
- ValidCard - The card that was cycled must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that was cycled.
|
||||
|
||||
## DamageDone
|
||||
|
||||
DamageDone-triggers go off whenever any source deals damage to any
|
||||
target.
|
||||
Examples: Abomination of Gudul, Prophetic Flamespeaker.
|
||||
There are 4 special parameters:
|
||||
|
||||
- ValidSource - The source of the damage must match this.
|
||||
- ValidTarget - The target of the damage must match this.
|
||||
- CombatDamage - If set to true, the trigger will only go off if the
|
||||
damage dealt was combat damage.If set to false, it will only go off
|
||||
if it wasn't combat damage.. If omitted, it will go off either way.
|
||||
- DamageAmount - Specifies how much damage must be done for the
|
||||
trigger to go off. It takes the form "<operator><operand>" where
|
||||
<operator> can be LT (Less Than),LE (Less than or Equal),EQ
|
||||
(Equal),GE (Greater than or Equal) or GT (Greater Than) and
|
||||
<operand> is a positive integer.
|
||||
|
||||
There are 3 Triggered-variables:
|
||||
|
||||
- Source - The source of the damage.
|
||||
- Target - The target of the damage.
|
||||
- DamageAmount - The amount of damage dealt(integer).
|
||||
|
||||
<b>BEWARE: Since the Target variable can be either a player or a card,
|
||||
you must take care to limit this in the ValidTarget parameter\!</b>
|
||||
|
||||
## DealtCombatDamageOnce
|
||||
|
||||
Goes off \*once\* for each creature that deals damage in combat.
|
||||
Examples: Arashin War Beast, Five Alarm Fire.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidTarget - The game entity that recieves damage must match this.
|
||||
- ValidSource - The card that dealt damage must match this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Target - The game entity that recieves damage.
|
||||
- Source - The card that dealt damage.
|
||||
|
||||
## Destroyed
|
||||
|
||||
Goes off when a permanent is destroyed. (Not if it it destroyed, but
|
||||
then regenerated.)
|
||||
Examples: Cobra Trap, Sacred Ground.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidCard - The card that was destroyed must match this.
|
||||
- ValidCauser - The player that activated the spell/ability that
|
||||
destroyed the card must match this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Card - The card that was destroyed.
|
||||
- Causer - The player that activated the spell/ability that destroyed
|
||||
the card.
|
||||
|
||||
## Devoured
|
||||
|
||||
Goes off when a creature is sacrificed for a Devour creature.
|
||||
Examples: Kresh the Bloodbraided Avatar.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidDevoured - The devoured creature must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Devoured - The devoured creature.
|
||||
|
||||
## Discarded
|
||||
|
||||
Discarded-triggers go off whenever a card is discarded from a players
|
||||
hand. (side note: ChangesZone-triggers may also go off here because the
|
||||
card is moved from the hand to the graveyard)
|
||||
Examples: Abyssal Nocturnus, Confessor.
|
||||
There are 3 special parameters:
|
||||
|
||||
- ValidPlayer - The player who discarded the card must match this.
|
||||
- ValidCard - The discarded card must match this.
|
||||
- ValidCause - The card that caused the player to discard must match
|
||||
this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that was discarded.
|
||||
|
||||
## Drawn
|
||||
|
||||
Drawn-triggers go off when a player draws a card.
|
||||
Examples: Booby Trap, Kederekt Parasite.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The drawn card must match this.
|
||||
|
||||
There are 2 Triggered-variable:
|
||||
|
||||
- Card - The card that was drawn.
|
||||
- Player - The player that drew the card.
|
||||
|
||||
## Evolved
|
||||
|
||||
Goes off when a creature gets a +1/+1 counter from evolving.
|
||||
Examples: Renegade Krasis.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The card that got the counter must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that got the counter.
|
||||
|
||||
## FlippedCoin
|
||||
|
||||
Goes off when a player flips a coin.
|
||||
Examples: Chance Encounter, Karplusan Minotaur.
|
||||
There are 2 special parameter:
|
||||
|
||||
- ValidPlayer - The player who flipped the coin must match this.
|
||||
- ValidResult - If this parameter is "Win", the player must win the
|
||||
flip.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Player - The player who flipped the coin.
|
||||
|
||||
## LandPlayed
|
||||
|
||||
LandPlayed-triggers of course go off when a land is played.
|
||||
Examples: Burgeoning, City of Traitors.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The played card must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that was played.
|
||||
|
||||
## LifeGained & LifeLost
|
||||
|
||||
These triggers go off on when a player either gains or loses life, of
|
||||
course.
|
||||
Examples: Cradle of Vitality, Exquisite Blood.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidPlayer - The player who gained or lost life must match this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Player - The player that gained/lost life.
|
||||
- LifeAmount - The amount of life lost/gained. (integer)
|
||||
|
||||
## LosesGame
|
||||
|
||||
Goes off when a player loses the game.
|
||||
Examples: Blood Tyrant, Elbrus, the Binding Blade // Withengar Unbound
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidPlayer - The player who lost the game must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Player - The Player who lost the game.
|
||||
|
||||
## NewGame
|
||||
|
||||
Goes off once at the start of each game, after mulligans.
|
||||
Examples: Maralen of the Mornsong Avatar, Worldknit.
|
||||
There are no special parameters or Triggered-variables.
|
||||
|
||||
## PayCumulativeUpkeep
|
||||
|
||||
Goes off when a player pays or doesn't pay the cumulative upkeep for a
|
||||
card.
|
||||
Examples: Balduvian Fallen, Heart of Bogardan.
|
||||
There are 2 special parameters:
|
||||
|
||||
- Paid - Wether or not the player must have paid the cumulative
|
||||
upkeep.
|
||||
- ValidCard - The card that has the cumulative upkeep must match this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Card - The card that has the cumulative upkeep.
|
||||
- PayingMana - A string representing the mana spent.
|
||||
|
||||
## PayEcho
|
||||
|
||||
Goes off when a player pays or doesn't pay the echo cost for a card.
|
||||
Examples: Shah of Naar Isle.
|
||||
There are 2 special parameters:
|
||||
|
||||
- Paid - Wether or not the player must have paid the echo.
|
||||
- ValidCard - The card that has the echo must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that has the echo.
|
||||
|
||||
## Phase
|
||||
|
||||
Phase-triggers go off at specific points in the turns.
|
||||
Examples: AEther Vial, see below.
|
||||
There are 2 special parameters:
|
||||
|
||||
- Phase - The phase during which the trigger should go off.
|
||||
- ValidPlayer - The player who's turn it should be.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Player - The player whose turn it is.
|
||||
|
||||
## PhaseIn & PhaseOut
|
||||
|
||||
Goes off when a permanent phases in or out, specifically only while that
|
||||
permanent is still on the battlefield since triggers don't work on
|
||||
phased out objects.
|
||||
Examples: Shimmering Efreet, Ertai's Familiar.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The card phasing in or out must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card phasing in or out.
|
||||
|
||||
## PlanarDice
|
||||
|
||||
Goes off when the planar dice is rolled in a Planechase game.
|
||||
Examples: Panopticon, Orzhova.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidPlayer - The player that rolled the dice must match this.
|
||||
- Result - The dice must roll this. Blank,Chaos or Planeswalk.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Player - The player that rolled the dice.
|
||||
|
||||
## PlaneswalkedTo & PlaneswalkedFrom
|
||||
|
||||
These triggers go off when a player planeswalks to or away from one or
|
||||
more planes.
|
||||
Examples: Panopticon, Orzhova.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - One of the planes must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Cards - A collection of all the cards planeswalked to or from.
|
||||
|
||||
## Sacrificed
|
||||
|
||||
Sacrificed-triggers go off whenever a player sacrifices a permanent.
|
||||
Examples: Dragon Appeasement, Mortician Beetle.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidPlayer - The player who sacrificed the card must match this.
|
||||
- ValidCard - The sacrificed card must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that was Sacrificed.
|
||||
|
||||
## Scry
|
||||
|
||||
Goes off after a player has scryed and put the card(s) in the proper
|
||||
place.
|
||||
Examples: Flamespeaker Adept, Knowledge and Power
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidPlayer - The player that scryed must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Player - The player that scryed.
|
||||
|
||||
## SearchedLibrary
|
||||
|
||||
Goes off when a player searches a library.
|
||||
Examples: Ob Nixilis, Unshackled.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidPlayer - The player searching must match this.
|
||||
- SearchOwnLibrary - If true, the player must be searching his or her
|
||||
own library (Didn't see that one coming, did ya? :P)
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Player - The player searching.
|
||||
|
||||
## SetInMotion
|
||||
|
||||
Goes off when an Archenemy Scheme is set in motion.
|
||||
Examples: A Display of My Dark Power, Perhaps You've Met My Cohort.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The scheme card that is set in motion must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Scheme - The scheme card that is set in motion.
|
||||
|
||||
## Shuffled
|
||||
|
||||
Shuffled-triggers go off whenever a player shuffles his/her library.
|
||||
Examples: Cosi's Trickster, Psychic Surgery.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidPlayer - The player who's turn it should be.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Player - The player whose turn it is.
|
||||
|
||||
## SpellCast, AbilityCast & SpellAbilityCast
|
||||
|
||||
Triggers go off whenever a spell,ability or either respectively is cast
|
||||
by either player.
|
||||
Examples: AEther Barrier, Burning-Tree Shaman, Grip of Chaos.
|
||||
There are 4 special parameters:
|
||||
|
||||
- ValidControllingPlayer - The player who controls the cast spell must
|
||||
match this.
|
||||
- ValidActivatingPlayer - The player who activated the ability/spell
|
||||
must match this. (NOTE: For spells, the activator and controller are
|
||||
the same. They usually the same for abilities too, with the
|
||||
exception being abilities that can be activated by any player)
|
||||
- ValidCard - The card the cast spell or ability originates from must
|
||||
match this.
|
||||
- TargetsValid - If this parameter is present, the spell or ability
|
||||
must be targeted and at least one of it's targets must match this.
|
||||
|
||||
There are 4 Triggered-variable:
|
||||
|
||||
- Card - The card that the cast spell or ability originates from.
|
||||
- SpellAbility - The SpellAbility object.
|
||||
- Player - The player that controls the card that the cast spell or
|
||||
ability originates from.
|
||||
- Activator - The player that activated the ability.
|
||||
|
||||
## Taps & Untaps
|
||||
|
||||
These triggers go off when a permanent taps or untaps.
|
||||
Examples: Artifact Possession, Frightshroud Courier.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The card that taps or untaps must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that was tapped/untapped.
|
||||
|
||||
## TapsForMana
|
||||
|
||||
This trigger goes off when a land is tapped for a mana ability.
|
||||
Examples: Bubbling Muck, Market Festival.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The card that taps.
|
||||
|
||||
There are 3 Triggered-variable:
|
||||
|
||||
- Card - The card that was tapped.
|
||||
- Player - the payer that did the tapping
|
||||
- Produced - a String of the Mana produced by this tapping.
|
||||
|
||||
## Transformed
|
||||
|
||||
Goes off when a card changes state from Original to Transformed or vice
|
||||
versa. (But not between any other 2 states)
|
||||
Examples: Afflicted Deserter // Werewolf Ransacker, Huntmaster of the
|
||||
Fells // Ravager of the Fells.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The card that changes state must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Transformer - The card that changes state.
|
||||
|
||||
## TurnFaceUp
|
||||
|
||||
Goes off when a card changes state from FaceDown to Original. (But not
|
||||
between any other 2 states)
|
||||
Examples: Aphetto Exterminator, Fatal Mutation.
|
||||
There is 1 special parameter:
|
||||
|
||||
- ValidCard - The card that changes state must match this.
|
||||
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- Card - The card that changes state.
|
||||
|
||||
## Unequip
|
||||
|
||||
Goes off when an equipment card is unattached from a creature, wether
|
||||
it's voluntarily unattached or not.
|
||||
Examples: Grafted Exoskeleton, Grafted Wargear.
|
||||
There are 2 special parameters:
|
||||
|
||||
- ValidCard - The card that the equipment is unattached from must
|
||||
match this.
|
||||
- ValidEquipment - The equipment being unattached must match this.
|
||||
|
||||
There are 2 Triggered-variables:
|
||||
|
||||
- Card - The card that the equipment is unattached from.
|
||||
- Equipment - The equipment being unattached.
|
||||
|
||||
## Vote
|
||||
|
||||
Goes off when a vote is called for, after all votes are cast.
|
||||
Examples: Grudge Keeper.
|
||||
There are no special parameters.
|
||||
There is 1 Triggered-variable:
|
||||
|
||||
- OtherVoters - A collection of every player who voted, EXCEPT the
|
||||
controller of the trigger's host card.
|
||||
|
||||
# Restrictions
|
||||
|
||||
You can use certain optional parameters to further restrict when a
|
||||
trigger should go off. These parameters are:
|
||||
|
||||
- TriggerZones - This parameter can be used to restrict which zone the
|
||||
card must be in in order for the trigger to go off. For example, if
|
||||
the trigger should only go off while the card is in the graveyard
|
||||
(As in Auntie's Snitch or Bridge from Below) you can use
|
||||
`TriggerZones$ Graveyard`
|
||||
- TriggerPhases - This parameter can be used to restrict the phases in
|
||||
which the trigger can trigger.
|
||||
- OpponentTurn - This parameter can be used to restrict the trigger to
|
||||
only trigger on your opponents turn.(True/False)
|
||||
- PlayerTurn - This parameter can be used to restrict the trigger to
|
||||
only trigger on your turn.(True/False)
|
||||
- Metalcraft - If this parameter is set to "True", the controller of
|
||||
this card must have 3 or more artifacts on the battlefield for the
|
||||
trigger to go off.
|
||||
- Threshold - As Metalcraft but requires the controller of the card to
|
||||
have 7 cards in the graveyard.
|
||||
- PlayersPoisoned - This parameter specifies that a certain or both
|
||||
players must have at least 1 poison counter. Valid values are "You",
|
||||
"Opponent" or "Each".
|
||||
- IsPresent - This parameter expects a
|
||||
[ValidCard](Forge_ValidCards "wikilink") formula and only lets the
|
||||
trigger go off if there is a permanent on the battlefield that
|
||||
matches it.
|
||||
- PresentCompare,PresentZone & PresentPlayer - These parameters only
|
||||
matter if the IsPresent parameter is used. They can be used to
|
||||
narrow down and how many valid cards must be present and where they
|
||||
must be.
|
||||
- IsPresent2,PresentCompare2,PresentZone2 & PresentPlayer2 - Second
|
||||
requirement (see above).
|
||||
- CheckSVar - Calculates the named SVar and compares the result
|
||||
against the accompanying SVarCompare parameter which takes the form
|
||||
of <operator><operand> where <operator> is in LT,LE,EQ,NE,GE,GT.
|
||||
|
||||
# Examples
|
||||
|
||||
Learn by example\!
|
||||
|
||||
## Contagion Clasp
|
||||
|
||||
(When Contagion Clasp enters the battlefield, put a -1/-1 counter on
|
||||
target creature.) The first thing to do is to identify the WHEN, i.e.
|
||||
when the trigger should go off so that we can decide on the proper mode.
|
||||
Here it should go off when a particular card moves from one zone into
|
||||
another, so ChangesZone is the logical mode here.
|
||||
`T:Mode$ ChangesZone`
|
||||
Next we look at which of ChangesZone's 3 special parameters we want to
|
||||
make use of. Well, we want the trigger to go off when the card enters
|
||||
one specific zone, so we'll want to use the Destination parameter. Also,
|
||||
we only care about when it's a specific card being moved so we'll use
|
||||
ValidCard.
|
||||
`T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$
|
||||
Card.Self`
|
||||
There, we've defined a trigger that goes off when this card moves from
|
||||
any zone (remember, Origin and Destination defaults to "Any") to the
|
||||
battlefield zone. But we still have to use two more parameters. First,
|
||||
"Execute". Execute should contain the name of the SVar that holds the
|
||||
Ability you want to be triggered.It can be any valid SVar name. I like
|
||||
to follow the convention "Trig<name of the abilityfactory used>" but
|
||||
your mileage may vary.
|
||||
`T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self |
|
||||
Execute$ TrigPutCounter`
|
||||
Lastly, "TriggerDescription". This is what will be put in the cards text
|
||||
box, i.e. the rules text. Always try to keep it as close to Oracle
|
||||
wording as possible. Also, you should use "CARDNAME" instead of the name
|
||||
of the card for convention.
|
||||
`T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self |
|
||||
Execute$ TrigPutCounter | TriggerDescription$ When CARDNAME enters the
|
||||
battlefield, put a -1/-1 counter on target creature.`
|
||||
So we're all done,then?No, it's not a triggered <b>ability</b> until it
|
||||
actually has an ability. To define the ability we want to be triggered
|
||||
we simply use an [AbilityFactory](Forge_AbilityFactory "wikilink") but
|
||||
instead of putting it on it's own line beginning with "A:", we put it in
|
||||
an SVar named what we put for the Execute parameter.
|
||||
`T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self |
|
||||
Execute$ TrigPutCounter | TriggerDescription$ When CARDNAME enters the
|
||||
battlefield, put a -1/-1 counter on target creature.`
|
||||
`SVar:TrigPutCounter:AB$PutCounter | Cost$ 0 | Tgt$ TgtC | CounterType$
|
||||
M1M1 | CounterNum$ 1`
|
||||
You may notice some strange things about the ability, namely the Cost
|
||||
parameter and the lack of a SpellDescription parameter.The reasoning for
|
||||
these things is that AbilityFactory requires non-drawback abilities to
|
||||
have a cost and drawback abilities not to have one. But if you want to
|
||||
do something like "When CARDNAME comes into play, you may pay 1 to...",
|
||||
that's where you'd use the Cost parameter here. The SpellDescription is
|
||||
missing because triggers use their own TriggerDescription instead.
|
||||
|
||||
## AEther Vial
|
||||
|
||||
(At the beginning of your upkeep, you may put a charge counter on AEther
|
||||
Vial.) Okay let's apply the procedure we learned when doing Contagion
|
||||
Clasp:
|
||||
Identify the WHEN - It should go off at the beginning of a specific
|
||||
PHASE so, well, Phase mode it is\!
|
||||
`T:Mode$ Phase`
|
||||
Next,look at Phase modes special parameters: Phase and ValidPlayer. We
|
||||
can tell right away that Phase should be "Upkeep" and ValidPlayer should
|
||||
be "You" since it should only trigger on your turn.
|
||||
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You`
|
||||
Next up, we have to make sure that the trigger only goes off when AEther
|
||||
Vial is in play. If it's not, why would we want to put counters on it?
|
||||
We can use the TriggerZones parameter to restrict where the card must be
|
||||
in order for the trigger to go off. It takes a comma-separated list of
|
||||
zones but since we only want AEther Vial to trigger when it's on the
|
||||
battlefield, we just give it "Battlefield".
|
||||
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$
|
||||
Battlefield`
|
||||
Now then, there's one word in the rules text that changes things: "may".
|
||||
To give the player a choice about wether or not to make use of the
|
||||
triggered ability, we can use the OptionalDecider parameter. It's used
|
||||
the same way as Defined parameters for AFs and the player it evaluates
|
||||
to gets to decide if the trigger happens.
|
||||
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$
|
||||
Battlefield | OptionalDecider$ You`
|
||||
Lastly as before, we add the Execute and TriggerDescription
|
||||
parameters...
|
||||
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$
|
||||
Battlefield | OptionalDecider$ You | Execute$ TrigPutCounter |
|
||||
TriggerDescription$ At the beginning of your upkeep, you may put a
|
||||
charge counter on CARDNAME.`
|
||||
...And our mad AbilityFactory-skills to write up the actual ability\!
|
||||
`T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$
|
||||
Battlefield | OptionalDecider$ You | Execute$ TrigPutCounter |
|
||||
TriggerDescription$ At the beginning of your upkeep, you may put a
|
||||
charge counter on CARDNAME.`
|
||||
`SVar:TrigPutCounter:AB$PutCounter | Cost$ 0 | Defined$ Self |
|
||||
CounterType$ CHARGE | CounterNum$ 1`
|
||||
DING\! Goblins are up\!
|
||||
|
||||
## Cinder Wall
|
||||
|
||||
(When Cinder Wall blocks, destroy it at end of combat.) This will be an
|
||||
example of a delayed trigger. A delayed trigger goes on the stack twice,
|
||||
once when the conditions are met, and once when the effect should
|
||||
happen.In Cinder Wall's case, once when it blocks, and once at the
|
||||
following end of combat step. To start with, we create a Blocks trigger
|
||||
that looks this.
|
||||
`T:Mode$ Blocks | ValidCard$ Card.Self`
|
||||
And this is where delayed triggers differ from ordinary triggers.We do
|
||||
NOT add an Execute parameter that points to an SVar containing the
|
||||
trigger's effect as an AF. Instead we add a DelayedTrigger parameter
|
||||
that points to an SVar containing ANOTHER trigger that specifies when
|
||||
the delayed effect should occur. This delayed trigger is built like an
|
||||
ordinary trigger and further points to an ability (Using the Execute
|
||||
parameter).Don't forget to omit the "T:" from the delayed trigger, for
|
||||
the same reason you omit "A:" from abilities in SVars\!
|
||||
`T:Mode$ Blocks | ValidCard$ Card.Self DelayedTrigger$ DelTrig |
|
||||
TriggerDescription$ When CARDNAME blocks, destroy it at end of
|
||||
combat.`
|
||||
`SVar:DelTrig:Mode$ Phase | Phase$ EndCombat | Execute$ TrigDestroy |
|
||||
TriggerDescription$ Destroy CARDNAME.` `SVar:TrigDestroy:AB$Destroy |
|
||||
Cost$ 0 | Defined$ Self`
|
||||
|
||||
# Test Cards
|
||||
|
||||
If you want some test cards for each trigger try these:
|
||||
|
||||
Always (Covetous Dragon)
|
||||
DeclareAttack (Lightmine Field)
|
||||
Attacks (Battle Cry)
|
||||
Unblocked (Abyssal Nightstalker)
|
||||
Blocked (Flanking)
|
||||
Blocks (Wall of Junk)
|
||||
Targeted (Tar Pit Warrior)
|
||||
ChangeZones (Wall of Blossoms, AEther Flash, Valakut)
|
||||
Phase (Bottomless Pit)
|
||||
SpellAbilityCast (Dragonlair Spider)
|
||||
Unqeuip (Grafted Wargear)
|
||||
Sacrifice (Grave Pact)
|
||||
Taps (Stonybrook Schoolmaster)
|
||||
Untaps (Hollowsage)
|
||||
Championed (Mistbind Clique/delayed due to input issue)
|
||||
ChangeControllers (?)
|
||||
CounterAdded (Flourishing Defenses)
|
||||
CounterRemoved (Fungal Behemoth)
|
||||
Clashed (Sylvan Echoes)
|
||||
Cycled (Bant Sojourners)
|
||||
Damaged (Akki Underminer, Living Artifact)
|
||||
Discarded (Megrim)
|
||||
Drawn (Underworld Dreams)
|
||||
LandPlayed (Horn of Greed)
|
||||
LifeGained (Ajani's Pridemate)
|
||||
LifeLost (Mindcrank)
|
||||
LoseGame
|
||||
SetInMotion
|
||||
Shuffled (Cosi's Trickster)
|
||||
TapsForMana (Mana Flare)
|
||||
Transformed (Afflicted Deserter)
|
||||
TurnFaceUp (Bloodstoke Howler)
|
||||
90
docs/Configure-Planes.md
Normal file
@@ -0,0 +1,90 @@
|
||||
Base settings of a plane is configured in
|
||||
config.json
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"screenWidth": 480,
|
||||
"screenHeight": 270,
|
||||
"skin": "skin/ui_skin.json",
|
||||
"playerBaseSpeed": 32,
|
||||
"minDeckSize": 40,
|
||||
"starterDecks": [
|
||||
"decks/starter/white.json",
|
||||
"decks/starter/black.json",
|
||||
"decks/starter/blue.json",
|
||||
"decks/starter/red.json",
|
||||
"decks/starter/green.json"
|
||||
],
|
||||
"restrictedCards": [
|
||||
"Black Lotus",
|
||||
"Ancestral Recall"
|
||||
],
|
||||
"restrictedEditions": [],
|
||||
"legalCards":{
|
||||
"editions": ["M22","M21"]
|
||||
},
|
||||
"difficulties": [
|
||||
{
|
||||
"name": "Easy",
|
||||
"startingLife": 16,
|
||||
"staringMoney": 500,
|
||||
"enemyLifeFactor": 0.8,
|
||||
"spawnRank": 0,
|
||||
"sellFactor": 0.6,
|
||||
"startItems": [
|
||||
"Manasight Amulet",
|
||||
"Leather Boots"
|
||||
]
|
||||
},{
|
||||
"name": "Normal",
|
||||
"startingLife": 12,
|
||||
"staringMoney": 250,
|
||||
"startingDifficulty": true,
|
||||
"enemyLifeFactor": 1.0,
|
||||
"spawnRank": 1,
|
||||
"sellFactor": 0.5,
|
||||
"startItems": [
|
||||
"Leather Boots"
|
||||
]
|
||||
},{
|
||||
"name": "Hard",
|
||||
"startingLife": 8,
|
||||
"staringMoney": 125,
|
||||
"enemyLifeFactor": 1.5,
|
||||
"spawnRank": 2,
|
||||
"sellFactor": 0.25
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Fields:
|
||||
|
||||
## **screenWidth**
|
||||
## **screenHeight**
|
||||
Logical screen with/height, changing this would require to change all ui elements and won't increase resolution.
|
||||
|
||||
## **skin**
|
||||
path to the used skin for adventure
|
||||
|
||||
## **playerBaseSpeed**
|
||||
base speed of player character
|
||||
|
||||
## **minDeckSize**
|
||||
minimum deck size for matches, decks with lesser cards will be filled with wastes.
|
||||
|
||||
## **starterDecks**
|
||||
string list of all starter decks
|
||||
## **restrictedCards**
|
||||
string list of restricted cards, those cards won't appear in random shops or rewards but it it still possible to get those cards if the plane specifically drops it.
|
||||
## **restrictedEditions**
|
||||
string list of restricted editions, behaves the same as restricedCards but with editions.
|
||||
|
||||
## **difficulties**
|
||||
list of DifficultyData
|
||||
## **legalCards**
|
||||
RewardData for legal cards, behaves similar as restrictedCards only as white list and not black ist.
|
||||
Also it is defined as RewardData see [Create-Rewards](https://github.com/Card-Forge/forge/wiki/Create-Rewards) for syntax
|
||||
23
docs/Configure-Sets.md
Normal file
@@ -0,0 +1,23 @@
|
||||
In order to edit which sets will show up in random rewards and shop you will need to edit the config file. You can find it here "forgefolder"\res\adventure\common\config.json. Add the sets you want to have restricted to the "restricted edition" section
|
||||

|
||||
|
||||
|
||||
|
||||
### Funny
|
||||
PCEL, PAST, UGL, UNH, HHO, DS0, PHTR, UST, PUST, PH17, PH18, CMB1, UND, PH19, PH20, PH21, UNF, DA1, PH22, MB2
|
||||
|
||||
### Commander
|
||||
COM, CM1, C13, C14, C15, C16, C17, CMA, CM2, C18, C19, C20, ZNC, KHC, C21, AFC, MIC, VOC, NEC, NCC, DMC, 40K, BRC, SCD, ONC, MOC, LTC, CMM, WOC, WHO, LCC, MKC, PIP, OTC, M3C, BLC, DSC, PDC
|
||||
|
||||
### Starters
|
||||
CLU, LTR, CLB, CMD, MB1
|
||||
|
||||
### Digital
|
||||
PRM, MBP, MED, ME2, ME3, TD0, TD1, ME4, VMA, TPR, PZ1, PZ2, ANA, PANA, HA1, HA2, HA3, AJMP, AKR, ANB, KLR, HA4, HA5, J21, YMID, YNEO, YSNC, HBG, EA1, HA6, YDMU, YBRO, EA2, YONE, SIS, SIR, EA3, HA7, YWOE, YMKM, YOTJ, YBLB, YDSK
|
||||
|
||||
### Promos
|
||||
DRC94, PHPR, PLGM, PMEI, PARL, PRED, PTMP, JGP, PSTH, PEXO, PALP, PUSG, PAL99, G99, PULG, PUDS, PPTK, PGRU, PWOR, PWOS, PMMQ, PSUS, PAL00, FNM, G00, PELP, PNEM, PPCY, PINV, PAL01, F01, G01, MPR, PPLS, PAPC, PSDG, PODY, PAL02, F02, G02, PTOR, PJUD, PHJ, PAL03, F03, PJJT, G03, P03, OVNT, PLGN, PONS, PSCG, P8ED, PAL04, F04, G04, P04, PDST, P5DN, PMRD, PCHK, PAL05, F05, G05, PJSE, P05, PMPS, PBOK, PSOK, P9ED, PRAV, P2HG, PAL06, DCI, F06, G06, PJAS, P06, PMPS06, PGPT, PCMP, PDIS, PCSP, PTSP, F07, G07, PMPS07, PRES, PPLC, PPRO, PGPX, PFUT, P10E, PLRW, F08, G08, P08, PMPS08, PMOR, P15A, PSHM, PEVE, PALA, PDTP, F09, G09, P09, PMPS09, PBOOK, PCON, PURL, PARB, PM10, PZEN, PDP10, F10, G10, P10, PMPS10, PWWK, PROE, PM11, PSOM, PDP12, F11, G11, OLGC, P11, PMPS11, PW11, PMBS, PNPH, PM12, PISD, PDP13, F12, PIDW, J12, PW12, PDKA, PAVR, PHEL, PM13, PRTR, PDP14, F13, J13, PGTC, WMC, PDGM, PM14, PSDC, PTHS, PDP15, F14, J14, PBNG, PJOU, PCNS, PS14, PPC1, PM15, PKTK, F15, J15, UGF, PFRF, PDTK, PTKDF, PS15, PORI, PSS1, PBFZ, F16, J16, POGW, PSOI, PEMN, PKLD, PS16, F17, J17, PAER, PAKH, PHOU, PS17, PXLN, PSS2, PXTC, J18, PRIX, PNAT, PDOM, PM19, PSS3, PS18, PGRN, PRWK, G18, PF19, PRNA, PRW2, J19, PWAR, PMH1, PM20, PPP1, PS19, PWCS, J20, PF20, PLG20, PL21, PW21, PLG21, Q06, P22, PL22, PW22, GDY, PLG22, SCH, PSVC, P30M, P30A, P30H, PRCQ, BOT, PEWK, P23, PR23, PW23, PL23, SLP, PF23, P30T, PMDA, REX, PF24, PW24, PL24, PSS4, PCBB, PLG24, PLTC, PF25, PSPL, PJSC
|
||||
|
||||
### Universes Beyond
|
||||
|
||||
40K,SLD,ACR,PCBB,PIP,REX,WHO,LTR,LTC,BOT,HBG,CLB,AFR
|
||||
43
docs/Console-and-cheats.md
Normal file
@@ -0,0 +1,43 @@
|
||||
Forge provides an in-game console in adventure mode.
|
||||
|
||||
You can access (and close) the console while exploring by pressing F9 (or Fn-F9).
|
||||
|
||||
To scroll the console window, click and drag the text box.
|
||||
|
||||
## Available commands
|
||||
|
||||
| Command Example | Description |
|
||||
| -- | -- |
|
||||
| resetMapQuests | Resets the map quests, resulting in all side-quest progress being lost and all side-quest types being re-picked |
|
||||
| give gold 1000 | Give 1000 gold |
|
||||
| give shards 1000 | Give 1000 shards |
|
||||
| give print lea 232 | Add an alpha (LEA set code) black lotus (232 collector number) |
|
||||
| give item <item name or code?> | Adds an in game item such as leather boots |
|
||||
| give set sld | Give 4 copies of every card in the Secret Lair Drop (set code SLD), flagged as having no sell value |
|
||||
| give nosell card forest | Gives a forest with no sell value |
|
||||
| give boosters leb | Add a booster from beta (LEB set code) |
|
||||
| give quest 123 | Add the quest by its number ID |
|
||||
| give life 10 | Add 10 life to yourself |
|
||||
| give card forest | Adds a forest to your inventory |
|
||||
| debug collision | Displays bounding boxes around entities |
|
||||
| debug map | TODO |
|
||||
| debug off | Turns off previously enable debugging |
|
||||
| teleport to 6000 5000 | Moves you 6000 tiles east and 5000 tiles north from the left bottom corner |
|
||||
| fullHeal | Returns your health back to baseline |
|
||||
| sprint 100 | Increases your speed for 100 seconds |
|
||||
| setColorId R | Sets the player color identity; Probably used for testing and shops |
|
||||
| clearnosell | Clears the no sell value flag from all cards you own that are not used in a deck |
|
||||
| remove enemy abc | Remove the enemy from the map with the map ID abc |
|
||||
| remove enemy all | Remove all the enemies from the map |
|
||||
| dumpEnemyDeckList | Print the enemy deck lists to terminal output stream |
|
||||
| getShards amount 100 | Similar to give shards command; Gives 100 shards to the player |
|
||||
| resetQuests | Resets the world quests. In progress quests are not abandonned or reset including dungeons. Does not reroll abandoned quests. Not really sure what this does. |
|
||||
| hide 100 | Enemies do not chase you for 100 seconds |
|
||||
| fly 100 | You can walk over obstacles for 100 seconds |
|
||||
| crack | Cracks a random item you are wearing |
|
||||
| spawn enemy Sliver | Spawns a Sliver on your screen |
|
||||
| listPOI | Prints all locations in terminal output stream as ID-type pairings |
|
||||
| leave | Gets you out of the current town/dungeon/cave |
|
||||
| dumpEnemyColorIdentity | Prints all enemies, their colour affinity and deck name to terminal output |
|
||||
| heal | Recover your full health |
|
||||
| dumpEnemyDeckColors | Prints all decks available to enemies and their affinities |
|
||||
107
docs/Create-Enemies.md
Normal file
@@ -0,0 +1,107 @@
|
||||
All Enemies are stored under `res/<AdventureName>/world/enemies.json`
|
||||
|
||||
Enemies spawned on the overworld map or on map stages will use this exact template to define their base behavior. These values can be modified or added to with additional settings on an individual enemy basis, details of which can be found within [map instance](Create-new-Maps).
|
||||
|
||||
Some ideas for custom enemy cards:
|
||||
- basic (CR or at least Alchemy conform) effects for normal mobs to add flavor
|
||||
- advanced + all UN stuff. but their mechanics could be combined in new ways not seen in print
|
||||
- boss/rare ones that are balanced around meta mechanics (=quests)
|
||||
|
||||
The Json file contains an Array of Objects and each Object is one enemy.
|
||||
EnemyObject:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Challenger 20",
|
||||
"nameOverride": "Challenger",
|
||||
"sprite": "sprites/monsters/doppelganger.atlas",
|
||||
"deck": [
|
||||
"decks/challenger/challenger_20_allied_fires.dck",
|
||||
"decks/challenger/challenger_20_cavalcade_charge.dck",
|
||||
"decks/challenger/challenger_20_final_adventure.dck",
|
||||
"decks/challenger/challenger_20_flash_of_ferocity.dck"
|
||||
],
|
||||
"ai": "",
|
||||
"randomizeDeck": true,
|
||||
"spawnRate": 0.25,
|
||||
"difficulty": 0.25,
|
||||
"speed": 28,
|
||||
"life": 22,
|
||||
"rewards": [],
|
||||
"colors": "UBRWG",
|
||||
"questTags": [
|
||||
"Challenger",
|
||||
"IdentityUnknown",
|
||||
"BiomeGreen",
|
||||
"BiomeRed",
|
||||
"BiomeColorless",
|
||||
"BiomeWhite",
|
||||
"BiomeBlue",
|
||||
"BiomeBlack"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
# Fields:
|
||||
|
||||
## **name**
|
||||
String - Has to be unique
|
||||
Name of the enemy, every time an other object will use an enemy, it will refer to this name.
|
||||
|
||||
## **nameOverride**
|
||||
String - If provided, this will be displayed in any references to this enemy in the game. If not provided, Name will be used.
|
||||
|
||||
## **sprite**
|
||||
String - Path to the sprite atlas for the enemy (from `res/<AdventureName>`)
|
||||
In fights against the enemy, the sprite under "Avatar" will be used as avatar picture.
|
||||
Every sprite under
|
||||
"Idle","Walk","Attack","Hit","Death"
|
||||
will be used as animation for the corresponding action.
|
||||
direction can be added to alter the animation depending on the direction like "IdleRight"
|
||||
Supported directions are "Right","Left","Up","Down","RightDown","LeftDown","LeftUp","RightUp"
|
||||
|
||||
## **deck**
|
||||
Array of strings containing paths to the decks used for this enemy (from `res/<AdventureName>`)
|
||||
If no decks are defined then the enemy will act like a treasure chest and give the rewards without a fight.
|
||||
(only for enemies in dungeons)
|
||||
The format for the deck file can be the normal forge *.dck syntax or a json file that will behave like a collection of [rewards](Create-Rewards) to get a random generated deck.
|
||||
|
||||
## **randomizeDeck**
|
||||
Boolean - if true then the enemy deck will be randomly selected from the deck array. If false, an algorithm will select a deck in sequential order based on the player's prior win/loss ratio against that opponent (discouraged and currently unused due to wild swings in ratio at low game count).
|
||||
|
||||
## **ai**
|
||||
String - Currently unused, this appears to be intended to allow different playstyles to be associated with this enemy.
|
||||
|
||||
## **boss**
|
||||
Boolean - Not used to any great extent at this time, but a value of true in this field indicates that this is a boss-level enemy for which the match presentation can be changed if desired. Currently, this causes capable Android devices to vibrate for a longer period on collision with the enemy sprite.
|
||||
|
||||
## **flying**
|
||||
Boolean - If true, this enemy ignores terrain collisions and can travel freely in their intended movement direction.
|
||||
|
||||
## **spawnRate**
|
||||
Decimal - Relative frequency with which this enemy will be picked to spawn in appropriate biomes (which are set in the biome json file). Existing values range from 0 to 1.0.
|
||||
|
||||
## **difficulty**
|
||||
Decimal - Relative estimated difficulty associated with this enemy. Currently unused, but will likely be factored in as a part of filtering enemies into early/late game appropriate opponents. Existing values range from 0 to 1.0.
|
||||
|
||||
## **speed**
|
||||
Integer - Movement speed of this enemy in overworld or on a [map instance](Create-new-Maps). For comparison, the player's base speed is set at a value of 32 (before any equipment / ability modifiers).
|
||||
|
||||
## **scale**
|
||||
Decimal - Default 1.0. For enemies whose sprites are too large or small for their intended usage, this serves as multiplier for the enemy's visual dimensions & collision area. By default, we work with 16x16 pixel sprites for most entities - this can be replicated with a more detailed 32x32 sprite by setting a scale of 0.5 for the enemy entry.
|
||||
|
||||
## **life**
|
||||
Integer - Base starting life total. This is modified universally by a value determined by the player's chosen difficulty, and can be adjusted further at the enemy object level on [map instances](Create-new-Maps).
|
||||
|
||||
## **rewards**
|
||||
Array - A collection of the rewards to be granted for defeating the enemy.
|
||||
see [Create Rewards](Create-Rewards) for the syntax.
|
||||
|
||||
## **equipment**
|
||||
Array - A collection of strings representing [equipment items](adventure-items) normally intended for player use that this enemy will have. Not used widely, usually when an enemy will drop that [equipment](adventure-items) and it does not use [mana shards](mana-shards).
|
||||
|
||||
## **colors**
|
||||
String - Any combination of "B" (Black), "U" (Blue), "C" (Colorless), "G" (Green), "R" Red, and "W" (White). Used to display color identity alongside the sprite when an active ability allows it.
|
||||
|
||||
## **questTags**
|
||||
Array- A collection of strings associated with this entity for filtering in regards to quests.
|
||||
178
docs/Create-Rewards.md
Normal file
@@ -0,0 +1,178 @@
|
||||
Rewards represent anything that can be obtained by the player in Adventure Mode. Cards are the most common type of reward, but rewards can also represent a pieces of equipment, gold, mana shards, maximum life increases, keys, or generic items to be interacted with via dialogs and quests.
|
||||
|
||||
Rewards are associated with...
|
||||
* Enemies - The loot they drop when defeated.
|
||||
* Lootable items - Treasure chests, piles of gold, spellbooks, or mana shards on dungeon maps.
|
||||
* Shops - The items available for purchase.
|
||||
* Dialog - Items given to the player during a dialog sequence OR required in order to have access to an option.
|
||||
* Quests - Rewards for completion of a quest.
|
||||
* Events - Rewards for completion of an event.
|
||||
|
||||
As a multipurpose concept, Reward data has a large number of fields, but almost all are optional and no one reward will use all of the fields.
|
||||
|
||||
The expected fields and behavior for the rewards are set based on the reward's `type` value. This is the one field that all reward data must contain, as it tells the game what other fields to expect data in and how to process them.
|
||||
|
||||
The simplest types are `gold` (give the player X gold), `shards` (give the player X [mana shards](mana-shards), and `life` (increase the player's current and maximum life total by this amount [given very sparingly]). Beyond their types, these three items only require a `count` field. Optional fields for these types include `probability` and `addMaxCount`, (see full list at bottom). An example of a `gold` reward that will grant the player 50 gold follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "gold",
|
||||
"count": 50
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`Item` rewards are slightly more specific, but still relatively simple, requiring `type`, `count`, and EITHER `itemName` or `itemNames`, examples of both follow. Optional parameters again include `probability` and `addMaxCount`. If `itemName` is specified, then `count` copies of the named item will be granted to the player. If `itemNames` is specified, `count` copies of items randomly selected from the list of named items provided will be granted instead.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "item",
|
||||
"count":1,
|
||||
"itemName": "Mithril Boots"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "item",
|
||||
"count":1,
|
||||
"itemNames": ["Mithril Boots", "Mithril Shield", "Mithril Armor"]
|
||||
}
|
||||
```
|
||||
|
||||
`Card` rewards are potentially the most complex from a definition standpoint, but almost all of the fields are optional. A simple and a complex example follow below.
|
||||
|
||||
Granting the player one completely random card that is legal for use in Adventure:
|
||||
```json
|
||||
{
|
||||
"type": "card",
|
||||
"count":1
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
As a contrasting and complicated example that will return no matching results, consider the card reward data below. 80% of the time, this will try to grant the player 1-9 (modified further by difficulty) cards that are rare, multicolor, contain both red and blue in their color identity, are from M21 or M22 editions, are either instants or creatures, have the Elf subtype, contain the word "dragon" in the card name, contain the word "destroy" and/or "exile" in the card text. Details on the individual fields can be found in the reference list at the bottom of this page.
|
||||
```json
|
||||
{
|
||||
"type": "card",
|
||||
"probability": 0.8,
|
||||
"count":1,
|
||||
"addMaxCount":8,
|
||||
"colors": ["red","blue"],
|
||||
"rarity": ["rare"],
|
||||
"editions": ["M22","M21"],
|
||||
"cardTypes": ["Instant","Creature"],
|
||||
"colorType": "MultiColor",
|
||||
"subTypes": ["Elf"],
|
||||
"superTypes" :["Legendary"]
|
||||
"cardName": "dragon",
|
||||
"cardText": "destroy|exile"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`Union` reward types are a purely structural element. This reward type does nothing on its own, but it is used as wrappers for multiple `card` instances that has distinctly separate parameters. Required elements are `type` (Union), `count`, and `cardUnion` (which contains additional `card` reward definitions). Optional parameters are, once again `addMaxCount` and `probability`. As an example, the following would award the player with a single red dragon from M21 OR a single green sorcery from M22, but never a red sorcery, M22 dragon, or so on. The individual card pools are conjoined, giving an equal chance of all possible cards across the pools.
|
||||
|
||||
```json
|
||||
{
|
||||
"count":1,
|
||||
"type":"Union",
|
||||
"cardUnion": [
|
||||
{
|
||||
"count":1,
|
||||
"editions": ["M21"],
|
||||
"subTypes": ["Dragon"],
|
||||
"colors": ["red"]
|
||||
},
|
||||
{
|
||||
"count":1,
|
||||
"editions": ["M22"],
|
||||
"cardTypes": ["Sorcery"],
|
||||
"colors": ["green"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Fields:
|
||||
|
||||
## **type**
|
||||
Defines the type of reward.
|
||||
Valid options are:
|
||||
* `gold` will reward the player with gold.
|
||||
* `life` will increase the maximum life of the player.
|
||||
* `shards` will reward the player with [mana shards](mana-shards).
|
||||
* `item` will give items to be added to the player's inventory.
|
||||
* `card` will create one or more cards matching a detailed set of filters to follow.
|
||||
* `union` is a wrapper for multiple `card` instances that can have mutually exclusive filters.
|
||||
* `deckCard` is only used with rewards from [enemies](Create-Enemies), this functions as a `card` reward that is limited to cards found in that enemy's deck.
|
||||
|
||||
`{"type": "card", ...}`
|
||||
|
||||
## **probability**
|
||||
The probability of this reward being given out, on a decimal scale from 0 to 1. (Defaults to 1 if not provided)
|
||||
|
||||
`{..., "probability": 0.5, ...}`
|
||||
|
||||
## **count**
|
||||
If given at all (see `probability`, above), get at least this many of the reward.
|
||||
|
||||
`{..., "count": 10, ...}`
|
||||
|
||||
## **addMaxCount**
|
||||
If given at all, an additional "addMaxCount" instances of the reward will be added. A pre-set multiplier based on the chosen difficulty is applied to this parameter. On normal difficulty, you will get the reward "count" to "addMaxCount" times, `{"type": gold", "count":10, "addMaxCount":5"}` would give anywhere from 10 to 15 gold. (Defaults to 0 if not provided)
|
||||
|
||||
`{..., "addMaxCount": 5, ...}`
|
||||
|
||||
## **colors**
|
||||
An array of the possible colors for `card` and `deckCard`.
|
||||
|
||||
`{..., "colors": ["red", "black"], ...}`
|
||||
|
||||
## **rarity**
|
||||
An array of the possible raritys for `card` and `deckCard`.
|
||||
|
||||
`{..., "rarity": ["basicland", "common", "uncommon", "rare", "mythicrare"], ...}`
|
||||
|
||||
## **editions**
|
||||
An array of the possible editions for `card` and `deckCard`, referenced by their 3 character set code.
|
||||
|
||||
`{..., "editions": ["ONE", "MOM", "MAT"], ...}`
|
||||
|
||||
## **cardTypes**
|
||||
An array of the possible cardTypes for `card` and `deckCard`.
|
||||
|
||||
`{..., "cardTypes": ["Creature", "Artifact", "Enchantment", "Instant", "Sorcery", "Planeswalker", "Battle"], ...}`
|
||||
|
||||
## **subTypes**
|
||||
An array of the possible subTypes for `card` and `deckCard`.
|
||||
Usually used for creature types, but can also denote names of planeswalkers, types of battles, etc.
|
||||
|
||||
`{..., "subTypes": ["Elf", "Dragon", "Gideon", "Urza", "Siege", "Tribal"], ...}`
|
||||
|
||||
## **superTypes**
|
||||
An array of the possible superTypes for `card` and `deckCard`.
|
||||
|
||||
`{..., "superTypes ": ["Legendary", "Basic", "Snow"], ...}`
|
||||
|
||||
## **cardName**
|
||||
The exact name of a `card` to use.
|
||||
|
||||
`{..., "cardName": "Llanowar Elves", ...}`
|
||||
|
||||
## **cardText**
|
||||
A regular expression defining text that must appear on the card. A sequence of plain text alphanumeric (A-Z, 0-9) characters can function here if you are unfamiliar with regular expressions, but many special characters will change the functionality. Useful for denoting keywords or identifying helper cards that are associated with but do not actually have a given type.
|
||||
|
||||
`{..., "cardText": "reveal the top card of", ...}`
|
||||
|
||||
`{..., "cardText": "cast (an instant|a sorcery) from your hand", ...}`
|
||||
|
||||
## **deckNeeds**
|
||||
This is a functional but partially implemented concept at best, as the result set is currently limited. Card scripts can be tagged as having certain attributes that are used for creating synergies in decks constructed by the game. If/when this feature is expanded to more of our script library, the using those tags will become more and more useful to use for defining rewards so as to allow "concepts" to be awarded as opposed to specific card names or text contents.
|
||||
|
||||
`{..., "deckNeeds": ["Ability$Graveyard"], ...}`
|
||||
|
||||
## **cardPack**
|
||||
This element should not be seen or used in JSON definitions of reward data. `cardPack` rewards are a type that represent a collection of cards whose contents have been pre-determined in game logic (such as a drafted deck that can be kept after an event), but that were not pre-determined at the time when the reward data was written. To manually implement granting specific or randomized cards we should use one or more `card` rewards, detailed above. When granted, this will create an item in the player's inventory that can subsequently be opened by the player to award the individual card contents of the associated deck.
|
||||
42
docs/Create-new-Maps.md
Normal file
@@ -0,0 +1,42 @@
|
||||
Maps in the adventure mode are created with the tool Tiled
|
||||
[](https://www.mapeditor.org/)
|
||||
|
||||
Open the tiled-project under `<adventure>/maps/main.tiled-project`
|
||||
|
||||

|
||||
|
||||
This will allow you to edit the maps and tile sets.
|
||||
To interact with the player, objects needs to be added to the Objects layer.
|
||||
|
||||
Objects templates are stored in the "obj" folder, but are not necessary.
|
||||
Impotent are the types of the object and his properties.
|
||||
|
||||
## Object types
|
||||
|
||||
# enemy
|
||||
will spawn an Enemy on the map. On collide with the player a magic duel will be started.
|
||||
If the player win, the enemy will be removed from the map and the player will get the reward.
|
||||
If the player loose, then the player will move 1 step back and receive the standard penalty.
|
||||
Loot is also defined as enemy without a deck, then the player will receive the reward right away.
|
||||
Properties:
|
||||
`enemy` name of the enemies
|
||||
|
||||
# shop
|
||||
Will spawn an shop on the map. On collide the player will enter the shop.
|
||||
|
||||
Properties:
|
||||
`shopList` List of possible shop, leave it empty for all shops.
|
||||
`signXOffset` x offset for the shop sign.
|
||||
`signYOffset` y offset for the shop sign.
|
||||
|
||||
# inn
|
||||
Will spawn an inn the map. On collide the player will enter the inn.
|
||||
|
||||
Properties:
|
||||
# entry
|
||||
Will be used as the map entry and exit. On collide the player will be teleported to an other map or the over world.
|
||||
|
||||
Properties:
|
||||
`direction` the position where to spawn. up means the player will be teleported to the upper edge of the object rectangle.
|
||||
`teleport` The map where the player gets teleported. If the property is empty, then the player will be teleported to the over world.
|
||||
`teleportObjectId` the object id where the player will be teleported. If empty then it will search for an entry object, that would teleport the player back to the source map.
|
||||
178
docs/Creating-a-Custom-Card.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Guide - Creating a Custom Card
|
||||
|
||||
Using the Forge API you can create your own custom cards and sets. This guide will walk you through the process of creating custom cards via the Force Custom Card Script.
|
||||
|
||||
The next tutorial will walk you through the process of adding your new cards to a custom set.
|
||||
|
||||
|
||||
## File Locations
|
||||
|
||||
### Custom Card Scripts
|
||||
|
||||
Windows:
|
||||
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/cards
|
||||
|
||||
Linux:
|
||||
> ~/.forge/custom/cards
|
||||
|
||||
Mac:
|
||||
> TODO Add filepath
|
||||
|
||||
### Custom Tokens
|
||||
|
||||
Windows:
|
||||
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/tokens
|
||||
|
||||
Linux:
|
||||
> ~/.forge/custom/tokens
|
||||
|
||||
Mac:
|
||||
> TODO Add filepath
|
||||
|
||||
### Custom Editions
|
||||
|
||||
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/editions
|
||||
|
||||
Linux:
|
||||
> ~/.forge/custom/editions
|
||||
|
||||
Mac:
|
||||
> TODO Add filepath
|
||||
|
||||
### Card Images
|
||||
|
||||
Windows:
|
||||
> C:/Users/<your username>/Application Data/Local/Forge/Cache/pics/cards
|
||||
|
||||
Linux:
|
||||
> ~/.cache/forge/pics/cards
|
||||
|
||||
Mac:
|
||||
> TODO Add filepath
|
||||
|
||||
## Creating a New Card
|
||||
|
||||
In this tutorial we'll create a new card called "Goblin Card Guide".
|
||||
|
||||
1. Open Script Directory
|
||||
|
||||
Navigate to your Custom Card Scripts folder and open (or create) the subfolder for the starting letter of your new card.
|
||||
In our example:
|
||||
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/cards/g
|
||||
|
||||
2. Add Card Script
|
||||
|
||||
Create a new .txt file using all lowercase letters. Spaces are represented with "_" and you can leave out special characters like commas or apostrophes.
|
||||
> goblin_card_guide.txt
|
||||
|
||||
Now we need to define our card. Add the following to your Goblin_Card_Guide text file:
|
||||
|
||||
```
|
||||
Name:Goblin Card Guide
|
||||
ManaCost:1 R
|
||||
Types:Creature Goblin
|
||||
PT:2/2
|
||||
K:Haste
|
||||
Oracle:Haste
|
||||
```
|
||||
|
||||
Let's break our card down:
|
||||
- Name - The name as it appears on the card.
|
||||
- ManaCost - The card's cost with colorless mana first and spaces between different mana symbols.
|
||||
- Types - The card's type and then any subtypes seperated by spaces.
|
||||
- PT - Power and Toughness, which is only used for Creatures (or some cards that turn into creatures like Vehicles)
|
||||
- K - A Keyword that gives our creature an ability.
|
||||
- Oracle - The actual text that appears on that card.
|
||||
|
||||
For a refrence of possible fields and their descriptions please see the [Card Scripting API](Card-scripting-API) document. Note that maintaining an up to date list of every ability of every Magic Card ever printed is not feasable. The API is meant to serve as a guide and includes most common used information.
|
||||
|
||||
3. Add Card to a Set
|
||||
|
||||
Now that we have a new card we need to add it to a set so that it will be included in the game. For the purposes of this turorial we'll add out card to an existing set. If you wish to create your own set you can follow this guide: `Guide Coming Soon`
|
||||
|
||||
Navigate to your Custom Editions folder and open the text file for the set you would like to add your card to. Let's add our card to the "DCI Promos" set.
|
||||
|
||||
In the "CDI Promos.txt" file we can see a list of the cards in the set. Add a new entry at the end of the list for our new card:
|
||||
|
||||
```
|
||||
78 R Circle of Flame @James Paick
|
||||
79 U Stormblood Berserker @Greg Staples
|
||||
80 R Dungrove Elder @Scott Chou
|
||||
81 R Goblin Card Guide @Forge Team
|
||||
```
|
||||
|
||||
The first number is the collector number for the card in the set. This must be unique.
|
||||
The next field is the card's rarity in the set - we've made our card a rare.
|
||||
Next is the card's name and finally the name of the artist for the cards artwork.
|
||||
|
||||
Speaking of which - our card doesn't have any artwork. Let's fix that.
|
||||
|
||||
4. Add Card Image
|
||||
|
||||
Open your Card Images folder. Find the code for the set you added your new card to and open the corrosponding subfolder. For us this will be:
|
||||
|
||||
> C:/Users/<your username>/Application Data/Local/Forge/Cache/pics/cards/PDCI
|
||||
|
||||
Add your card image in the folder. Ther are various online tools to create custom cards. For the purposes of the guide you can use this image:
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/55363e68-0232-42e2-a1f7-8971686119e6" width="250"/>
|
||||
|
||||
5. Add a Triggered Ability
|
||||
|
||||
So we've created a creature card and added it to the game. However our creature is a little... boring, so let's update it to have a triggered ability.
|
||||
|
||||
"Whenever Goblin Card Guide deals damage to an opponent, draw a card."
|
||||
|
||||
Open the txt file again:
|
||||
> goblin_card_guide.txt
|
||||
|
||||
And update the contents to the following:
|
||||
|
||||
```
|
||||
Name:Goblin Card Guide
|
||||
ManaCost:1 R
|
||||
Types:Creature Goblin
|
||||
PT:2/2
|
||||
K:Haste
|
||||
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Opponent | TriggerZones$ Battlefield | Execute$ TrigDraw | TriggerDescription$ Whenever CARDNAME deals damage to an opponent, draw a card.
|
||||
SVar:TrigDraw:DB$ Draw | Defined$ You | NumCards$ 1
|
||||
Oracle:Haste
|
||||
\nWhenever Goblin Card Guide deals damage to an opponent, draw a card.
|
||||
```
|
||||
|
||||
Let's take a look at our changes:
|
||||
- T:Mode$ DamageDone - Trigger when Damage is Done.
|
||||
- ValidSource$ Card.Self - The source of the trigger. "Self" means our our card must be the source of the damage for the trigger to occur.
|
||||
- ValidTarget$ Opponent - The target of the trigger. "Oppenent" means our oppenent must receive damage for the trigger to occur.
|
||||
- TriggerZones$ Battlefield - Means the card must be on the battlefield for the trigger to occur.
|
||||
- Execute$ TrigDraw - What happens then the trigger occurs - in this case trigger a Draw effect.
|
||||
- SVar:TrigDraw:DB$ Draw - A String Variable used by the script. This one takes the Triggered Draw (TrigDraw) and tells the script to draw a card. "DB$ Draw" means "Drawback" "Draw".*
|
||||
- Defined$ You - Who is drawing the card(s), in this case the controller of the card.
|
||||
- NumCards$ 1 - Draw 1 card. This could be changed to any number to trigger drawing that many card.
|
||||
- TriggerDescription$ - The description of the trigger.
|
||||
|
||||
*"Drawback" is a connotation that has since been replaced with "SubAbility".
|
||||
The original connotation differentiated between AB (Ability), SP (Spell) and DB (Drawback).
|
||||
AP and SP require costs, whereas Drawback do not. The card scripts still use the "DB$" connotation.
|
||||
|
||||
6. Updating our Image
|
||||
|
||||
Finally since we createda new effect on our card, we need to update the image. Card images are simply .jpg files and don't update or read from any scripts or gamefiles.
|
||||
|
||||
I used the same online card creator to make the change to our card:
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/9c1df095-7d94-4a1a-a30f-9d0f64f6da38" width="250"/>
|
||||
|
||||
Simply save and rename the image to "Goblin Card Guide.fullborder.jpg" then overwrite the previous file we used in:
|
||||
|
||||
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/cards/g
|
||||
|
||||
7. Next Steps
|
||||
|
||||
You can check the [Abilities](AbilityFactory) and [Triggers](Triggers) documentation for more information on this topic. These documents are meant as a guide and are unlikely to contain information about every ability in the game.
|
||||
|
||||
The sinplest method for creating an effect on your card is to find another card that does the same thing and copying the ability. These can be found in your Forge folder:
|
||||
|
||||
>./Forge/res/cardsfolder/cardsfolder.zip
|
||||
|
||||
Unzipping this file will sllow you to search for any card in the containing subfolders.
|
||||
258
docs/Creating-a-custom-set.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Creating a custom set
|
||||
|
||||
This is a tutorial to start creating your own custom set, or implementing an existing one. We'll take you step by step into implementing a few cards from [MSEM Champions](https://msem-instigator.herokuapp.com/set/MPS_MSE). This is a basic guide to help you get started.
|
||||
|
||||
**Note:** This tutorial is currently for **Windows only**.
|
||||
|
||||
## Where do the files go?
|
||||
|
||||
### Non-image files
|
||||
|
||||
Everything but your cards images go into `%appdata%/Forge/custom`. You will need to put the files in the correct directories in order for the game to load them correctly.
|
||||
|
||||
The important folders are the following (you can create them if they don't exist):
|
||||
|
||||
* **cards**: Your card rules (logic) go inside this folder (.txt). I suggest you create subfolders inside it to keep everything clean.
|
||||
* **editions**: Your editions (sets) definition files (.txt) go inside this folder.
|
||||
* **tokens**: Your tokens definition files (.txt) go inside this folder.
|
||||
|
||||
### Image files
|
||||
|
||||
Your card and token images go into `%localappdata%/Forge/Cache/pics`.
|
||||
|
||||
The important folders are the following:
|
||||
|
||||
* **cards**: This is where you will put your card images. You'll want to create a folder with the Code of your edition. The images are named using this convention `Card Name.full.jpg`. If you have multiple art for the same card (something like basic lands, or maybe alternate art of the same card), then you can name them `Card Name1.full.jpg`, `Card Name2.full.jpg`, and so forth.
|
||||
* **tokens**: Same as the cards folder, your tokens will go inside this folder. The naming convention is `token_script_name.jpg`. So if your token script name is `b_5_5_golem_trample`, then you can put your token image inside your edition folder named `b_5_5_golem_trample.jpg`. If there is a collector number, append it at the beginning, such as `1_b_5_5_golem_trample.jpg`
|
||||
|
||||
## Creating your edition definition file
|
||||
|
||||
As mentioned in the opening section, we'll be partially implementing the **MSEM Champions** set. Let's create a new text file (.txt) inside `%appdata%/Forge/custom/editions`. Let's name it `MSEM Champions.txt`.
|
||||
|
||||
> Note: The file's name don't matter, but it'll be easier to find it if you ever need to edit anything.
|
||||
|
||||
Let's paste the following inside it:
|
||||
|
||||
```
|
||||
[metadata]
|
||||
Code=MSEM_CHAMPIONS
|
||||
Name=MSEM Champions
|
||||
Date=2017-10-01
|
||||
Type=Custom
|
||||
|
||||
[cards]
|
||||
7 M Master Chef
|
||||
33 M Golden Touch
|
||||
34 M Avatar of Basat
|
||||
35 M Exeunt
|
||||
62 M Unearth
|
||||
78 M Fox of the Orange Orchard
|
||||
78★ S Fox of the Orange Orchard
|
||||
107 M Inked Summoner
|
||||
107★ S Inked Summoner
|
||||
130 M Plains
|
||||
131 M Island
|
||||
132 M Swamp
|
||||
133 M Mountain
|
||||
134 M Forest
|
||||
|
||||
[tokens]
|
||||
b_1_1_bird_flying
|
||||
b_3_3_cat_deathtouch
|
||||
b_5_5_golem_trample
|
||||
|
||||
[CreatureTypes]
|
||||
Artist:Artists
|
||||
```
|
||||
|
||||
Let's break it down.
|
||||
|
||||
```
|
||||
[metadata]
|
||||
Code=MSEM_CHAMPIONS
|
||||
Name=MSEM Champions
|
||||
Date=2017-10-01
|
||||
Type=Custom
|
||||
```
|
||||
|
||||
The **[metadata]** section contains the information about your edition.
|
||||
|
||||
* **Code** is an **unique** identifier for your edition.
|
||||
* **Name** is the name of your edition.
|
||||
* **Date** is the date the set was first released/created.
|
||||
* **Type** should be `Custom` as Forge do things differently for them, including but not limited to skipping the automatic download of the images.
|
||||
|
||||
```
|
||||
[cards]
|
||||
7 M Master Chef
|
||||
34 M Avatar of Basat
|
||||
35 M Exeunt
|
||||
62 M Unearth
|
||||
78 M Fox of the Orange Orchard
|
||||
78★ S Fox of the Orange Orchard
|
||||
107 M Inked Summoner
|
||||
107★ S Inked Summoner
|
||||
130 L Plains
|
||||
131 L Island
|
||||
132 L Swamp
|
||||
133 L Mountain
|
||||
134 L Forest
|
||||
```
|
||||
|
||||
The **[cards]** section contains the cards of your edition. Any card appearing under this section has a chance to appear as a reward and in shops.
|
||||
|
||||
Each line is as follow: `CollectorNumber Rarity CardName @ArtistName`.
|
||||
|
||||
* The collector number should be unique in a given set. You can see some numbers have a ★ next to their name. In this case it denotes an alternate art of a card, but it could also have been a different number altogether.
|
||||
> Note: While it generally doesn't matter what collector number you use, avoid using the collector number F followed by only digits (ie. `F001`) as it represents a "Funny" card inside a normal edition. (Think of Funny cards as Un- sets cards)
|
||||
* The rarity is a one letter representation. It can be L (Basic Land), C (Common), U (Uncommon), R (Rare), M (Mythic Rare), S (Special).
|
||||
* The card name is self-explanatory. It should match the name of the corresponding card rule. (More of that later)
|
||||
* The artist is optional, but it should be `@Artist Name` if present. We'll omit it in this tutorial.
|
||||
|
||||
> Note: You can put the cards in the list even if they aren't scripted yet. Forge will skip over them.
|
||||
|
||||
```
|
||||
[tokens]
|
||||
b_1_1_bird_flying
|
||||
b_3_3_cat_deathtouch
|
||||
b_5_5_golem_trample
|
||||
```
|
||||
|
||||
The **[tokens]** section is optional, and only needed if you want to use specific token images in this set. They should be named using the name of their token script. `b_1_1_bird_flying` means it is a black 1/1 bird with flying. More on that later.
|
||||
|
||||
If you load the game with just file, you'll be able to see that Master Chef, Unearth and the basic lands can already be found in game. That's because they share a name with existing Magic the Gathering cards. **However**, [Master Chef](https://msem-instigator.herokuapp.com/card?q=Master+Chef) from MSEM and [Master Chef](https://scryfall.com/card/clb/241/master-chef) from MTG are two different cards! You must ensure that your custom cards do not have the same name as an existing one, unless you just want it to be another print, just like [Unearth](https://msem-instigator.herokuapp.com/card/CHAMPIONS/62/Unearth) and the basic lands in this example.
|
||||
|
||||
Let's comment out Master Chef to avoid a name conflict with an existing MTG card:
|
||||
|
||||
```
|
||||
[cards]
|
||||
#7 M Master Chef
|
||||
33 M Golden Touch
|
||||
...
|
||||
```
|
||||
|
||||
Save your file, and let's move onto another step.
|
||||
|
||||
## Scripting your first cards
|
||||
|
||||
As mentioned earlier, your custom card rules need to be located inside `%appdata%/Forge/custom/cards`. I recommend creating subfolders for each starting letter (`Forge/custom/cards/a`, `Forge/custom/cards/b`, etc.) to quickly find if a card has a duplicate name.
|
||||
|
||||
Now, you might remember than Unearth was an existing MTG card so we do not need to create a custom card rule for it. Let's create a few others.
|
||||
> This tutorial will not teach you to script your cards, and they might not be perfect. Please check out [Creating a Custom Card](https://github.com/Card-Forge/forge/wiki/Creating-a-Custom-Card) if you want more info, or look at the existing cards to learn more complex scripting.
|
||||
|
||||
Let's create the following files:
|
||||
|
||||
avatar_of_basat.txt
|
||||
```
|
||||
Name:Avatar of Basat
|
||||
ManaCost:R
|
||||
Types:Creature Avatar
|
||||
PT:2/1
|
||||
S:Mode$ CantBlock | ValidCard$ Card.Self | Description$ CARDNAME can't block.
|
||||
K:Menace
|
||||
Oracle:Menace\nAvatar of Basat can't block.
|
||||
```
|
||||
|
||||
exhunt.txt
|
||||
```
|
||||
Name:Exeunt
|
||||
ManaCost:B
|
||||
Types:Instant
|
||||
A:SP$ Sacrifice | SacValid$ Creature | Defined$ Player | SpellDescription$ Each player sacrifices a creature.
|
||||
AI:RemoveDeck:All
|
||||
Oracle:Each player sacrifices a creature.
|
||||
```
|
||||
|
||||
fox_of_the_orange_orchard.txt
|
||||
```
|
||||
Name:Fox of the Orange Orchard
|
||||
ManaCost:1 W
|
||||
Types:Creature Fox Spirit
|
||||
PT:3/1
|
||||
Oracle:
|
||||
```
|
||||
|
||||
inked_summoner.txt
|
||||
```
|
||||
Name:Inked Summoner
|
||||
ManaCost:1 B
|
||||
Types:Creature Human Warlock Artist
|
||||
PT:1/2
|
||||
T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigBranch | CheckSVar$ X | SVarCompare$ GE2 | TriggerDescription$ At the beginning of your end step, if you lost 2 or more life this turn, create a 1/1 black Bird creature token with flying. If you lost 4 or more life this turn, instead create a 3/3 black Cat creature token with deathtouch. If you lost 6 or more life this turn, instead create a 5/5 black Golem creature token with trample.
|
||||
SVar:TrigBranch:DB$ Branch | BranchConditionSVar$ Y | TrueSubAbility$ TrigBranch2 | FalseSubAbility$ DBBird
|
||||
SVar:TrigBranch2:DB$ Branch | BranchConditionSVar$ Z | TrueSubAbility$ DBGolem | FalseSubAbility$ DBCat
|
||||
SVar:DBBird:DB$ Token | TokenScript$ b_1_1_bird_flying | TokenAmount$ 1
|
||||
SVar:DBCat:DB$ Token | TokenScript$ b_3_3_cat_deathtouch | TokenAmount$ 1
|
||||
SVar:DBGolem:DB$ Token | TokenScript$ b_5_5_golem_trample | TokenAmount$ 1
|
||||
SVar:X:PlayerCountPropertyYou$LifeLostThisTurn
|
||||
SVar:Y:Count$Compare X GE4.1.0
|
||||
SVar:Z:Count$Compare X GE6.1.0
|
||||
Oracle:At the beginning of your end step, if you lost 2 or more life this turn, create a 1/1 black Bird creature token with flying.\nIf you lost 4 or more life this turn, instead create a 3/3 black Cat creature token with deathtouch.\nIf you lost 6 or more life this turn, instead create a 5/5 black Golem creature token with trample.
|
||||
```
|
||||
|
||||
If you load your game now, you should be able to find these cards you just scripted! You'll also notice that Inked Summoner is only listed as a Human Warlock, missing the Artist subtype. That's because Artist is not a real MTG subtype. You can add custom types directing inside the set definition file by following the sections found inside the `res/lists/TypeLists.txt` file. Duplicates will be ignored.
|
||||
|
||||
```
|
||||
[CreatureTypes]
|
||||
Artist:Artists
|
||||
```
|
||||
|
||||
Oh no! If you play with Inked Summoner now, you will crash when summoning a token. That's because they don't exist in MTG and we need to define them! Let's go onto the next step!
|
||||
|
||||
## Scripting custom tokens
|
||||
|
||||
The token scripts are located at `%appdata%/Forge/custom/tokens`.
|
||||
|
||||
Let's add the new tokens we need to make Inked Summoner work!
|
||||
> Just like for card scripting, this tutorial will not teach you about scripting them.
|
||||
|
||||
b_1_1_bird.flying.txt
|
||||
```
|
||||
Name:Bird Token
|
||||
ManaCost:no cost
|
||||
Colors:black
|
||||
Types:Creature Bird
|
||||
PT:1/1
|
||||
K:Flying
|
||||
Oracle:Flying
|
||||
```
|
||||
|
||||
b_3_3_cat_deathtouch.txt
|
||||
```
|
||||
Name:Cat Token
|
||||
ManaCost:no cost
|
||||
Colors:black
|
||||
Types:Creature Cat
|
||||
PT:3/3
|
||||
K:Deathtouch
|
||||
Oracle:Deathtouch
|
||||
```
|
||||
|
||||
b_5_5_golem_trample.txt
|
||||
```
|
||||
Name:Golem Token
|
||||
ManaCost:no cost
|
||||
Colors:black
|
||||
Types:Creature Golem
|
||||
PT:5/5
|
||||
K:Trample
|
||||
Oracle:Trample
|
||||
```
|
||||
|
||||
Great! Now Inked Summoner no longer make the game crash! Now let's add some images to spice it all up.
|
||||
|
||||
## Adding card and token images
|
||||
|
||||
You can find the card images for the MSEM Champions edition [here](https://msem-instigator.herokuapp.com/set/CHAMPIONS). Find the ones you need and save them inside `%appdata%/../Local/Forge/Cache/pics/cards/MSEM_CHAMPIONS` Remember the filename format should be something like `Swamp.full.jpg` if you only have one variant in your edition. If you have multiples, then it should be something like `Fox of the Orange Orchard1.full.jpg`, `Fox of the Orange Orchard2.full.jpg`, etc. You can find the alternate images from [here](https://msem-instigator.herokuapp.com/set/MPS_MSE) if you want.
|
||||
|
||||
For the tokens, we can deposit them inside `%localappdata%/Forge/Cache/pics/tokens/MSEM_CHAMPIONS`. They should be named the same as their token script so `b_1_1_bird_flying.jpg` and so forth.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
You can now start your game again, and see that the art loads correctly now.
|
||||
|
||||
## 🎉 Congratulations!
|
||||
|
||||
You’ve just added your first custom set in Forge! There's still much more to explore — scripting advanced abilities, custom mechanics, and set structures — but you now have a solid foundation to build from.
|
||||
34
docs/Credit-and-Thanks.md
Normal file
@@ -0,0 +1,34 @@
|
||||
## Special Thanks
|
||||
|
||||
1. Quest icons created by Teekatas, from his Legendora set - [Link](http://raindropmemory.deviantart.com/)
|
||||
1. Thanks to the XMage team for permission to use their targeting arrows.
|
||||
1. Thanks to http://www.freesound.org/browse/ for providing some sound files.
|
||||
1. Credit to Kevin Macleod for the royalty free music that is used.
|
||||
1. Credit to Juhani Junkala for the royalty free music for boss battles - [Link](https://opengameart.org/content/boss-battle-music)
|
||||
|
||||
## Adventure Mode Sprites and tiles.
|
||||
|
||||
Some of the sprites and tiles that are used in Adventure mode come from external artists. These fall under the ownership of their respective artists and may be used only under the conditions laid out in their respective licence.
|
||||
|
||||
1. Credit to Reddit user [wampastompah](https://www.reddit.com/user/wampastompah) for the sprites of iconic MTG characters that are used in Adventure mode
|
||||
1. Credit to [Aleksandr Makarov's](https://itch.io/profile/iknowkingrabbit) pixelart (tiles and sprites) that are used in Adventure mode.
|
||||
1. Credit to [Elthen](https://linktr.ee/elthen) for his sprites that are used in adventure mode.
|
||||
1. Credit to [DeepDiveGameStudio](https://deepdivegamestudio.itch.io) for some of the sprites that are used in adventure mode.
|
||||
1. Credit to [Krishna Palacio](https://itch.io/profile/krishna-palacio) for some of the sprites and tiles that are used in adventure mode.
|
||||
1. Credit to https://opengameart.org/ for some of the tiles. More in dept creditation of the tiles used can be found [Here](https://docs.google.com/spreadsheets/d/e/2PACX-1vTD0L8yDltZnn4025CZ9exhziCj0rbpd2aKE-0qeHikVBz51OKaAHLsgFuaDuIWrkrqIJicxzdn3SG1/pubhtml)
|
||||
|
||||
## Soundtrack
|
||||
|
||||
### Adventure
|
||||
1. Battle = Rising Legion - Derek Fiechter//Assyrian Fortress - Derek Fiechter//Goblin Raid - Brandon Fiechter// As we Marched - Vindsvept // Mecha Dwarves - Derek Fiechter
|
||||
1. Boss =
|
||||
1. Inn= Wild Boar's Inn - Brandon Fiechter// Black Rock - Logan Epic Canto //The Land of Gnomes - Logan Epic Canto//Dance of the Red Lights- Logan Epic Canto // Poland - Brandon Fiechter // Tir Nan Og - Logan Epic Canto // Village Consort -Kevin Macleod
|
||||
1. Dungeon = Swamp Witch - Derek Fiechter // Giant Mushrooms - Derek Fiechter// Cobweb Fairies - Derek Fiechter
|
||||
1. Menus = Vindsvept - Wanderer // Vindsvept - Lake of Light
|
||||
1. Waste biome = Vindsvept - Through the Woods we Ran // Vindsvept - Adventure
|
||||
1. Swamp biome = Merky Swamp - Brandon Fiechter // Masquerade of the Ghosts - Brandon Fiechter
|
||||
1. Island biome = Pirates of the Coast - Brandon Fiechter & Derek Fiechter//Buried Treasure - Brandon Fiechter & Derek Fiechter
|
||||
1. Forest biome = Enchanted Forest - Logan Epic Canto // The Celtic King
|
||||
1. Plains biome = Cobblestone Village - Brandon Fiechter // Camelot - Brandon Fiechter
|
||||
1. Mountain biome = Goblin Tinker - Brandon Fiechter // Goblin King's Tavern - Brandon Fiechter
|
||||
1. Castle = Horus - Brandon Fiechter & Derek Fiechter
|
||||
46
docs/Currency.md
Normal file
@@ -0,0 +1,46 @@
|
||||
There are many currencies in the game, and most of them can be interchanged.
|
||||
|
||||
# Cards
|
||||
|
||||
Acquired by:
|
||||
- World drops
|
||||
- Match reward
|
||||
- Draft wins
|
||||
- Shop purchases
|
||||
- Quests
|
||||
|
||||
Spent on:
|
||||
- Selling to shops
|
||||
|
||||
# Gold
|
||||
|
||||
Acquired by:
|
||||
- World drop
|
||||
- Match reward
|
||||
- Draft reward
|
||||
- Quests
|
||||
|
||||
Spent on:
|
||||
- Cards and items from shops
|
||||
- Drafts games
|
||||
- Crafting cards
|
||||
- Shards
|
||||
|
||||
# Shards
|
||||
|
||||
Acquired by
|
||||
- World drop
|
||||
- Quest reward
|
||||
|
||||
Spent on
|
||||
- Crafting cards
|
||||
- Shop Re-rolls
|
||||
- Gold exchange
|
||||
|
||||
# Challenge Coins
|
||||
|
||||
Acquired by
|
||||
- At the start of the game
|
||||
|
||||
Spent on
|
||||
- Drafts
|
||||
35
docs/Custom-Music.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Custom music for adventure mode
|
||||
|
||||
In the `res/adventure/common/music` folder resides all the music that is played in various parts of adventure mode:
|
||||
|
||||
* `black` - Tracks that are played in the black magic biome in the overworld
|
||||
* `blue` - Tracks that are played in the blue magic biome in the overworld
|
||||
* `boss` - Tracks that are played in a boss battle
|
||||
* `castle` - Tracks that are played inside biome boss dungeons
|
||||
* `cave` - Tracks that are played in any dungeon that is not a biome boss dungeon
|
||||
* `colorless` - Tracks that are played in the colorless magic (wastes) biome in the overworld
|
||||
* `green` - Tracks that are played in the green magic biome in the overworld
|
||||
* `match` - Tracks that are played during a non-boss battle
|
||||
* `menus` - Tracks that are played in the opening menu of adventure mode
|
||||
* `red` - Tracks that are played in the red magic biome in the overworld
|
||||
* `town` - Tracks that are played when you are in any town
|
||||
* `white` - Tracks that are played in the white magic biome in the overworld
|
||||
|
||||
The game will play a track from any of the above folders at random. The tracks can be any
|
||||
file name with a `.mp3`, `.wav` or `.m4a` extension.
|
||||
|
||||
NOTE: On Steam Deck, if you added Forge Adventure mode to Steam Deck gaming mode, the game may
|
||||
have issues playing tracks that have non-ascii characters in its name. A current
|
||||
workaround is to rename such tracks with accented characters to their ascii equivalents (eg. `é` to `e`)
|
||||
and such tracks will play again. Reference: https://github.com/Card-Forge/forge/issues/8290
|
||||
|
||||
To customize the music in adventure mode, simply [add to -OR- replace] the existing music track files in the folders referenced above.
|
||||
|
||||
# Custom music for regular Forge
|
||||
|
||||
In the `res/music` folder resides all the music that is played in regular Forge if you have music enabled.
|
||||
|
||||
* `match` - Tracks that are played inside a game
|
||||
* `menus` - Tracks that are played outside of any game (ie. menus)
|
||||
|
||||
All the same playback rules apply. The process for customizing the music here is the same as adventure mode.
|
||||
28
docs/Deck-Building-Tips.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Adding basic lands and special arts
|
||||
|
||||
You can add lands by clicking the triple dots icon in the right top of the deck building interface.
|
||||
|
||||
Initially you only have access to jumpstart basic land arts - to get more, you need to purchase the landscape sketch books from the basic land shop (The Cartographers Guild).
|
||||
|
||||
# 40-card deck recommendation
|
||||
|
||||
40-card decks give you a much more predictable curve.
|
||||
|
||||
In a 40-card deck, each individual card has a 2.5% chance of being drawn.
|
||||
|
||||
In a 60-card deck, each individual card has a 1.6% chance of being drawn.
|
||||
|
||||
When you use a smaller deck, the significance of each individual card is much higher and will give you more predictable performance.
|
||||
|
||||
# Autosell
|
||||
|
||||
When you click a card that is not part of any decks, you get the option to move it to auto-sell.
|
||||
|
||||
The numbers in the interface indicate how many copies you have.
|
||||
|
||||
To sell the cards you have marked, go to any town and enter the Inn.
|
||||
In the Inn, the middle icon of the coin called Sell (E) will sell all your cards you have marked to sell.
|
||||
|
||||
Individual card values vary by rarity and reputation with the town you sell them in.
|
||||
|
||||
Since dying will cause you to lose a percentage of your gold, you may want to not sell all your cards immediately.
|
||||
46
docs/Development/Android-Builds.md
Normal file
@@ -0,0 +1,46 @@
|
||||
In order to build and sign the android release, you will need the `forge.keystore` file (which is not present in the repository). This file will need to be placed in the `forge-gui-android` folder. This file should **never** be committed to the repository.
|
||||
|
||||
In preparation for the android release, update the version recorded in the following files:
|
||||
|
||||
```
|
||||
forge-gui-android/pom.xml
|
||||
forge-gui-ios/pom.xml
|
||||
forge-gui-mobile/src/forge/Forge.java
|
||||
```
|
||||
|
||||
In the first two, you're looking for `alpha-version` (~line 10 in both files) and setting the string accordingly.
|
||||
In the last one, you're looking for the declaration of `CURRENT_VERSION` (~line 37) and setting it to the same value as the previous two files.
|
||||
|
||||
Commit the changes to these three files to the repository with an appropriately descriptive commit message.
|
||||
|
||||
A script such as the following will compile and sign the android build:
|
||||
|
||||
```
|
||||
export ANDROID_HOME=/opt/android-sdk/
|
||||
export _JAVA_OPTIONS="-Xmx2g"
|
||||
mvn -U -B clean \\
|
||||
-P android-release-build,android-release-sign \\
|
||||
install \\
|
||||
-Dsign.keystore=forge.keystore \\
|
||||
-Dsign.alias=Forge \\
|
||||
-Dsign.storepass=${FORGE_STOREPASS} \\
|
||||
-Dsign.keypass=${FORGE_KEYPASS}
|
||||
```
|
||||
|
||||
Once the above build has successfully completed and passed any desired testing, the following will build, sign, and publish the build:
|
||||
|
||||
```
|
||||
export ANDROID_HOME=/opt/android-sdk/
|
||||
export _JAVA_OPTIONS="-Xmx2g"
|
||||
mvn -U -B clean \\
|
||||
-P android-release-build,android-release-sign,android-release-upload \\
|
||||
install \\
|
||||
-Dsign.keystore=forge.keystore \\
|
||||
-Dsign.alias=Forge \\
|
||||
-Dsign.storepass=${FORGE_STOREPASS} \\
|
||||
-Dsign.keypass=${FORGE_KEYPASS} \\
|
||||
-Dcardforge.user=${FORGE_FTP_USER} \\
|
||||
-Dcardforge.pass=${FORGE_FTP_PASS}
|
||||
```
|
||||
|
||||
After this, the appropriate forum posts will need to be made announcing the new release.
|
||||
139
docs/Development/Android-Debugging.md
Normal file
@@ -0,0 +1,139 @@
|
||||
1. Setup IntelliJ with Android (plugin, SDKs etc).
|
||||
2. Get the Android 13 image for the emulator (important: Android 13), then run the emulator. *Skip this step if you use a physical device.*
|
||||
3. Make sure you can build the APK.
|
||||
a. `mvn clean verify -P android-debug` using the working directory forge-gui-android/
|
||||
4. Use `android-debug` profile for creating the debug version from below in the forge-gui-android/pom.xml
|
||||
5. Use this maven command: `mvn android:deploy-apk android:run`
|
||||
6. Look at the emulator (or device), it should say "Waiting for debugger to connect".
|
||||
7. CRTL+SHIFT+A in IntelliJ and type in `attach android` and click on "Attach Debugger to Android Process".
|
||||
8. Tick "Show all processes" and you should be able to pick `forge.app` from the list and click "Okay".
|
||||
9. Ready to debug
|
||||
|
||||
Profile XML for debugging:
|
||||
```
|
||||
<profile>
|
||||
<id>android-debug</id>
|
||||
<properties>
|
||||
<android.debug>true</android.debug>
|
||||
<android.run.debug>true</android.run.debug>
|
||||
<android.deployapk.filename>target\forge-android-${snapshot-version}-aligned-debugSigned.apk</android.deployapk.filename>
|
||||
<packaging.type>apk</packaging.type>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>SignV2</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<workingDirectory>${pom.basedir}</workingDirectory>
|
||||
<executable>java</executable>
|
||||
<arguments>
|
||||
<argument>-jar</argument>
|
||||
<argument>${pom.basedir}/tools/uber-apk-signer.jar</argument>
|
||||
<argument>-a</argument>
|
||||
<argument>${pom.basedir}/target/</argument>
|
||||
<argument>--debug</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.cardforge.maven.plugins</groupId>
|
||||
<artifactId>android-maven-plugin</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>2.3.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.xml.bind</groupId>
|
||||
<artifactId>jaxb-impl</artifactId>
|
||||
<version>2.3.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>sun</groupId>
|
||||
<artifactId>misc</artifactId>
|
||||
<version>1</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${pom.basedir}/libs/sun-misc.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<version>4.6.2</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<sign>
|
||||
<debug>false</debug>
|
||||
</sign>
|
||||
<sdk>
|
||||
<platform>${androidPlatform}</platform>
|
||||
<buildTools>${androidBuildTools}</buildTools>
|
||||
</sdk>
|
||||
<dexForceJumbo>true</dexForceJumbo>
|
||||
<assetsDirectory>${project.basedir}/assets</assetsDirectory>
|
||||
<resourceDirectory>${project.basedir}/res</resourceDirectory>
|
||||
<nativeLibrariesDirectory>${project.basedir}/libs</nativeLibrariesDirectory>
|
||||
<extractDuplicates>true</extractDuplicates>
|
||||
<proguard>
|
||||
<skip>false</skip>
|
||||
<config>${project.basedir}/proguard.cfg</config>
|
||||
</proguard>
|
||||
<proguardProguardJarPath>${pom.basedir}/tools/proguard.jar</proguardProguardJarPath>
|
||||
<release>false</release>
|
||||
<dexCompiler>d8</dexCompiler>
|
||||
<d8>
|
||||
<minApi>26</minApi>
|
||||
<jvmArguments>
|
||||
<argument>${build.min.memory}</argument>
|
||||
<argument>${build.max.memory}</argument>
|
||||
</jvmArguments>
|
||||
</d8>
|
||||
<dex>
|
||||
<multi-dex>true</multi-dex>
|
||||
<jvmArguments>
|
||||
<argument>${build.min.memory}</argument>
|
||||
<argument>${build.max.memory}</argument>
|
||||
</jvmArguments>
|
||||
<dexArguments>--min-sdk-version=26</dexArguments>
|
||||
</dex>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>update-manifest</id>
|
||||
<goals>
|
||||
<goal>manifest-merger</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>se.bjurr.gitchangelog</groupId>
|
||||
<artifactId>git-changelog-maven-plugin</artifactId>
|
||||
<version>2.2.0</version>
|
||||
<inherited>false</inherited>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>GenerateGitChangelog</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>git-changelog</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
```
|
||||
8
docs/Development/Deck-Generation.md
Normal file
@@ -0,0 +1,8 @@
|
||||
This is based on Latent Dirichlet allocation:
|
||||
|
||||
1) use Agetian's getmtgdecks.sh scripts to download recent decks from mtgdecks.net and convert them to forge format and separate into Standard/Modern/Pioneer using his python tools (deck2forge_3.5.py and deckSorter_1.4a)
|
||||
2) copy the new decks into forge-gui/res/deckgendecks/$format for each format and remove the $format.lda.dat and $format.raw.dat files from that folder
|
||||
3) check out the Austin/ldastream branch and merge it with the latest
|
||||
4) run the forge.deck.generate.LDAModelGenerator class in the forge-gui-desktop module
|
||||
this will generate updated .raw.dat and .lda.dat files that can then be checked in
|
||||
probably need to do step 3 before 2 actually
|
||||
183
docs/Development/DevMode.md
Normal file
@@ -0,0 +1,183 @@
|
||||
Developer Mode is a Mode that allows Developers to try out different
|
||||
things and gain different shortcuts during play. In a Normal program,
|
||||
this Mode would be available in a Debug build, but removed from the code
|
||||
during the Retail build. Since Forge is in constant Beta, this Mode is
|
||||
available during our Beta releases. On the NewGame Screen, just make
|
||||
sure it's checked and you'll be able to edit the options at any point
|
||||
during the game.
|
||||
|
||||
## Lose By Decking
|
||||
|
||||
This one is a simple Checkbox. If your Library is Empty and you try to
|
||||
try a card, you normally lose the game. This checkbox should be on by
|
||||
Default (meaning you lose) but if you are running a deck without many
|
||||
cards, uncheck this box so you don't lose when failing to draw a card
|
||||
when you need to.
|
||||
|
||||
\[THIS OPTION NOT READILY VISIBLE IN VERSIONS 1.6.23\]
|
||||
|
||||
## View Zone
|
||||
|
||||
There are a few View Zone menu item which help to make sure your Library
|
||||
has what you think or the AI isn't being too overly foolish. They do
|
||||
exactly what you might think. Click on the menu item, and all Cards in
|
||||
that Zone are revealed to you.
|
||||
|
||||
## Generate Mana
|
||||
|
||||
Useful for not needing Land in your Deck while you are just trying to
|
||||
test something out, by selecting Generate Mana, 7 of each color and 7
|
||||
colorless is generated and put into your Mana Pool.
|
||||
|
||||
## Setup Game State
|
||||
|
||||
The Setup Game State is a Command that lets you open an external file to
|
||||
Add things into the Game State. Currently the command uses an external
|
||||
text file that is opened through a file open dialog when you click on
|
||||
the Setup Game State menu item. The file structure is simple. It must
|
||||
contain any of the following lines defining what needs to be changed in
|
||||
the current game state:
|
||||
|
||||
### HumanLife
|
||||
|
||||
Defines the value to set the human's current life to. Example:
|
||||
HumanLife=6
|
||||
|
||||
### AILife
|
||||
|
||||
Defines the value to set the computer opponent's life to. Example:
|
||||
AILife=14
|
||||
|
||||
### HumanCardsInPlay
|
||||
|
||||
Defines the cards that are to be placed under Human's control on the
|
||||
battlefield. Can be just one card name or a list of card names separated
|
||||
with semicolons. Example: HumanCardsInPlay=Swamp; Swamp; Drudge
|
||||
Skeletons
|
||||
|
||||
### AICardsInPlay
|
||||
|
||||
Defines the cards that are to be placed under Computer's control on the
|
||||
battlefield. Can be just one card name or a list of card names separated
|
||||
with semicolons. Example: AICardsInPlay=Island
|
||||
|
||||
### HumanCardsInHand
|
||||
|
||||
Defines the cards that are to replace the Human's current hand. Old
|
||||
cards in the Human's card will be removed. Can be just one card name or
|
||||
a list of card names separated with semicolons, see HumanCardsInPlay or
|
||||
AICardsInPlay above for examples.
|
||||
|
||||
### AICardsInHand
|
||||
|
||||
Defines the cards that are to replace the Computer's current hand. Old
|
||||
cards in the Computer's card will be removed. Can be just one card name
|
||||
or a list of card names separated with semicolons, see HumanCardsInPlay
|
||||
or AICardsInPlay above for examples.
|
||||
|
||||
### HumanCardsInGraveyard
|
||||
|
||||
Defines the cards that are to replace the Human's current graveyard. Old
|
||||
cards in the Human's graveyard will be removed. Can be just one card
|
||||
name or a list of card names separated with semicolons, see
|
||||
HumanCardsInPlay or AICardsInPlay above for examples.
|
||||
|
||||
### AICardsInGraveyard
|
||||
|
||||
Defines the cards that are to replace the Computer's current graveyard.
|
||||
Old cards in the Computer's graveyard will be removed. Can be just one
|
||||
card name or a list of card names separated with semicolons, see
|
||||
HumanCardsInPlay or AICardsInPlay above for examples.
|
||||
|
||||
### HumanCardsInLibrary
|
||||
|
||||
Defines the cards that are to replace the Human's current library. Old
|
||||
cards in the Human's library will be removed. Can be just one card name
|
||||
or a list of card names separated with semicolons, see HumanCardsInPlay
|
||||
or AICardsInPlay above for examples.
|
||||
|
||||
### AICardsInLibrary
|
||||
|
||||
Defines the cards that are to replace the Computer's current library.
|
||||
Old cards in the Computer's library will be removed. Can be just one
|
||||
card name or a list of card names separated with semicolons, see
|
||||
HumanCardsInPlay or AICardsInPlay above for examples.
|
||||
|
||||
### HumanCardsInExile
|
||||
|
||||
Defines the cards that are to replace the Human's current exile area.
|
||||
Old cards in the Human's exile area will be removed. Can be just one
|
||||
card name or a list of card names separated with semicolons, see
|
||||
HumanCardsInPlay or AICardsInPlay above for examples.
|
||||
|
||||
### AICardsInExile
|
||||
|
||||
Defines the cards that are to replace the Computer's current exile area.
|
||||
Old cards in the Computer's exile area will be removed. Can be just one
|
||||
card name or a list of card names separated with semicolons, see
|
||||
HumanCardsInPlay or AICardsInPlay above for examples.
|
||||
|
||||
### ActivePlayer
|
||||
|
||||
Defines the active player that must be given priority when the game
|
||||
state is set up. If this line is absent, the current player is not
|
||||
changed. There are two valid values for this option: Human and AI. Note
|
||||
that this option does not change the current phase, only the current
|
||||
player. Example: ActivePlayer=AI
|
||||
|
||||
### ActivePhase
|
||||
|
||||
Changes the current phase of the game to the one specified. The valid
|
||||
values are Untap, Upkeep, Draw, Main1, Declare Attackers, Declare
|
||||
Blockers, Main2, End of Turn, and Cleanup. Note that the phase names are
|
||||
case-sensitive.
|
||||
|
||||
### Comments
|
||||
|
||||
Game state files are allowed to have comments - any line that is
|
||||
prefixed with a pound sign (\#) is not parsed and is considered to be a
|
||||
text fragment. Also, empty lines are ignored. Example: <b>\# This is a
|
||||
comment.</b>
|
||||
|
||||
### Specifying a Set for Spawned Cards
|
||||
|
||||
When specifying card names, you can optionally add the set code by
|
||||
appending the pipe sign (|) and the three-letter set code to the card
|
||||
name you are spawning. That will allow you to spawn cards from specific
|
||||
sets. For example, <b>HumanCardsInPlay=Mountain; Mountain</b> will just
|
||||
spawn two Mountains on the human battlefield, using the latest possible
|
||||
set for them, while <b>HumanCardsInPlay=Mountain|4ED; Mountain|4ED</b>
|
||||
will spawn two Mountains from the 4th Edition on the human battlefield.
|
||||
|
||||
### Samples
|
||||
|
||||
Here are two examples of valid state configuration files:
|
||||
|
||||
HumanLife=5
|
||||
AILife=6
|
||||
HumanCardsInPlay=Forest; Forest; Forest; Llanowar Elves
|
||||
AICardsInPlay=Mountain; Mountain; Mountain
|
||||
HumanCardsInHand=Island; Raging Goblin
|
||||
AICardsInHand=Swamp; Swamp; Forest
|
||||
|
||||
Sets human's life to 5, AI life to 6, puts 3 Forests and Llanowar Elves
|
||||
on the human battlefield, puts 3 Mountains on the computer battlefield,
|
||||
replaces the human hand with Island and Raging Goblin, replaces the AI
|
||||
hand with 2 Swamps and a Forest. Does not add any cards to either
|
||||
graveyard and does not replace either library.
|
||||
|
||||
HumanCardsInPlay=Plains|10E
|
||||
AICardsInPlay=Mountain; Raging Goblin
|
||||
AICardsInGraveyard=Force of Nature; Raging Goblin; Amulet of Kroog
|
||||
ActivePlayer=Human
|
||||
ActivePhase=Main1
|
||||
|
||||
Does not change the human's life and the AI life, puts a Plains from the
|
||||
10th Edition on the human battlefield, puts a Mountain and a Raging
|
||||
Goblin on the opponent's battlefield, does not replace the human and the
|
||||
AI hands and leaves them intact, and adds Force of Nature, Raging
|
||||
Goblin, and Amulet of Kroog to the computer's graveyard. Does not
|
||||
replace either library. Sets the current player to Human and the current
|
||||
phase to Main 1.
|
||||
|
||||
It's important to note you won't get achievements in a game once you started cheating.
|
||||
BIN
docs/Development/IntelliJ-setup/01-01-initial-prompt.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/Development/IntelliJ-setup/01-02-ui-theme-selection.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/Development/IntelliJ-setup/01-03-checkout-from-git.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/Development/IntelliJ-setup/01-04-fill-in-the-form.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/Development/IntelliJ-setup/01-05-do-not-open-it.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/Development/IntelliJ-setup/01-06-click-open.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/Development/IntelliJ-setup/01-07-open-the-pom.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/Development/IntelliJ-setup/01-08-open-as-project.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/Development/IntelliJ-setup/02-01-project-settings.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/Development/IntelliJ-setup/02-02-new-jdk.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/Development/IntelliJ-setup/02-03-jdk-homedirectory.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/Development/IntelliJ-setup/02-04-after-jdk-setup.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/Development/IntelliJ-setup/02-05-edit-configurations.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
docs/Development/IntelliJ-setup/02-06-application.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/Development/IntelliJ-setup/02-07-debug-setup.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
141
docs/Development/IntelliJ-setup/IntelliJ-setup.md
Normal file
@@ -0,0 +1,141 @@
|
||||
## Part 1
|
||||
|
||||
### Install IntelliJ
|
||||
|
||||
IntelliJ comes in two versions:
|
||||
|
||||
- **Community Edition**: Free to download and sufficient for this project (it is not a trial version that expires).
|
||||
- **Ultimate Edition**: Paid version that has support for various frameworks (which we don't need).
|
||||
|
||||
You can find versions for Windows, Linux, and macOS on the official website.
|
||||
|
||||
No additional plugins are required for this project.
|
||||
|
||||
* start IntelliJ
|
||||
|
||||

|
||||
|
||||
|
||||
* select **Do not import settings**
|
||||
* click **OK**
|
||||
|
||||

|
||||
|
||||
* select Theme
|
||||
* click **Skip Remaining and Set Defaults**
|
||||
|
||||

|
||||
|
||||
* select **Check out from Version Control**
|
||||
* select **Git**
|
||||
|
||||

|
||||
|
||||
* enter your repository/branch details
|
||||
* click **Test**
|
||||
|
||||
* click **Clone**
|
||||
|
||||
* -- wait ---
|
||||
|
||||
You will receive a prompt indicating that your checkout from version control contained a project file. In my experience, attempting to open this file fails to do anything useful.
|
||||
|
||||

|
||||
|
||||
|
||||
* click **No**
|
||||
|
||||

|
||||
|
||||
* click **Open**
|
||||
|
||||

|
||||
|
||||
Browse to the folder you specified for the parent and inside the folder you specified for the git checkout. You should see a *pom.xml* file.
|
||||
* select the *pom.xml* file
|
||||
* click **OK**
|
||||
|
||||

|
||||
|
||||
|
||||
* select **Open as Project**
|
||||
|
||||
At this point IntelliJ should be open with the Forge project. However, it will take a while for IntelliJ to download all the project dependencies. This is reflected in the bottom status bar by default.
|
||||
|
||||
|
||||
## Part 2
|
||||
* select **File**
|
||||
* select **Project Structure...**
|
||||
|
||||

|
||||
|
||||
|
||||
* select **New**
|
||||
* select **JDK**
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
If necessary browse to the JDK directory.
|
||||
* click **OK**
|
||||
|
||||

|
||||
|
||||
|
||||
* click **OK**
|
||||
|
||||
* select **Run** from the top menu
|
||||
* select **Debug...** from the drop down
|
||||
|
||||

|
||||
|
||||
|
||||
* select **Edit Configurations...**
|
||||
|
||||
* click the **+** in the upper left
|
||||
|
||||

|
||||
|
||||
|
||||
* select "Application"
|
||||
|
||||

|
||||
|
||||
* set the **Name** to: Forge
|
||||
* set the **Main class** to: forge.view.Main
|
||||
* Latest IntelliJ Versions: click Modify options and check Add VM Options
|
||||
* set the **VM options** to:
|
||||
* **(JAVA 17 and above)**
|
||||
> -Xms768m -XX:+UseParallelGC -Dsun.java2d.xrender=false --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true
|
||||
|
||||
* set the **Working directory** to %MODULE_WORKING_DIR%
|
||||
* set **Use classpath of module** to: forge-gui-desktop
|
||||
* click **Debug**
|
||||
|
||||
* -- wait --
|
||||
|
||||
If all goes well, you should eventually see the Forge splash screen followed by the main UI.
|
||||
|
||||
### Adventure Mode debugging on Desktop
|
||||
|
||||
Follow the same steps to create a Run Configuration, but use forge-gui-mobile-dev instead of forge-gui-desktop as the module and directory.
|
||||
|
||||
* select **Run** from the top menu
|
||||
* select **Debug...** from the drop down
|
||||
* select **Edit Configurations...**
|
||||
* click the **+** in the upper left
|
||||
* select "Application"
|
||||
|
||||
* set the **Name** to: Forge
|
||||
* set the **Main class** to: forge.app.Main
|
||||
* Latest IntelliJ Versions: click Modify options and check Add VM Options
|
||||
* set the **VM options** to:
|
||||
* **(JAVA 17 and above)**
|
||||
> -Xms768m -XX:+UseParallelGC -Dsun.java2d.xrender=false --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true
|
||||
|
||||
* set the **Working directory** to %MODULE_WORKING_DIR%
|
||||
* set **Use classpath of module** to: forge-gui-mobile-dev
|
||||
* click **Debug**
|
||||
44
docs/Development/Network-Play-Rewrite.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Why Rewrite Network Play?
|
||||
|
||||
The current implementation of **Network Play** relies on [Java serialization/deserialization](https://www.geeksforgeeks.org/serialization-in-java/) via [Netty](https://netty.io/). While this does work, it is inefficient, transferring large amounts of unnecessary (duplicate) data. The transferring of duplicate data has two negatives:
|
||||
|
||||
1. increased latency
|
||||
2. increased bandwidth
|
||||
|
||||
The increased latency is very noticeable throughout a network game.
|
||||
The increased bandwidth is a potential concern for mobile players, not everyone has an unlimited data plan.
|
||||
|
||||
Testing of the existing **Network Play** implementation has shown an individual **Game** transferring over 300MB of data.
|
||||
|
||||
# The Rewrite
|
||||
|
||||
The rewrite will utilize [protobuf](https://developers.google.com/protocol-buffers) and be approached in phases:
|
||||
|
||||
1. Lobby
|
||||
2. Match
|
||||
3. Game
|
||||
|
||||
## Lobby
|
||||
|
||||
The **Lobby** portion will handle:
|
||||
|
||||
* Handshake
|
||||
* Player
|
||||
* Name
|
||||
* Avatar
|
||||
* Game Rules Selection
|
||||
* Deck Submission
|
||||
|
||||
The **Handshake** portion of the **Lobby** will be responsible for ensuring that it is a **Forge** client that is connecting **and** that the client is running a compatible **Network Play** implementation.
|
||||
|
||||
## Match
|
||||
|
||||
Number of **Games** that comprise a **Match**, normally first player to win 2 **Games**. This is important, because it is not *technically* best of 3. For example if either of the first two games of a **Match** are a *draw*, it is entirely possible to play a fourth **Game** in a **Match**. (Need a judge ruling reference on this)
|
||||
|
||||
## Game
|
||||
|
||||
This will be broken down in more detail, but the important bits to hand first are:
|
||||
|
||||
* Phases
|
||||
* Passing of **priority**
|
||||
* Notification upon receipt of **priority**
|
||||
119
docs/Development/mana-conversion-matrices.md
Normal file
@@ -0,0 +1,119 @@
|
||||
Spend as other mana: The easiest way would probably be to intercept mana costs before they are paid and replace them with different mana costs (so it only changes what you're asked to pay).
|
||||
|
||||
Effects are:
|
||||
Celestial Dawn: You may spend white mana as though it were mana of any color. You may spend other mana only as though it were colorless mana.
|
||||
False Dawn: Until end of turn, you may spend white mana as though it were mana of any color.
|
||||
Mycosynth Lattice: Players may spend mana as though it were mana of any color.
|
||||
North Star: For one spell this turn, you may spend mana as though it were mana of any color to pay that spell's mana cost.
|
||||
Quicksilver Elemental: You may spend blue mana as though it were mana of any color to pay the activation costs of Quicksilver Elemental's abilities.
|
||||
Sunglasses of Urza: You may spend white mana as though it were red mana.
|
||||
|
||||
Generic mana costs can be paid by any mana... so that's not relevant to any of the conversion matrices.
|
||||
|
||||
The left side of the matrix is Mana in your ManaPool and the top of the matrix is ManaCosts.
|
||||
|
||||
For Colored or Colorless specific costs the tables will look like:
|
||||
|
||||
Identity Matrix
|
||||
|
||||
| | W | U | B | R | G | C |
|
||||
| - | - | - | - | - | - | - |
|
||||
W| X | O | O | O | O | O |
|
||||
U| O | X | O | O | O | O |
|
||||
B| O | O | X | O | O | O |
|
||||
R| O | O | O | X | O | O |
|
||||
G| O | O | O | O | X | O |
|
||||
C| O | O | O | O | O | X |
|
||||
|
||||
By default, there are no restrictions on what mana can pay for what. So this gives us a restriction MatriX | Of
|
||||
|
||||
| | W | U | B | R | G | C |
|
||||
| - | - | - | - | - | - | - |
|
||||
W| X | X | X | X | X | X |
|
||||
U| X | X | X | X | X | X |
|
||||
B| X | X | X | X | X | X |
|
||||
R| X | X | X | X | X | X |
|
||||
G| X | X | X | X | X | X |
|
||||
C| X | X | X | X | X | X |
|
||||
|
||||
Since these are the default matrices, we take too mutable matrices. An additive matrix (that starts out as the identity and ORs any modifiers to itself) and a restriction matrix that starts as completely unrestricted and ANDs any restrictions to itself.
|
||||
|
||||
Once all of the modifiers have been assigned you take the additive matrix AND it to the restriction matrix and that's how you determine what Mana you have can pay for a Mana Cost.
|
||||
|
||||
|
||||
Celestial Dawn modifies the table to look like this:
|
||||
|
||||
| | W | U | B | R | G | C |
|
||||
| - | - | - | - | - | - | - |
|
||||
W| X | X | X | X | X | O |
|
||||
U| O | O | O | O | O | X |
|
||||
B| O | O | O | O | O | X |
|
||||
R| O | O | O | O | O | X |
|
||||
G| O | O | O | O | O | X |
|
||||
C| O | O | O | O | O | X |
|
||||
|
||||
The False Dawn table looks like this:
|
||||
|
||||
| | W | U | B | R | G | C |
|
||||
| - | - | - | - | - | - | - |
|
||||
W| X | X | X | X | X | O |
|
||||
U| O | X | O | O | O | O |
|
||||
B| O | O | X | O | O | O |
|
||||
R| O | O | O | X | O | O |
|
||||
G| O | O | O | O | X | O |
|
||||
C| O | O | O | O | O | X |
|
||||
|
||||
Mycosynth Lattice
|
||||
|
||||
| | W | U | B | R | G | C |
|
||||
| - | - | - | - | - | - | - |
|
||||
W| X | X | X | X | X | O |
|
||||
U| X | X | X | X | X | O |
|
||||
B| X | X | X | X | X | O |
|
||||
R| X | X | X | X | X | O |
|
||||
G| X | X | X | X | X | O |
|
||||
C| X | X | X | X | X | X |
|
||||
|
||||
North Star (although specific tO | One spell, so might be better in a different location)
|
||||
|
||||
| | W | U | B | R | G | C |
|
||||
| - | - | - | - | - | - | - |
|
||||
W| X | X | X | X | X | X |
|
||||
U| X | X | X | X | X | X |
|
||||
B| X | X | X | X | X | X |
|
||||
R| X | X | X | X | X | X |
|
||||
G| X | X | X | X | X | X |
|
||||
C| X | X | X | X | X | X |
|
||||
|
||||
|
||||
Quicksilver Elemental, the table is similar to False Dawn:
|
||||
|
||||
| | W | U | B | R | G | C |
|
||||
| - | - | - | - | - | - | - |
|
||||
W| X | O | O | O | O | O |
|
||||
U| X | X | X | X | X | X |
|
||||
B| O | O | X | O | O | O |
|
||||
R| O | O | O | X | O | O |
|
||||
G| O | O | O | O | X | O |
|
||||
C| O | O | O | O | O | X |
|
||||
|
||||
|
||||
Sunglasses of Urza
|
||||
|
||||
| | W | U | B | R | G | C |
|
||||
| - | - | - | - | - | - | - |
|
||||
W| X | O | O | X | O | O |
|
||||
U| O | X | O | O | O | O |
|
||||
B| O | O | X | O | O | O |
|
||||
R| O | O | O | X | O | O |
|
||||
G| O | O | O | O | X | O |
|
||||
C| O | O | O | O | O | X |
|
||||
|
||||
|
||||
|
||||
RECAP:
|
||||
|
||||
1. Start with original matrix (each color/colorless being able to pay for itself)
|
||||
2. OR all of the positive allowances to the matrix (which is all of them except for the restriction in Celestial Dawn)
|
||||
3. AND that to the payment restriction (just second half of Celestial Dawn)
|
||||
4. Apply the payment matrix when trying to pay mana.
|
||||
64
docs/Development/ownership.md
Normal file
@@ -0,0 +1,64 @@
|
||||
Everyone is free to work on whatever part they're interested in. This is just meant to be a rough overview for new contributors which areas are currently "understaffed" and could use some love.
|
||||
The "Ancestors" column is basically there to list currently inactive developers that might still be around to provide implementation details on Discord. In that case please use the Search function and make a decent effort to understand the code first, unnecessary pinging for simple things might give you a bad reputation :P
|
||||
|
||||
## DevOps
|
||||
|
||||
| Concept | Owners | Ancestors | Example tasks |
|
||||
| - | - | - | - |
|
||||
| PC Releases | friarsol | Agetian | - update Maven dependencies |
|
||||
| Android Releases | | kevlahnota | |
|
||||
| Sentry | JaminCollins | | - watch for rare/unusual crashes |
|
||||
|
||||
## Ingame Engine
|
||||
| Concept | Owners | Ancestors | Example tasks |
|
||||
| - | - | - | - |
|
||||
| Ability Effects | friarsol, Hanmac, Northmoc, TRT | | - support new mechanics<br>- debug complicated reports<br>- improve stackdescriptions<br>- research rulings<br>- compare with Arena |
|
||||
| Adventure | TabletopGeneral | Grimm | |
|
||||
|
||||
## Magic Metadata
|
||||
| Concept | Owners | Ancestors | Example tasks |
|
||||
| - | - | - | - |
|
||||
| Deck generation | | Austinio | |
|
||||
| Netdecks | churrufli | | |
|
||||
| Editions | Snoops | friarsol | |
|
||||
| Images | | kevlahnota | |
|
||||
|
||||
## User Interface
|
||||
| Concept | Owners | Ancestors | Example tasks |
|
||||
| - | - | - | - |
|
||||
| Desktop | | | |
|
||||
| Android | | DrDev, kevlahnota | - test new libGDX versions |
|
||||
| Localization | | Alumi | - update card translation files<br>- update engine text (native speaker not required) |
|
||||
| Sound effects | | | |
|
||||
|
||||
## Forge Script DSL
|
||||
| Concept | Owners | Ancestors | Example tasks |
|
||||
| - | - | - | - |
|
||||
| [Card Scripting](cardscripting) | TRT, Northmoc, Simisays, Fulgur14, Dracontes | a lot | - implement new Sets<br>- clean up outdated elements<br>- apply Oracle updates |
|
||||
| ForgeScribe | | Austinino | |
|
||||
|
||||
## Modes
|
||||
| Concept | Owners | Ancestors | Example tasks |
|
||||
| - | - | - | - |
|
||||
| Quest | friarsol | | |
|
||||
| [Network Play](network-play) | | JaminCollins | |
|
||||
| Gauntlet | | | |
|
||||
| Draft | | | |
|
||||
| Planar Conquest | | DrDev | |
|
||||
| Puzzle | Agetian | | |
|
||||
| Headless | | | |
|
||||
| Adventure (Content) | TabletopGeneral, Simisays | Grimm | - balancing |
|
||||
|
||||
## Artificial Intelligence
|
||||
| Concept | Owners | Ancestors | Example tasks |
|
||||
| - | - | - | - |
|
||||
| Effect APIs | Hanmac, Agetian | | |
|
||||
| Combat | Agetian, TRT | | |
|
||||
| Limited | | | |
|
||||
| Simulation | | Myrd | |
|
||||
| DeckHints | | | |
|
||||
|
||||
## Miscellaneous
|
||||
| Concept | Owners | Ancestors | Example tasks |
|
||||
| - | - | - | - |
|
||||
| Documentation | | | - update Wiki |
|
||||
40
docs/Different-Planes.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# The Different Planes of Adventure Mode
|
||||
|
||||
In Adventure Mode, you can select different planes (AKA worlds) to experience. The default experience is the Shandalar plane, and is the basis of gameplay found in all the others. The other planes are community projects, and are in various states of completion. Below there is a list of each plane in the game (as of this writing), and it's status.
|
||||
|
||||
To change planes, open up Adventure Mode. On the main screen where you can either load a game or start a new one. Go to the Settings menu, and you will find a drop down on the top right with a list of the planes currently available. When changing planes, you will need to restart Forge to apply the change. Save files are unique to each plane, and will load back in when you change to the appropriate plane.
|
||||
|
||||
## Amonkhet
|
||||
|
||||
A plane set in the world of Amonkhet, and is intended to have new quests, story, enemies, etc. based on that Magic The Gathering plane.
|
||||
|
||||
**STATUS:** Very broken. Not recommended for play, unless you want to dip your head in and see a very different overworld map. Pretty much nothing else works properly. _You have been warned._
|
||||
|
||||
|
||||
## Crystal Kingdoms
|
||||
|
||||
A plane with a focus on the Final Fantasy cards. Intended to have a unique story, quests, enemies, etc. based on the Final Fantasy games.
|
||||
|
||||
**STATUS:** Very pre-alpha. The plane 'works' but is almost identical to the default Shandalar plane. The big differences is new starting decks, and the card rewards in shops and from enemies, have a stronger Final Fantasy bent than usual.
|
||||
|
||||
## Innistrad
|
||||
|
||||
A plane set in the lore and world of Magic the Gathering's Innistrad plane. The card pool and enemies are limited to Innistrad, with some minor exceptions. There is also a distinctly different overworld, story, enemies, etc. Every map is new and unique compared to Shandalar.
|
||||
|
||||
**STATUS:** Technically alpha. Everything is 'functional' in that it is all closed off and separate from the Shandalar plane. The enemies are new, the quests include some base and some new (though the tutorial has some strong similarities). The shops are new-ish (they currently use the base shops, but with a limited card pool to just the plane. This is actively being changed to entirely new shops however, and should become notable in the next few updates.) Every town, dungeon, and cave is unique to Innistrad and been custom built for the plane. Every enemy is also new to Innistrad.
|
||||
|
||||
_HOWEVER_: Everything is still very early in design. Most of the biomes are empty. The new quests are limited. The shops under work. The story ends partway through the tutorial. The first bosses are being worked on. The enemies are few in number, but growing rapidly in development. Etc. This plane is best for short plays to see what is going on, but don't expect to spend much time here, Yet. That said, it is the community plane receiving the most work and most consistent updates. With changes coming at the minimum, monthly, preferably weekly.
|
||||
|
||||
In it's current state, expect to find bugs. if you do, please report them to Shenshinoman in the Forge Discord.
|
||||
|
||||
## Shandalar
|
||||
|
||||
The base plane, where all the magic happens, and where the dev team members working on Adventure generally focus their efforts. The story is mostly complete, but there _is_ further work planned. Everything works, and you can easily spend dozens of hours, or more, in this plane.
|
||||
|
||||
**STATUS:** Fully functional and live. Enjoy!
|
||||
|
||||
## Shandalar Old Border
|
||||
|
||||
So, you like the Shandalar plane, but miss "ye good olde days" of yore? Then Shandalar Old Border is for you. This is the Shandalar base plane, but with modifications so that everything is from the Scourge set, or older.
|
||||
|
||||
**STATUS:** 99% functional and live. Enjoy! (There may be the occasional bug found. if so, please report it so we can get it resolved.)
|
||||
97
docs/Docker-Setup.md
Normal file
@@ -0,0 +1,97 @@
|
||||
Directions here to use a docker container to play Forge:
|
||||
|
||||
# FORGE-DESKTOP-SNAPSHOT
|
||||
|
||||
Pull docker image.
|
||||
```
|
||||
- docker pull xanxerdocker/forge-desktop-snapshot
|
||||
```
|
||||
|
||||
Run container and remove container after exit:
|
||||
```
|
||||
- docker run --rm -it -p 3002:3000 forge-desktop-snapshot bash
|
||||
```
|
||||
|
||||
Run container in detached mode. Will retain data after exit:
|
||||
```
|
||||
- docker run -d -it -p 3002:3000 forge-desktop-snapshot bash
|
||||
```
|
||||
Access container at localhost:3002 in your web browser.
|
||||
|
||||
Once inside the container. Run the setup script using sudo:
|
||||
```
|
||||
- bash setup_forge_desktop.sh
|
||||
```
|
||||
After the setup is complete. To play forge-desktop-SNAPSHOT or forge-adventure mode run:
|
||||
```
|
||||
- bash forge_game_selector.sh
|
||||
```
|
||||
|
||||
# FORGE DEV ENVIRONMENT:
|
||||
Dockerized apps for a forge development environment
|
||||
- intellij-ce
|
||||
- Magic Set Editor 2 - Advanced
|
||||
- Tiled - Map Editor
|
||||
|
||||
Pull docker images.
|
||||
```
|
||||
- docker pull xanxerdocker/intellij-ce-ide
|
||||
- docker pull xanxerdocker/magic-set-editor-2-advanced
|
||||
- docker pull xanxerdocker/tiled-map-editor
|
||||
```
|
||||
# Intellij-ce IDE container
|
||||
|
||||
Run Intellij-ce container and remove container after exit:
|
||||
```
|
||||
- docker run --rm -it -p 3003:3000 xanxerdocker/intellij-cd-ide bash
|
||||
```
|
||||
Run detached Intellij-ce container. Will retain data after exit")
|
||||
```
|
||||
- docker run -d -it -p 3003:3000 xanxerdocker/intellij-ce-ide bash
|
||||
```
|
||||
Access container via localhost:3003 in web browser and to start application run:
|
||||
```
|
||||
- bash run_intellij-ce.sh
|
||||
```
|
||||
|
||||
|
||||
# Magic Set Editor 2 Advanced
|
||||
|
||||
Run Magic Set Editor 2 - Advanced container and remove container after exit.
|
||||
```
|
||||
- docker run --rm -it -p 3004:3000 xanxerdocker/magic-set-editor-2-advanced bash
|
||||
```
|
||||
|
||||
Run detached: Magic Set Editor 2 - Advanced container. Will retain data after exit.
|
||||
```
|
||||
- docker run -d -it -p 3004:3000 xanxerdocker/magic-set-editor-2-advanced bash
|
||||
```
|
||||
|
||||
Access container via localhost:3004 in web browser.
|
||||
|
||||
Once inside the container to compile the app and finish setup run using sudo:
|
||||
```
|
||||
- bash install-mse-adv-full.sh
|
||||
```
|
||||
|
||||
After the app is compiled, to start app run:
|
||||
```
|
||||
- bash run_mse.sh
|
||||
```
|
||||
|
||||
# Tiled - Map Editor
|
||||
|
||||
Run Tiled - Map Editor container and remove container after exit:
|
||||
```
|
||||
- docker run --rm -it -p 3005:3000 xanxerdocker/tiled-map-editor bash
|
||||
```
|
||||
|
||||
Run detached Tiled - Map Editor container. Will retain data after exit.
|
||||
```
|
||||
- docker run -d -it -p 3005:3000 xanxerdocker/tiled-map-editor bash
|
||||
```
|
||||
|
||||
Access container via localhost:3005 in web browser and to start application run:
|
||||
```
|
||||
- bash run_tiled_map_editor.sh
|
||||
```
|
||||
82
docs/Dungeons.md
Normal file
@@ -0,0 +1,82 @@
|
||||
## Story Dungeons
|
||||
|
||||
Currently, the story is very much a work in progress and will change a lot in the future. The end goal will consist of multiple "tiers" of more difficult dungeons, where you will be guided by quests. Currently, only some of the story dungeons are available, and there are no quests to guide you to them. You can enter these dungeons and fight the bosses.
|
||||
|
||||
### Tier 1: Castles
|
||||
The first tier of story bosses will consist of five dungeons, each found in the center of the five colored biomes. Each boss gives a plethora of loot (cards, life, shards, gold) and if you are able to defeat all five bosses, you can get a Mox card of your choice.
|
||||
|
||||
**Overworld Icons**
|
||||
|
||||

|
||||
|
||||
### Tier 2: Planeswalker Temples
|
||||
Currently only the Temple of Chandra (red biome) and Temple of Liliana (black biome) are available. These tend to be harder than the castles mentioned above.
|
||||
|
||||
**Overworld Icons**
|
||||
|
||||

|
||||
|
||||
## Generic Dungeons
|
||||
|
||||
### Aerie
|
||||
### Barbarian Camp
|
||||
|
||||
Comes in Goblin, Orc, Bandit and Kobold variaties
|
||||
### Cat Lair
|
||||
### Caves
|
||||
### Crypt
|
||||
### Djinn Palace
|
||||
### Evil Grove
|
||||
### Factory
|
||||
### Fort
|
||||
### Graveyard
|
||||
### Grove
|
||||
### Farm
|
||||
### Hostile Towns
|
||||
### Lavaforge
|
||||
### Magetower
|
||||
### Maze
|
||||
### Merfolk Pool
|
||||
### Monastery
|
||||
### Phyrexian Outposts
|
||||

|
||||
### Plains Castles
|
||||
### Skull Cave
|
||||
### Snow Abbey
|
||||
### Vampire Castle
|
||||
### Demon Tower
|
||||
|
||||
## Sidequest Dungeons
|
||||
|
||||
### The Skep
|
||||
#### Location: Island Biome
|
||||
### Forest of Garruk
|
||||
#### Location: Forest Biome
|
||||
### Hydra´s Lair
|
||||
#### Location: Forest Biome
|
||||
### Outpost of Jace
|
||||
#### Location: Island Biome
|
||||
### Unhallowed Abbey
|
||||
#### Location: Plain Biome
|
||||
### Nahiri's Encampment
|
||||
#### Location: Plain Biome
|
||||
### Tibalt's Fortress
|
||||
#### Location: Mountain Biome
|
||||
### City of Zedruu
|
||||
#### Location: Mountain Biome
|
||||
### Old Sewers
|
||||
#### Location: Waste Biome
|
||||
### Xira's Hive
|
||||
#### Location: Waste Biome
|
||||
### Slobads Factory
|
||||
#### Location: Waste Biome
|
||||
### Grolnoks Bog
|
||||
#### Location: Swamp Biome
|
||||
### Slimefoot's Swamp
|
||||
#### Location: Swamp Biome
|
||||
### Teferi's Hideout
|
||||
#### Location: Island Biome
|
||||
### Kiora's Island
|
||||
#### Location: Island Biome
|
||||
### Sorin's Dungeon
|
||||
#### Location: Swamp Biome
|
||||
562
docs/Equipments-and-Items.md
Normal file
@@ -0,0 +1,562 @@
|
||||
# Quick Links
|
||||
|
||||
1. [Neck item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#neck)
|
||||
1. [Left item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#left)
|
||||
1. [Right item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#right)
|
||||
1. [Body item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#body)
|
||||
1. [Boot item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#boots)
|
||||
1. [Ability 1 item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#ability1)
|
||||
1. [Ability 2 item slot](https://github.com/Card-Forge/forge/wiki/Equipments-and-Items#ability2)
|
||||
|
||||
## NECK
|
||||
|
||||
#### Dark Amulet
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Swamp Capital Cities
|
||||
1. **Price** : 3000 gold
|
||||
|
||||

|
||||
|
||||
#### Hallowed Sigil
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Monastery
|
||||
|
||||

|
||||
|
||||
#### Jeweled Amulet
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 1000 gold
|
||||
|
||||
#### Life Amulet
|
||||
1. **Effect** : +2 starting life
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 4000 gold
|
||||
|
||||
#### Manasight Amulet
|
||||
1. **Effect** : Grants Manasight, letting you know the colors used by your adversaries.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 1000 gold
|
||||
|
||||
#### Phoenix Charm
|
||||
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/9c917d2c-c30d-4638-92ff-2df40120599c)
|
||||
1. **Location** : Chandra's Temple
|
||||
|
||||

|
||||
|
||||
#### Piper's Charm
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 4000 gold
|
||||
|
||||

|
||||
|
||||
#### Sorin's Amulet
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Sorin's Amulet)
|
||||
Starting life Modifier: +2
|
||||
1. **Location** : Sorin
|
||||
|
||||
#### Traveler's Amulet
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Traveler's Amulet)
|
||||
1. **Location** : Blue Capital City
|
||||
1. **Price** : 5000 gold
|
||||
|
||||
#### Xira's Fancy Hat
|
||||
1. **Effect** : +2 starting life + Extra card starting in your command zone.
|
||||
1. **Location** : Xira's Hive
|
||||
|
||||

|
||||
|
||||
## LEFT
|
||||
|
||||
#### Axt
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Cost** : 2500
|
||||
1. **Location** : Capital Cities
|
||||
|
||||

|
||||
|
||||
#### Battle Standard
|
||||
1. **Effect** : Extra card starting in your battlefield zone. and -1 starting life total
|
||||
1. **Location** : Goblin encampments
|
||||
|
||||

|
||||
|
||||
#### Bronze Sword
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Cost** : 2500
|
||||
1. **Location** : Waste towns
|
||||
|
||||

|
||||
|
||||
#### Chandra's Tome
|
||||
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/e88b95ce-dd83-48ed-bf77-df997b71bbb6)
|
||||
1. **Location** : Chandra's Temple
|
||||
|
||||

|
||||
|
||||
#### Dagger
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Cost** : 2500
|
||||
1. **Location** : Capital Cities
|
||||
|
||||

|
||||
|
||||
#### Disrupting Scepter
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Disrupting Scepter)
|
||||
1. **Cost** : 2500
|
||||
1. **Location** : Green Capital City Arena
|
||||
|
||||
#### Farmer's Tools
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 6000 gold
|
||||
|
||||

|
||||
|
||||
#### Flame Sword
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Cost** : 3500
|
||||
|
||||

|
||||
|
||||
#### Garruk's Mighty Axe
|
||||
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/47041427-b5d3-4faf-8f7b-1e1e004fa8ab?as=visual&with=usd)
|
||||
Starting life Modifier": +3
|
||||
1. **Location** : Garruk Forest
|
||||
|
||||

|
||||
|
||||
#### Giant Scythe
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
Life Modifier: + 1
|
||||
1. **Location** : Scarecrow Farm
|
||||
|
||||

|
||||
|
||||
#### Heart-Piercer
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Capital Cities
|
||||
|
||||

|
||||
|
||||
#### Heavy Arbalest
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Heavy Arbalest)
|
||||
1. **Cost** : 1500
|
||||
1. **Location** : Red Capital City
|
||||
|
||||
#### Hivestone
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Skep
|
||||
|
||||

|
||||
|
||||
#### Kiora's Bident
|
||||
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/7fdd8d06-c0c5-487e-a7e7-3050f6d19bed)
|
||||
Starting life Modifier": -1,
|
||||
Extra card starting in your battlefield zone: Kraken Hatchling
|
||||
1. **Location** : Kiora Island
|
||||
|
||||

|
||||

|
||||
|
||||
#### Mad Staff
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Power Struggle)
|
||||
1. **Location** : Black Capital City
|
||||
1. **Price** : 1000 gold
|
||||
|
||||
#### Nine-Ringed Bo
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Nine-Ringed Bo)
|
||||
1. **Location** : White Capital City
|
||||
1. **Price** : 1000 gold
|
||||
|
||||
#### Presence of the Hydra
|
||||
1. **Effect** : Extra card starting in your command zone. [Spellbook](https://scryfall.com/@Simidhimi/decks/86798768-3af5-49c0-a120-feb62e1a0e95)
|
||||
1. **Location** : Hydra boss
|
||||
|
||||

|
||||
|
||||
#### Sleep Wand
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 4000 gold
|
||||
|
||||

|
||||
|
||||
#### Slimefoot's Slimy Staff
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
Life Modifier: + 2
|
||||
1. **Location** : Slimefoot's Swamp
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### Spell Book
|
||||
1. **Effect** : Starts with an extra card in your hand.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 3000 gold
|
||||
|
||||
#### Steel Sword
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Cost** : 4500
|
||||
1. **Location** : Waste towns
|
||||
|
||||

|
||||
|
||||
#### Teferi's Staff
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
Life Modifier: + 1, Card reward bonus +1
|
||||
1. **Location** : Teferi Hideout
|
||||
|
||||

|
||||
|
||||
#### The Underworld Cookbook
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Zedruu's City
|
||||
|
||||

|
||||
|
||||
#### Tibalt's Bag of Tricks
|
||||
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
[Tibalt's Devils](https://scryfall.com/@Simidhimi/decks/1d5cc465-ac4a-496f-bac1-dc313c540375)
|
||||
[Tibalt's Spells](https://scryfall.com/@Simidhimi/decks/8267bb6a-a226-4ec2-946e-a419372b97dc)
|
||||
[Tibalt's Spellbook](https://scryfall.com/@Simidhimi/decks/b2d248bc-e16f-49fe-94dc-28ac2b6ab5b5)
|
||||
Life Modifier: + 1
|
||||
1. **Location** : Tibalt's Fortress
|
||||
|
||||

|
||||
|
||||
#### Unerring Sling
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Unerring Sling)
|
||||
1. **Cost** : 4000
|
||||
1. **Location** : Green Capital City
|
||||
|
||||
#### Wood Bow
|
||||
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Capital Cities
|
||||
|
||||

|
||||
|
||||
#### Zedruu's Lantern
|
||||
1. **Effect** : Extra card starting in your command zone. [Spellbook Content](https://scryfall.com/@Simidhimi/decks/bdaa39ed-b4b3-4157-aa85-98e313750dd0)
|
||||
Starting life Modifier: +1
|
||||
1. **Location** : Zedruu's City
|
||||
|
||||

|
||||
|
||||
## Right
|
||||
|
||||
#### Aladdin's Lamp
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 3000 gold
|
||||
|
||||

|
||||
|
||||
#### Aladdin's Ring
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 3000 gold
|
||||
|
||||

|
||||
|
||||
#### Chicken Egg
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Secret :)
|
||||
|
||||

|
||||
|
||||
#### Cursed Ring
|
||||
1. **Effect** : 3 cards starting in **your opponents** battlefield zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 3000 gold
|
||||
|
||||

|
||||
|
||||
#### Cursed Treasure
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 4000 gold
|
||||
|
||||

|
||||
|
||||
#### Dark Shield
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Barrier of Bones)
|
||||
Lifemodifier: -3
|
||||
1. **Location** : Black Capital City
|
||||
|
||||
#### Death Ring
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Cost** : 3500
|
||||
|
||||

|
||||
|
||||
#### Demonic Contract
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Temple of Liliana
|
||||
|
||||

|
||||
|
||||
#### Dungeon Map
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 3000 gold
|
||||
|
||||

|
||||
|
||||
#### Entrancing Lyre
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Entrancing Lyre)
|
||||
1. **Location** : White Capital City
|
||||
1. **Cost** : 1000
|
||||
|
||||
#### Gold Shield
|
||||
1. **Effect** : + 3 starting life total
|
||||
1. **Location** : Blue Capital Arena
|
||||
|
||||
#### Grolnok's Skin
|
||||
1. **Effect** : Extra card starting in your battlefield zone. Lifemodifier: +3 [Spellbook](https://scryfall.com/@Simidhimi/decks/bf16e555-4efb-488b-a05c-25296807777c)
|
||||
1. **Location** : Grolnok's Bog
|
||||
|
||||

|
||||
|
||||
#### Hill Giant Club
|
||||
1. **Effect** : Extra card starting in your command zone.
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 2000 gold
|
||||
|
||||

|
||||
|
||||
#### Iron Shield
|
||||
1. **Effect** : + 2 starting life total
|
||||
1. **Location** : Waste Town
|
||||
1. **Cost** : 3500
|
||||
|
||||
#### Jandor's Ring
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Jandor's Ring)
|
||||
1. **Location** : Red Capital Arena
|
||||
|
||||
#### Jungle Shield
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost** : 3500
|
||||
|
||||

|
||||
|
||||
#### Kite Shield
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Kite Shield)
|
||||
Opponent starting life -2
|
||||
1. **Location** : White Capital City
|
||||
1. **Cost** : 1500
|
||||
|
||||
#### Magic Shard
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Magic Shard)
|
||||
1. **Location** : Blue Capital City
|
||||
1. **Cost** : 3500
|
||||
|
||||
#### Mirror Shield
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Mirror Shield)
|
||||
1. **Location** : Capital Cities
|
||||
1. **Price** : 2500 gold
|
||||
|
||||
#### Mithril Shield
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (c_0_4_a_wall_defender)
|
||||
1. **Location** : Blue Capital City
|
||||
1. **Cost** : 6500
|
||||
|
||||
#### Nahiri's Armory
|
||||
1. **Effect** : Extra card starting in your battlefield zone. Lifemodifier: +3 [Spellbook](https://scryfall.com/@Simidhimi/decks/b7a18cab-c10c-4ad4-80ba-aba88649ef27)
|
||||
1. **Location** : Nahiri's Outpost
|
||||
|
||||

|
||||
|
||||
#### Prism Ring
|
||||
1. **Effect** : Opponent starting life -2
|
||||
1. **Location** : Capital cities
|
||||
|
||||
#### Ring of Immortals
|
||||
1. **Effect** : Opponent starting life -2
|
||||
1. **Location** : Capital cities
|
||||
|
||||
#### Ring of Renewal
|
||||
1. **Effect** : Opponent starting life -2
|
||||
1. **Location** : Capital cities
|
||||
|
||||
#### Ring of Three Wishes
|
||||
1. **Effect** : Extra card starting in your battlefield zone. (Ring of Three Wishes)
|
||||
1. **Location** : Green Capital City
|
||||
1. **Cost** : 2500
|
||||
|
||||
#### Steel Shield
|
||||
1. **Effect** : Extra card starting in your battlefield zone
|
||||
1. **Location** : Waste Town
|
||||
1. **Cost** : 6500
|
||||
|
||||

|
||||
|
||||
#### Unhallowed Sigil
|
||||
1. **Effect** : Extra card starting in your battlefield zone.
|
||||
1. **Location** : Monastery
|
||||
|
||||

|
||||
|
||||
## Boots
|
||||
|
||||
#### Dark Boots
|
||||
1. **Effect** : Extra card starting in your battlefield zone. Movement speed + 30%. -2 starting life total
|
||||
1. **Location** : Swamp Capital Shop
|
||||
1. **Price** : 3000 gold
|
||||
|
||||

|
||||
|
||||
#### Gold Boots
|
||||
1. **Effect** : Movement speed + 30%. +2 starting life total
|
||||
1. **Location** : Plains Capital Arena
|
||||
1. **Price** : 7500 gold
|
||||
|
||||
#### Iron Boots
|
||||
1. **Effect** : Movement speed + 20%.
|
||||
1. **Location** : Waste towns
|
||||
1. **Price** : 2000 gold
|
||||
|
||||
#### Leather Boots
|
||||
1. **Effect** : Movement speed + 15%.
|
||||
1. **Location** : Starting item on Easy and Medium difficulty
|
||||
|
||||
#### Lightbringers Boots
|
||||
1. **Effect** : Extra card starting in your battlefield zone (Ajani's Mantra).
|
||||
Opponent starting life +5
|
||||
1. **Location** : Forgotten Cave
|
||||
|
||||
#### Mithril Boots
|
||||
1. **Effect** : Movement speed + 30%. +2 starting life total
|
||||
1. **Location** : Island Capital Arena
|
||||
1. **Price** : 10000 gold
|
||||
|
||||
#### Sandals
|
||||
1. **Effect** : Movement speed + 10%.
|
||||
1. **Location** : Starting item on Hard and Insane difficulty
|
||||
|
||||
#### Slime-Covered Boots
|
||||
1. **Effect** : Extra card starting in your battlefield zone. Movement speed + 20%. -1 starting life total
|
||||
1. **Location** : Ooze Boss
|
||||
|
||||

|
||||
|
||||
#### Slobad's Iron Boots
|
||||
1. **Effect** : Extra card starting in your command zone. + 1 starting life total + movement speed + 35%
|
||||
1. **Location** : Slobad's Dungeon
|
||||
|
||||

|
||||
|
||||
#### Steel Boots
|
||||
1. **Effect** : Movement speed + 20%. +1 starting life total
|
||||
1. **Location** : Waste towns
|
||||
1. **Price** : 4500 gold
|
||||
|
||||
## Body
|
||||
|
||||
#### Armor of the Hivelord
|
||||
1. **Effect** : +5 starting life total. And start with an extra card in your hand.
|
||||
1. **Location** : Skep
|
||||
|
||||
#### Dark Armor
|
||||
1. **Effect** : Extra card starting in your battlefield zone. And -2 starting life
|
||||
1. **Location** : Black Capital Shop
|
||||
1. **Price** : 3000 gold
|
||||
|
||||

|
||||
|
||||
#### Gold Armor
|
||||
1. **Effect** : +4 starting life total
|
||||
1. **Location** : Plains Capital Arena
|
||||
1. **Price** : 7500 gold
|
||||
|
||||
#### Iron Armor
|
||||
1. **Effect** : +2 starting life total
|
||||
1. **Location** : Waste towns
|
||||
1. **Price** : 3000 gold
|
||||
|
||||
#### Jace's Signature Hoodie
|
||||
1. **Effect** : Extra card starting in your command zone. And -1 starting life. And additional reward multiplier
|
||||
1. **Location** : Jace Boss
|
||||
|
||||

|
||||
|
||||
#### Mantle of Ancient Lore
|
||||
1. **Effect** : Extra card starting in your command zone. And +1 starting life
|
||||
1. **Location** : Zedruu dungeon
|
||||
|
||||

|
||||
|
||||
#### Mithril Armor
|
||||
1. **Effect** : +5 starting life total
|
||||
1. **Location** : Island Capital Arena
|
||||
1. **Price** : 15000 gold
|
||||
|
||||
#### Steel Armor
|
||||
1. **Effect** : +3 starting life total
|
||||
1. **Location** : Waste towns
|
||||
1. **Price** : 5000 gold
|
||||
|
||||
## ABILITY1
|
||||
|
||||
#### White Staff
|
||||
1. **Effect**: Heals half of your life for 5 shards
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 5000
|
||||
|
||||
#### Black Staff
|
||||
1. **Effect**: Hide from your enemies for 5 shards
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 5000
|
||||
|
||||
#### Blue Staff
|
||||
1. **Effect**: Let's you fly for 5 shards
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 5000
|
||||
|
||||
#### Red Staff
|
||||
1. **Effect**: Kills the closest enemy in the overworld (non-dungeon) for 5 shards
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 5000
|
||||
|
||||
#### Green Staff
|
||||
1. **Effect**: Doubles your speed for 5 shards
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 5000
|
||||
|
||||
## ABILITY2
|
||||
|
||||
#### Green Rune
|
||||
1. **Effect**: Teleports you to the forest capital for 1 shard.
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 500
|
||||
|
||||
#### Black Rune
|
||||
1. **Effect**: Teleports you to the swamp capital for 1 shard.
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 500
|
||||
|
||||
#### White Rune
|
||||
1. **Effect**: Teleports you to the plains capital for 1 shard.
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 500
|
||||
|
||||
#### Red Rune
|
||||
1. **Effect**: Teleports you to the mountain capital for 1 shard.
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 500
|
||||
|
||||
#### Blue Rune
|
||||
1. **Effect**: Teleports you to the island capital for 1 shard.
|
||||
1. **Location** : Capital cities
|
||||
1. **Cost**: 500
|
||||
|
||||
#### Blue Rune
|
||||
1. **Effect**: Teleports you to the center for 1 shard.
|
||||
1. **Location** : Starting area
|
||||
29
docs/File-Formats.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# About
|
||||
There are many different file formats used in forge, due to the longevity of forge and it's growth and change, older formats are used and new ones are being created. This is an attempt to capture the requirements of these files for general creation, and modification. Unfortunately these "formats" may change and this list, or the data in them may become obsolete as changes happen, especially in newer features like Adventure Mode.
|
||||
|
||||
# List
|
||||
This is a quick hand jammed list of the formats that I have found looking through the /res/ folder. This is not complete, but is a quick reference and a start to identifying the files that can be "customized" or modified to make the engine work for you.
|
||||
|
||||
The list is (roughly identified) "format name", and the folder location and filename or just the extension if a specific filename is not required. Where "plane folder" is used, it is a subfolder of that game mode, `Adventure/Shandalar/` for example, or `Conquest/planes/Shandalar/` for conquests, and `quest/world/Shandalar/` for quests.
|
||||
|
||||
- Deck Files - .dck
|
||||
- Card Files - .txt
|
||||
- Editions - .txt
|
||||
- Draft - .draft
|
||||
- Blocks - blocks.txt
|
||||
- Formats - type/name.txt
|
||||
- Puzzle - .pzl
|
||||
- Quest
|
||||
- Theme - .thm
|
||||
- Worlds - World folder/worlds.txt
|
||||
- Adventure - Plane Folder
|
||||
- Plane Configuration - config.json
|
||||
- Generated Enemy - .json
|
||||
- Generated Starter Deck - .json
|
||||
- Conquest - Plane Folder
|
||||
- Banned Cards - banned_cards.txt
|
||||
- Cards - cards.txt
|
||||
- Plane Cards - plane_cards.txt
|
||||
- Regions - regions.txt
|
||||
- Sets - sets.txt
|
||||
- Events - set/_events.txt
|
||||
115
docs/Forge-historical-reference.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# An oral/written history of Forge
|
||||
|
||||
Here is a roughly written history of some notable points in Forge's history along with some Java 8 playable versions of the game linked for some of the older ones...
|
||||
|
||||
## 2007-04-26 - Forge introduced on https://mtgrares.blogspot.com/ by mtgrares.
|
||||
|
||||
365 cards available.
|
||||
|
||||
`The card set is the best of Magic, and includes such cards like Ancestral Recall, Juzam Djinn, Serendib Efreet, Flametongue Kavu, Man-o'-War, all the moxes, Kokusho, the Evening Star, Keiga, the Tide Star, as well as old favorites like Wrath of God, Psychic Blast, Serra Angel, and a few Planar Chaos cards like Pyrohemia, Serra Sphinx, Damnation.`
|
||||
|
||||
All of the blogposts are still available as of this writing (May 2023), but most of the links are defunct since they are pointing to since abandoned locations.
|
||||
|
||||
## 2008-07/08 - Oldest known historic archive of Forge.
|
||||
|
||||
850 cards available. Original white-gray Java Swing interface. **All** cards live in a cards.txt file. No card "scripting" exists yet.
|
||||
|
||||
## 2010-07 - Improved original interface
|
||||
|
||||
Still using cards.txt file. Beginnings of the "res" folder for resources to be used inside Forge.
|
||||
|
||||
## 2011-09 - Start transitioning from "vintage" Forge to "modern" Forge.
|
||||
|
||||
Mostly old-style interface with new age loading screen. Initial card scripting is being used along with the cardsfolder structure. Although the individual cards scripts, contain lots of vestiges of the past. (SetInfo, End tags, Picture tags, even the original scripting format)
|
||||
|
||||
## 2012-02 - Desktop interface transitions
|
||||
|
||||
Supports over 10,000 cards. We start using a reconstructed UI that allows for better theming. And makes things feel less like an "Application" and more like a "Game"
|
||||
|
||||
## 2014-05 - Android app is published via Maven
|
||||
|
||||
## 2015-04 - Network play is rudimentary but available
|
||||
|
||||
## 2018-04 - Focus on adding automation tooling
|
||||
|
||||
## 2021-12 - Adventure mode functional
|
||||
|
||||
## 2024-07 - _All non-Un set cards have been added to Forge_
|
||||
|
||||
## 2024-08 - Bloomburrow release
|
||||
|
||||
## 2024-09 - Duskmouth Release 1.6.65 (**Last release that supports Java 1.8**)
|
||||
|
||||
Supports over 28,500 cards.
|
||||
|
||||
### Some significant events that need to be dated
|
||||
|
||||
* Quest mode
|
||||
* Enable all phases
|
||||
* AI can cast instants
|
||||
* Creation of the Ability Factory/Scripting 2.0 system
|
||||
* Creation of costing system
|
||||
* Allowance of more than one player
|
||||
|
||||
# Legends
|
||||
|
||||
## Recounting of "Forge 2.0" by @Agetian
|
||||
|
||||
My favorite "Forge legend" is probably the legend about Forge 2.0 (that never came to be, at least thus far). Forge 2.0 was something that was brought up multiple times in Forge history, different targets were set for the hypothetical "future Forge" that would be worthy of increasing the major version number to 2, including, but not limited to, network play support, better UI, better AI (such as Minimax etc.), 100% support for standard cards, and more. Quite amusingly, the absolute majority of those targets have been met since, and even far surpassed, what with the addition of the mobile port, Planar Conquest, Adventure mode, and much more, but Forge is still using the 1.x version scheme it's been using since way back, hehe. Originally, Forge 2.0 was brought up my MtgRares (including a few times on his blog), but the original developer quit the active development before reaching his 2.0 targets, sadly. Thus, at the moment "Forge 2.0" represents a certain hypothetical, future version of Forge that's significantly and unconditionally better than what we currently have, whatever it may be.
|
||||
|
||||
The alternative variation of this historical legend is the legend about "Forge 2" - something that was also brought up a couple times in Forge history, mostly coming, unfortunately, from single developers. The hypothetical "Forge 2" was the complete, from-the-ground-up rewrite of Forge that would solve the many poor design choices that are difficult to solve through refactoring, and would thus pave the way for better UI/UX and AI implementations, make the game more streamlined and easier to code, among other things. Sadly, "Forge 2", just as well as "Forge 2.0", hasn't come to be, since the task to recreate all the game mechanics essentially from scratch, mostly as a single developer effort, has proven to be insurmountable.
|
||||
|
||||
## Original Adventure Mode
|
||||
|
||||
https://www.slightlymagic.net/forum/viewtopic.php?f=52&t=2907
|
||||
|
||||
For any of you curious about what the first attempt at making a Shandalar-like Adventure mode in Forge looked like (a development effort from 2010-2011), here are a few screenshots posted by the original developer who left circa 2011 or 2012. The project was abandoned, as far as I know. Pretty sure I had a test version somewhere in my backups (where you could walk around a mostly empty World Map and maybe start a battle or something, I don't remember the details), but I can't find it anymore.
|
||||
|
||||
Interestingly, the above-mentioned thread refers to the proposed game mode as "Adventure Mode" a few times (closer to the end of the thread), something that we actually currently have and actively develop 👍
|
||||
The interesting aspect of that development was the day/night cycle, I don't know why but I remember I thought that it looked quite atmospheric back in the day
|
||||
|
||||
|
||||
|
||||
## Abe Sergeant writes about Forge
|
||||
|
||||
2009-09-11 - Here is the article on star city.
|
||||
|
||||
https://articles.starcitygames.com/articles/the-kitchen-table-302-an-mtg-forge-quest/
|
||||
|
||||
And here is it in archive.org in case that gets taken down
|
||||
|
||||
https://web.archive.org/web/20210707215155/https://articles.starcitygames.com/articles/the-kitchen-table-302-an-mtg-forge-quest/
|
||||
|
||||
MTG Forge comes with a Quest Mode. In Quest Mode, you begin with a random selection of cards, and have to build a 60-card deck. Then you play against decks by various computer opponents, and as you win, you get more cards, and the difficulty of your opponents increases.
|
||||
|
||||
Quest is the most fun I’ve had playing Magic in a year.
|
||||
|
||||
What I am going to do is show you a quick 10-game win in quest. You can play to 10, 20, 30, or 40 wins, but I’m just going to show you a 10 win in quest. I will show you where the game has bugs too, so you can see what I am talking about when I say WIP. I’m not holding anything back.
|
||||
|
||||
# Major disruptions
|
||||
|
||||
One interesting thing about Forge is the way it grew. Much of the first few years was solely on the back of an Amateur software engineer called "MTG Rares" soon enough "Dennis Bergkamp" came along and was doing a bunch more development. New Software Engineers joined the ranks off and on. From there on people would jump in, help for a handful of years and get too busy with life, or stop really playing magic or whatever. A handful of us have been around the longhaul, but not too many.
|
||||
|
||||
## Sourceforge SVN (2007-2008)
|
||||
Original location. Moved when the name changed.
|
||||
## Google Code SVN (2008-2011)
|
||||
Google code was shutting down "soon" so we used it as an excuse to find a new home
|
||||
## Bitbucket/GIT (2011-2011)
|
||||
This one lasted barely a few weeks. It was hated pretty universally, even by the people who suggested it.
|
||||
## Slightly Magic SVN (2011-2017)
|
||||
The nice folks at Slightly Magic hosted an SVN for us.
|
||||
## Hosted Gitlab (2017?-2021)
|
||||
One of the devs was comfortable in Git/Gitlab and moved everything over. Took a few attempts to get there but we made it.
|
||||
## Github (2021-Present)
|
||||
Conversion from Git to Git was a lot easier.
|
||||
|
||||
|
||||
## Jendave's modularization (2011)
|
||||
The initial modularization attempt pulled everything from living under forge-gui/ module and built out some of the Maven structure.
|
||||
|
||||
## Maxmtg's modularization (2013?)
|
||||
Took this the next step further massively reorganizing the codebase. It caused major issues and took a few months to get resolved.
|
||||
|
||||
## Hanmac's modularization (2017?
|
||||
This was a smaller modularization mostly within certains areas that ultimately was positive, but led to some headaches during the process.
|
||||
|
||||
93
docs/Frequently-Asked-Questions.md
Normal file
@@ -0,0 +1,93 @@
|
||||
This is a list of basic troubleshooting questions that come up for most new players, before running to the discord about your issue, please review this FAQ for some of the more common issues.
|
||||
|
||||
### Check the FAQ in Discord
|
||||
|
||||
https://discord.com/channels/267367946135928833/1095026912927154176
|
||||
|
||||
### Search the help posts in Discord
|
||||
|
||||
https://discord.com/channels/267367946135928833/1047001034788196452
|
||||
|
||||
### Write a post in help section of Discord
|
||||
|
||||
https://discord.com/channels/267367946135928833/1047001034788196452
|
||||
|
||||
Note: For now, please also check [this](https://www.slightlymagic.net/forum/viewtopic.php?f=26&t=11825) forum topic for some additional information.
|
||||
|
||||
# General
|
||||
|
||||
### How do I download content?
|
||||
|
||||
Forge has content downloaders within the app itself, you can use those tools to update the graphics assets. More information about card and token image assets can be found here. [Card Images, Downloading](Card-Images#downloading)
|
||||
|
||||
|
||||
|
||||
* If you have an older Android device for increased performance or to save bandwidth it might be a good idea to use lower resolution images instead: https://www.slightlymagic.net/forum/viewtopic.php?f=15&t=29104
|
||||
|
||||
### How do I extract Forge?
|
||||
|
||||
* Forge uses a .tar.bz2 format for archiving. Depending on your operating system, different utilities can be used to untar the archive.
|
||||
* If you use Windows, you may want to try 7-Zip (http://www.7-zip.org/download.html).
|
||||
|
||||
### I think I found a bug in Forge. What do I do?
|
||||
|
||||
*Most users, who are running beta versions of Forge, should continue to use these instructions. As for alpha testers, these instructions have yet to be made congruent with the latest automatic bug reporting from within Forge.*
|
||||
|
||||
Bug reports from users are the lifeblood of Forge. Please keep in mind that "beta" releases *are* test releases. Forge is constantly evolving, so we do not yet have "stable" or "production" releases. Because of the pace at which new cards are added to the multiverse by external forces, this will be the norm for some time. We do not expect everything to work 100%. We have a small number of developers and a handful of slightly less technical people actively improving the game. We simply cannot devote the resources to test every single card, much less the nearly infinite ways the cards can interact.
|
||||
|
||||
For starters, please take note of (1) what you had in play, (2) what your opponent had in play and (3) what you were doing when the error occurred. If you get a Crash Report from inside Forge, please save the data to a file. This information is very important when reporting a problem. Don't worry if you didn't think of that right away, until your next start, the "Forge.log" in the game directory will also provide that information.
|
||||
|
||||
If you did not get a Crash Report, but you have experienced a problem in how Forge handled one or more cards or game rules, *please read the cards (and the Oracle rulings) carefully* to make sure you understand how they work. You may be surprised to find that Forge is actually enforcing the rules
|
||||
correctly.
|
||||
|
||||
Because duplicate bug reports use up our limited resources, please research your bug with the **Search** box on Forge's [issue tracker](https://git.cardforge.org/core-developers/forge/-/issues) to see if your bug has already been reported there. For Crash Reports, use key words from the second paragraph of the Crash Report.
|
||||
|
||||
* If you find a matching issue, examine it to see if you have anything new to contribute. For example, a different way of reproducing a problem can sometimes be helpful. If the issue was posted to the forum, you may post your additional information there.
|
||||
|
||||
* If you find nothing, please try to reproduce the problem and take notes. If we can use your notes to reproduce the bug for ourselves, it is *much* easier to fix!
|
||||
|
||||
* If you're unsure, you can also post on one of the support channels of the discord. In case you do not get a timely response, please submit a new issue anyway to make sure it doesn't get lost.
|
||||
|
||||
### I have an idea to make Forge better. What do I do?
|
||||
|
||||
Follow the directions in [Bug Reports](Frequently-Asked-Questions#i-think-i-found-a-bug-in-forge-what-do-i-do), keeping in mind that you are not reporting a bug, but rather a **Feature Request**.
|
||||
|
||||
# Development
|
||||
|
||||
### I want to help develop Forge. How do I get started?
|
||||
|
||||
Forge is written in Java, so knowledge in that language (or similar Object Oriented languages like C++ or C\#) is very helpful. However, it is possible to learn the grammar for writing the data objects of cards without programming experience.
|
||||
|
||||
A development environment such as [IntelliJ](https://www.jetbrains.com/idea) is beneficial, as it helps writing, compiling and testing your changes.
|
||||
|
||||
Thanks to the nature of how cards are implemented, you can also contribute these as small plain text files. This is especially helpful during a preview season, when there are a lot of new cards in the backlog. This is mostly coordinated in #card-scripting on the Discord (and the pins there).
|
||||
|
||||
To obtain the source code of Forge, read our [Development Guide]((SM-autoconverted)--how-to-get-started-developing-forge).
|
||||
|
||||
### My system is all setup to help. What now?
|
||||
|
||||
Take a look through the /res/cardsfolder folder. This is where all the card data lives. If you know of cards that are missing from Forge, see if there are similar cards that already exist.
|
||||
|
||||
# Gameplay
|
||||
|
||||
### Where do I use Flashback or a similar ability that is in an External area?
|
||||
|
||||
Click on the Lightning Bolt icon in the player panel. Since cards with External Activations aren't as clear to activate, we created this shortcut for this specific purpose.
|
||||
|
||||
### How do I target a player?
|
||||
|
||||
Just click on the player's Avatar in the Player Panel when prompted to select a Player as a target.
|
||||
|
||||
### Where did my mana go?
|
||||
|
||||
If you have an effect that generated you some mana, and you don't know where it is. Check out the Player Panel. There are 6 different mana subpools one for each color/colorless that should have it. If you accidentally tapped your mana before your Main Phase, your mana is gone. Sorry, we don't have a way at this time to revert these actions. In general, I'd say it's easier/better to start casting a spell first, then activate your mana so this doesn't happen.
|
||||
|
||||
# Quest Mode
|
||||
|
||||
### What is the difference between Fantasy Quest and Normal Quest?
|
||||
|
||||
In Normal Quest, you start with 20 life and only have access to the Card Shop. In Fantasy Quest, you start at 15 life and gain additional access to the Bazaar which allows you to buy things like extra life points, Pets, Plants and more.
|
||||
|
||||
### Sealed Deck Mode
|
||||
|
||||
[HOW-TO: Customize your Sealed Deck games with fantasy blocks](https://www.slightlymagic.net/forum/viewtopic.php?f=26&t=8164)
|
||||
35
docs/Gameplay-Guide.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Getting started
|
||||
|
||||
|
||||
## Difficulty
|
||||
|
||||
The difficulty you choose will alter your starting health (15 for easy to 7 for extreme), and the amount of cards you start with when you choose the "pile" mode. Also, cards will sell for less in shops and enemies will have more health.
|
||||
|
||||
Another difference is that on hard/extreme difficulty, enemies that use computer-generated decks (using .json files) will instead use completely random decks that will have nothing to do with their original deck theme.
|
||||
|
||||
## Starting cards
|
||||
|
||||
You will have multiple options as to how your starting pool of cards will look
|
||||
|
||||
1. __Pile__. This will give you a random pile of cards based on the color you choose. The harder the difficulty you choose, the fewer cards you will get.
|
||||
When you use this option, it is recommended that you create a 40-card deck (minimum deck size) out of the 60 cards you get.
|
||||
|
||||
2. __Chaos__. This is a unique gameplay mode that completely randomizes the enemy decks and the deck you start with.
|
||||
__This setting is not recommended for new players__
|
||||
|
||||
3. __Standard__. With this start, you will get three 20-card preconstructed jumpstart decks from the set you choose (or all of them).
|
||||
|
||||
4. __Constructed__. With this setting, you start with a custom preconstructed deck based on a popular theme that's easy to expand during your run. (**Recommended for new players**). Your starting color will determine which starter deck you will get
|
||||
|
||||
## General hints and tips
|
||||
|
||||
1. Don't immediately sell cards that are not useful in your current deck. Some cards can be very useful in certain boss fights, for instance, cards with " protection from red" are very strong against most red bosses.
|
||||
2. Make sure to travel to the biome capitals early in your run. Every capital contains a shop that sells 1 life and an item that allows you to teleport back to that capital. Also, they sell items that can't be found elsewhere.
|
||||
3. Every boss gives you +1 starting life. So if you're having trouble with a certain boss, it might be better to try your hand with other bosses.
|
||||
4. Arena challenges in the capitals can be a good source of money, items, and cards. Though you need a rather strong deck to consistently win there.
|
||||
|
||||
### Insane Guide
|
||||
|
||||
If you are playing on Insane and need some pointers, user KingNishi has written up a nice doc about it
|
||||
|
||||
https://docs.google.com/document/d/1RgO20QpdVOpU-g_E1RL8q1r0C5GUy6y-nnxgXNbi9Wg/pub
|
||||
57
docs/Home.md
Normal file
@@ -0,0 +1,57 @@
|
||||
> [!CAUTION]
|
||||
> - if you want to contribute to this Wiki please only make pull requests against the main repositories *docs* folder or your changes might get lost
|
||||
> - due to GitHub limitations all filenames should be unique
|
||||
|
||||
# What is Forge?
|
||||
|
||||
Forge is a "Rules Engine" for the game Magic: the Gathering.
|
||||
Forge is not related in any way with Wizards of the Coast.
|
||||
Forge is open source software released under the GNU Public License.
|
||||
|
||||
Up to 8 players are supported, with control of each assigned to human or AI control. Player decks can be imported, user-created with the Deck Editor, or automatically generated. Over 99% (and counting) of all cards in Magic's existence are available, with the missing ones mostly being pointless to implement in the context (e. g. the notorious Chaos Orb) or impossible. That's more than the official Magic Online!
|
||||
|
||||
For a complete list of unimplemented cards, either check the most recent release topic on the forums or use the "Audit Card and Image Data" check from "Content Downloaders" menu.
|
||||
|
||||
Forge creates a unique experience by combining this enormous card library with some RPG elements in [**Quest mode**](https://www.slightlymagic.net/forum/viewtopic.php?f=26&t=9258) (comparable to the 'Shandalar', the late 90's PC Game) on the desktop version, and **Planar Conquest** on the mobile version. Forge now includes a Graphical Map Based game mode called "Adventure Mode" which is more akin to 'Shandalar.'
|
||||
|
||||
Forge also features a wide variety of puzzles. For more details and features see the MANUAL.txt in your game folder.
|
||||
|
||||
Currently, Forge functions best in Human vs. AI matches. Playing against another human player over the Internet with Online Multiplayer mode is functional, but may still result in game play errors. See the Network Play section of this wiki.
|
||||
|
||||
# Adventure Mode
|
||||
Forge now has an Adventure Mode, along with the Classic deck building and match game modes with AI.
|
||||
|
||||
Adventure mode is a work-in-progress game mode where you explore the ever-changing landscape of Shandalar, and duel creatures to gain gold and new cards ~~to become the best and collect them all!~~ to battle the bosses in the castles for each color. You can visit towns to buy equipment and cards, crawl through dungeons to find artifacts and loot to help you on your journey. Adventure mode is an awesome reimagining of the original 'Shandalar' 90's PC Game in Forge.
|
||||
|
||||
Adventure is baked into the Android/Mobile release of Forge, and as a separate executable already provided in the Desktop release package.
|
||||
|
||||
# Download and Install
|
||||
|
||||
* Most users please see the [User Guide.](User-Guide)
|
||||
|
||||
* For SteamDeck or Bazzite Installation see [Steam Deck Install.](Steam-Deck-and-Bazzite-Install)
|
||||
|
||||
# Support
|
||||
[Basic Troubleshooting](Troubleshooting-FAQ) - Check here first.
|
||||
|
||||
[Join the Discord](https://discord.com/invite/3v9JCVr)! - We're happy to help you get going.
|
||||
|
||||
[Other Frequently Asked Questions](Frequently-Asked-Questions) - For more advanced questions about forge mechanics and gameplay.
|
||||
|
||||
# Forge Developers
|
||||
|
||||
The original programmer can be found at http://mtgrares.blogspot.com. A while
|
||||
back he open sourced the project and let the other developers improve it
|
||||
to their heart's content. He's taken a hands off approach for some time
|
||||
now, but still talks about it on his blog linked above.
|
||||
|
||||
At this time, there are many developers who help in different stages of
|
||||
the game. Some of us work on the infrastructure of the program, others
|
||||
work on creating new abilities and cards that use them, but there is a
|
||||
lot of behind the scenes action going on. For each release, it is common
|
||||
for the release developer to give a shout out for those that helped
|
||||
specifically for that version. Feel free to give kudos there.
|
||||
|
||||
If you are curious about the written/oral history of Forge, we're working on recreating some of that [Forge Historical reference](Forge-historical-reference)
|
||||
|
||||
> <span style="color: red;">Note:</span> if you're reading this locally here's the [Table of Contents](_sidebar.md)
|
||||
48
docs/How-to-Build-a-Cube.md
Normal file
@@ -0,0 +1,48 @@
|
||||
Setting up your cube in Forge is relatively straightforward. Forge needs to know two things: the contents of your cube and the contents of a cube pack
|
||||
|
||||
1) Define the contents of your cube
|
||||
For this you will need to add your cube list to the 'cube' folder, which can be found under the 'res' folder in your Forge installation directory.
|
||||
Cube lists are defined in .dck files. You can either create a new one with a text editor, or copy an existing one and change its file name and contents. Regardless of your approach, you should end up with a file that roughly looks like this:
|
||||
|
||||
```
|
||||
[metadata]
|
||||
Name=Two Card Cube
|
||||
[Main]
|
||||
1 Abbot of Keral Keep|ORI
|
||||
1 Abrade|HOU
|
||||
```
|
||||
|
||||
**Don't change the first and third line**. On the second line you can put in the name of your cube after 'Name=', in this example the name of our cube is Two Card Cube. Note this name, as you will later need to refer to it.
|
||||
Starting on the fourth line, you can list each card in your cube. Always start with the number of times a given card is included in your cube (important for those breaking singleton), then the name of the card, then a pipe (the '|' character), and finally the three letter set code.
|
||||
Tip: An easy way to find the three letter code for a card is to look up that card on www.scryfall.com and search up the correct version of the card. The set code will appear at the top of the prints overview behind the full set name.
|
||||
Note that this example cube contains just two cards. Adding more is simply a matter of following the same pattern and adding a line for each new cards.
|
||||
|
||||
Once you're done with your file, save it in the 'cube' folder. Use the value of the Name field as the filename and give it the .dck file extension. In this example, we would save the file as 'Two Card Cube.dck'.
|
||||
|
||||
2) Define the contents of your boosters
|
||||
The next step is to define the draft file that instructs Forge how your boosters look. For this you will need to create a .draft file in the 'draft' folder, which can again be found under the 'res' folder in your Forge installation directory.
|
||||
Note that the options I will describe here are somewhat limited. Unfortunately seeding boosters is not easily achieved. Let's again look at an example:
|
||||
|
||||
```
|
||||
Name:The Two Card Cube
|
||||
DeckFile:Two Card Cube
|
||||
Singleton:True
|
||||
|
||||
Booster: 15 Any
|
||||
NumPacks:3
|
||||
```
|
||||
|
||||
The first line contains the name of the cube as it will be presented in Forge.
|
||||
The second line contains should match the name field of the .dck file you just created.
|
||||
The third line defines whether this is a singleton draft format or a regular set format. Note that for cubes breaking singleton, you still want to set this value to True, since you define which cards you break singleton on in the .dck file.
|
||||
|
||||
The fifth line defines how many cards appear in a booster. You can change the number, but do not touch the 'Any', that part instructs Forge to pull cards from the entirety of your cube list.
|
||||
Finally, the sixth line defines the number of packs used when drafting your cube.
|
||||
|
||||
Once you're done with this file, save it in the 'draft' folder. You can again use the value in the name field, this time in conjunction with the file extension 'draft'. In this example, we would save the file as 'The Two Card Cube.draft'.
|
||||
|
||||
3) Backup both files
|
||||
The next step is to backup both your .dck file and your .draft file. You'll be happy you did when Forge needs to be reinstalled from scratch for some reason.
|
||||
|
||||
4) Draft!
|
||||
You've done it! Fire up Forge, and select Booster Draft in the left side menu (it's one of the options under Sanctioned Formats). Click the New Booster Draft Game button, select the option Custom Cube, click OK, choose the cube you just created (remember, the name presented here is the value for the field Name you entered in the .draft file), and click OK again. That's it, you're in. Happy drafting!
|
||||
11
docs/Keyboard-Shortcuts.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Keyboard Shortcuts
|
||||
|
||||
The following shortcuts are available for the player during the match:
|
||||
|
||||
- Space: Confirm (most times, is the button shown on the left)
|
||||
- Escape: Cancel (most times, is the button shown on the right)
|
||||
- E: end turn
|
||||
- Ctrl + Q: concede
|
||||
- Ctrl + Z: undo
|
||||
- Y: auto-yield to a trigger (always YES)
|
||||
- N: auto-yield to a trigger (always NO)
|
||||
13
docs/Mana-Shards.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Mana Shards
|
||||
Mana Shards are a custom resource created for Adventure Mode in Forge. Shards serve as a secondary form of currency in some parts of the game economy, can be bought and sold via dedicated Shard Trader merchants, and are also used to power the player's special abilities both on the Adventure world map as well as in almost all Adventure Mode duels. During a duel your Shards will appear as a usable resource visible alongside your life total akin to how Energy tokens are used. Your Shards are not tokens or counters, and are thus immune to any abilities that would affect tokens or counters outside of using [equipment](adventure-items) that specifically requires their expenditure. Spending your shards is a permanent choice, however, as your final total at the end of the match will follow you back into your Adventure.
|
||||
|
||||
As Mana Shards are not self-replenishing, using them for activated abilities that affect the game world and in-match gameplay is a way of letting the player have a constant challenge of their own comfort level. If the player is comfortable taking on a fight without using extra abilities afforded to them by use of Shards, they will have more of them available to spend on in town, or can simply save their full might for boss-level entities.
|
||||
|
||||
The most consistent source of Mana Shards is from match wins, players receive one Shard per win without any rewards having to be explicitly added to the encounter. However, when used to power equipment and abilities, one Shard per match will definitely not break even, as most custom Adventure cards and items will cost 2-4 per use. However, Mana Shards can also be purchased in towns, picked up in dungeons, and can be received in large quantities from completing quests.
|
||||
|
||||
Mana Shards can be used for:
|
||||
* Activated in-match abilities
|
||||
* Activated game map abilities
|
||||
* Refreshing card shop inventories
|
||||
* Spellsmith card purchases as an alternative to gold
|
||||
* Event entry fees as an alternative to gold
|
||||
77
docs/Missing-Cards-in-Forge.md
Normal file
@@ -0,0 +1,77 @@
|
||||
## Missing tournament legal cards
|
||||
|
||||
Total number of unique cards in Forge (as of the release of Final Fantasy): 30,331
|
||||
**For the latest updates during spoiler season** check out the "Projects" tab on Github for information which cards have been added. It takes some time to implement cards, and WotC doesn't send us spoilers ahead of time, so we barely have any time to implement new cards. Please be patient, and don't ask when cards will be ready.
|
||||
|
||||
| Format | Total Cards | Missing Cards | % Completed |
|
||||
| --------------- | --------------- | --------------- | --------------- |
|
||||
| Standard | 4,088 | 0 | 100 % |
|
||||
| Modern | 20,584 | 0 | 100 % |
|
||||
| Historic (including Alchemy cards) | 12,921 | 0 | 100 % |
|
||||
| Vintage/Legacy/Commander (including banned & restricted cards)| 29,185 | 46 | 99.8 % |
|
||||
|
||||
|
||||
### Stickers [46 Cards]
|
||||
|
||||
Introduces an entirely new type of game piece to track, which comes in four different varieties - name, ability, art, and stats. They were not widely used and are currently banned in all official formats except commander.
|
||||
|
||||
|
||||
1. [Baaallerina](https://scryfall.com/card/unf/35/baaallerina)
|
||||
1. [Bioluminary](https://scryfall.com/card/unf/38/bioluminary)
|
||||
1. [Croakid Amphibonaut](https://scryfall.com/card/unf/43/croakid-amphibonaut)
|
||||
1. [Glitterflitter](https://scryfall.com/card/unf/48/glitterflitter)
|
||||
1. [Make a _____ Splash](https://scryfall.com/card/unf/50/make-a-_____-splash)
|
||||
1. [Prize Wall](https://scryfall.com/card/unf/57/prize-wall)
|
||||
1. [_____ _____ _____ Trespasser](https://scryfall.com/card/unf/61/_____-_____-_____-trespasser)
|
||||
1. [Unlawful Entry](https://scryfall.com/card/unf/62/unlawful-entry)
|
||||
1. [Wizards of the _____](https://scryfall.com/card/unf/64/wizards-of-the-_____)
|
||||
1. [Carnival Carnivore](https://scryfall.com/card/unf/68/carnival-carnivore)
|
||||
1. [Last Voyage of the _____](https://scryfall.com/card/unf/78/last-voyage-of-the-_____)
|
||||
1. [Scampire](https://scryfall.com/card/unf/89/scampire)
|
||||
1. [Scared Stiff](https://scryfall.com/card/unf/90/scared-stiff)
|
||||
1. [Wolf in _____ Clothing](https://scryfall.com/card/unf/95/wolf-in-_____-clothing)
|
||||
1. [_____ Balls of Fire](https://scryfall.com/card/unf/100/_____-balls-of-fire)
|
||||
1. [Big Winner](https://scryfall.com/card/unf/101/big-winner)
|
||||
1. [_____ Goblin](https://scryfall.com/card/unf/107/_____-goblin)
|
||||
1. [Goblin Airbrusher](https://scryfall.com/card/unf/108/goblin-airbrusher)
|
||||
1. [Minotaur de Force](https://scryfall.com/card/unf/114/minotaur-de-force)
|
||||
1. [_____ _____ Rocketship](https://scryfall.com/card/unf/191/_____-_____-rocketship)
|
||||
1. [Ticketomaton](https://scryfall.com/card/unf/195/ticketomaton)
|
||||
1. [Wicker Picker](https://scryfall.com/card/unf/196/wicker-picker)
|
||||
1. [Tusk and Whiskers](https://scryfall.com/card/unf/182/tusk-and-whiskers)
|
||||
1. [Aerialephant](https://scryfall.com/card/unf/2/aerialephant)
|
||||
1. [Robo-Piñata](https://scryfall.com/card/unf/25/robo-pi%C3%B1ata)
|
||||
1. [Sanguine Sipper](https://scryfall.com/card/unf/26/sanguine-sipper)
|
||||
1. [_____ Bird Gets the Worm](https://scryfall.com/card/unf/5/_____-bird-gets-the-worm)
|
||||
1. [Park Bleater](https://scryfall.com/card/unf/21/park-bleater)
|
||||
1. [Pin Collection](https://scryfall.com/card/unf/23/pin-collection)
|
||||
1. [Sword-Swallowing Seraph](https://scryfall.com/card/unf/30/sword-swallowing-seraph)
|
||||
1. [Proficient Pyrodancer](https://scryfall.com/card/unf/120/proficient-pyrodancer)
|
||||
1. [Wee Champion](https://scryfall.com/card/unf/127/wee-champion)
|
||||
1. [Chicken Troupe](https://scryfall.com/card/unf/133/chicken-troupe)
|
||||
1. [Clandestine Chameleon](https://scryfall.com/card/unf/134/clandestine-chameleon)
|
||||
1. [Done for the Day](https://scryfall.com/card/unf/136/done-for-the-day)
|
||||
1. [Fight the _____ Fight](https://scryfall.com/card/unf/138/fight-the-_____-fight)
|
||||
1. [Finishing Move](https://scryfall.com/card/unf/139/finishing-move)
|
||||
1. [Roxi, Publicist to the Stars](https://scryfall.com/card/unf/177/roxi-publicist-to-the-stars)
|
||||
1. [Grabby Tabby](https://scryfall.com/card/unf/140/grabby-tabby)
|
||||
1. [Lineprancers](https://scryfall.com/card/unf/146/lineprancers)
|
||||
1. [_____-o-saurus](https://scryfall.com/card/unf/148/_____-o-saurus)
|
||||
1. [Stiltstrider](https://scryfall.com/card/unf/157/stiltstrider)
|
||||
1. [Costume Shop](https://scryfall.com/card/unf/206a/costume-shop)
|
||||
1. [Ambassador Blorpityblorpboop](https://scryfall.com/card/unf/161/ambassador-blorpityblorpboop)
|
||||
1. [A Good Day to Pie](https://scryfall.com/card/unf/12/a-good-day-to-pie)
|
||||
1. [Command Performance](https://scryfall.com/card/unf/42/command-performance)
|
||||
|
||||
## Missing variant cards
|
||||
|
||||
### [Bounty](https://mtg.fandom.com/wiki/Outlaws_of_Thunder_Junction/Commander_decks#Bounty_cards) (Casual Variant)
|
||||
The release of the Outlaws of Thunder Junction Commander Decks included a set of twelve "Bounty" cards and rules for a variant format that uses them. This format is not defined in the comprehensive rules, and is not currently implemented in Forge.
|
||||
|
||||
### [Hero's Path](https://mtg.fandom.com/wiki/Hero's_Path) (Promotional Variant)
|
||||
During the Theros Block, a series of promotional events was held. These featured three PvE encounters with unique rules, along with nontraditional cards for use by both the player and enemies in these encounters. The PvE encounters could be reconstructed in Forge, but supporting the Heroes and Godslayer weapons would add many complications.
|
||||
|
||||
|
||||
## Missing nonlegal and funny cards
|
||||
|
||||
Complete support for Un-cards and other non-legal cards is a non-goal of Forge, but individual cards may be implemented when they work within the scope of the project. For a complete list, see [Un-cards, Playtest Cards, and Other Funny Cards](https://github.com/Card-Forge/forge/wiki/Un%E2%80%90cards,-Playtest-Cards,-and-Other-Funny-Cards).
|
||||
28
docs/Modding-and-Development.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## Modding and Development
|
||||
|
||||
With the addition of new planes in Adventure Mode, comes a framework to allow greater customization and even expansion of the available planes. The details behind the framework and each category can be found below. As well as a new section being built with some basic information on each piece of plane construction and modification. The sections will also define some Best Practices to be maintained.
|
||||
|
||||
### Getting Started
|
||||
|
||||
Modding Adventure mode comes in many fashions. From making small changes to a core plane, such as changing the music that plays on the overworld. To something as complex as an entirely new plane. Regardless of your intended goals, the first thing to do is set-up a back-up method. As any changes you haven't had incorporated into the main game, will potentially be lost on each update of Forge. Since this is a Git project, the method that will be recommended by this wiki, and referenced for the future, is simply to create your own git branch, and use a local repository to control all your files. It is also recommended to follow the directions to [set-up IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup), to manage your local files. (Again, this is the method that will be referenced elsewhere in this wiki.)
|
||||
|
||||
### Tools
|
||||
|
||||
The following additional tools can also be very useful, or even mandatory, to have for your mod, depending on what all you want to do in your mod/addition.
|
||||
|
||||
**[Tiled](https://www.mapeditor.org/)**: If you want to modify or create any maps; (caves, dungeons, towns, etc.) Forge utilizes Tiled. If you want to learn more on this topic, you can find it in the [Create new Maps](https://github.com/Card-Forge/forge/wiki/Create-new-Maps) section of the Wiki.
|
||||
|
||||
**[GIMP](https://www.gimp.org/)**: Many of the art files such as the tilesets used in Tiled, are made in GIMP. (A free graphical manipulation program, similar to Photoshop.) While not required to work in Forge's files, if you want to create your own art assets, this is the program that will be used for examples in this Wiki.
|
||||
|
||||
|
||||
### Tutorials
|
||||
|
||||
The following pages on this Wiki contain some basic tutorials on various facets of modding Adventure mode, to help get you started.
|
||||
|
||||
[Tutorial 1, Create your First Plane](https://github.com/Card-Forge/forge/wiki/Tutorial-1-Create-your-First-Plane)
|
||||
|
||||
[Tutorial 2, A New Look (creating your first map.)](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look)
|
||||
|
||||
[Tutorial 3, Configuration (Configuring your Plane)](https://github.com/Card-Forge/forge/wiki/Tutorial-3-Configuration)
|
||||
|
||||
UNDER CONSTRUCTION, STAY TUNED
|
||||
34
docs/Network-FAQ.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Frequently Asked Questions About Networking Games
|
||||
|
||||
### Can I play you?
|
||||
|
||||
* No. Thanks for the offer, but right now I don't have time. And the truth is I'm just not that good at magic really, the AI is a better opponent than me.
|
||||
|
||||
### Can Mac Players play against Windows?
|
||||
|
||||
* Yes! Forge is completely cross platform, as it's built in java the same software is running on every system, this allows cross platform compatibility; Android to Windows to MacOS/OSX... potentially anything that can run Java.
|
||||
|
||||
Note: Forge is not built for iOS, as iOS doesn't support or run java.
|
||||
|
||||
### Can I play random players?
|
||||
|
||||
* No. Network play is designed, currently, to play against someone you know. If you want to play against random people, you can make a request in the discord.
|
||||
|
||||
### Why can't my friend connect?
|
||||
|
||||
* There's three major reasons
|
||||
|
||||
1. You're not on the same network and you're trying to use the local IP address. Please read [this part of the guide](network-play#remote-network).
|
||||
1. You're on the same network and your local firewall isn't configured correctly. Please read [this part of the guide](network-play#host-based-firewall).
|
||||
1. You're not on the same network and your router ports aren't forwarded. Please read [this part of the guide](network-play#port-forwarding-or-nat).
|
||||
* IF you're not on the same network and you can't access port forwards, you'll need this [Network Extra](Networking-Extras).
|
||||
|
||||
### Is it possible to play with people not on the same Wi-Fi?
|
||||
|
||||
* Yes. There's two primary ways to handle this:
|
||||
|
||||
1. On a home internet: Open and forward ports on your home router to your computer hosting the game.
|
||||
* Additional information is here: [Remote Networking](network-play#remote-network)
|
||||
2. On public Wi-Fi hotspots or cell networks: Use a VPN to create a private network.
|
||||
* Additional information is here: [Network Extra](Networking-Extras)
|
||||
* This may be more specific to getting you going: [VPN Providers](Networking-Extras#virtual-private-network-service-providers)
|
||||
216
docs/Network-Play.md
Normal file
@@ -0,0 +1,216 @@
|
||||
### Disclaimer:
|
||||
|
||||
While multiplayer over a network *does* currently work, and complete games *have* been played, it is still very much a "*work in progress*". This means;
|
||||
|
||||
* You **will** find bugs. When they appear;
|
||||
* They will likely be mid-game.
|
||||
* They will likely make it impossible to complete the current game/match.
|
||||
* You **will** need to restart both the client's and host's games.
|
||||
|
||||
**You've been warned!** But please report bugs and issues, this will help make network play more stable.
|
||||
|
||||
***
|
||||
|
||||
### Support:
|
||||
**We have a discord channel dedicated for network play related matters: https://discord.gg/nsAhGwD**
|
||||
|
||||
I've created a Networking FAQ, check the sidebar for it. I'll update it as I get **Q's** in discord that are **FA**.
|
||||
|
||||
|
||||
Note: This guide has been written to be as comprehensive as possible; from a quick start, to troubleshooting, to network configuration and concepts. It's not possible to identify all network configurations or issues, but the typical and most basic are represented in this guide. I personally want network play to be easy to use, set up, and for it to be stable. The more people playing over network the more its play tested, and more bugs can be found.
|
||||
|
||||
***
|
||||
|
||||
### Requirements:
|
||||
|
||||
* **At least two devices**, all devices must be running the same version of Forge.
|
||||
* Host: The device running forge as the "server."
|
||||
* Client: The device connecting to the Forge server.
|
||||
* **A Network**;
|
||||
* Local: Wi-Fi in the same home, "Wi-Fi Direct" between two devices, or Ethernet for PC
|
||||
* Remote: IPv4 Internet (Home Wi-Fi Router to the internet, Ethernet to Router to internet)
|
||||
* **Firewall Exclusions and Port Forwards**;
|
||||
* NOTE: Don't DISABLE your firewall, use exclusions to allow forge app or the port specifically.
|
||||
* By default Forge (when running as a server) will listen on port **36743**.
|
||||
* For clients to be able to connect to the server, this port will need to be allowed through the server host's local firewall (Windows Defender Firewall, Ubuntu/Linux UFW, etc.).
|
||||
* Additionally, if the client is "remote" or outside your local network, the above port will need to be forwarded through the *host's router's firewall* (see below) **and** through the *host's local firewall*.
|
||||
|
||||
***
|
||||
|
||||
### Quick Start:
|
||||
|
||||
* Start Forge on both devices; please confirm versions numbers are the same before continuing.
|
||||
* Mobile players: Choose "Classic Mode"
|
||||
* Go to
|
||||
* Mobile: "Play Online"
|
||||
* Desktop: "Online Multiplayer" > "Lobby" > Click "Connect to Server"
|
||||
* Decide who will "host" the game.
|
||||
* That person must **not** put anything in the "Connect to Server" popup box, and click OK.
|
||||
* Forge will attempt to show the IP Address of the Host's machine verify before providing to the other player:
|
||||
* **For local play,** the IP address would probably start with 192.168... verify with your device's network settings. ```Forge may recommend to use the word "localhost", ignore this. I suspect this is a java issue with naming the network address.```
|
||||
* **For remote play,** the IP address should be verified with https://canyouseeme.org/
|
||||
* The person **not** hosting is a "client" and MUST to put the IP Address determined from the above step into the "Connect to Server" popup.
|
||||
* You can then start a network match;
|
||||
* The host can select the type of match, team configuration, number of matches per game, and other settings.
|
||||
* All Players can select their decks, their sleeves (if mobile), and avatar.
|
||||
* Toggle the ready switch to signal you're ready.
|
||||
* When all players are ready to start, the host can start the match.
|
||||
|
||||
***
|
||||
|
||||
### Troubleshooting:
|
||||
Folks in discord are there to help you get your game going!
|
||||
#### Current Network-based Multiplayer Known Issues
|
||||
|
||||
* Some points where the game is waiting on an opponent's decision/action do not properly indicate this. #158
|
||||
* On mobile, if you are in the same room without Wi-Fi access, try "personal hotspot"/"WiFi-Direct" options first, you don't need a port forward that way.
|
||||
* If you are on the same Private Wi-Fi, you do not need to do this.
|
||||
* If you are on a Public Wi-Fi or Hotspot, you will probably not be able to communicate, using a "personal hotspot" would be a better option.
|
||||
* Due to the lack of traffic optimizations, playing over network feels choppy/laggy. A single game can transfer hundreds of megabytes between all clients! _Slow networks will be slow._
|
||||
* If your ISP uses IPv6 but uses something called Dual Stack lite, network play won't work. Network play **requires IPv4 addressing** or IPv6 using (full) Dual Stack!
|
||||
|
||||
#### "Disconnected From Lobby"
|
||||
* A common cause for this is that the client and server resource (**res**) folder content differs. This can be verified by checking the game log and looking for an IOException referring to a "Card ... not found". #175
|
||||
|
||||
***
|
||||
|
||||
## Network Configuration - Setup and Testing
|
||||
_Please consider reading "Hosting a Server" section for the concepts behind the setup and testing, and terminology._
|
||||
|
||||
### Local Network:
|
||||
|
||||
#### **Host Based Firewall**
|
||||
|
||||
* Mobile Device: There shouldn't be much you need to do, start Forge, host a server. Anyone else on the same local network should be able to connect.
|
||||
* Desktop Device: Depending on your OS, you may need to allow Forge through the host based firewall. Windows Defender Firewall, Ubuntu would have UFW.
|
||||
* Either allow the app itself.
|
||||
* or the default port of 36743.
|
||||
|
||||
#### **Validate**
|
||||
|
||||
* Android Device: You can test from your mobile device with PortDroid, to scan if the port is open:
|
||||
* Setup
|
||||
* Install [**PortDroid**](https://play.google.com/store/apps/details?id=com.stealthcopter.portdroid)
|
||||
* Open **PortDroid**
|
||||
* From the menu in the upper left, select *Port Scanner*
|
||||
* Select the three dot menu in the upper right.
|
||||
* Select *Port Scanner Settings*
|
||||
* At the bottom under *Port Lists*, select *Add New Scan*
|
||||
* Enter *Forge* for the *Scan Title*
|
||||
* Enter *36743* for the ports to scan.
|
||||
* Select *ADD SCAN*
|
||||
* Test
|
||||
* From the menu in the upper left, select *Port Scanner*
|
||||
* In the first field on the left, enter the internal IP of the host system.
|
||||
* From the middle drop down, select *Forge*
|
||||
* Select *SCAN*
|
||||
* If all goes well, you'll receive an indication that 1 port is open.
|
||||
|
||||
* Windows Device: You can run telnet to test if the port is open.
|
||||
* Setup
|
||||
* Install Telnet: As telnet is no longer available in Windows by default, [please follow the instructions found here](https://social.technet.microsoft.com/wiki/contents/articles/38433.windows-10-enabling-telnet-client.aspx) before continuing.
|
||||
* Test
|
||||
* Open a **command prompt**
|
||||
* Type: *telnet INTERNALIP 36743* (where INTERNALIP is your forge host's internal IP address)
|
||||
* Press *Enter*
|
||||
* If all goes well, your command prompt title bar will change to *Telnet* followed by the host system's IP address. At this point you can close the window.
|
||||
* If it doesn't you'll see something like the following: ```Could not open connection to the host, on port 36743: Connect failed```
|
||||
|
||||
* Mac OSX Device:
|
||||
* I'll quote Apple "Telnet is insecure and you should move to more secure communications methods, like HTTPS."
|
||||
* You can't be trusted to use this on your own.
|
||||
* Maybe check the app store for a "port scanner" follow their instructions for scanning for port "36743" on "localhost".
|
||||
|
||||
* Linux Device: You probably don't need our help. Install telnet from your package distro and run telnet similar to Windows.
|
||||
|
||||
If you've passed the validation, then your configuration should be good and you can provide your partner the IP address of your device. Please confirm your IP address with your devices' network configuration settings.
|
||||
|
||||
### Remote Network:
|
||||
|
||||
A Remote Network setup **MUST** pass the local network setup above.
|
||||
|
||||
#### **Port Forwarding or "NAT"**
|
||||
|
||||
_Each router is different, so instead of specific step by step instructions here, this is general theory._
|
||||
|
||||
* Setup
|
||||
* You'll need to find the admin interface for your router. On most consumer models, this is a web page that can be accessed at `http://_._._.1` where the first three parts of the address are the same as your system's internal IP address (as reported by Forge). For example, if your internal IP address is 192.168.0.54, the admin page would likely be at: `http://192.168.0.1/`.
|
||||
* Once inside the interface, you'll need to look for "port forwarding". This is sometimes buried under different menu options, like "advanced". Once located, you'll want to add a new port forward. The interface will likely ask for several bits of information:
|
||||
* remote or external IP
|
||||
* external port
|
||||
* internal host or IP
|
||||
* internal port
|
||||
* Both port values should be set to **36743**.
|
||||
* The remote or external IP should be left **blank** or set to **any**.
|
||||
* The internal host or IP would be the machine running Forge.
|
||||
|
||||
Help for your specific router may be here: https://portforward.com/router.htm
|
||||
*Don't download software you don't know what it does, that website's software is NOT recommended to be used, just use the guides they provide.*
|
||||
|
||||
* Testing
|
||||
* For the external testing use a site like: [CanYouSeeMe.org](http://canyouseeme.org)
|
||||
* Your external IP should already be detected by the site.
|
||||
* Simply enter **36743** for the port to check and click *Check Port*.
|
||||
* If all goes well, you'll get a *Success* message back.
|
||||
|
||||
If you've passed both of the local and remote testing, then your configuration should be good and you can provide your partner the IP address of your network. Please confirm the IP address with [CanYouSeeMe.org](http://canyouseeme.org).
|
||||
|
||||
***
|
||||
|
||||
## Network Configuration - Hosting a Server
|
||||
|
||||
Welcome to hosting a server, this is a more descriptive view as to what's going on when you host a server. These concepts can be applied to most server set ups, but we will focus on Forge in three different configurations:
|
||||
|
||||
* **Local Networks (Over your local network)**
|
||||
* Local Private Wi-Fi, "Wi-Fi Direct", or "Hotspot" from a mobile device, also wired networks for PC players.
|
||||
* **Remote Networks (Over the Internet)**
|
||||
* Networks you don't control. (Cell to Cell, or Public Hotspots.)
|
||||
* Network you do control. (Home internet.)
|
||||
|
||||
#### Local Private Network:
|
||||
|
||||
A local network's IP addresses are typically self managed by your router, with what is called the DHCP server. These addresses are assigned to your devices automatically and are what you would use to connect to each other device. These devices will talk to each other over the Wi-Fi Access Point (which is typically your router.) In a wired environment they will talk across the wire through a switch (also typically your router.)
|
||||
|
||||
When you do a "Wi-Fi direct" in Android, this allows one device to be the Wi-Fi access point, other devices can connect to this network as a local private network.
|
||||
|
||||
#### Remote Network:
|
||||
|
||||
Remote networks are two networks separated by the internet, or by what is called VLANs. On public hotspots, two devices on that "same Wi-Fi" will be separated by these VLANs, and can not talk to each other directly. Similarly, two cell phones on the same provider will be separated by VLANs, or just by given an IPv6 address.
|
||||
|
||||
If you do not control your internet connection, like a public hotspot or a cell provided internet you'll need to do a software defined network (SDN), or a virtual private network (VPN) which is outside the scope of this wiki and any discord help. It is recommended to connect via "Wi-Fi Direct" if you are near each other instead.
|
||||
|
||||
If you control your internet connection's network, such as it's your home network and can access the router configuration, you can host a game by opening the ports on your network, and allowing the game through the router's firewall. This allows the remote player, on practically any other internet connected network to connect through your router to your server.
|
||||
|
||||
### Firewalls:
|
||||
|
||||
Firewalls are kind of archaic, but are still implemented, and are designed to block your connection intentionally.
|
||||
|
||||
A quick history; Computer OS's used to respond with a port being "closed" if no service was available on it, and "open" if a connection was able to be opened. An OS should not report a port as "closed", just ignore the request. (Reporting a port as "closed" is like hearing a knock on the door, opening the door and slamming it shut.) This wasn't a security concern at the time, so attackers were able to identify if a computer was "alive" if it responded with a "closed" port, and would moved on if it didn't get a response at all. Any response from a computer triggered a deeper attack to find which ports were actually "open." Thus firewalls were born...
|
||||
|
||||
Firewalls were initially implemented to prevent open ports from talking to just any random device, limiting access to only approved connection requests and limiting attackers from gaining access, however ports were still reporting as "closed" in the early days. Firewalls then implemented a function to prevent ports without services on them from reporting "closed" at all and just ignored the traffic instead, making it appear as there was no computer at that address.
|
||||
|
||||
In devices with firewalls you need to allow a port for a service through the Host Based Firewall. You'll also need to do this on your router for remote games as well for the Network Based Firewall. I believe, most Linux devices (including Android) no longer report "closed" for ports without services, and firewalls are typically not needed as the only ports that should be open are ones with services needing ports open. In Windows this is not the case, and is why Windows Defender Firewall is implemented and ports will need to be opened to allow Forge to accept a connection. You should however be able to connect to a mobile device without concerns of a firewall blocking you.
|
||||
|
||||
### Port Forwarding:
|
||||
|
||||
In routers, just after the firewall is a routing function called Network Address Translation. "NAT" allows a computer behind the router (from the internet perspective) to provide its service as if it is the router itself. In most routers, when you enable a NAT/Port Forward, it will also allow the Port through the Firewall.
|
||||
|
||||
```[Device](Internet) ---> 36743{Router} ---> 36743[Host]```
|
||||
|
||||
Once this initial communication is started, the two devices will talk over their designated and determined ports and have a TCP "conversation" (called a stream) until the connection is completed or closed. Once the TCP stream is closed, another device can connect.
|
||||
|
||||
If you've used PlayStation systems you might recognize the term "NAT"; level 1, 2, or 3. This naming is not part of the standard, and is bad and confusing; (I believe) "Level 1" meant no NAT'ing all traffic was redirected through PlayStation servers, "Level 2" meant Half Open or Static NAT (which is what we will do for Forge), and "Level 3" mean the use of UPnP/DLNA which was automatic NAT services and allows ALL software to control your firewall for you. (UPnP should be concerning from a security perspective.)
|
||||
|
||||
### Network Topology:
|
||||
|
||||
Here's how your typical network topology and network flow will look, when connecting and playing with Forge.
|
||||
|
||||
* Typical Wi-Fi Home Network with 2 devices
|
||||
* A Figure of Local Game Connection through Wi-Fi with PC Host.
|
||||

|
||||
* Mobile/Android would be similar, but without the firewall on the host. (Personally, I believe this may change because of how stupid android "security" is becoming, and you'll have a firewall you can't tune, or make exceptions.)
|
||||
|
||||
* Typical Remote Network Connection with 2 devices
|
||||
* A Figure of Remote Game Connection through Firewall and Port Forward on Router.
|
||||

|
||||
|
||||
83
docs/Networking-Extras.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Disclaimer
|
||||
|
||||
This page should be considered "experimental" and **at your own risk**.
|
||||
|
||||
Forge, it's developers, and supporters won't be able to help you with this. I'll try to provide some details about setting up the software and networks listed here, but please direct any support to the platform or provider's support and documentation. This should mostly be just a list of options of how to set up a remote game without access to port forwards on your network.
|
||||
|
||||
_Please don't use this guide to get around security of your work/school network on a work/school computer, only use it for accessible networks like hotspots or the like, with your personal devices._
|
||||
|
||||
_Attempting to subvert security measures may be considered a crime, in the least you'll get fired or expelled, I like you I don't want that on you._
|
||||
|
||||
# Networks
|
||||
|
||||
So you found out you don't have control over your network to open ports for a remote game connection, what can you do? **Well, hopefully this guide will get you on the right track.**
|
||||
|
||||
Some scenarios where this may be;
|
||||
+ you're on a cell data connection
|
||||
+ you're on a public Wi-Fi Hotspot
|
||||
+ you're at school or work
|
||||
+ dad is kind of a tightwad and doesn't want games to have open ports on the router.
|
||||
+ sorry, my dad was actually cool about this, and let me manage the network router.
|
||||
|
||||
These connections don't allow you to forward ports to your device's IP address, and that's a problem with Forge for remote connections. You'll need to be able to communicate with a server or service outside of the network first to set up a line of communication, which then allows you to make direct connections to another device. The most common concept is a Virtual Private Network or VPN, allowing a virtual network to be instantiated, and connected to. Additionally newer concepts such as Software Defined Networks can provide similar functions, without the layers of security/encryption a VPN would.
|
||||
|
||||
## Virtual Private Network Software
|
||||
|
||||
VPNs were not designed to subvert security, but to provide a secure communication path on less secure networks. Allowing enterprise or work computers the ability to remotely connect to the work network.
|
||||
|
||||
Today, most VPN service providers provide a secure channel to the internet for "completely legitimate" reasons. However, going back to the original intent you can stand up a virtual private network that you and friends can access using VPN Software.
|
||||
|
||||
**Don't confuse a VPN Provider (PIA, Nord, Etc.) as a VPN Software or Service you run (OpenVPN).**
|
||||
|
||||
### Software:
|
||||
If you know of other VPN software (NOT SERVICE PROVIDERS), please expand this list.
|
||||
|
||||
+ OpenVPN
|
||||
- VPN Software you can run on a router or a server.
|
||||
+ IPSec Tunnel (?)
|
||||
- A part of VPN that you typically run on two routers?
|
||||
|
||||
|
||||
### Notes:
|
||||
|
||||
VPN Software will still require control of **a network**, a place to install the VPN, and have remote access to it. So this is where complexity is introduced, you'll either need to set up the VPN on a home network you can control, or on a server that you can rent from a provider with a public IP address you can access. Once this is done, both players can install the VPN software on their devices, and connect to that server.
|
||||
|
||||
Once you've connected to a VPN Server, you can return to the Network Play guide and test your connections as a "Local Private Network".
|
||||
|
||||
## Virtual Private Network Service Providers
|
||||
|
||||
Some VPN Service Providers provide an actual VPN service, and not just a pipe for "completely legitimate internet access" purposes. Since these are rarer please only put these types of VPN providers here, **everyone can google** the others.
|
||||
|
||||
### Providers:
|
||||
|
||||
+ [RadminVPN](radmin-vpn.com)
|
||||
- A free VPN provider for actual Private Networking Access.
|
||||
+ [Hamachi by LogMeIn](https://www.vpn.net/)
|
||||
- Free for up to 5 computers.
|
||||
|
||||
### Note:
|
||||
Some "completely legitimate internet access" VPN Service providers may allow you to have port forwards through their networks. Check with the provider before signing up, however both players might need the same provider but might not. You can search google for those VPN providers, **we don't need to list them here.**
|
||||
|
||||
|
||||
## Software Defined Networks
|
||||
|
||||
A software defined network is similar to a VPN, but without the encryption and stuff that can slow and prevent a connection out. A Software Defined Network by a provider means you don't need to have your own server, they provide the remote access. Therefore you only need to configure the basic network settings like the IP Schema, and then allow access.
|
||||
|
||||
Once you've configured the "virtual switch", users can connect with the provider's software, you can accept their access, then assign an IP address, and connect to each other.
|
||||
|
||||
### Providers:
|
||||
Please expand this list if you know of others!
|
||||
|
||||
+ [ZeroTier One](https://www.zerotier.com/download/) - It's free for personal use.
|
||||
|
||||
### Notes:
|
||||
|
||||
Configuring a network can get complex, however a simple network should already be configured by the provider. You should be able to follow the instructions from the provider to stand up your first network, provide your friend the link to the software, the access code to the network, and accept their access request.
|
||||
|
||||
Once you've done this, you can return to the Network Play guide and test your connections as a "Local Private Network".
|
||||
|
||||
## Security
|
||||
|
||||
As a general note, if you use a provider for network traffic including a VPN provider; They have access to **everything** you do on their network! They may not see the data if you use HTTPS for encryption, but they can see where you go, and can collect that meta data.
|
||||
|
||||
Secondly, being on a private network (VPN or SDN) with anyone else does put your device at risk, especially if your device has security vulnerabilities that aren't patched. It is recommended to only provide access to people you TRUST on these private networks you stand up, and disconnect when you're not using them. **Don't create a network and provide access to hundreds of people and stay connected, you're ONLY asking for trouble.**
|
||||
@@ -0,0 +1,23 @@
|
||||
**[WARNING!!!]**
|
||||
|
||||
Page imported from the old SlightlyMagic wiki. To be integrated into other wiki pages and/or README... or deleted.
|
||||
|
||||
---
|
||||
|
||||
## Duplicate decks checking tool
|
||||
|
||||
This python script will check through your deck files and identify which
|
||||
ones are similar to each other.
|
||||
|
||||
It has been tested with Python 2.6.
|
||||
|
||||
The source can be found on [it's bitbucket
|
||||
page](https://bitbucket.org/asret/forge/src/tip/deckdupcheck.py), or
|
||||
[downloaded
|
||||
directly](https://bitbucket.org/asret/forge/raw/tip/deckdupcheck.py).
|
||||
|
||||
To run it, download it to your forge's "res" directory and invoke the
|
||||
python interpretor on it.
|
||||
|
||||
It still needs work. At present it checks all decks against every other.
|
||||
This means it will output each matching pair twice - once for each deck.
|
||||
125
docs/Retired/(SM-autoconverted)-maven-build-system.md
Normal file
@@ -0,0 +1,125 @@
|
||||
**[WARNING!!!]**
|
||||
|
||||
Page imported from the old SlightlyMagic wiki. To be integrated into other wiki pages and/or README... or deleted.
|
||||
|
||||
---
|
||||
|
||||
**WORK IN PROGRESS**
|
||||
|
||||
### Install Maven
|
||||
|
||||
<http://maven.apache.org/download.html#Installation>.
|
||||
|
||||
To test your installation you should execute the following command:
|
||||
|
||||
`mvn --version`
|
||||
|
||||
You should see something like like this:
|
||||
|
||||
`Apache Maven 3.0.3 (r1075438; 2011-02-28 09:31:09-0800)`
|
||||
`Maven home: /opt/local/share/java/maven3`
|
||||
`Java version: 1.6.0_24, vendor: Apple Inc.`
|
||||
`Java home: /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home`
|
||||
`Default locale: en_US, platform encoding: MacRoman`
|
||||
`OS name: "mac os x", version: "10.6.7", arch: "x86_64", family: "mac"`
|
||||
|
||||
#### Mac OSX
|
||||
|
||||
Maven should already be installed. To test installation, open the
|
||||
Terminal application run the test command above.
|
||||
|
||||
#### Windows
|
||||
|
||||
1. Use the above link to download the zip file.
|
||||
2. Unzip the file into a directory.
|
||||
3. Add the directory to your "PATH" variable.
|
||||
4. Open a command window
|
||||
5. Execute the test command above.
|
||||
|
||||
#### Linux
|
||||
|
||||
1. Open your package manager e.g. Synaptic on Debian
|
||||
2. Install the Maven2 package
|
||||
3. Open a Terminal
|
||||
4. Execute the test command above.
|
||||
|
||||
### Install SVN
|
||||
|
||||
<http://subversion.tigris.org/>
|
||||
|
||||
To test your installation you should execute the following command:
|
||||
|
||||
`svn --version`
|
||||
|
||||
You should see something like like this:
|
||||
|
||||
`svn, version 1.6.17 (r1128011)`
|
||||
`compiled Jun 2 2011, 09:40:34`
|
||||
|
||||
#### Mac OSX
|
||||
|
||||
Maven should already be installed. To test installation, open the
|
||||
Terminal application run the test command above.
|
||||
|
||||
#### Windows
|
||||
|
||||
1. Use the above link to download the zip file.
|
||||
2. Unzip the file into a directory.
|
||||
3. Add the directory to your "PATH" variable.
|
||||
4. Open a command window
|
||||
5. Execute the test command above.
|
||||
|
||||
#### Linux
|
||||
|
||||
1. Open your package manager e.g. Synaptic on Debian
|
||||
2. Install the Subversion package
|
||||
3. Open a Terminal
|
||||
4. Execute the test command above.
|
||||
|
||||
### Build Forge
|
||||
|
||||
From a terminal window, go to the directory where forge was checked out
|
||||
via GIT. Update to the latest version of the code
|
||||
|
||||
`mvn scm:update`
|
||||
|
||||
Use this command to perform a simple build of the jar file
|
||||
|
||||
`mvn -U -B clean install`
|
||||
|
||||
Use this command to do a snapshot package build
|
||||
|
||||
`mvn -U -B clean -P osx,windows-linux install`
|
||||
|
||||
Use this command to do a snapshot package build of the Windows/Linux
|
||||
package only
|
||||
|
||||
`mvn -U -B clean -P windows-linux install`
|
||||
|
||||
Use this command to do a snapshot package build of the Mac OSX package
|
||||
only
|
||||
|
||||
`mvn -U -B clean -P osx install`
|
||||
|
||||
Use this command to do a snapshot package build and site deployment
|
||||
|
||||
`mvn -U -B clean -P osx,windows-linux install site deploy site:deploy`
|
||||
|
||||
Use this command to do full package build and upload to GoogleCode
|
||||
|
||||
`mvn -U -B clean -P osx,windows-linux install site release:clean release:prepare release:perform -Dusername="`<user>`" -Dpassword="`<password>`"`
|
||||
|
||||
where <user> and <password> are your GoogleCode credentials (typically
|
||||
something like "you@gmail.com" "w4e4sdg")
|
||||
|
||||
### Build System Utilities
|
||||
|
||||
These utilities are used in the build process. They are automatically
|
||||
included in the build. The links are for reference only.
|
||||
|
||||
[Google Upload](http://code.google.com/p/maven-gcu-plugin/wiki/Usage)
|
||||
|
||||
[Jar Bundler](http://www.informagen.com/JarBundler/)
|
||||
|
||||
[Create DMG Script](http://www.yoursway.com/free/#createdmg)
|
||||
|
||||
73
docs/Retired/cardscripting.md
Normal file
@@ -0,0 +1,73 @@
|
||||
What follows is a rough start of an API document for cardscripts.
|
||||
|
||||
| Property | Description
|
||||
| - | -
|
||||
|`A`|Ability
|
||||
|`Colors`|Color(s) of the card<br /><br />When a card's color is determined by a color indicator rather than shards in a mana cost, this property must be defined. If no identifier is needed, this property should be omitted.<br /><br />*`Colors:red` - This is used on Kobolds of Kher Keep, which has a casting cost of {0} and requires a red indicator to make it red.<br /><br />*`Colors:red,green` - Since Arlinn, Embraced by the Moon has no casting cost (it's the back of a double-faced card), the red and green indicator must be included.
|
||||
|`DeckHints`|AI-related hints for a deck including this card
|
||||
|`K`|Keyword
|
||||
|`Loyalty`|Number of starting loyalty counters
|
||||
|`ManaCost`|Cost to cast the card shown in mana shards<br /><br />This property is required. It has a single parameter that is a mana cost.<br /><br />* `ManaCost:no cost` for cards that cannot be cast<br />* `ManaCost:1 W W` sets the casting cost to {1}{W}{W}
|
||||
|`Name`|Name of the card<br /><br />A string of text that serves as the name of the card. Note that the registered trademark symbol cannot be included, and this property must have at least one character.<br /><br />Example:<br />* `Name:A Display of My Dark Power` sets the card's name to "A Display of My Dark Power"
|
||||
|`Oracle`|Oracle text
|
||||
|`PT`|Power and toughness
|
||||
|`R`|Replacement effect
|
||||
|`S`|Static ability
|
||||
|`SVar`|String variable. Used throughout scripting in a handful of different ways.
|
||||
|`T`|Triggered ability
|
||||
|`Text`|Text on card
|
||||
|`Types`|Card types and subtypes<br /><br />Include all card types and subtypes, separated by spaces.<br /><br />Example:<br />* `Types:Enchantment Artifact Creature Golem` for a card that reads Enchantment Artifact Creature -- Golem
|
||||
|
||||
** Parameters for abilities and variables and known accepted values **
|
||||
|
||||
(incomplete list):
|
||||
|
||||
* `AB$`: Ability
|
||||
* `Ability$`: Ability
|
||||
* `ActiveZones$`: Zone
|
||||
* `Affected$`: Card
|
||||
* `AffectedZone$`: Zone
|
||||
* `ChangeNum$`: Integer
|
||||
* `ChangeValid$`: CardType
|
||||
* `ColorOrType$`: `Type`
|
||||
* `Cost$`: Cost
|
||||
* `Count$`: `xPaid`
|
||||
* `DB$`: DB
|
||||
* `Defined$`: Player, Card
|
||||
* `Description$`: Text
|
||||
* `Destination$`: Zone
|
||||
* `DestinationZone$`: Zone
|
||||
* `DigNum$`: Integer
|
||||
* `Duration$`: UntilYourNextTurn
|
||||
* `Event$`: Event
|
||||
* `Execute$`: DB
|
||||
* `Hidden$`: boolean
|
||||
* `KW$`: Keyword
|
||||
* `LifeAmount$`: Integer
|
||||
* `MayPlay$: boolean
|
||||
* `Mode$`: Mode
|
||||
* `Name$`: Text
|
||||
* `NotCause$`: Ability
|
||||
* `NumAtt$`: `+1`
|
||||
* `NumCards$`: Integer
|
||||
* `Optional$`: boolean
|
||||
* `Origin$`: Zone
|
||||
* `Produced$`: ManaType
|
||||
* `References$`: SVar
|
||||
* `ReflectProperty$`: Property
|
||||
* `ReplaceWith$`: Text
|
||||
* `SP$`: Spell
|
||||
* `SpellDescription$`: Text
|
||||
* `StackDescription$`: Text
|
||||
* `Static$`: boolean
|
||||
* `SVars$`: SVar
|
||||
* `TargetMax$`: Integer
|
||||
* `TargetMin$`: Integer
|
||||
* `TargetPrompt$`: Text
|
||||
* `TriggerDescription$`: Text
|
||||
* `TriggeredCard$`: Property
|
||||
* `Triggers$`: Mode
|
||||
* `TriggerZones$`: Zone
|
||||
* `Valid$`: `Triggered`
|
||||
* `ValidCard$`: Card
|
||||
* `ValidTgts$`: Player, CardType
|
||||
5
docs/Retired/images.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Forge can work with card images of your choice.
|
||||
|
||||
The naming convention is as follows:
|
||||
|
||||
If you have an older Android device for increased performance or to save bandwidth it might be a good idea to use lower resolution images instead: https://www.slightlymagic.net/forum/viewtopic.php?f=15&t=29104
|
||||
46
docs/Snapshots-and-Releases.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## Snapshots
|
||||
|
||||
Currently (as of Dec 2023), Desktop and Android snapshots have been automated via GithubActions.
|
||||
|
||||
You can see the workflows here.
|
||||
[](https://github.com/Card-Forge/forge/actions/workflows/snapshot-both-pc-android.yml)
|
||||
These run daily at around 2pm ET time for people looking for them. The upload scripts should take care of removing the previous files uploaded on the last run. If necessary, you can run these scripts manually. Occasionally these builds fail to upload due to unclear reasons. Retrying the snapshot usually seems to fix the issue. (As of 2/23/2025, the tag is tagging an old SHA. We'll try to resolve that, but know that the snapshot file time represents which code is utilized.)
|
||||
|
||||
The latest daily snapshots can be found [here](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots)
|
||||
|
||||
## Releases
|
||||
|
||||
Currently (as of Dec 2023), Desktop releases run an automated script with a few manual pieces.
|
||||
|
||||
Currently (as of Dec 4, 2023), Android releases are not run. Although, hopefully with the Android snapshot now automated we'll be able to get back to getting a full release for Android as well.
|
||||
|
||||
### Desktop Release Process
|
||||
|
||||
It would be useful for other people to be comfortable with the release process. Currently, GitHub has the secrets required to add a snapshot or full release, so it's mostly just running through the steps below to kick off and validate a release.
|
||||
|
||||
1. Run Build
|
||||
* Verify test suite is passing on the repo
|
||||
[](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml)
|
||||
* Create a new branch from the default branch
|
||||
* Run the https://github.com/Card-Forge/forge/actions/workflows/maven-publish.yml workflow on the new branch
|
||||
2. Verify
|
||||
* After it completes, download the finished package. Extract it and do a quick verification that it works.
|
||||
3. For snapshot updates, a few files currently need manual updates. Check out this PR for an example: https://github.com/Card-Forge/forge/pull/4293/files
|
||||
(Hopefully in the next few iterations, we'll have this automated during the release)
|
||||
* forge-gui-desktop/pom.xml
|
||||
* fromRef
|
||||
* forge-gui-android/AndroidManifest.xml
|
||||
* android::versionCode
|
||||
* android::versionName
|
||||
* forge-gui-android/pom.xml
|
||||
* alpha-version
|
||||
* forge-gui-mobile/src/forge/Forge.java
|
||||
* CURRENT_VERSION
|
||||
4. Merge
|
||||
* Create a PR from your branch and get it merged as quickly as you can (ideally before other PRs are merged).
|
||||
* Create a new release from https://github.com/Card-Forge/forge/releases
|
||||
* Upload the package and its sha to the create new release page
|
||||
|
||||
|
||||
4. Marketing
|
||||
* Advertise in the #announcements channel in the Discord
|
||||
69
docs/Steam-Deck-and-Bazzite-Install.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# SteamDeck and Bazzite Support
|
||||
|
||||
_This instruction was written using Bazzite, however should be similar enough for SteamOS._
|
||||
|
||||
In order to support the SteamDeck "natively" for full Forge Desktop mode, we would likely need to have a flatpack installer for the best user install experience, currently Forge has no intention to have a flatpack. The current **best** and recommended way to have Forge on your SteamDeck is to install and run the Android APK version in Waydroid Android Container.
|
||||
|
||||
* You will need to have installed Waydroid first, this reddit post may work for you; https://www.reddit.com/r/SteamDeck/comments/1ay7ev8/how_to_install_waydroid_android_on_your_steam_deck/
|
||||
|
||||
## Installing Forge Android in Waydroid (Recommended Method)
|
||||
Once you've installed Waydroid, you can follow the same steps you would in any Android device.
|
||||
|
||||
1. Open browser to https://github.com/Card-Forge/forge
|
||||
|
||||
1. Open Snapshots from the Link in the ReadMe Page
|
||||
|
||||
1. Download the Snapshot APK
|
||||
|
||||
1. Open the APK from the download notification in your Android OS.
|
||||
|
||||
1. Give permissions to allow the browser to run APK installers.
|
||||
|
||||
1. Once installed, Forge should request image storage and audio permissions.
|
||||
|
||||
1. Forge sends you to the Settings Page to provide permissions.
|
||||
|
||||
1. Run Forge from the Application Drawer (likely Forge is still running)
|
||||
|
||||
1. You can tap anywhere else on the screen and Forge will restart.
|
||||
|
||||
1. Once Forge restarts it will ask to update the Assets.
|
||||
|
||||
1. Congratulations you can play Forge on your SteamDeck.
|
||||
|
||||
### Running Forge Android in Steam GameMode
|
||||
|
||||
My understanding is that Waydroid running detached Android apps doesn't work at the moment, so the best way to run Forge is to start Waydroid itself from Steam Gamemode and then start the Forge App. To do this you will need to add Waydroid to Steam as a non-Steam Game, controller support should be natively supported this way, you should also enable touch screen pass-through in the Steam Controller configuration if you didn't already during install of Waydroid.
|
||||
|
||||
1. Add Waydroid as Non-steam game.
|
||||
|
||||
1. Configure/Enable controller support with touchscreen.
|
||||
|
||||
1. Start Waydroid from Steam GameMode.
|
||||
|
||||
1. Let the Waydroid Android OS boot.
|
||||
|
||||
1. Start Forge
|
||||
|
||||
## Forge Desktop (JAR or BZ2 File)
|
||||
|
||||
### Installing Forge Desktop Natively (Not Recommended)
|
||||
|
||||
Barring a flatpack (and packing Java with Forge), the correct way to install Forge Desktop natively (installer JAR or BZ2 archive) would be to install Java OpenJDK in the OS globally. **This is against Steam and Bazzite Dev recommendations**, however is doable;
|
||||
|
||||
* For Bazzite: simply use `rpm-ostree install java-21-openjdk`
|
||||
|
||||
> You can then install forge as you would in any Linux OS, following the [user guide](user-guide).
|
||||
> Again, this is against Bazzite devs recommendations, and they will likely belittle you and tell you you can't ask for support, if you tell them you did this like a normal Linux user would.
|
||||
|
||||
* For SteamOS: I believe you need to unlock the OS, install java from the package repo, then lock the OS again.
|
||||
|
||||
> This Wiki will not provide instructions for this, if you feel you can do this you can probably look up guides to help you.
|
||||
|
||||
### Installing Forge in a Container/VM (Boxes or Other)
|
||||
|
||||
Another option is to install Forge in a Linux Container or VM, where you can download and install Java JDK into the container, also download and install Forge into the container then run Forge. This is not recommended as it can be confusing to a novice Linux user, adds system overhead, likely UI problems, and would be difficult to perform on a handheld device.
|
||||
|
||||
> This Wiki will not provide instructions for this, if you feel you can do this you can probably look up guides to help you.
|
||||
|
||||
|
||||
3
docs/Themes.md
Normal file
@@ -0,0 +1,3 @@
|
||||
This page is a placeholder for Theme creation information.
|
||||
|
||||
I'm working on creating an atlas data file for the skin sprite sheets. I'll separate this into 3 sections; Skins, Music and Sounds, and Card Images.
|
||||
101
docs/Towns-&-Capitals.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Towns & Capitals
|
||||
|
||||
While exploring the world of Shandalar, you will find multiple cities across all biomes. These settlements are a integral part of the game, and can help the player by giving quests, hosting draft/jumpstart events, as well as selling cards and booster packs.
|
||||
|
||||
## Towns
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Different towns have different buildings, but some of them are common to them. Buildings are identified by the sign right in front of them.
|
||||
|
||||
- The Inn
|
||||
|
||||

|
||||
|
||||
At the inn, players can buy temporary healthpoints, sell cards and join events. Different inns host different events. For example, a town can be hosting a draft for "Urza's Saga Block", while the next one can be hosting a Jumpstart 2025 event.
|
||||
|
||||
- Job Board
|
||||
|
||||

|
||||
|
||||
Going to the job board the player can accept quests, which will reward them with gold and sometimes local reputation. Local reputation reduces the prices of cards sold by shops.
|
||||
|
||||
- Shard Trader
|
||||
|
||||

|
||||
|
||||
The shard trader allows player to trade gold for shards and shards for golds. Shards are used to reroll cards at the shop, as well as joining events.
|
||||
|
||||
- Card Shops
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
Card shops sells cards according to the biome and the shop itself. For example, shops in a green city will tend to sell green cards, while shops in a black city will focus on black cards.
|
||||
Different shops sells different types of cards, and most of them can be deduced from their name. For example, the shop called "Control Magic" will sell blue cards used to control opponents (counter, tap, untap, etc), while a shop called "Swords, Plowshares, and Beyond" will sell cards that exile permanents. The shop information can be a found in [this user-friendly JSON](https://github.com/Card-Forge/forge/blob/master/forge-gui/res/adventure/Shandalar/world/shops.json).
|
||||
|
||||
Shops with an Hour Glass sign change their stock daily. All other shops restock only using shards to reroll.
|
||||
|
||||
Shop contents based on sign:
|
||||
|
||||
|
||||
<img width="360" alt="shop-icons" src="https://github.com/user-attachments/assets/20144079-c8c2-407e-b523-6e995e75d78c" />
|
||||
|
||||
| |A |B |C |D |E |F |G
|
||||
|---------------|---------------|---------------|---------------|---------------|---------------|---------------|---------------|
|
||||
|1|-|Instants|Creatures|Green|Red|White|Pirates
|
||||
|2|-|Basic Lands|Colorless|Black|Blue|Gold|-
|
||||
|3|-|Selesnya G/W|Azorius U/W|Dimir B/U|Rakdos B/R|Gruul G/R|-
|
||||
|4|-|Simic G/U|Orzhov B/W|Izzet R/U|Golgari B/G| Boros R/W|-
|
||||
|5|-|Goblins|Zombies|Humans|Elves|Merfolk|-
|
||||
|6|-|Item Shop|Artifacts|Angels|Golems|Slivers|Assassins
|
||||
|7|-|Mardu B/R/W|Jeskai R/W/U|Naya R/G/W|Enchantments|5 Color|Squirrels
|
||||
|8|Rogues|Non-Basic Land|Space Marines|Necrons|Chaos|Tyranids|Dragons
|
||||
|9|Assembly Workers/Constructs/Myr|Vampires|Vehicles|Grixis R/U/B|Jund R/G/B|Temur R/G/U|Minotaurs
|
||||
|10|Dinosaurs|Esper U/W/B|Sultai U/G/B|Bant B/W/G|Abzan G/W/B|Dwarfs|Devils
|
||||
|11|Ogres|Item Shop|Equipment|Soldiers|Boosters|D&D|Demons
|
||||
|12|Rotating Stock|Druids|Activated Artifacts|Birds|Wolves|Knights|Walls
|
||||
|13|Shard Shop|Planeswalkers|Skeletons|Birds|Shamans|Wizards|Sagas
|
||||
|14|Player Equipment|Phyrexian|Sphinx|Hydras|Spiders|Insects|Changelings
|
||||
|15|Cats|Eldrazi|Clerics|Mutants/Mutate|Horrors|Transform Cards|Universe Beyond
|
||||
|16|Battles|Sags|Nobles/Monarch|Giants|Sea Creatures|Snow|Boosters
|
||||
|
||||
|
||||
|
||||
## Capitals
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Capitals are bigger and have more buildings than normal towns, but have certain exclusive shops that can really useful for the player.
|
||||
|
||||
- Item Shop
|
||||
|
||||

|
||||
|
||||
Item shops sell items and equipments that the players can use on their journey - such as a rune that allows them to teleport to that capital when on the overworld.
|
||||
|
||||
- Arena
|
||||
|
||||

|
||||
|
||||
At the Arena players can participate in a tournament awarding gold and equipments.
|
||||
|
||||
- Smith
|
||||
|
||||

|
||||
|
||||
The Smith is one of the most important buildings while trying to build a specific deck. Through a filtering system, players can try to create specific cards, by entering the set, mana value, colors and rarity of one or more cards. The more filters you use, the more expensive it gets.
|
||||
|
||||
There are other buildings for you to explore on your journey through Shandalar. Have fun!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
14
docs/Transfer-PC-saves-to-Android.md
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
to copy from pc to android:
|
||||
on your pc, browse to
|
||||
C:\Users\ [user]\AppData\Roaming\Forge\adventure\Shandalar
|
||||
copy whichever saves you want, then on your android, browse to
|
||||
internal storage\android\obb\forge.app\forge\data\adventure\Shandalar
|
||||
and paste the saves you want in there
|
||||
|
||||
to copy from android to pc:
|
||||
on your android, browse to
|
||||
internal storage\android\obb\forge.app\forge\data\adventure\Shandalar
|
||||
copy whichever saves you want, then on your pc, browse to
|
||||
C:\Users\ [user]\AppData\Roaming\Forge\adventure\Shandalar
|
||||
and paste the saves you want to move in there
|
||||
6
docs/Tutorial-1-Create-your-First-Plane.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## Tutorial: 1 Create your First Plane
|
||||
|
||||
Creating a new plane allows you to fully customize your Adventure mode experience, and can even be shared with the community. The most basic step in doing so, is to add a new directory in `...\YourIntelliJProjectFolder\forge-gui\res\adventure\`, copy the 'config.json' file from `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common`, paste it into your new directory. Then copy the contents of `...\YourIntelliJProjectFolder\forge-gui\res\adventure\Shandalar\world` into your new directory. Congratulations, that is the bare minimum to create a new plane. If you tell IntelliJ to run the Forge project using the Adventure mode settings, you can load up Adventure mode, find your plane in the drop-down list, and load into it.
|
||||
|
||||
Of course, now many of you will come back here and say "nothing looks different". Don't worry, nothing is broken. As you haven't actually changed any files from their default settings, everything will load up in the exact same way as the default game. This is intentional, so that you can make as many, or as few, changes from the default Shandalar plane as you like, without breaking anything. Look to the other tutorials for guides on how to change each piece of a plane to match your desire.
|
||||
|
||||
221
docs/Tutorial-2-A-New-Look.md
Normal file
@@ -0,0 +1,221 @@
|
||||
## Tutorial 2: A New Look (Creating your first map)
|
||||
|
||||
Okay, you've got a new plane, now let's make that plane our own. This tutorial is going to teach you how to make a very basic map and get it into the game in your plane. This is a much lengthier tutorial, so we are going to split it up into parts.
|
||||
|
||||
1. [Part 1: Setting up Tiled](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look#part-1-setting-up-tiled)
|
||||
|
||||
2. [Part 2: Building a Basic Map](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look#part-2-building-a-basic-map)
|
||||
|
||||
3. [Part 3: Adding the Map to your Plane](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look#part-3-adding-the-map-to-your-plane)
|
||||
|
||||
4. [Part 4: Put it to the Test!](https://github.com/Card-Forge/forge/wiki/Tutorial-2-A-New-Look#part-4-put-it-to-the-test)
|
||||
|
||||
### Part 1: Setting up Tiled
|
||||
|
||||
To make everything work smoothly, we need to do some set-up of your Tiled project. First, in IntelliJ, navigate to your plane directory (in our case, we named it Test) like so `...\YourIntelliJProjectFolder\forge-gui\res\adventure\Test\`, and create a new directory named 'maps'. Inside that directory make another directory called 'map'. Now open up Tiled, click on New Project, and navigate to the maps directory you just created, (the one with the "map" directory inside it.) Create a new project by picking a name in the file name line, (it is recommended you name it the same as your plane, for ease of bug solving,) and click save.
|
||||
|
||||
Next we need to tell Tiled where to find the files Forge uses. Click on Project > "Add Folder to Project...", navigate to `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\maps`. Select the 'obj' directory, and click "Select Folder". If you are planning to use any of Forge's tilesets, then do the same thing, but this time selecting the 'tileset' directory from `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\maps` aswell. This adds access to the various objects Forge uses to actually interact with the player. (Enemies, rewards, map transfers, etc.) The tilesets provide the visual appearance of your map, _as well as the collision mapping_. **_Any of these you intend to modify, you must create a new entry in an appropriate subdirectory inside your own plane's directory!_** Otherwise you will be changing it for every single map in every plane that touches those objects or tilesets, which quickly causes unintended consequences. (Further details on how to modify these items, what the subdirectory should look like, and what needs to remain unchanged to be properly read by Forge, will be described in their specific Wiki pages. For now, just keep this information in mind.)
|
||||
|
||||
Next click on Project > "Project Properties..." In the 'Paths & Files' section you will a text bow that says 'Extensions Directory' and three dots to the right. Click on those three dots, and navigate to `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\maps`, select the extensions directory, and click the "Select Folder" button. Then click on "OK". This adds the extensions written for Forge to your project. Without this, your maps will throw errors when Forge tries to do certain object actions, and crash the game. (Learn from my fail on this one.)
|
||||
|
||||
Voila! That is the minimum work necessary to prepare your project for adding, or editing, maps unique to your plane.
|
||||
|
||||
|
||||
|
||||
### Part 2: Building a Basic Map
|
||||
|
||||
Alright, so you've got your fancy project, and while this gray screen with a view of multiple folders on the left is cool and all. We know you really came here because you wanted to actually _make a new map_. (Well, technically, you may want to just slightly modify an existing map. But to have it unique to your plane, it will still be effectively a new map in the eyes of IntelliJ.) So let's get to it, let's make a very basic map for Forge. Step one, click the fancy, tempting, "New Map..." button I know you've been staring at. (And possibly already clicked, then came back here to see what you need to do on that next window.) This will bring up a new dialog box, with a bunch of options.
|
||||
|
||||
1. Orientation should be Orthogonal
|
||||
2. tile layout format should be CSV
|
||||
3. Tile render order should Right Down
|
||||
|
||||
Those are the Tiled defaults for those options at the time of this writing, but I want it here in case that changes.
|
||||
|
||||
Next up we have the Map size section, the bigger the map, the more you can add, and the longer it will take you to finish. I recommend for our first map, a simple 15 by 15 tiles. You'll make bigger maps, but for your first, don't get overwhelmed. **_DON'T CLICK OK YET!_** I know you want to get started, but we have to change a couple more things. The Tile size needs to be set to 16 wide by 16 high. This is the Forge standard, and the tilesets use it too. Different settings can lead to very unintended side effects. **NOW** you can click "OK".
|
||||
|
||||
Welcome to the gray box that is Tiled's default map menu. There's a lot here, but we are going to build a map together, so relax and just follow along. First off, you will often find yourself in need of multiple layers, otherwise your map is simply not going to look good. So, on the right side, you will see a small box with a text line that says "Tile Layer 1". Double click that line, and rename it to "Ground" without the quotes. Next, right-click and select New > Tile Layer. Name this one "Clutter". Do it again, but name this one "Walls". Finally right-click one more time and select New > Object Layer, name this one "Objects". This gives a basic series of layers to allow us to make a map. You can add more if really needed, but try to limit yourself to six or fewer Tile layers to preserve loading speeds. **Only have ONE object layer**, having a second one will confuse the heck out of Forge.
|
||||
|
||||
Now that we have our layers, let's make sure they are in the right order. Click and drag each layer so that, from top to bottom, they rea: Objects, Walls, Clutter, Ground. In tiled, like many programs, tiles on layers farther up will appear on top of ones below them. Objects on top ensure nothing gets lost, or accidentally put under a wall, and can easily be seen. Next up, while we have an Object layer, we need to actually tell Forge what layer to render sprites on. (This allows you make roofs or arches, etc, that a character can walk "under"... it also tells Forge where to actually _put_ the sprite, since it doesn't actually default to the Object layer.) For our map, we want the player to be walking on top of any dirt or grime we add to the map, so left click on the Walls layer. On the left side of the screen, you'll see the properties toolbox. Scroll down to the bottom and you will see the "Custom Properties" line. Right click > Add Property. Click the drop-down that says "string" and choose "bool". In the text box, type "spriteLayer" without the quotes. **This is a value read by Forge, so it is case sensitive. Lowercase 's', uppercase 'L', no spaces.** Click "OK". You will see it has added the property to the custom properties, with a check-box. The box is unchecked, check it. (If you had not added this property, made it a bool, set it's name correctly, and checked the box... Any attempts to load this map would cause Forge to bug-out pretty hard and require a restart to just work right again.)
|
||||
|
||||
Now that our map-space is set-up, let's go ahead and save all this work we have done. File > Save As. Open your "map" directory inside the "maps" directory, this is a directory of all your maps you are going to use. To save yourself future head-aches, organize maps into sub-directories from the get-go. In our tutorial case, I will be making a beach, so first I will right-click > new > folder while inside of 'map' and name it "beach". Then I will double-click on that "beach" folder. In this sub-directory I name my tutorial map as "test", but you should name your map as something you'll remember it by, then click save. Now that, that's done, let's get around to the art part of this. Click on the Ground layer. You'll notice you can move your mouse over the map and it'll turn red or blue depending on it you are inside the tile space or not. But no matter where you click, nothing happens. That's because we need to actually choose a tileset to use. On the far left side of the screen, you'll see the various folders we imported into our project. Open the tileset folder, and double click on "main.tsx". This will open a new tab, that shows the entire contents of the "main" tileset from Forge. _Since we don't plan to actually make ANY changes to this tileset_ (gentle reminder.) Simply click on the tab for your map (test.tmx in my case,) and you'll see the "main" tileset has appeared in the bottom-right corner of your screen.
|
||||
|
||||
Since I chose a beach, I'm going to make a beach for this tutorial, so I'm going to click on the sand-colored tile near the top-left of the "main" tileset, right next to the default empty one. If you move your mouse back over your map, you will see it shows that sand-colored tile for every square you move it over. If you click, it will even place that color on that tile. While you could individually click on each tile one at a time, for filling in large areas that's much more exasperating. Clicking and dragging will simply apply each tile the mouse moves over. So, instead, along the top, you will see a paint-bucket. Click on that. now if you move your mouse back to the map, it'll show a ghostly tan color across the entire map, minus any squares you already clicked on. (The highlight will only apply to every tile of the same type that you hover over, which is not completely separated from the mouse by a different tileset tile. If you are confused, this will make more sense as you make or modify maps.) Click on the empty space, and it will fill in the entire map with our sandy ground.
|
||||
|
||||
Now, a blank sandy beach may be more than you had before, but it's not that interesting, so let's add some detail. Click on the Clutter layer. Then, on our "main" tile-set on the bottom right, click on the water tile just below the blank one in the top left. If you move your mouse back to the map, you will see it wants to change every tile to water. That's because we are still in Bucket Fill mode. On the top of the window, you'll see a rubber stamp, two tools to the left of the pain bucket. Click on that. Now we are once again designing our map one tile at a time. I'm going to draw out some water tiles along the edges of the map, leaving just a couple sandy ones at the bottom center of the map.
|
||||
|
||||
Now, The map is clearly a sand-bar jutting out into water, but it looks pretty rudimentary, even for Forge. As such, let's click on the Walls layer. Then, on the tile-set on the bottom right, there are a bunch of sandy edges looking like they are meant for the edge of water. I'm going to carefully select various tiles from among them and make my coast-line look much nicer. Exactly which tiles where will depend on how you built the previous layer. If you ever place a tile and it doesn't look right. You can click on the Eraser tool at the top of the screen, and then click that tile on your map. As long as you are on the right layer, it will erase that tile.
|
||||
|
||||
Huzzah! You've made you little sandbar... But how can you be certain where players and enemies can walk? Well, that's called Collision. Go to View > and make sure there is a check-mark in "Show Tile Collision Shapes". The water tiles will have much strongly defined border now, and depending on your sand borders and how you built it, you may have new lines in them, or not. Collision is defined by the tile in the Tileset. Each tile in the tile set either has collision shapes, or doesn't. **If you are not happy with the collisions as-is, you will need to either change your map some, or make your own tileset.** As a reminder DO NOT MODIFY THE TILESETS FROM MAIN FORGE. If you need to make changes, look for my future tutorial that explains building your own tile-set and modifying it.
|
||||
|
||||
Alright, you have a map, it has water, it has collision... Still looks pretty empty. You could add more details on various layers to make it look nicer, but that won't change the most important things. Namely, there's nothing to _do_ on our sandy beach. Let's fix that. On the left side of the screen, open up the "obj" folder, this has all the objects in Forge. Since they are all Objects, let's click on the Objects layer. Now, click and drag a "gold.tx" object from the obj folder, onto our sand. (make sure its on a portion of sand that isn't blocked by collision.) Unlike the tile layers, objects don't have to be centered to any tile. They can even sit on a grid intersection without issue.
|
||||
|
||||
Alright, we got some gold on our map, but that's a bit boring. let's spice it up, by adding an enemy. So click and drag an "enemy.tx" onto the map. Now, all our objects have many properties that make sure they work. But most of them come pre-populated to some degree, so you can just drag-and-drop them. Enemies, however, are diverse and different enough we need to actually define some things about them in order to even function. So click on your placed enemy object, and scroll down the left side to the Custom Properties section. In here, the only option you _have_ define, is the "enemy" property. (Remember, the one in Custom Properties, not the space earlier in the normal properties... Again, learn from my mistakes.) In our case, click on the text box to the right of "enemy", and type "Ghost" without the quotation marks. (While a later tutorial will give more details on all the properties, just know that for this one. It is looking for an entry in your plane's "enemies.json" file. If your plane does not have that file, like ours, it will instead look for the entry in the enemies.json file found in `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\world`... A later tutorial will guide you through making your own enemies.) You have now defined the enemy, but as-is, it will just stand in place, even if a player gets near. Let's liven things up a little, and make our ghost a bit more annoyed and less sleeping on the job. Go down to the "pursueRange" custom property, and set that text box to 50. Then scroll down further to the "threatRange" box, and set that, also, to 50. Now, if the player gets within 50 pixels of it, the ghost will chase them. The ghost will only move to a distance of 50 pixels from their starting point, doing so, however. (For a more lively enemy that patrols, or jump out, etc. You can find that information in the Configuring Enemies portion of the Wiki.)
|
||||
|
||||
Alright, we got a map, we got a reward, and we got an enemy to protect that reward. All good, except, Forge has no idea where to spawn a player who enters the map, and no idea where the exit to the map is. So, go and drag an "entry_up.tx" object to the little entrance of our sandbar, and resize it so that the arrow takes up the majority of the entry space. Congratulations, your map is made, don't forget to **_SAVE_**.
|
||||
|
||||
### Part 3: Adding the Map to your Plane
|
||||
|
||||
Alright, so you have a map you want to play, it's saved in your plane, why can't you find it? Well, that's because Forge doesn't **actually** know it's ready to _be_ found yet. We need to get some "biomes" and "points of interest" added to your plane, aswell as. So, first, let's give them a place to go in your plane. Go to your plane's directory in IntelliJ, and open the 'world' sub-directory. Inside of 'world' create a new subdirectory called 'biomes'. Navigate to `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\world\biomes` and copy the following files to your plane's biomes directory, 'base.json' and 'colorless.json'. We also need to copy the 'points_of_interest.json' from `...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\world` to our custom plane's 'world' directory. Now, open up the 'points_of_interest.json' from our plane's 'world' sub-directory. Inside you will find all the maps used by Forge. If you want to add a map to the list, but keep all the others, you would add it's info the existing json. But for the sake of simplicity in this tutorial, we are going to delete everything inside the json except for the following.
|
||||
```
|
||||
[
|
||||
{
|
||||
"name": "Aerie",
|
||||
"displayName": "Aerie",
|
||||
"type": "dungeon",
|
||||
"count": 30,
|
||||
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
|
||||
"sprite": "Aerie",
|
||||
"map": "../common/maps/map/aerie/aerie_0.tmx",
|
||||
"radiusFactor": 0.8,
|
||||
"questTags": [
|
||||
"Hostile",
|
||||
"Nest",
|
||||
"Dungeon",
|
||||
"Sidequest"
|
||||
]
|
||||
},
|
||||
```
|
||||
and
|
||||
```
|
||||
{
|
||||
"name": "Spawn",
|
||||
"displayName": "Secluded Encampment",
|
||||
"type": "town",
|
||||
"count": 1,
|
||||
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
|
||||
"sprite": "Spawn",
|
||||
"map": "../common/maps/map/main_story/spawn.tmx",
|
||||
"questTags": [
|
||||
"Story",
|
||||
"Spawn",
|
||||
"BiomeColorless"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
For this tutorial, we need to know the following details.
|
||||
|
||||
"name" is the map name in Tiled, so we will change `"name": "Aerie",` to `"name": "YOUR MAP NAME",` ("Test" in our case.)
|
||||
|
||||
"displayName" is the name that appears when the player enters a map. So we will change `"displayName": "Aerie",` to `"displayName": "YOUR MAP DESCRIPTION",` (in our case, I'm going with "Fanciest Beach").
|
||||
|
||||
"count" is how many of this map can spawn in the overworld. Since we want to actually find it, let's change `"count": 1,` to `"count": 30,`
|
||||
|
||||
and "map" is where we actually STORED the map file. So we are going to change `"map": "../common/maps/map/aerie/aerie_0.tmx",` to `"map": "../YOUR_PLANE_NAME/maps/map/YOUR_FOLDER_NAME/YOUR_MAP.tmx"` . For my tutorial, that becomes `"map": "../Test/maps/map/beach/test.tmx",`.
|
||||
|
||||
You can leave the rest of the Aerie code-block alone for this tutorial. If you followed me exactly, "points_of_interest.json" file should now look like this.
|
||||
```
|
||||
[
|
||||
{
|
||||
"name": "Test",
|
||||
"displayName": "Fanciest Beach",
|
||||
"type": "dungeon",
|
||||
"count": 1,
|
||||
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
|
||||
"sprite": "Aerie",
|
||||
"map": "../Test/maps/map/beach/test.tmx",
|
||||
"radiusFactor": 0.8,
|
||||
"questTags": [
|
||||
"Hostile",
|
||||
"Nest",
|
||||
"Dungeon",
|
||||
"Sidequest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Spawn",
|
||||
"displayName": "Secluded Encampment",
|
||||
"type": "town",
|
||||
"count": 1,
|
||||
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
|
||||
"sprite": "Spawn",
|
||||
"map": "../common/maps/map/main_story/spawn.tmx",
|
||||
"questTags": [
|
||||
"Story",
|
||||
"Spawn",
|
||||
"BiomeColorless"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
|
||||
Now, since we want to play our new map, we still need to make sure it spawns in Forge. Go to "colorless.json" file we copied into our Plane's 'biomes' directory. Scroll down until you see the `"pointsOfInterest":` array. We are going to delete everything in this array except for `"Spawn",` and then we are going to add `"YOUR_MAP_NAME"` ("Test" in my case) immediately afterwards. If you copied this tutorial exactly so far, it would like this.
|
||||
```
|
||||
"pointsOfInterest": [
|
||||
"Spawn",
|
||||
"Test"
|
||||
],
|
||||
```
|
||||
|
||||
### Part 4: Put it to the test!
|
||||
|
||||
Alright, time for the fun part. If you've come this far, everything is ready to go. Tell IntelliJ to fire up Forge, go into Adventure mode. If you hadn't previously, set your plane to your custom plane, (and restart Forge in that case.) You'll still have the tutorial quest prompts and spawn to work through. But when done, when you leave the portal, things will immediately be visibly different. If you don't see a dungeon right away, bring up the Map, track down your dungeon icon in the Wastes biome, and walk on over. (You may need to avoid a few spawns along the way.) Enter, and, assuming you following perfectly, you'll show up in your new map. If you have questions or run into any problems, reach out to me (shenshinoman) on the Forge discord and I will be happy to help.
|
||||
|
||||
After you've done that, you may notice the entire world is devoid of any map except the overworld, the spawn to the old man, and the map you just built. While this is nice for testing a new map, we should probably add a city back in, that way we actually have a place to heal up and test other changes to the plane. To make life really easy, we are just going to add a basic town back to the "Wastes". Navigate to ``...\YourIntelliJProjectFolder\forge-gui\res\adventure\common\world' and open up the "points_of_interest.json" file. Towards the bottom we will find the code block for "Waste Town Generic", select it, and copy it into the "points_of_interest.json" inside your plane's 'world' directory. So, if you've followed along precisely, your "points_of_interest.json" should look like this.
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"name": "Test",
|
||||
"displayName": "Fanciest Beach",
|
||||
"type": "dungeon",
|
||||
"count": 30,
|
||||
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
|
||||
"sprite": "Aerie",
|
||||
"map": "../Test/maps/map/beach/test.tmx",
|
||||
"radiusFactor": 0.8,
|
||||
"questTags": [
|
||||
"Hostile",
|
||||
"Nest",
|
||||
"Dungeon",
|
||||
"Sidequest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Spawn",
|
||||
"displayName": "Secluded Encampment",
|
||||
"type": "town",
|
||||
"count": 1,
|
||||
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
|
||||
"sprite": "Spawn",
|
||||
"map": "../common/maps/map/main_story/spawn.tmx",
|
||||
"questTags": [
|
||||
"Story",
|
||||
"Spawn",
|
||||
"BiomeColorless"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Waste Town Generic",
|
||||
"type": "town",
|
||||
"count": 30,
|
||||
"spriteAtlas": "../common/maps/tileset/buildings.atlas",
|
||||
"sprite": "WasteTown",
|
||||
"map": "../common/maps/map/towns/waste_town_generic.tmx",
|
||||
"radiusFactor": 0.8,
|
||||
"questTags": [
|
||||
"Town",
|
||||
"TownGeneric",
|
||||
"BiomeColorless",
|
||||
"Sidequest",
|
||||
"QuestSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Now we need to go and tell the game to actually spawn this town. Inside your plane's "world\biomes" directory, open back up the "colorless.json" file, and add "Waste Town Generic" to the `"pointsOfInterest"` array. If you followed along, that array should now look like this.
|
||||
|
||||
```
|
||||
pointsOfInterest": [
|
||||
"Spawn",
|
||||
"Test",
|
||||
"Waste Town Generic"
|
||||
],
|
||||
```
|
||||
|
||||
Now if you open up your plane, you have a bunch of dungeons and a bunch of towns. Congratulations, you have completed this tutorial. Pat yourself on the back, cus it was a long one. (Also, don't forget to save if your system doesn't auto-save.)
|
||||
56
docs/Tutorial-3-Configuration.md
Normal file
@@ -0,0 +1,56 @@
|
||||
## Tutorial 3 Configuration, (Configuring your Plane)
|
||||
|
||||
Alright, our third tutorial will be a simpler one again. Customizing your plane. A common desire when making a new plane, is to change things such as which sets are available to play, or which cards an be bought or gained as rewards. Maybe you want to be able to more easily build super powerful decks from the get go, or just have entirely new decks. All these settings, and more, can be adjusted from the 'config.json' file in your plane's directory. For this tutorial, to get a basic idea of what things can be changed, we are going to make some minor adjustments to the plane. Further details on other changes and what they do will be covered in other sections and tutorials.
|
||||
|
||||
For now, in IntelliJ, open up the 'config.json' file found in `...\YourIntelliJProjectFolder\forge-gui\res\adventure\YourPlane`, as a reminder for our tutorials, I am using a directory called Test. Inside the config file, there are a large number of entries, but we are going to start with the `"restrictedCards"` array. After-all, we are here to have fun, so let's just bring in almost all the craziness. We are going to delete every entry in the array, _except_ for "Black Lotus"... Why not Black Lotus you ask? Because it's too rare, clearly. (Or, maybe just because it is the first entry and I'm being lazy, and keeping one entry is a reminder how this array is formatted.) So, when you are done, the array should look like this.
|
||||
```
|
||||
"restrictedCards": [
|
||||
"Black Lotus"
|
||||
],
|
||||
```
|
||||
|
||||
To better explain what that array does, is any card in the array will not appear in any generic rewards or shops. This is the closest thing to banning individual cards that one can. However, it should be noted that if those cards are ever specifically added as a reward or a shop's content. Doing so overrides this setting. Essentially allowing you to award cards for very special cases, or hide a fancy shop somewhere in your game.
|
||||
|
||||
Now, restricting one card at a time is a very laborious process, especially if there are a large number of cards you want removed. In many cases, you will want entire sets removed from your game, which brings us to the `"restrictedEditions"` array. Any card whose set code appears in this array will be restricted from the game in the exact same way as `"restrictedCards"`. There's already several entries in here, but since I'm making a really wacky unbalanced plane here, we're going to remove every entry from this array, except for "UNF". Why not UNF you ask? Because it's a traitor and made some of it's cards legal and others not, clearly. (Or, maybe again it's because I'm lazy, and at the time of this writing, it's the last entry in the list. As well as once again keeping one entry as a reminder how this array is formatted.) So if you follow my flawless example, it will look like this.
|
||||
|
||||
```
|
||||
"restrictedEditions": [
|
||||
"UNF"
|
||||
],
|
||||
```
|
||||
|
||||
Now that we've opened the floodgates for our awards, we could go have fun in the wacky world. However, our Inn's drafts and jumpstarts won't be as open as the rest of the game still. Why? Because the array `"restrictedEvents"` tells our Inn what "events" (aka sealed and draft editions) it can't spawn. Just like `"restrictedEditions"` any set in this array won't appear for draft events (but if it is not in the `"restrictedEditions"` it can still appear as rewards.) As such, let's go ahead and open those flood gates further. In my infinite wisdom, I have decided, for no specific reason at all, that we will remove every set from this list except for "CMM". (Yes, you got the picture, I'm lazy but trying to be helpful still.) If you followed me so far, the entries between `"colorIDNames"` and `"difficulties"` should appear as follows.
|
||||
|
||||
```
|
||||
"restrictedCards": [
|
||||
"Black Lotus"
|
||||
],
|
||||
"restrictedEditions": [
|
||||
"UNF"
|
||||
],
|
||||
"restrictedEvents": [
|
||||
"CMM"
|
||||
],
|
||||
```
|
||||
|
||||
Now if you load up the game at this timeframe, and go hopping through towns, you'll notice the occasional card you hadn't seen before. This is good, but man there are a lot of cards in Magic, and it can take a while to find ones to show you've made a change. For a tutorial, that's bad. Also, if you want to make a plane like "Shandalar Old Border" or "Innistrad", you'd have to go through and add every unwanted set to the `"restrictedEditions"` array. including updating this array every time Wizards of the Coast releases another set, and that's **worse** than bad... Luckily, we have a solution for this. We are going to add two new arrays to our 'config.json' file. Where the previous arrays act as a black-list for the entries, the following arrays are a overwrite. If they are present, the plane will only spawn cards and events found in them. The first one is `"allowedEditions"`. For this tutorial, I am going to go completely unhinged and risk making some of our games unstable, but what mod never comes unglued? If you didn't pick up the hints, my array is going to add `"UNG"`, `"UNH"`, and `"UST"`. So my array looks as follows.
|
||||
|
||||
```
|
||||
"allowedEditions": [
|
||||
"UNG",
|
||||
"UNH",
|
||||
"UST"
|
||||
],
|
||||
```
|
||||
|
||||
The second array I'm going to add is the `"allowedJumpstart"` array. This changes which Jumpstart sets are available, and will allow us to still have them even when the previous arrays have blacklisted them in any way. For this tutorial, I know we all want to get a jump on things, so I'm just going to add `"Jumpstart"`.
|
||||
|
||||
```
|
||||
"allowedJumpstart": [
|
||||
"Jumpstart"
|
||||
],
|
||||
```
|
||||
|
||||
You might have noticed that unlike the previous arrays, this one uses the set name, not code. This is a current limitation in Forge, and one we will have to live with. Luckily, there are far fewer jumpstart sets than than other sets. (**NOTE: Adding a set to this list that does not have any official Jumpstart packs will NOT enable them for jumpstart events.** Some of you who have tooled around will have noticed there are Jumpstarts in "Innistrad" that don't exist in normal jumpstarts. Well, that is for a much later tutorial. My apologies.)
|
||||
|
||||
As a final note, while this changes the rewards from enemies and shops, as well as what can appear in the Inns. It has no bearing on your starting decks, or enemy decks, just the rewards and shop options. So yes, if you followed along so far, you will playing with 'normal' cards, against normal cards, to earn Silver-Bordered cards.. yeah, it's a bit crazy, but that works... For those who want to customize their plane even further, the next tutorial will focus on modifying both starting decks, and enemy decks. See you there. For now, enjoy, and again. if you have any problems, please reach out me "Shenshinoman" on the discord and I will be happy to help.
|
||||
1793
docs/Un‐cards,-Playtest-Cards,-and-Other-Funny-Cards.md
Normal file
116
docs/User-Guide.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Downloads
|
||||
* **Snapshots**;
|
||||
* READ THESE NOTES BEFORE DOWNLOADING SNAPSHOTS:
|
||||
* **Please use snapshots for Adventure Mode!**
|
||||
* May contain more bugs, bug fixes, **definitely gets newest cards faster** and newer features.
|
||||
* These are **NOW** automatically released daily.
|
||||
* If the snapshot isn't in the location below, it's because its in the middle of uploading a new snapshot. Come back later to grab it.
|
||||
* [_**CLICK HERE FOR DOWNLOAD LINKS - Forge SNAPSHOT Version (DESKTOP/ANDROID)**_](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots)
|
||||
* For desktop, grab the installer file that ends in .jar
|
||||
* For android, grab the android file that ends in .apk
|
||||
* **Android Installation Guide**
|
||||
* Quick Guide for installing Android Snapshots: <br />
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/7a0c7bb8-7cf9-4800-8091-bcc30ff2f4d8
|
||||
|
||||
|
||||
|
||||
* **Releases**;
|
||||
* READ THESE NOTES BEFORE DOWNLOADING RELEASES:
|
||||
- "Releases" are really intended where "99% cards implemented are working and stable."
|
||||
- If you are looking for newly spoiled cards as soon as possible, grab the snapshot instead.
|
||||
- The current release mechanism is failing unexpectedly for Android. So just stick with snapshots for Android users.
|
||||
* [_**CLICK HERE FOR DOWNLOAD LINKS - RELEASE DESKTOP**_](https://github.com/Card-Forge/forge/releases/latest)
|
||||
- Grab the installer file that ends in .jar
|
||||
|
||||
|
||||
# Java Requirement
|
||||
|
||||
**Forge Requires Java** to run, please make sure you have Java installed on your machine prior to attempting to run.
|
||||
|
||||
* **Java 17** is required as minimum version and can be acquired through the Standard Edition Development Kit (JDK) or the OpenJDK. Continued development provides new features in those editions, therefore you need the Java Development Kit to have those newer editions;
|
||||
- Download - [https://jdk.java.net/](https://jdk.java.net/)
|
||||
- Source Code - [https://github.com/openjdk/jdk/](https://github.com/openjdk/jdk/)
|
||||
|
||||
Most people who have problems setting up Forge, do not have Java setup properly. If you are having trouble, open your terminal/command line and run `java --version`. That number should be 17 or higher.
|
||||
|
||||
# Install and Run
|
||||
|
||||
Forge requires Java to run.
|
||||
|
||||
_**Download and unpack /install the package to their own new folder!**_
|
||||
|
||||
### Install Wizard (jar)
|
||||
* Run/Double click "**forge-installer**-VERSION.jar" where VERSION is the current release version and click next until the Target Path window appears. If double clicking the .jar file doesn't load the main interface you can run it via terminal/command line ```java -jar FILENAME.jar``` where FILENAME is the name of the installer.
|
||||
|
||||
* Browse to your preferred install directory (create a new directory for clean installation) and click next until installation starts.
|
||||
|
||||

|
||||
|
||||
* After the installation finishes, close the installer. Run the executable forge|forge-adventure (.exe/.sh/.cmd)
|
||||
|
||||
### Manual Extraction (tar.bz2)
|
||||
|
||||
* **Desktop Windows**:
|
||||
* Unpack "forge...tar.**bz2**" with any unpacking/unzipping app (e.g. 7-zip, winrar, etc)
|
||||
* You'll end up with "forge...**tar**".
|
||||
* Unpack that ".tar" file once more into its own folder.
|
||||
* Run Forge app/exe
|
||||
* **Desktop Linux/Mac**:
|
||||
* Unpack "forge...**tar.bz2**" with any unpacking app. (Check your package repository, or app store.)
|
||||
* You'll probably end up with just a folder, and fully extracted.
|
||||
* If you do end up with a ".tar" file, unpack that file also into it's own folder.
|
||||
* Run Forge script;
|
||||
* Linux: Run the ".sh" file in a terminal (double clicking might work.)
|
||||
* MacOS/OSX: Run the ".command" file by double clicking in Finder, or run from the terminal.
|
||||
* If the command file doesn't appear to do anything, you'll need to [modify the permissions to be executable.](https://support.apple.com/guide/terminal/make-a-file-executable-apdd100908f-06b3-4e63-8a87-32e71241bab4/mac) (This is a temporary bug in the build process.)
|
||||
* Additionally OSX needs to have a JRE AND a JDK installed because reasons.
|
||||
* **Android**:
|
||||
* Sideload/Install "forge...apk"
|
||||
* Run Forge
|
||||
|
||||
## Play Adventure Mode on Desktop
|
||||
|
||||
* Run the Adventure Mode EXE or Script in the Folder you extracted.
|
||||
* The game will start with an option for Adventure or Classic Mobile UI.
|
||||
* Android/Mobile builds are built as the Adventure Mode or Mobile UI and nothing special is needed.
|
||||
- If adventure mode option does not show up;
|
||||
- check you're up to date with your version.
|
||||
- check in the settings that the "Selector Mode" is set to `Default`
|
||||
|
||||
# System Requirements and Historic Details
|
||||
|
||||
Since Forge is written in Java, it is compatible on any Operating System
|
||||
that can run the Java Runtime Environment. Forge requires
|
||||
Java 17 (Forge is not backwards compatible with older versions of Java).
|
||||
If you have difficulties with your System not working with Forge,
|
||||
please come to the Discord so we can attempt to help.
|
||||
This program works best with a screen resolution of **1280 by 720** or
|
||||
better. Forge can now have it's window minimized to **800 by 600**
|
||||
pixels but this may make the display area cramped and possibly limit
|
||||
your ability to play. (This means Forge may not compatible with some
|
||||
netbook computers.)
|
||||
|
||||
The memory requirements for Forge have fluctuated over time. The default
|
||||
setting on your computer for the java heap space may not be enough to
|
||||
prevent the above problems. If you launch Forge by double-clicking the
|
||||
file **run-forge.jar** you will eventually receive a **java heap space
|
||||
error**. The name of the forge jar file has changed as part of our new
|
||||
Maven based build and release system. The name format now used is:
|
||||
|
||||
**forge-**{version number}**-jar-with-dependencies.jar**
|
||||
|
||||
We have created several scripts that will launch Forge with a greater
|
||||
allotment of system resources. (We do this by passing **-Xmx1024m** as
|
||||
an argument to the Java VM.) People using Windows OS should double click
|
||||
the **forge.exe** file. People using Apple's Mac OS X should use the Mac
|
||||
OS version and double click the **forge.command** file. People using one of the
|
||||
other \*nix OS should double click the **forge.sh** file.
|
||||
|
||||
# What if double-clicking doesn’t work?
|
||||
|
||||
Sometimes double-clicking will open the jar file in a different program.
|
||||
In Windows, you may need to right-click and open the properties to
|
||||
change the launching program to Java. This might be different in OSX or
|
||||
Linux systems (file permission related).
|
||||
65
docs/_sidebar.md
Normal file
@@ -0,0 +1,65 @@
|
||||
### [Forge Wiki Home](Home.md)
|
||||
- [User Guide](User-Guide.md)
|
||||
- [FAQ](Frequently-Asked-Questions.md)
|
||||
- [SteamDeck/Bazzite](Steam-Deck-and-Bazzite-Install.md)
|
||||
- [AI](ai.md)
|
||||
- [Network Play](network-play.md)
|
||||
- [Network FAQ](Network-FAQ.md)
|
||||
- [Network Extra](Networking-Extras.md)
|
||||
|
||||
- [Adventure Mode](Adventure-Mode.md)
|
||||
|
||||
- Gameplay Guide
|
||||
|
||||
- [Getting Started](Gameplay-Guide.md)
|
||||
- [Different Planes](Different-Planes.md)
|
||||
- [Currency](Currency.md)
|
||||
- [Deck Building Tips](Deck-Building-Tips.md)
|
||||
- [Towns & Capitals](Towns-&-Capitals.md)
|
||||
- [Dungeons](Dungeons.md)
|
||||
- [Equipments and Items](Equipments-and-Items.md)
|
||||
- [Keyboard Shortcuts](Keyboard-Shortcuts.md)
|
||||
- [Console and Cheats](Console-and-Cheats.md)
|
||||
|
||||
- [Modding and Development](Modding-and-Development.md)
|
||||
|
||||
- [Create Enemies](Create-Enemies.md)
|
||||
- [Create Rewards](Create-Rewards.md)
|
||||
- [Create Maps](Create-new-Maps.md)
|
||||
- [Configure Planes](Configure-Planes.md)
|
||||
- [Configure Starting Sets](Configure-Sets.md)
|
||||
|
||||
- Tutorials
|
||||
- [Tutorial 1, Create your first Plane](Tutorial-1-Create-your-First-Plane.md)
|
||||
- [Tutorial 2, A New Look (creating your first map.)](Tutorial-2-A-New-Look.md)
|
||||
- [Tutorial 3, Configuration (Configuring your Plane)](Tutorial-3-Configuration.md)
|
||||
|
||||
|
||||
- [Card Scripting API](Card-scripting-API/Card-scripting-API.md)
|
||||
- [Ability effects](Card-scripting-API/AbilityFactory.md)
|
||||
- [Triggers](Card-scripting-API/Triggers.md)
|
||||
- [Replacements](Card-scripting-API/Replacements.md)
|
||||
- Statics
|
||||
- [Costs](Card-scripting-API/Costs.md)
|
||||
- [Affected / Targets](Card-scripting-API/Targeting.md)
|
||||
- [Restrictions / Conditions](Card-scripting-API/Restrictions.md)
|
||||
- [Guide: Creating a Custom Card](Card-scripting-API/Creating-a-Custom-Card.md)
|
||||
|
||||
- [Development]((SM-autoconverted)-how-to-get-started-developing-forge.md)
|
||||
- [IntelliJ Setup](Development/IntelliJ-setup/IntelliJ-setup.md)
|
||||
- [Snapshots and Releases](Snapshots-and-Releases.md)
|
||||
- [Android Builds](Development/android-builds.md)
|
||||
- [Ownership](Development/ownership.md)
|
||||
- [Docker Container](docker-setup.md)
|
||||
|
||||
- [Customization and Themes.md](Themes.md)
|
||||
- Skins
|
||||
- Sounds
|
||||
- [Music](Custom-Music.md)
|
||||
- [Card Images](Card-Images.md)
|
||||
- [File Formats](File-Formats.md)
|
||||
- [Creating your first custom set](Creating-a-custom-set.md)
|
||||
|
||||
- [Missing Cards in Forge](Missing-Cards-in-Forge.md)
|
||||
- [Un‐cards, Playtest Cards, and Other Funny Cards](Un‐cards,-Playtest-Cards,-and-Other-Funny-Cards.md)
|
||||
- [Credit and Thanks](Credit-and-Thanks.md)
|
||||
9
forge-ai/src/main/java/forge/ai/AiAbilityDecision.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package forge.ai;
|
||||
|
||||
public record AiAbilityDecision(int rating, AiPlayDecision decision) {
|
||||
private static int MIN_RATING = 30;
|
||||
|
||||
public boolean willingToPlay() {
|
||||
return rating > MIN_RATING && decision.willingToPlay();
|
||||
}
|
||||
}
|
||||
@@ -145,13 +145,15 @@ public class AiAttackController {
|
||||
sa.setActivatingPlayer(defender);
|
||||
if (sa.isCrew() && !ComputerUtilCost.checkTapTypeCost(defender, sa.getPayCosts(), c, sa, tappedDefenders)) {
|
||||
continue;
|
||||
} else if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
|
||||
}
|
||||
if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
|
||||
continue;
|
||||
}
|
||||
Card animatedCopy = AnimateAi.becomeAnimated(c, sa);
|
||||
if (animatedCopy.isCreature()) {
|
||||
// TODO imprecise, only works 100% for colorless mana
|
||||
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
|
||||
sa.getPayCosts().getTotalMana().getCMC() : 0; // FIXME: imprecise, only works 100% for colorless mana
|
||||
sa.getPayCosts().getTotalMana().getCMC() : 0;
|
||||
if (totalMana - manaReserved >= saCMC) {
|
||||
manaReserved += saCMC;
|
||||
defenders.add(animatedCopy);
|
||||
@@ -273,27 +275,6 @@ public class AiAttackController {
|
||||
return false;
|
||||
}
|
||||
|
||||
// the attacker will die to a triggered ability (e.g. Sarkhan the Masterless)
|
||||
for (Card c : ai.getOpponents().getCardsIn(ZoneType.Battlefield)) {
|
||||
for (Trigger t : c.getTriggers()) {
|
||||
if (t.getMode() == TriggerType.Attacks) {
|
||||
SpellAbility sa = t.ensureAbility();
|
||||
if (sa == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sa.getApi() == ApiType.EachDamage && "TriggeredAttacker".equals(sa.getParam("Defined"))) {
|
||||
List<Card> valid = CardLists.getValidCards(c.getController().getCreaturesInPlay(), sa.getParam("ValidCards"), c.getController(), c, sa);
|
||||
// TODO: this assumes that 1 damage is dealt per creature. Improve this to check the parameter/X to determine
|
||||
// how much damage is dealt by each of the creatures in the valid list.
|
||||
if (attacker.getNetToughness() <= valid.size()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("TRUE".equals(attacker.getSVar("HasAttackEffect"))) {
|
||||
return true;
|
||||
}
|
||||
@@ -412,7 +393,7 @@ public class AiAttackController {
|
||||
if (ai.getController().isAI()) {
|
||||
PlayerControllerAi aic = ((PlayerControllerAi) ai.getController());
|
||||
pilotsNonAggroDeck = aic.pilotsNonAggroDeck();
|
||||
playAggro = !pilotsNonAggroDeck || aic.getAi().getBooleanProperty(AiProps.PLAY_AGGRO);
|
||||
playAggro = !pilotsNonAggroDeck || aic.getAi().getBoolProperty(AiProps.PLAY_AGGRO);
|
||||
}
|
||||
// TODO make switchable via AI property
|
||||
int thresholdMod = 0;
|
||||
@@ -633,13 +614,7 @@ public class AiAttackController {
|
||||
|
||||
// if true, the AI will attempt to identify which blockers will already be taken,
|
||||
// thus attempting to predict how many creatures with evasion can actively block
|
||||
boolean predictEvasion = false;
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
if (aic.getBooleanProperty(AiProps.COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION)) {
|
||||
predictEvasion = true;
|
||||
}
|
||||
}
|
||||
boolean predictEvasion = AiProfileUtil.getBoolProperty(ai, AiProps.COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION);
|
||||
|
||||
CardCollection accountedBlockers = new CardCollection(this.blockers);
|
||||
CardCollection categorizedAttackers = new CardCollection();
|
||||
@@ -879,12 +854,12 @@ public class AiAttackController {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
simAI = aic.usesSimulation();
|
||||
if (!simAI) {
|
||||
playAggro = aic.getBooleanProperty(AiProps.PLAY_AGGRO);
|
||||
playAggro = aic.getBoolProperty(AiProps.PLAY_AGGRO);
|
||||
chanceToAttackToTrade = aic.getIntProperty(AiProps.CHANCE_TO_ATTACK_INTO_TRADE);
|
||||
tradeIfTappedOut = aic.getBooleanProperty(AiProps.ATTACK_INTO_TRADE_WHEN_TAPPED_OUT);
|
||||
tradeIfTappedOut = aic.getBoolProperty(AiProps.ATTACK_INTO_TRADE_WHEN_TAPPED_OUT);
|
||||
extraChanceIfOppHasMana = aic.getIntProperty(AiProps.CHANCE_TO_ATKTRADE_WHEN_OPP_HAS_MANA);
|
||||
tradeIfLowerLifePressure = aic.getBooleanProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
|
||||
predictEvasion = aic.getBooleanProperty(AiProps.COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION);
|
||||
tradeIfLowerLifePressure = aic.getBoolProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
|
||||
predictEvasion = aic.getBoolProperty(AiProps.COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -993,6 +968,16 @@ public class AiAttackController {
|
||||
return aiAggression;
|
||||
}
|
||||
|
||||
// Only do decisive attacks against token-generating players
|
||||
if (!bAssault && defender instanceof Player) {
|
||||
Player opponent = (Player)defender;
|
||||
if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Rabble Rousing"))
|
||||
- CardLists.count(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Darien, King of Kjeldor"))
|
||||
- CardLists.count(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Kazuul, Tyrant of the Cliffs")) < 0) {
|
||||
return aiAggression;
|
||||
}
|
||||
}
|
||||
|
||||
if (bAssault && defender == defendingOpponent) { // in case we are forced to attack someone else
|
||||
if (LOG_AI_ATTACKS)
|
||||
System.out.println("Assault");
|
||||
@@ -1003,7 +988,7 @@ public class AiAttackController {
|
||||
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
|
||||
return aiAggression;
|
||||
|
||||
// TODO if lifeInDanger use chance to hold back some
|
||||
// TODO if lifeInDanger use chance to hold back some (especially in multiplayer)
|
||||
if (canAttackWrapper(attacker, defender) && isEffectiveAttacker(ai, attacker, combat, defender)) {
|
||||
combat.addAttacker(attacker, defender);
|
||||
}
|
||||
@@ -1100,7 +1085,8 @@ public class AiAttackController {
|
||||
for (final Card pCard : myList) {
|
||||
// if the creature can attack then it's a potential attacker this
|
||||
// turn, assume summoning sickness creatures will be able to
|
||||
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
|
||||
// TODO: Account for triggered power boosts.
|
||||
if (ComputerUtilCombat.canAttackNextTurn(pCard) && (pCard.getNetCombatDamage() > 0 || "TRUE".equals(pCard.getSVar("HasAttackEffect")))) {
|
||||
candidateAttackers.add(pCard);
|
||||
candidateUnblockedDamage += ComputerUtilCombat.damageIfUnblocked(pCard, defendingOpponent, null, false);
|
||||
computerForces++;
|
||||
@@ -1431,7 +1417,7 @@ public class AiAttackController {
|
||||
// Check if maybe we are too reckless in adding this attacker
|
||||
if (canKillAllDangerous) {
|
||||
boolean avoidAttackingIntoBlock = ai.getController().isAI()
|
||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
|
||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBoolProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
|
||||
boolean attackerWillDie = defPower >= attacker.getNetToughness();
|
||||
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
|
||||
boolean noContributionToAttack = attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
|
||||
|
||||
@@ -861,10 +861,9 @@ public class AiBlockController {
|
||||
return;
|
||||
}
|
||||
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
final int evalThresholdToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
|
||||
final int evalThresholdNonToken = aic.getIntProperty(AiProps.THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER);
|
||||
final boolean onlyIfLethal = aic.getBooleanProperty(AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
|
||||
final int evalThresholdToken = AiProfileUtil.getIntProperty(ai, AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
|
||||
final int evalThresholdNonToken = AiProfileUtil.getIntProperty(ai, AiProps.THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER);
|
||||
final boolean onlyIfLethal = AiProfileUtil.getBoolProperty(ai, AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
|
||||
|
||||
if (evalThresholdToken > 0 || evalThresholdNonToken > 0) {
|
||||
// detect how much damage is threatened to each of the planeswalkers, see which ones would be
|
||||
@@ -1047,7 +1046,7 @@ public class AiBlockController {
|
||||
clearBlockers(combat, possibleBlockers);
|
||||
|
||||
diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade
|
||||
if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO)) {
|
||||
if (diff > 0 && AiProfileUtil.getBoolProperty(ai, AiProps.PLAY_AGGRO)) {
|
||||
diff = 0;
|
||||
}
|
||||
|
||||
@@ -1284,9 +1283,9 @@ public class AiBlockController {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
// simulation must get same results or it may crash
|
||||
if (!aic.usesSimulation()) {
|
||||
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
|
||||
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
|
||||
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
|
||||
enableRandomTrades = aic.getBoolProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
|
||||
randomTradeIfBehindOnBoard = aic.getBoolProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
|
||||
randomTradeIfCreatInHand = aic.getBoolProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
|
||||
minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
|
||||
maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
|
||||
chanceModForEmbalm = aic.getIntProperty(AiProps.CHANCE_DECREASE_TO_TRADE_VS_EMBALM);
|
||||
|
||||
@@ -59,7 +59,6 @@ public class AiCardMemory {
|
||||
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
|
||||
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
|
||||
BOUNCED_THIS_TURN, // These cards were bounced this turn
|
||||
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
|
||||
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
|
||||
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
|
||||
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part
|
||||
|
||||
@@ -66,13 +66,11 @@ import io.sentry.Breadcrumb;
|
||||
import io.sentry.Sentry;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
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;
|
||||
|
||||
@@ -99,6 +97,7 @@ public class AiController {
|
||||
private int lastAttackAggression;
|
||||
private boolean useLivingEnd;
|
||||
private List<SpellAbility> skipped;
|
||||
private boolean timeoutReached;
|
||||
|
||||
public AiController(final Player computerPlayer, final Game game0) {
|
||||
player = computerPlayer;
|
||||
@@ -378,7 +377,7 @@ public class AiController {
|
||||
|
||||
if (card.isSaga()) {
|
||||
for (final Trigger tr : card.getTriggers()) {
|
||||
if (tr.getMode() != TriggerType.CounterAdded || !tr.isChapter()) {
|
||||
if (!tr.isChapter()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -394,6 +393,7 @@ public class AiController {
|
||||
return false;
|
||||
}
|
||||
|
||||
// usually later chapters make use of an earlier one
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -483,7 +483,7 @@ public class AiController {
|
||||
|
||||
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
|
||||
// don't play MDFC land if other side is spell and enough lands are available
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -764,7 +764,7 @@ public class AiController {
|
||||
return predictSpellToCastInMain2(exceptSA, true);
|
||||
}
|
||||
private SpellAbility predictSpellToCastInMain2(ApiType exceptSA, boolean handOnly) {
|
||||
if (!getBooleanProperty(AiProps.PREDICT_SPELLS_FOR_MAIN2)) {
|
||||
if (!getBoolProperty(AiProps.PREDICT_SPELLS_FOR_MAIN2)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -888,79 +888,18 @@ public class AiController {
|
||||
private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
|
||||
// Check a predefined condition
|
||||
if (sa.hasParam("AICheckSVar")) {
|
||||
final String svarToCheck = sa.getParam("AICheckSVar");
|
||||
String comparator = "GE";
|
||||
int compareTo = 1;
|
||||
|
||||
if (sa.hasParam("AISVarCompare")) {
|
||||
final String fullCmp = sa.getParam("AISVarCompare");
|
||||
comparator = fullCmp.substring(0, 2);
|
||||
final String strCmpTo = fullCmp.substring(2);
|
||||
try {
|
||||
compareTo = Integer.parseInt(strCmpTo);
|
||||
} catch (final Exception ignored) {
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
|
||||
}
|
||||
}
|
||||
|
||||
int left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
|
||||
if (!Expressions.compare(left, comparator, compareTo)) {
|
||||
return AiPlayDecision.AnotherTime;
|
||||
}
|
||||
if (sa.hasParam("AICheckSVar") && !aiShouldRun(sa, sa, host, null)) {
|
||||
return AiPlayDecision.AnotherTime;
|
||||
}
|
||||
|
||||
int oldCMC = -1;
|
||||
boolean xCost = sa.costHasX() || host.hasKeyword(Keyword.STRIVE) || sa.getApi() == ApiType.Charm;
|
||||
if (!xCost) {
|
||||
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
|
||||
// when the AI won't even be able to play the spell in the first place (even if it could afford it)
|
||||
return AiPlayDecision.CantAfford;
|
||||
}
|
||||
// TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable
|
||||
if (!sa.getAllTargetChoices().isEmpty()) {
|
||||
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
|
||||
}
|
||||
}
|
||||
|
||||
AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
|
||||
// this is the "heaviest" check, which also sets up targets, defines X, etc.
|
||||
AiPlayDecision canPlay = canPlaySa(sa);
|
||||
|
||||
if (canPlay != AiPlayDecision.WillPlay) {
|
||||
return canPlay;
|
||||
}
|
||||
|
||||
// Account for possible Ward after the spell is fully targeted
|
||||
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
|
||||
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
|
||||
if (!sa.isSpell() || sa.isCounterableBy(null)) {
|
||||
for (TargetChoices tc : sa.getAllTargetChoices()) {
|
||||
for (Card tgt : tc.getTargetCards()) {
|
||||
// TODO some older cards don't use the keyword, so check for trigger instead
|
||||
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(host.getController())) {
|
||||
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
|
||||
if (wardCost.hasManaCost()) {
|
||||
xCost |= wardCost.getTotalMana().getCMC() > 0;
|
||||
}
|
||||
SpellAbilityAi topAI = new SpellAbilityAi() {};
|
||||
if (!topAI.willPayCosts(player, sa, wardCost, host)) {
|
||||
return AiPlayDecision.CostNotAcceptable;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if some target raised cost
|
||||
if (!xCost && oldCMC > -1) {
|
||||
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
|
||||
if (finalCMC > oldCMC) {
|
||||
xCost = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xCost && !ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
// for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made
|
||||
// on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay)
|
||||
return AiPlayDecision.CantAfford;
|
||||
@@ -968,13 +907,11 @@ public class AiController {
|
||||
|
||||
// check if enough left (pass memory indirectly because we don't want to include those)
|
||||
Set<Card> tappedForMana = AiCardMemory.getMemorySet(player, MemorySet.PAYS_TAP_COST);
|
||||
if (tappedForMana != null && tappedForMana.isEmpty() &&
|
||||
if (tappedForMana != null && !tappedForMana.isEmpty() &&
|
||||
!ComputerUtilCost.checkTapTypeCost(player, sa.getPayCosts(), host, sa, new CardCollection(tappedForMana))) {
|
||||
return AiPlayDecision.CantAfford;
|
||||
}
|
||||
|
||||
// if we got here, looks like we can play the final cost and we could properly set up and target the API and
|
||||
// are willing to play the SA
|
||||
return AiPlayDecision.WillPlay;
|
||||
}
|
||||
|
||||
@@ -993,7 +930,7 @@ public class AiController {
|
||||
final Card card = sa.getHostCard();
|
||||
|
||||
// Trying to play a card that has Buyback without a Buyback cost, look for possible additional considerations
|
||||
if (getBooleanProperty(AiProps.TRY_TO_PRESERVE_BUYBACK_SPELLS)) {
|
||||
if (getBoolProperty(AiProps.TRY_TO_PRESERVE_BUYBACK_SPELLS)) {
|
||||
if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyback() && !canPlaySpellWithoutBuyback(card, sa)) {
|
||||
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
||||
}
|
||||
@@ -1017,7 +954,7 @@ public class AiController {
|
||||
Sentry.setExtra("Card", card.getName());
|
||||
Sentry.setExtra("SA", sa.toString());
|
||||
|
||||
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa);
|
||||
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayWithSubs(player, sa).willingToPlay();
|
||||
|
||||
// remove added extra
|
||||
Sentry.removeExtra("Card");
|
||||
@@ -1362,27 +1299,13 @@ public class AiController {
|
||||
}
|
||||
|
||||
public String getProperty(AiProps propName) {
|
||||
return AiProfileUtil.getAIProp(getPlayer().getLobbyPlayer(), propName);
|
||||
return AiProfileUtil.getProperty(getPlayer(), propName);
|
||||
}
|
||||
|
||||
public int getIntProperty(AiProps propName) {
|
||||
String prop = AiProfileUtil.getAIProp(getPlayer().getLobbyPlayer(), propName);
|
||||
|
||||
if (prop == null || prop.isEmpty()) {
|
||||
return Integer.parseInt(propName.getDefault());
|
||||
}
|
||||
|
||||
return Integer.parseInt(prop);
|
||||
return AiProfileUtil.getIntProperty(getPlayer(), propName);
|
||||
}
|
||||
|
||||
public boolean getBooleanProperty(AiProps propName) {
|
||||
String prop = AiProfileUtil.getAIProp(getPlayer().getLobbyPlayer(), propName);
|
||||
|
||||
if (prop == null || prop.isEmpty()) {
|
||||
return Boolean.parseBoolean(propName.getDefault());
|
||||
}
|
||||
|
||||
return Boolean.parseBoolean(prop);
|
||||
public boolean getBoolProperty(AiProps propName) {
|
||||
return AiProfileUtil.getBoolProperty(getPlayer(), propName);
|
||||
}
|
||||
|
||||
public AiPlayDecision canPlayFromEffectAI(Spell spell, boolean mandatory, boolean withoutPayingManaCost) {
|
||||
@@ -1393,11 +1316,11 @@ public class AiController {
|
||||
|
||||
final Card card = spell.getHostCard();
|
||||
if (spell instanceof SpellApiBased) {
|
||||
boolean chance = false;
|
||||
boolean chance;
|
||||
if (withoutPayingManaCost) {
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory);
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory).willingToPlay();
|
||||
} else {
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
|
||||
chance = SpellApiToAi.Converter.get(spell).doTrigger(player, spell, mandatory);
|
||||
}
|
||||
if (!chance) {
|
||||
return AiPlayDecision.TargetingFailed;
|
||||
@@ -1531,7 +1454,7 @@ public class AiController {
|
||||
CardCollection inHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.NON_LANDS);
|
||||
CardCollectionView otb = player.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
if (getBooleanProperty(AiProps.HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS)) {
|
||||
if (getBoolProperty(AiProps.HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS)) {
|
||||
if (!otb.anyMatch(CardPredicates.NON_LANDS)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1710,8 +1633,10 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<SpellAbility> future = executor.submit(() -> {
|
||||
// in case of infinite loop reset below would not be reached
|
||||
timeoutReached = false;
|
||||
|
||||
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
|
||||
//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)) {
|
||||
@@ -1720,6 +1645,11 @@ public class AiController {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timeoutReached) {
|
||||
timeoutReached = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (sa.getHostCard().hasKeyword(Keyword.STORM)
|
||||
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
|
||||
&& player.getZone(ZoneType.Hand).contains(
|
||||
@@ -1789,11 +1719,21 @@ public class AiController {
|
||||
return null;
|
||||
});
|
||||
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
Thread t = new Thread(future);
|
||||
t.start();
|
||||
try {
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
future.cancel(true);
|
||||
try {
|
||||
t.stop();
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
// Android and Java 20 dropped support to stop so sadly thread will keep running
|
||||
timeoutReached = true;
|
||||
future.cancel(true);
|
||||
// TODO wait a few more seconds to try and exit at a safe point before letting the engine continue
|
||||
// TODO mark some as skipped to increase chance to find something playable next priority
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1804,9 +1744,9 @@ public class AiController {
|
||||
|
||||
for (int i = 0; i < numToExile; i++) {
|
||||
Card chosen = null;
|
||||
for (final Card c : grave) { // Exile noncreatures first in
|
||||
// case we can revive. Might wanna do some additional
|
||||
// checking here for Flashback and the like.
|
||||
for (final Card c : grave) {
|
||||
// Exile noncreatures first in case we can revive
|
||||
// Might wanna do some additional checking here for Flashback and the like
|
||||
if (!c.isCreature()) {
|
||||
chosen = c;
|
||||
break;
|
||||
@@ -1827,12 +1767,12 @@ public class AiController {
|
||||
return toExile;
|
||||
}
|
||||
|
||||
public boolean doTrigger(SpellAbility spell, boolean mandatory) {
|
||||
if (spell instanceof WrappedAbility)
|
||||
return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory);
|
||||
if (spell.getApi() != null)
|
||||
return SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
|
||||
if (spell.getPayCosts() == Cost.Zero && !spell.usesTargeting()) {
|
||||
public boolean doTrigger(SpellAbility sa, boolean mandatory) {
|
||||
if (sa instanceof WrappedAbility)
|
||||
return doTrigger(((WrappedAbility) sa).getWrappedAbility(), mandatory);
|
||||
if (sa.getApi() != null)
|
||||
return SpellApiToAi.Converter.get(sa).doTrigger(player, sa, mandatory);
|
||||
if (sa.getPayCosts() == Cost.Zero && !sa.usesTargeting()) {
|
||||
// For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
|
||||
return true;
|
||||
}
|
||||
@@ -1845,14 +1785,9 @@ public class AiController {
|
||||
* @param sa the sa
|
||||
* @return true, if successful
|
||||
*/
|
||||
public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) {
|
||||
Card hostCard = effect.getHostCard();
|
||||
if (hostCard.hasAlternateState()) {
|
||||
hostCard = game.getCardState(hostCard);
|
||||
}
|
||||
|
||||
public final boolean aiShouldRun(final CardTraitBase effect, final SpellAbility sa, final Card host, final GameEntity affected) {
|
||||
if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) {
|
||||
final Player controller = hostCard.getController();
|
||||
final Player controller = host.getController();
|
||||
if (affected instanceof Player) {
|
||||
return !((Player) affected).isOpponentOf(controller);
|
||||
}
|
||||
@@ -1861,7 +1796,6 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
if (effect.hasParam("AICheckSVar")) {
|
||||
System.out.println("aiShouldRun?" + sa);
|
||||
final String svarToCheck = effect.getParam("AICheckSVar");
|
||||
String comparator = "GE";
|
||||
int compareTo = 1;
|
||||
@@ -1874,9 +1808,9 @@ public class AiController {
|
||||
compareTo = Integer.parseInt(strCmpTo);
|
||||
} catch (final Exception ignored) {
|
||||
if (sa == null) {
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect);
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), effect);
|
||||
} else {
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa);
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1884,13 +1818,12 @@ public class AiController {
|
||||
int left = 0;
|
||||
|
||||
if (sa == null) {
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect);
|
||||
left = AbilityUtils.calculateAmount(host, svarToCheck, effect);
|
||||
} else {
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
|
||||
left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
|
||||
}
|
||||
System.out.println("aiShouldRun?" + left + comparator + compareTo);
|
||||
return Expressions.compare(left, comparator, compareTo);
|
||||
} else if (effect.hasParam("AICheckDredge")) {
|
||||
} else if (effect.isKeyword(Keyword.DREDGE)) {
|
||||
return player.getCardsIn(ZoneType.Library).size() > 8 || player.isCardInPlay("Laboratory Maniac");
|
||||
} else return sa != null && doTrigger(sa, false);
|
||||
}
|
||||
|
||||
@@ -29,12 +29,15 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
private final CardCollection tapped;
|
||||
|
||||
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) {
|
||||
this(ai0, sa, effect, false);
|
||||
}
|
||||
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect, final boolean payMana) {
|
||||
super(ai0, effect, sa, sa.getHostCard());
|
||||
|
||||
discarded = new CardCollection();
|
||||
tapped = new CardCollection();
|
||||
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
|
||||
if (tappedForMana != null) {
|
||||
if (!payMana && tappedForMana != null) {
|
||||
tapped.addAll(tappedForMana);
|
||||
}
|
||||
}
|
||||
@@ -110,7 +113,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability);
|
||||
}
|
||||
return PaymentDecision.card(randomSubset);
|
||||
} else if (type.equals("DifferentNames")) {
|
||||
} else if (type.contains("+WithDifferentNames")) {
|
||||
CardCollection differentNames = new CardCollection();
|
||||
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
|
||||
while (c > 0) {
|
||||
@@ -563,7 +566,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
|
||||
if (thisRemove > 0) {
|
||||
removed += thisRemove;
|
||||
table.put(null, prefCard, CounterType.get(cType), thisRemove);
|
||||
table.put(null, prefCard, cType, thisRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -573,7 +576,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
@Override
|
||||
public PaymentDecision visit(CostRemoveAnyCounter cost) {
|
||||
final int c = cost.getAbilityAmount(ability);
|
||||
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
|
||||
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source);
|
||||
|
||||
if (c <= 0) {
|
||||
return null;
|
||||
@@ -716,7 +719,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
|
||||
if (over > 0) {
|
||||
toRemove += over;
|
||||
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
|
||||
table.put(null, crd, CounterEnumType.QUEST, over);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -767,6 +770,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
public PaymentDecision visit(CostRemoveCounter cost) {
|
||||
final String amount = cost.getAmount();
|
||||
final String type = cost.getType();
|
||||
final GameEntityCounterTable counterTable = new GameEntityCounterTable();
|
||||
|
||||
// TODO Help AI filter card with most useless counters and put those counters in countertable for things like
|
||||
// Moxite Refinery, similar to CostRemoveAnyCounter
|
||||
// Probably a lot of that decision making can be re-used or pulled out for both PaymentDecisions to use
|
||||
if (cost.counter == null) return null;
|
||||
|
||||
int c;
|
||||
|
||||
@@ -795,7 +804,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
}
|
||||
for (Card card : typeList) {
|
||||
if (card.getCounters(cost.counter) >= c) {
|
||||
return PaymentDecision.card(card, c);
|
||||
counterTable.put(null, card, cost.counter, c);
|
||||
return PaymentDecision.counters(counterTable);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -806,7 +816,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PaymentDecision.card(source, c);
|
||||
counterTable.put(null, source, cost.counter, c);
|
||||
return PaymentDecision.counters(counterTable);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,21 +1,52 @@
|
||||
package forge.ai;
|
||||
|
||||
public enum AiPlayDecision {
|
||||
WillPlay,
|
||||
// Play decision reasons
|
||||
WillPlay,
|
||||
MandatoryPlay,
|
||||
PlayToEmptyHand,
|
||||
ImpactCombat,
|
||||
ResponseToStackResolve,
|
||||
AddBoardPresence,
|
||||
Removal,
|
||||
Tempo,
|
||||
CardAdvantage,
|
||||
|
||||
// Play later decisions
|
||||
WaitForCombat,
|
||||
WaitForMain2,
|
||||
WaitForEndOfTurn,
|
||||
StackNotEmpty,
|
||||
AnotherTime,
|
||||
|
||||
// Don't play decision reasons
|
||||
CantPlaySa,
|
||||
CantPlayAi,
|
||||
CantAfford,
|
||||
CantAffordX,
|
||||
WaitForMain2,
|
||||
AnotherTime,
|
||||
DoesntImpactCombat,
|
||||
DoesntImpactGame,
|
||||
MissingLogic,
|
||||
MissingNeededCards,
|
||||
TimingRestrictions,
|
||||
MissingPhaseRestrictions,
|
||||
ConditionsNotMet,
|
||||
NeedsToPlayCriteriaNotMet,
|
||||
StopRunawayActivations,
|
||||
TargetingFailed,
|
||||
CostNotAcceptable,
|
||||
LifeInDanger,
|
||||
WouldDestroyLegend,
|
||||
WouldDestroyOtherPlaneswalker,
|
||||
WouldBecomeZeroToughnessCreature,
|
||||
WouldDestroyWorldEnchantment,
|
||||
BadEtbEffects,
|
||||
CurseEffects
|
||||
CurseEffects;
|
||||
|
||||
public boolean willingToPlay() {
|
||||
return switch (this) {
|
||||
case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, ImpactCombat, ResponseToStackResolve, Removal, Tempo, CardAdvantage -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package forge.ai;
|
||||
|
||||
import forge.LobbyPlayer;
|
||||
import forge.game.player.Player;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.FileUtil;
|
||||
import forge.util.TextUtil;
|
||||
@@ -113,6 +114,23 @@ public class AiProfileUtil {
|
||||
return profileMap;
|
||||
}
|
||||
|
||||
public static String getProperty(final Player p, final AiProps propName) {
|
||||
String prop = AiProfileUtil.getAIProp(p.getLobbyPlayer(), propName);
|
||||
|
||||
if (prop == null || prop.isEmpty()) {
|
||||
// TODO if p is human try to predict some values from previous plays or something
|
||||
return propName.getDefault();
|
||||
}
|
||||
|
||||
return prop;
|
||||
}
|
||||
public static int getIntProperty(final Player p, final AiProps propName) {
|
||||
return Integer.parseInt(getProperty(p, propName));
|
||||
}
|
||||
public static boolean getBoolProperty(final Player p, final AiProps propName) {
|
||||
return Boolean.parseBoolean(getProperty(p, propName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an AI property value for the current profile.
|
||||
*
|
||||
|
||||
@@ -345,27 +345,24 @@ public class ComputerUtil {
|
||||
return sacMeList.getFirst();
|
||||
} else {
|
||||
// empty sacMeList, so get some viable average preference if the option is enabled
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
boolean enableDefaultPref = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ENABLE);
|
||||
if (enableDefaultPref) {
|
||||
int minCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MIN_CMC);
|
||||
int maxCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CMC);
|
||||
int maxCreatureEval = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL);
|
||||
boolean allowTokens = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS);
|
||||
List<String> dontSac = Arrays.asList("Black Lotus", "Mox Pearl", "Mox Jet", "Mox Emerald", "Mox Ruby", "Mox Sapphire", "Lotus Petal");
|
||||
CardCollection allowList = CardLists.filter(typeList, card -> {
|
||||
if (card.isCreature() && ComputerUtilCard.evaluateCreature(card) > maxCreatureEval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (allowTokens && card.isToken())
|
||||
|| (card.getCMC() >= minCMC && card.getCMC() <= maxCMC && !dontSac.contains(card.getName()));
|
||||
});
|
||||
if (!allowList.isEmpty()) {
|
||||
CardLists.sortByCmcDesc(allowList);
|
||||
return allowList.getLast();
|
||||
boolean enableDefaultPref = AiProfileUtil.getBoolProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_ENABLE);
|
||||
if (enableDefaultPref) {
|
||||
int minCMC = AiProfileUtil.getIntProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_MIN_CMC);
|
||||
int maxCMC = AiProfileUtil.getIntProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_MAX_CMC);
|
||||
int maxCreatureEval = AiProfileUtil.getIntProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL);
|
||||
boolean allowTokens = AiProfileUtil.getBoolProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS);
|
||||
List<String> dontSac = Arrays.asList("Black Lotus", "Mox Pearl", "Mox Jet", "Mox Emerald", "Mox Ruby", "Mox Sapphire", "Lotus Petal");
|
||||
CardCollection allowList = CardLists.filter(typeList, card -> {
|
||||
if (card.isCreature() && ComputerUtilCard.evaluateCreature(card) > maxCreatureEval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (allowTokens && card.isToken())
|
||||
|| (card.getCMC() >= minCMC && card.getCMC() <= maxCMC && !dontSac.contains(card.getName()));
|
||||
});
|
||||
if (!allowList.isEmpty()) {
|
||||
CardLists.sortByCmcDesc(allowList);
|
||||
return allowList.getLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -767,7 +764,7 @@ public class ComputerUtil {
|
||||
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
|
||||
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
|
||||
|
||||
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
|
||||
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
|
||||
|
||||
if (untap) {
|
||||
typeList.remove(activate);
|
||||
@@ -864,7 +861,7 @@ public class ComputerUtil {
|
||||
|
||||
// Run non-mandatory trigger.
|
||||
// These checks only work if the Executing SpellAbility is an Ability_Sub.
|
||||
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) {
|
||||
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTrigger(ai, exSA, false)) {
|
||||
// AI would not run this trigger if given the chance
|
||||
return sacrificed;
|
||||
}
|
||||
@@ -1074,6 +1071,80 @@ public class ComputerUtil {
|
||||
return prevented;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card card = sa.getHostCard();
|
||||
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
|
||||
@@ -1245,80 +1316,6 @@ public class ComputerUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final SpellAbility sub = sa.getSubAbility();
|
||||
@@ -1327,7 +1324,6 @@ public class ComputerUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cipher spells
|
||||
if (sub != null) {
|
||||
final ApiType api = sub.getApi();
|
||||
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
|
||||
@@ -1385,13 +1381,14 @@ public class ComputerUtil {
|
||||
|
||||
// returns true if the AI should stop using the ability
|
||||
public static boolean preventRunAwayActivations(final SpellAbility sa) {
|
||||
int activations = sa.getActivationsThisTurn();
|
||||
|
||||
if (!sa.isIntrinsic()) {
|
||||
return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory
|
||||
if (!sa.isActivatedAbility()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activations < 10) { //10 activations per turn should still be acceptable
|
||||
int activations = sa.getActivationsThisTurn();
|
||||
|
||||
//10 activations should still be acceptable
|
||||
if (activations < 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1621,7 +1618,6 @@ public class ComputerUtil {
|
||||
damage = dmg;
|
||||
}
|
||||
|
||||
// Triggered abilities
|
||||
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
|
||||
for (final Trigger t : c.getTriggers()) {
|
||||
if (TriggerType.Attacks.equals(t.getMode())) {
|
||||
@@ -2000,8 +1996,7 @@ public class ComputerUtil {
|
||||
else if ((threatApi == ApiType.Attach && (topStack.isCurse() || "Curse".equals(topStack.getParam("AILogic"))))
|
||||
&& (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|
||||
|| saviourApi == ApiType.Protection || saviourApi == null)) {
|
||||
AiController aic = aiPlayer.isAI() ? ((PlayerControllerAi)aiPlayer.getController()).getAi() : null;
|
||||
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
|
||||
boolean enableCurseAuraRemoval = AiProfileUtil.getBoolProperty(aiPlayer, AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE);
|
||||
if (enableCurseAuraRemoval) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card c) {
|
||||
@@ -2042,17 +2037,14 @@ public class ComputerUtil {
|
||||
// a creature will [hopefully] die from a spell on stack
|
||||
boolean willDieFromSpell = false;
|
||||
boolean noStackCheck = false;
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
|
||||
// See if permission is on stack and ignore this check if there is and the relevant AI flag is set
|
||||
// TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
|
||||
for (SpellAbilityStackInstance si : game.getStack()) {
|
||||
SpellAbility sa = si.getSpellAbility();
|
||||
if (sa.getApi() == ApiType.Counter) {
|
||||
noStackCheck = true;
|
||||
break;
|
||||
}
|
||||
if (AiProfileUtil.getBoolProperty(ai, AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
|
||||
// See if permission is on stack and ignore this check if there is and the relevant AI flag is set
|
||||
// TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
|
||||
for (SpellAbilityStackInstance si : game.getStack()) {
|
||||
SpellAbility sa = si.getSpellAbility();
|
||||
if (sa.getApi() == ApiType.Counter) {
|
||||
noStackCheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2081,8 +2073,7 @@ public class ComputerUtil {
|
||||
* @return a filtered list with no dying creatures in it
|
||||
*/
|
||||
public static CardCollection filterCreaturesThatWillDieThisTurn(final Player ai, final CardCollection list, final SpellAbility excludeSa) {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
if (aic.getBooleanProperty(AiProps.AVOID_TARGETING_CREATS_THAT_WILL_DIE)) {
|
||||
if (AiProfileUtil.getBoolProperty(ai, AiProps.AVOID_TARGETING_CREATS_THAT_WILL_DIE)) {
|
||||
// Try to avoid targeting creatures that are dead on board
|
||||
List<Card> willBeKilled = CardLists.filter(list, card -> card.isCreature() && predictCreatureWillDieThisTurn(ai, card, excludeSa));
|
||||
list.removeAll(willBeKilled);
|
||||
@@ -2255,25 +2246,14 @@ public class ComputerUtil {
|
||||
boolean bottom = false;
|
||||
|
||||
// AI profile-based toggles
|
||||
int maxLandsToScryLandsToTop = 3;
|
||||
int minLandsToScryLandsAway = 8;
|
||||
int minCreatsToScryCreatsAway = 5;
|
||||
int minCreatEvalThreshold = 160; // just a bit higher than a baseline 2/2 creature or a 1/1 mana dork
|
||||
int lowCMCThreshold = 3;
|
||||
int maxCreatsToScryLowCMCAway = 3;
|
||||
boolean uncastablesToBottom = false;
|
||||
int uncastableCMCThreshold = 1;
|
||||
if (player.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)player.getController()).getAi();
|
||||
maxLandsToScryLandsToTop = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_STILL_NEED_MORE);
|
||||
minLandsToScryLandsAway = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_NOT_NEED_MORE);
|
||||
minCreatsToScryCreatsAway = aic.getIntProperty(AiProps.SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES);
|
||||
minCreatEvalThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE);
|
||||
lowCMCThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_CMC_THRESHOLD);
|
||||
maxCreatsToScryLowCMCAway = aic.getIntProperty(AiProps.SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC);
|
||||
uncastablesToBottom = aic.getBooleanProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM);
|
||||
uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
|
||||
}
|
||||
int maxLandsToScryLandsToTop = AiProfileUtil.getIntProperty(player, AiProps.SCRY_NUM_LANDS_TO_STILL_NEED_MORE);
|
||||
int minLandsToScryLandsAway = AiProfileUtil.getIntProperty(player, AiProps.SCRY_NUM_LANDS_TO_NOT_NEED_MORE);
|
||||
int minCreatsToScryCreatsAway = AiProfileUtil.getIntProperty(player, AiProps.SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES);
|
||||
int minCreatEvalThreshold = AiProfileUtil.getIntProperty(player, AiProps.SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE);
|
||||
int lowCMCThreshold = AiProfileUtil.getIntProperty(player, AiProps.SCRY_EVALTHR_CMC_THRESHOLD);
|
||||
int maxCreatsToScryLowCMCAway = AiProfileUtil.getIntProperty(player, AiProps.SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC);
|
||||
boolean uncastablesToBottom = AiProfileUtil.getBoolProperty(player, AiProps.SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM);
|
||||
int uncastableCMCThreshold = AiProfileUtil.getIntProperty(player, AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
|
||||
|
||||
CardCollectionView allCards = player.getAllCards();
|
||||
CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand);
|
||||
@@ -2543,7 +2523,7 @@ public class ComputerUtil {
|
||||
|
||||
boolean opponent = controller.isOpponentOf(ai);
|
||||
|
||||
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
|
||||
final CounterType p1p1Type = CounterEnumType.P1P1;
|
||||
|
||||
if (!sa.hasParam("AILogic")) {
|
||||
return Aggregates.random(options);
|
||||
@@ -2552,7 +2532,7 @@ public class ComputerUtil {
|
||||
String logic = sa.getParam("AILogic");
|
||||
switch (logic) {
|
||||
case "Torture":
|
||||
return "Torture";
|
||||
return options.get(1);
|
||||
case "GraceOrCondemnation":
|
||||
List<ZoneType> graceZones = new ArrayList<ZoneType>();
|
||||
graceZones.add(ZoneType.Battlefield);
|
||||
@@ -2560,12 +2540,12 @@ public class ComputerUtil {
|
||||
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
|
||||
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
|
||||
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
|
||||
return aiGrace > humanGrace ? "Grace" : "Condemnation";
|
||||
return options.get(aiGrace > humanGrace ? 0 : 1);
|
||||
case "CarnageOrHomage":
|
||||
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
|
||||
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
|
||||
CardCollection computerlist = ai.getCreaturesInPlay();
|
||||
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
|
||||
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
|
||||
case "Judgment":
|
||||
if (votes.isEmpty()) {
|
||||
CardCollection list = new CardCollection();
|
||||
@@ -2579,68 +2559,71 @@ public class ComputerUtil {
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "Protection":
|
||||
if (votes.isEmpty()) {
|
||||
List<String> restrictedToColors = Lists.newArrayList();
|
||||
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
|
||||
for (Object o : options) {
|
||||
if (o instanceof String) {
|
||||
restrictedToColors.add((String) o);
|
||||
}
|
||||
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
|
||||
restrictedToColors.put(sp.getOriginalDescription(), sp);
|
||||
}
|
||||
}
|
||||
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
|
||||
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
|
||||
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
|
||||
}
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "FeatherOrQuill":
|
||||
SpellAbility feather = (SpellAbility)options.get(0);
|
||||
SpellAbility quill = (SpellAbility)options.get(1);
|
||||
// try to mill opponent with Quill vote
|
||||
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
|
||||
int numQuill = votes.get("Quill").size();
|
||||
int numQuill = votes.get(quill).size();
|
||||
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
|
||||
}
|
||||
}
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? "Feather" : "Quill";
|
||||
return opponent ? feather : quill;
|
||||
}
|
||||
// if source is not on the battlefield anymore, choose +1/+1 ones
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? "Feather" : "Quill";
|
||||
return opponent ? feather : quill;
|
||||
}
|
||||
// if no hand cards, try to mill opponent
|
||||
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
return opponent ? "Quill" : "Feather";
|
||||
return opponent ? quill : feather;
|
||||
}
|
||||
|
||||
// AI has something to discard
|
||||
if (ai.equals(controller)) {
|
||||
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
|
||||
return "Quill";
|
||||
return quill;
|
||||
}
|
||||
}
|
||||
|
||||
// default card draw and discard are better than +1/+1 counter
|
||||
return opponent ? "Feather" : "Quill";
|
||||
return opponent ? feather : quill;
|
||||
case "StrengthOrNumbers":
|
||||
SpellAbility strength = (SpellAbility)options.get(0);
|
||||
SpellAbility numbers = (SpellAbility)options.get(1);
|
||||
// similar to fabricate choose +1/+1 or Token
|
||||
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
|
||||
int numStrength = votes.get("Strength").size();
|
||||
int numNumbers = votes.get("Numbers").size();
|
||||
int numStrength = votes.get(strength).size();
|
||||
int numNumbers = votes.get(numbers).size();
|
||||
|
||||
Card token = TokenAi.spawnToken(controller, saToken);
|
||||
Card token = TokenAi.spawnToken(controller, numbers);
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
return opponent ? strength : numbers;
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
return opponent ? strength : numbers;
|
||||
}
|
||||
|
||||
// token would not survive
|
||||
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
|
||||
return opponent ? "Numbers" : "Strength";
|
||||
return opponent ? numbers : strength;
|
||||
}
|
||||
|
||||
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
|
||||
@@ -2661,35 +2644,40 @@ public class ComputerUtil {
|
||||
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
|
||||
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
|
||||
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
|
||||
case "SproutOrHarvest":
|
||||
SpellAbility sprout = (SpellAbility)options.get(0);
|
||||
SpellAbility harvest = (SpellAbility)options.get(1);
|
||||
// lifegain would hurt or has no effect
|
||||
if (opponent) {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return "Harvest";
|
||||
return harvest;
|
||||
}
|
||||
} else {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return "Sprout";
|
||||
return sprout;
|
||||
}
|
||||
}
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
return opponent ? sprout : harvest;
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
return opponent ? sprout : harvest;
|
||||
}
|
||||
// TODO add Lifegain to +1/+1 counters trigger
|
||||
|
||||
// for now +1/+1 counters are better
|
||||
return opponent ? "Harvest" : "Sprout";
|
||||
return opponent ? harvest : sprout;
|
||||
case "DeathOrTaxes":
|
||||
int numDeath = votes.get("Death").size();
|
||||
int numTaxes = votes.get("Taxes").size();
|
||||
SpellAbility death = (SpellAbility)options.get(0);
|
||||
SpellAbility taxes = (SpellAbility)options.get(1);
|
||||
|
||||
int numDeath = votes.get(death).size();
|
||||
int numTaxes = votes.get(taxes).size();
|
||||
|
||||
if (opponent) {
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
@@ -2697,29 +2685,29 @@ public class ComputerUtil {
|
||||
// would need to sacrifice more creatures than AI has
|
||||
// sacrifice even more
|
||||
if (aiCreatures.size() <= numDeath) {
|
||||
return "Death";
|
||||
return death;
|
||||
}
|
||||
// would need to discard more cards than it has
|
||||
if (aiCardsInHand.size() <= numTaxes) {
|
||||
return "Taxes";
|
||||
return taxes;
|
||||
}
|
||||
|
||||
// has cards with SacMe or Token
|
||||
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
|
||||
return "Death";
|
||||
return death;
|
||||
}
|
||||
|
||||
// has cards with DiscardMe
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
|
||||
return "Taxes";
|
||||
return taxes;
|
||||
}
|
||||
|
||||
// discard is probably less worse than sacrifice
|
||||
return "Taxes";
|
||||
return taxes;
|
||||
} else {
|
||||
// ai is first voter or ally of controller
|
||||
// both are not affected, but if opponents control creatures, sacrifice is worse
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
|
||||
}
|
||||
default:
|
||||
return Iterables.getFirst(options, null);
|
||||
@@ -3097,41 +3085,38 @@ public class ComputerUtil {
|
||||
|
||||
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
|
||||
final Card source = sa.getHostCard();
|
||||
if (source == null) { return srcList; }
|
||||
|
||||
if (sa.hasParam("AITgts")) {
|
||||
CardCollection list;
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
if (aiTgts.startsWith("BetterThan")) {
|
||||
int value = 0;
|
||||
if (aiTgts.endsWith("Source")) {
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy()) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (aiTgts.contains("EvalRating.")) {
|
||||
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
|
||||
} else {
|
||||
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
}
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
||||
} else {
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
} else {
|
||||
return srcList;
|
||||
}
|
||||
if (source == null || !sa.hasParam("AITgts")) {
|
||||
return srcList;
|
||||
}
|
||||
|
||||
CardCollection list;
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
if (aiTgts.startsWith("BetterThan")) {
|
||||
int value = 0;
|
||||
if (aiTgts.endsWith("Source")) {
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy()) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (aiTgts.contains("EvalRating.")) {
|
||||
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
|
||||
} else {
|
||||
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
}
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
||||
} else {
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
}
|
||||
return srcList;
|
||||
}
|
||||
|
||||
|
||||
@@ -345,6 +345,10 @@ public class ComputerUtilAbility {
|
||||
if (source.hasSVar("AIPriorityModifier")) {
|
||||
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
|
||||
}
|
||||
// try to use it before it's gone
|
||||
if (source.isInPlay() && source.hasSVar("EndOfTurnLeavePlay")) {
|
||||
p += 1;
|
||||
}
|
||||
if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) {
|
||||
p -= 10;
|
||||
}
|
||||
|
||||
@@ -500,7 +500,7 @@ public class ComputerUtilCard {
|
||||
}
|
||||
|
||||
List<Card> lands = CardLists.filter(list, CardPredicates.LANDS);
|
||||
if (lands.size() > 6) {
|
||||
if (lands.size() > 6 || lands.size() == Iterables.size(list)) {
|
||||
return getWorstLand(lands);
|
||||
}
|
||||
|
||||
@@ -919,14 +919,14 @@ public class ComputerUtilCard {
|
||||
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
|
||||
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
|
||||
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
|
||||
for (byte c : MagicColor.WUBRG) {
|
||||
if ((colors & c) != 0) {
|
||||
return MagicColor.toLongString(c);
|
||||
}
|
||||
}
|
||||
return restrictedToColors.get(0); // no difference, there was no prominent color
|
||||
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static List<String> getColorByProminence(final List<Card> list) {
|
||||
@@ -1060,7 +1060,6 @@ public class ComputerUtilCard {
|
||||
|
||||
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
final Game game = ai.getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final PhaseType phaseType = ph.getPhase();
|
||||
@@ -1207,7 +1206,7 @@ public class ComputerUtilCard {
|
||||
}
|
||||
} else if (c.isPlaneswalker()) {
|
||||
threat = 1;
|
||||
} else if (aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS) && ((c.isArtifact() && !c.isCreature()) || (c.isEnchantment() && !c.isAura()))) {
|
||||
} else if (AiProfileUtil.getBoolProperty(ai, AiProps.ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS) && ((c.isArtifact() && !c.isCreature()) || (c.isEnchantment() && !c.isAura()))) {
|
||||
// non-creature artifacts and global enchantments with suspicious intrinsic abilities
|
||||
boolean priority = false;
|
||||
if (c.getOwner().isOpponentOf(ai) && c.getController().isOpponentOf(ai)) {
|
||||
@@ -1311,7 +1310,7 @@ public class ComputerUtilCard {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
simAI = aic.usesSimulation();
|
||||
if (!simAI) {
|
||||
holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
|
||||
holdCombatTricks = aic.getBoolProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
|
||||
chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
|
||||
}
|
||||
}
|
||||
@@ -1640,7 +1639,7 @@ public class ComputerUtilCard {
|
||||
// if we got here, Berserk will result in the pumped creature dying at EOT and the opponent will not lose
|
||||
// (other similar cards with AILogic$ Berserk that do not die only when attacking are excluded from consideration)
|
||||
if (ai.getController() instanceof PlayerControllerAi) {
|
||||
boolean aggr = ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.USE_BERSERK_AGGRESSIVELY)
|
||||
boolean aggr = ((PlayerControllerAi) ai.getController()).getAi().getBoolProperty(AiProps.USE_BERSERK_AGGRESSIVELY)
|
||||
|| sa.hasParam("AtEOT");
|
||||
if (!aggr) {
|
||||
return false;
|
||||
@@ -1819,18 +1818,18 @@ public class ComputerUtilCard {
|
||||
* @param sa Pump* or CounterPut*
|
||||
* @return
|
||||
*/
|
||||
public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) {
|
||||
public static AiAbilityDecision canPumpAgainstRemoval(Player ai, SpellAbility sa) {
|
||||
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
for (final Card card : cards) {
|
||||
if (objects.contains(card)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
}
|
||||
}
|
||||
// For pumps without targeting restrictions, just return immediately until this is fleshed out.
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
@@ -1849,11 +1848,11 @@ public class ComputerUtilCard {
|
||||
}
|
||||
if (!sa.isTargetNumberValid()) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
public static boolean isUselessCreature(Player ai, Card c) {
|
||||
@@ -1907,17 +1906,10 @@ public class ComputerUtilCard {
|
||||
return oppCards;
|
||||
}
|
||||
|
||||
boolean enablePriorityRemoval = false;
|
||||
boolean priorityRemovalOnlyInDanger = false;
|
||||
int priorityRemovalThreshold = 0;
|
||||
int lifeInDanger = 5;
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
enablePriorityRemoval = aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE);
|
||||
priorityRemovalThreshold = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD);
|
||||
priorityRemovalOnlyInDanger = aic.getBooleanProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR);
|
||||
lifeInDanger = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR);
|
||||
}
|
||||
boolean enablePriorityRemoval = AiProfileUtil.getBoolProperty(ai, AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE);
|
||||
int priorityRemovalThreshold = AiProfileUtil.getIntProperty(ai, AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD);
|
||||
boolean priorityRemovalOnlyInDanger = AiProfileUtil.getBoolProperty(ai, AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR);
|
||||
int lifeInDanger = AiProfileUtil.getIntProperty(ai, AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR);
|
||||
|
||||
if (!enablePriorityRemoval) {
|
||||
// Nothing to do here, the profile does not allow prioritizing
|
||||
|
||||
@@ -177,16 +177,16 @@ public class ComputerUtilCombat {
|
||||
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
|
||||
int damage = attacker.getNetCombatDamage();
|
||||
int sum = 0;
|
||||
if (attacked instanceof Player player && !player.canLoseLife()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ask ReplacementDamage directly
|
||||
if (isCombatDamagePrevented(attacker, attacked, damage)) {
|
||||
if (attacked instanceof Player p && !p.canLoseLife()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!attacker.hasKeyword(Keyword.INFECT)) {
|
||||
// ask ReplacementDamage directly
|
||||
if (isCombatDamagePrevented(attacker, attacked, damage)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
damage += predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities);
|
||||
sum = predictDamageTo(attacked, damage, attacker, true);
|
||||
if (attacker.hasDoubleStrike()) {
|
||||
@@ -443,13 +443,12 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
}
|
||||
|
||||
int threshold = 0;
|
||||
int maxTreshold = 0;
|
||||
if (ai.getController().isAI()) {
|
||||
threshold = ((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD);
|
||||
maxTreshold = ((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_MAX_THRESHOLD) - threshold;
|
||||
if (resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int threshold = AiProfileUtil.getIntProperty(ai, AiProps.AI_IN_DANGER_THRESHOLD);
|
||||
int maxTreshold = AiProfileUtil.getIntProperty(ai, AiProps.AI_IN_DANGER_MAX_THRESHOLD) - threshold;
|
||||
int chance = MyRandom.getRandom().nextInt(80) + 5;
|
||||
while (maxTreshold > 0) {
|
||||
if (MyRandom.getRandom().nextInt(100) < chance) {
|
||||
@@ -458,10 +457,6 @@ public class ComputerUtilCombat {
|
||||
maxTreshold--;
|
||||
}
|
||||
|
||||
if (resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife());
|
||||
}
|
||||
|
||||
@@ -974,17 +969,13 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -998,12 +989,11 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1107,17 +1097,13 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1131,12 +1117,11 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
@@ -1305,6 +1290,7 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
continue;
|
||||
@@ -1314,11 +1300,8 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
}
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
@@ -1333,13 +1316,14 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
return power;
|
||||
}
|
||||
@@ -1444,7 +1428,11 @@ public class ComputerUtilCombat {
|
||||
int damage = AbilityUtils.calculateAmount(source, sa.getParam("NumDmg"), sa);
|
||||
|
||||
toughness -= predictDamageTo(attacker, damage, source, false);
|
||||
continue;
|
||||
} else if (sa.getApi() == ApiType.EachDamage && "TriggeredAttackerLKICopy".equals(sa.getParam("Defined"))) {
|
||||
List<Card> valid = CardLists.getValidCards(source.getController().getCreaturesInPlay(), sa.getParam("ValidCards"), source.getController(), source, sa);
|
||||
// TODO: this assumes that 1 damage is dealt per creature. Improve this to check the parameter/X to determine
|
||||
// how much damage is dealt by each of the creatures in the valid list.
|
||||
toughness -= valid.size();
|
||||
} else if (ApiType.Pump.equals(sa.getApi())) {
|
||||
if (!sa.hasParam("NumDef")) {
|
||||
continue;
|
||||
@@ -1530,16 +1518,14 @@ public class ComputerUtilCombat {
|
||||
if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) {
|
||||
continue;
|
||||
}
|
||||
if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1553,10 +1539,11 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
@@ -2020,35 +2007,35 @@ public class ComputerUtilCombat {
|
||||
*
|
||||
* @param self
|
||||
* a {@link forge.game.player.Player} object.
|
||||
* @param attacker
|
||||
* @param combatant
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param block
|
||||
* @param opposedCombatants
|
||||
* @param dmgCanDeal
|
||||
* a int.
|
||||
* @param defender
|
||||
* @param overrideOrder overriding combatant order
|
||||
*/
|
||||
public static Map<Card, Integer> distributeAIDamage(final Player self, final Card attacker, final CardCollectionView block, final CardCollectionView remaining, int dmgCanDeal, GameEntity defender, boolean overrideOrder) {
|
||||
public static Map<Card, Integer> distributeAIDamage(final Player self, final Card combatant, CardCollectionView opposedCombatants, final CardCollectionView remaining, int dmgCanDeal, GameEntity defender, boolean overrideOrder) {
|
||||
Map<Card, Integer> damageMap = Maps.newHashMap();
|
||||
Combat combat = attacker.getGame().getCombat();
|
||||
Combat combat = combatant.getGame().getCombat();
|
||||
|
||||
boolean isAttacking = defender != null;
|
||||
|
||||
// Check for Banding, Defensive Formation
|
||||
boolean isAttackingMe = isAttacking && combat.getDefenderPlayerByAttacker(attacker).equals(self);
|
||||
boolean isBlockingMyBand = attacker.getController().isOpponentOf(self) && AttackingBand.isValidBand(block, true);
|
||||
boolean isAttackingMe = isAttacking && combat.getDefenderPlayerByAttacker(combatant).equals(self);
|
||||
boolean isBlockingMyBand = combatant.getController().isOpponentOf(self) && AttackingBand.isValidBand(opposedCombatants, true);
|
||||
final boolean aiDistributesBandingDmg = isAttackingMe || isBlockingMyBand;
|
||||
|
||||
final boolean hasTrample = attacker.hasKeyword(Keyword.TRAMPLE);
|
||||
final boolean hasTrample = combatant.hasKeyword(Keyword.TRAMPLE);
|
||||
|
||||
if (combat != null && remaining != null && hasTrample && attacker.isAttacking() && !aiDistributesBandingDmg) {
|
||||
if (combat != null && remaining != null && hasTrample && combatant.isAttacking() && !aiDistributesBandingDmg) {
|
||||
// if attacker has trample and some of its blockers are also blocking others it's generally a good idea
|
||||
// to assign those without trample first so we can maximize the damage to the defender
|
||||
for (final Card c : remaining) {
|
||||
if (c == attacker || c.hasKeyword(Keyword.TRAMPLE)) {
|
||||
if (c == combatant || c.hasKeyword(Keyword.TRAMPLE)) {
|
||||
continue;
|
||||
}
|
||||
final CardCollection sharedBlockers = new CardCollection(block);
|
||||
final CardCollection sharedBlockers = new CardCollection(opposedCombatants);
|
||||
sharedBlockers.retainAll(combat.getBlockers(c));
|
||||
if (!sharedBlockers.isEmpty()) {
|
||||
// signal skip for now
|
||||
@@ -2058,12 +2045,21 @@ public class ComputerUtilCombat {
|
||||
// TODO sort remaining tramplers for DamageDone triggers
|
||||
}
|
||||
|
||||
if (block.size() == 1) {
|
||||
final Card blocker = block.getFirst();
|
||||
// Order the combatants in preferred order in case legacy ordering is disabled
|
||||
if (isAttacking && overrideOrder) {
|
||||
if (combatant.isAttacking()) {
|
||||
opposedCombatants = AiBlockController.orderBlockers(combatant, new CardCollection(opposedCombatants));
|
||||
} else {
|
||||
opposedCombatants = AiBlockController.orderAttackers(combatant, new CardCollection(opposedCombatants));
|
||||
}
|
||||
}
|
||||
|
||||
if (opposedCombatants.size() == 1) {
|
||||
final Card blocker = opposedCombatants.getFirst();
|
||||
int dmgToBlocker = dmgCanDeal;
|
||||
|
||||
if (hasTrample && isAttacking && !aiDistributesBandingDmg) { // otherwise no entity to deliver damage via trample
|
||||
dmgToBlocker = getEnoughDamageToKill(blocker, dmgCanDeal, attacker, true);
|
||||
dmgToBlocker = getEnoughDamageToKill(blocker, dmgCanDeal, combatant, true);
|
||||
|
||||
if (dmgCanDeal < dmgToBlocker) {
|
||||
// can't kill so just put the lowest legal amount
|
||||
@@ -2082,9 +2078,9 @@ public class ComputerUtilCombat {
|
||||
// Does the attacker deal lethal damage to all blockers
|
||||
//Blocking Order now determined after declare blockers
|
||||
Card lastBlocker = null;
|
||||
for (final Card b : block) {
|
||||
for (final Card b : opposedCombatants) {
|
||||
lastBlocker = b;
|
||||
final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, attacker, true);
|
||||
final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, combatant, true);
|
||||
if (dmgToKill <= dmgCanDeal) {
|
||||
damageMap.put(b, dmgToKill);
|
||||
dmgCanDeal -= dmgToKill;
|
||||
@@ -2109,15 +2105,15 @@ public class ComputerUtilCombat {
|
||||
} else {
|
||||
// In the event of Banding or Defensive Formation, assign max damage to the blocker who
|
||||
// can tank all the damage or to the worst blocker to lose as little as possible
|
||||
for (final Card b : block) {
|
||||
final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, attacker, true);
|
||||
for (final Card b : opposedCombatants) {
|
||||
final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, combatant, true);
|
||||
if (dmgToKill > dmgCanDeal) {
|
||||
damageMap.put(b, dmgCanDeal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (damageMap.isEmpty()) {
|
||||
damageMap.put(ComputerUtilCard.getWorstCreatureAI(block), dmgCanDeal);
|
||||
damageMap.put(ComputerUtilCard.getWorstCreatureAI(opposedCombatants), dmgCanDeal);
|
||||
}
|
||||
}
|
||||
return damageMap;
|
||||
|
||||