Compare commits
726 Commits
9090086a47
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1960e28298 | ||
|
|
b18b24b085 | ||
|
|
f743b27696 | ||
|
|
80f249f8a2 | ||
|
|
b4aa07577f | ||
|
|
b54a47f333 | ||
|
|
0996074287 | ||
|
|
81aac701f4 | ||
|
|
a81289311f | ||
|
|
b44354315a | ||
|
|
216dcab316 | ||
|
|
c6259e5a14 | ||
|
|
2f7a2305da | ||
|
|
86d86f6ead | ||
|
|
2b6c7bd6a4 | ||
|
|
869f9ce13d | ||
|
|
0bb0f46abc | ||
|
|
2d65fcd91c | ||
|
|
d848f4a03f | ||
|
|
2d13f0b72d | ||
|
|
a6e5c9f0bc | ||
|
|
b5b4781693 | ||
|
|
17591a6ebd | ||
|
|
d078f275f8 | ||
|
|
300eee36e9 | ||
|
|
f40ecb8ca7 | ||
|
|
1813217c16 | ||
|
|
c1eec9057a | ||
|
|
742058403c | ||
|
|
b2a5b69423 | ||
|
|
366a71688d | ||
|
|
0d32d2167b | ||
|
|
9f7e78691b | ||
|
|
206be77a86 | ||
|
|
b117fe825c | ||
|
|
11215bd69f | ||
|
|
f204656f4d | ||
|
|
da6b448598 | ||
|
|
2f4420bf15 | ||
|
|
b50d69d3a5 | ||
|
|
483cc1c1e0 | ||
|
|
6123a64b37 | ||
|
|
9ff6c70faf | ||
|
|
b447f5032d | ||
|
|
51ef460d39 | ||
|
|
ee808a60a2 | ||
|
|
8de78ba7ed | ||
|
|
5334263793 | ||
|
|
c2c3abc6f0 | ||
|
|
6ec6a9c3fc | ||
|
|
d759465cf2 | ||
|
|
349d7f32cd | ||
|
|
30cb4727f6 | ||
|
|
38a21e5e72 | ||
|
|
5cf3cf70e8 | ||
|
|
259e3ebe29 | ||
|
|
c72b692560 | ||
|
|
d767944a83 | ||
|
|
21ee113d21 | ||
|
|
4aa1367b6f | ||
|
|
f1d96ff554 | ||
|
|
9130d6de11 | ||
|
|
d01294ebb4 | ||
|
|
37d89aa602 | ||
|
|
f9a971a546 | ||
|
|
00fbf1a185 | ||
|
|
77ad614258 | ||
|
|
fb1e6829b7 | ||
|
|
bea5a1efd4 | ||
|
|
015c7ce259 | ||
|
|
f394529c8c | ||
|
|
51595b0c7d | ||
|
|
0d036e1bff | ||
|
|
82dc877639 | ||
|
|
645c32e11c | ||
|
|
9f65326449 | ||
|
|
998402ebc3 | ||
|
|
7118b92522 | ||
|
|
833bd85d36 | ||
|
|
57d231cd56 | ||
|
|
6c7e8ae8ad | ||
|
|
b9c29b53d9 | ||
|
|
7d7cf699f8 | ||
|
|
d762c5766f | ||
|
|
7e4a9654b2 | ||
|
|
feee6e7c09 | ||
|
|
83e89455e2 | ||
|
|
7a582f310e | ||
|
|
f1d881b26a | ||
|
|
ca36792be3 | ||
|
|
6098682162 | ||
|
|
fafd7ea183 | ||
|
|
2b35ae0890 | ||
|
|
c1d7cd2441 | ||
|
|
bf51dabcff | ||
|
|
2ce399ff87 | ||
|
|
f991ef762f | ||
|
|
eea596c6ec | ||
|
|
a6a8c6b1a4 | ||
|
|
ce32a9c6b7 | ||
|
|
9d6a5cc3b9 | ||
|
|
7e757ebcd2 | ||
|
|
6d60edaa2a | ||
|
|
7a82f4e189 | ||
|
|
d3488c3bc6 | ||
|
|
20ddba4c5f | ||
|
|
5a23f93152 | ||
|
|
fbac66fd0b | ||
|
|
45344e6d83 | ||
|
|
ab4545b274 | ||
|
|
c789d836f1 | ||
|
|
9e93a7b198 | ||
|
|
8af1be6555 | ||
|
|
1ba75c565a | ||
|
|
18da7fd19e | ||
|
|
fe146fde50 | ||
|
|
4e756a6c46 | ||
|
|
92da7819f4 | ||
|
|
3b5f2e18b3 | ||
|
|
4829e8c263 | ||
|
|
addeb021d2 | ||
|
|
2a8c44a6d7 | ||
|
|
25751462f4 | ||
|
|
97e4664cc4 | ||
|
|
8bd8d1db2f | ||
|
|
f1c96ed37d | ||
|
|
a30a496265 | ||
|
|
d3508d7bd4 | ||
|
|
693fba5003 | ||
|
|
daaec20afb | ||
|
|
3285097d1b | ||
|
|
3add58f939 | ||
|
|
2e8f9ab64e | ||
|
|
7a02cb3626 | ||
|
|
c5a511e4fe | ||
|
|
0d7136787a | ||
|
|
51419d15cd | ||
|
|
b73f0f7157 | ||
|
|
07ffc8906d | ||
|
|
b1d17fb97d | ||
|
|
f0977ab45c | ||
|
|
e96cc3363c | ||
|
|
35e9847911 | ||
|
|
50294c10b1 | ||
|
|
ca91fca028 | ||
|
|
fcc30ffe5e | ||
|
|
fcd64c8c11 | ||
|
|
eb52890251 | ||
|
|
6e8af4e68b | ||
|
|
5e0723c182 | ||
|
|
e982300c6d | ||
|
|
fa86739ac2 | ||
|
|
6740313446 | ||
|
|
eaa7b4d85c | ||
|
|
c9841c012b | ||
|
|
f6cbde723b | ||
|
|
83f82efe1b | ||
|
|
e6a561b30f | ||
|
|
bfd99c4829 | ||
|
|
feb47f6437 | ||
|
|
73286ba78b | ||
|
|
ac0886389a | ||
|
|
25e9ae9573 | ||
|
|
6b2c825eb8 | ||
|
|
2f6bc5b6c0 | ||
|
|
0fe45517da | ||
|
|
ffc0fcaa43 | ||
|
|
2fd0aac5b6 | ||
|
|
f6e3b67be1 | ||
|
|
c47f8d9fa7 | ||
|
|
201158db5d | ||
|
|
1a108fa393 | ||
|
|
2fd42c10cf | ||
|
|
c77d789558 | ||
|
|
7370a35e9c | ||
|
|
c3590e9c9b | ||
|
|
eee8450144 | ||
|
|
4f76fb21f0 | ||
|
|
9fc97abee7 | ||
|
|
c8314bd3c0 | ||
|
|
62a28fe2d4 | ||
|
|
b13cfa0fad | ||
|
|
c47ae93027 | ||
|
|
171f07ec74 | ||
|
|
2b506574e7 | ||
|
|
1078b1ef50 | ||
|
|
f63d1cc2e2 | ||
|
|
ad58994b8e | ||
|
|
59cb089c97 | ||
|
|
3a88b27752 | ||
|
|
5c5a58af3c | ||
|
|
981f903504 | ||
|
|
7d3d92981e | ||
|
|
57f1f0c25e | ||
|
|
38ab33a765 | ||
|
|
fc54dac081 | ||
|
|
6e9c5c059f | ||
|
|
8bb392dcd6 | ||
|
|
d4364ae4b1 | ||
|
|
e5e05deadb | ||
|
|
8c9fe7a1b9 | ||
|
|
629ae6bf98 | ||
|
|
9376b35db2 | ||
|
|
755f7fd148 | ||
|
|
67f91e230e | ||
|
|
db76808e64 | ||
|
|
be10425819 | ||
|
|
795f7a6bc8 | ||
|
|
7e5ff0a58f | ||
|
|
7ac13f73f2 | ||
|
|
a1c0c2d312 | ||
|
|
4be630bd09 | ||
|
|
fce02a6250 | ||
|
|
2ddc86943e | ||
|
|
d01e3da869 | ||
|
|
9cd3f40a2f | ||
|
|
4b9a603cd4 | ||
|
|
a28e3ba334 | ||
|
|
f32492b6c9 | ||
|
|
70a2c3e8ed | ||
|
|
b47741d2a5 | ||
|
|
de1b7362c9 | ||
|
|
f9982f5249 | ||
|
|
3dcd53933a | ||
|
|
0455ff649e | ||
|
|
c3bd39f2cb | ||
|
|
824309ec44 | ||
|
|
78f310c2b3 | ||
|
|
4491c6c7f3 | ||
|
|
a00e90c74a | ||
|
|
957af3d1ec | ||
|
|
0d9d8acae0 | ||
|
|
23a6596558 | ||
|
|
550cedbf1e | ||
|
|
bb80815b01 | ||
|
|
13f902ce58 | ||
|
|
2d32051cc0 | ||
|
|
9ff8948903 | ||
|
|
1007cf24d2 | ||
|
|
9b8a7f1e37 | ||
|
|
578d0a75ef | ||
|
|
d119d2c277 | ||
|
|
7e167b1cef | ||
|
|
3e8901eec3 | ||
|
|
7d4da69f22 | ||
|
|
e70f32db79 | ||
|
|
a3e445cf2f | ||
|
|
564b1d678f | ||
|
|
c6fb411861 | ||
|
|
99129c786c | ||
|
|
e9af7af1b8 | ||
|
|
77c99cc230 | ||
|
|
24180d5b4b | ||
|
|
ed88683fa0 | ||
|
|
b229885259 | ||
|
|
3f5d97cb2f | ||
|
|
6f7fcfe28e | ||
|
|
11c98bf67b | ||
|
|
75f3d8ea5b | ||
|
|
617714ebea | ||
|
|
63cb3248b4 | ||
|
|
56652c7034 | ||
|
|
7bd2c0f2d7 | ||
|
|
a0aa58cfbe | ||
|
|
c0309061fa | ||
|
|
61080adace | ||
|
|
b6edfbcf15 | ||
|
|
f9c7e6124b | ||
|
|
5c3835f4fd | ||
|
|
62251482e4 | ||
|
|
155ccf0a48 | ||
|
|
d8b5ef950d | ||
|
|
b6af8bfb7d | ||
|
|
4b38a9d3e0 | ||
|
|
ac78e809cd | ||
|
|
ba18790156 | ||
|
|
e9493e24c4 | ||
|
|
b0e3783757 | ||
|
|
f21ab7a38c | ||
|
|
8085119439 | ||
|
|
ca9ad5db8f | ||
|
|
963adbbd1b | ||
|
|
3fe378d801 | ||
|
|
3ccd986e65 | ||
|
|
3feb535072 | ||
|
|
a916afe924 | ||
|
|
3b6b8d3c94 | ||
|
|
e98bd10dbe | ||
|
|
c360d639f2 | ||
|
|
bca94648f7 | ||
|
|
f0026972cb | ||
|
|
f9c671c089 | ||
|
|
58994a53c9 | ||
|
|
3c8a00c928 | ||
|
|
6e85a6b2db | ||
|
|
6b580c622d | ||
|
|
506d286529 | ||
|
|
030d3b8057 | ||
|
|
b295fd8f09 | ||
|
|
486f72f4a0 | ||
|
|
268b864e28 | ||
|
|
503c382646 | ||
|
|
db2f711894 | ||
|
|
aacd7b56ad | ||
|
|
c03fb3c139 | ||
|
|
d35818360f | ||
|
|
538099ff4b | ||
|
|
477faf3df3 | ||
|
|
3e2931b085 | ||
|
|
e5ec8a0de1 | ||
|
|
7c36933c06 | ||
|
|
73d2c4e1b8 | ||
|
|
57d91236a0 | ||
|
|
4f89463f9c | ||
|
|
85c43a9a72 | ||
|
|
95554cee04 | ||
|
|
afe2984075 | ||
|
|
feb387d3d5 | ||
|
|
2d31037648 | ||
|
|
8052ed60ec | ||
|
|
a533e9d89d | ||
|
|
633c29b44f | ||
|
|
ae11fe0957 | ||
|
|
84ed07d3af | ||
|
|
feaa149f04 | ||
|
|
c27bf444a5 | ||
|
|
ae94d62357 | ||
|
|
05d8169012 | ||
|
|
fb1e91cdf1 | ||
|
|
c551d7b05e | ||
|
|
d76b96b339 | ||
|
|
a198174ede | ||
|
|
0a8a84b2cc | ||
|
|
1685b509c3 | ||
|
|
ee23985055 | ||
|
|
c7e61c6f8d | ||
|
|
8f270a87f0 | ||
|
|
9fc1d055d8 | ||
|
|
672a89ed46 | ||
|
|
9f11896f7b | ||
|
|
a6b9a942ab | ||
|
|
5c4e427fab | ||
|
|
10d9f74d05 | ||
|
|
3991bcc653 | ||
|
|
898b872edd | ||
|
|
24f9b2240e | ||
|
|
8aead62116 | ||
|
|
d87f9beb81 | ||
|
|
51e9dd2094 | ||
|
|
45c4461515 | ||
|
|
bf783639c1 | ||
|
|
8723cff998 | ||
|
|
fee84cca24 | ||
|
|
a4685a9188 | ||
|
|
07eb6f1c05 | ||
|
|
bb63b0df2f | ||
|
|
7c4aa89d45 | ||
|
|
a55187e10e | ||
|
|
7eaf13893e | ||
|
|
b65cf81977 | ||
|
|
3866c93065 | ||
|
|
d7bb44973a | ||
|
|
b17c802581 | ||
|
|
0095bdf0cf | ||
|
|
8112b3f81f | ||
|
|
0ecd565774 | ||
|
|
43c0a6cf31 | ||
|
|
225c20daeb | ||
|
|
28b1d7531a | ||
|
|
0bb19320df | ||
|
|
ca5a1e926d | ||
|
|
2789dc08cb | ||
|
|
db054205b3 | ||
|
|
72dc1887d9 | ||
|
|
8a97dd0194 | ||
|
|
875a4baa29 | ||
|
|
ac78473a3e | ||
|
|
b75eb512ea | ||
|
|
560feb231a | ||
|
|
2d7cee38d3 | ||
|
|
91472df6fc | ||
|
|
bbc3a47f7a | ||
|
|
b2f3d25be0 | ||
|
|
d665f88067 | ||
|
|
acd0997cfb | ||
|
|
fcdcc5d590 | ||
|
|
553a79c795 | ||
|
|
9079715da6 | ||
|
|
ae7ff25af0 | ||
|
|
479548fa56 | ||
|
|
136149ed6b | ||
|
|
1ef101f851 | ||
|
|
007513e55c | ||
|
|
8a5b8b747d | ||
|
|
70b2872589 | ||
|
|
5021217134 | ||
|
|
ff8e4abea8 | ||
|
|
decdef29cf | ||
|
|
e30fdb7570 | ||
|
|
96ae090a3a | ||
|
|
f24fc7c643 | ||
|
|
d4926e31d6 | ||
|
|
1904ae4c0c | ||
|
|
6077844ee8 | ||
|
|
e3cc745a61 | ||
|
|
b0488e3f2e | ||
|
|
f589283572 | ||
|
|
01f9bb722a | ||
|
|
a0fdb0f3c5 | ||
|
|
89b3d23bde | ||
|
|
0e8f5427c3 | ||
|
|
ef3d7bb82b | ||
|
|
133346e3e8 | ||
|
|
2588388d9d | ||
|
|
a364f539ad | ||
|
|
677b1f1392 | ||
|
|
eaf6d7160b | ||
|
|
34c4ca0237 | ||
|
|
e4fbb155e4 | ||
|
|
21b7ddf6ae | ||
|
|
4dd61343aa | ||
|
|
453865ade2 | ||
|
|
8cce667e02 | ||
|
|
0764b8646f | ||
|
|
e4691d616b | ||
|
|
19cc7980d1 | ||
|
|
5ee270eb16 | ||
|
|
118be92dc5 | ||
|
|
c7745d8785 | ||
|
|
97b9b6fe42 | ||
|
|
78a6fe93fb | ||
|
|
d9fbb67f0c | ||
|
|
9217de3aa1 | ||
|
|
c279b811ad | ||
|
|
9e8ebd1b2b | ||
|
|
77fb93f3ea | ||
|
|
e06d97ef6f | ||
|
|
0b859f1c8e | ||
|
|
01b0c64a63 | ||
|
|
e7dac31d52 | ||
|
|
cc32223d7d | ||
|
|
ccc9b41473 | ||
|
|
1252f7bd35 | ||
|
|
5525dbee24 | ||
|
|
f765d5c7d4 | ||
|
|
9776873073 | ||
|
|
b1397e3a3e | ||
|
|
66c456c1c8 | ||
|
|
31f26655ba | ||
|
|
7914bab84e | ||
|
|
d093d872ae | ||
|
|
905fdc780a | ||
|
|
ff51c33b6c | ||
|
|
88ed8254af | ||
|
|
1dd6a70e8d | ||
|
|
f19da481c3 | ||
|
|
d3c4b3083e | ||
|
|
680630050b | ||
|
|
f6aee5b0f8 | ||
|
|
30b457c9a0 | ||
|
|
0fd4f5f9f7 | ||
|
|
a306295fe2 | ||
|
|
22a4825265 | ||
|
|
82b7d1e940 | ||
|
|
87051737da | ||
|
|
3ade8ff4f5 | ||
|
|
c0c2bb5772 | ||
|
|
4cc8b3c01c | ||
|
|
383044dd8f | ||
|
|
6ff4f70f1a | ||
|
|
8b1f613407 | ||
|
|
6c6664f011 | ||
|
|
a786599416 | ||
|
|
0a2eddd920 | ||
|
|
50790a706c | ||
|
|
dd231b081d | ||
|
|
a033bf361a | ||
|
|
2ee40c6df7 | ||
|
|
2c62a7be80 | ||
|
|
df0e132459 | ||
|
|
c37c04c1b7 | ||
|
|
b6bf9b7495 | ||
|
|
26bc4fe2ab | ||
|
|
7c4bc2966f | ||
|
|
a310697830 | ||
|
|
4ba7d8f6d0 | ||
|
|
369edd4537 | ||
|
|
a2cec7c99e | ||
|
|
ad4d4e0646 | ||
|
|
9436a0d21b | ||
|
|
45fa3d75bf | ||
|
|
4d6da23443 | ||
|
|
57dd524d9f | ||
|
|
61013fcf5c | ||
|
|
1113e52f94 | ||
|
|
91909c5755 | ||
|
|
ea340cde21 | ||
|
|
b0b9901c42 | ||
|
|
1ba3f57709 | ||
|
|
46d8239d5a | ||
|
|
301da9eeca | ||
|
|
a821b7f6b4 | ||
|
|
1f3f81d878 | ||
|
|
a1cf1d7519 | ||
|
|
3bc38b407b | ||
|
|
02e86cf16c | ||
|
|
b181257aaa | ||
|
|
220e5699cd | ||
|
|
fc504af496 | ||
|
|
8e017c9d10 | ||
|
|
ac2f31103d | ||
|
|
1af56b4ec4 | ||
|
|
1e2e18e828 | ||
|
|
6381e4da51 | ||
|
|
dc19289818 | ||
|
|
668779e8a4 | ||
|
|
f8ca4a0ae9 | ||
|
|
98fb3c5fcd | ||
|
|
2aa0bdefec | ||
|
|
65e0e0fb09 | ||
|
|
09b8c49743 | ||
|
|
f272690a31 | ||
|
|
e245d5b158 | ||
|
|
cc40e2d2e8 | ||
|
|
d550f66481 | ||
|
|
ba49852c42 | ||
|
|
a98b60dd36 | ||
|
|
30a723322c | ||
|
|
2df18425c4 | ||
|
|
721cd738d7 | ||
|
|
5a8ca8853f | ||
|
|
4f5b4c6a6b | ||
|
|
f535642109 | ||
|
|
d49b435e53 | ||
|
|
6e7f0dc4c9 | ||
|
|
5faa5e2445 | ||
|
|
1dbc5fa831 | ||
|
|
b32583ef48 | ||
|
|
50e4989b77 | ||
|
|
1470406e17 | ||
|
|
670e980dc2 | ||
|
|
61ae086332 | ||
|
|
9c9d0fc41f | ||
|
|
d9d29fbfea | ||
|
|
d66d1d8c6e | ||
|
|
517f855112 | ||
|
|
1c386b5ed0 | ||
|
|
0169ec57b4 | ||
|
|
3c36124fa7 | ||
|
|
f7407fe382 | ||
|
|
72c8fe627b | ||
|
|
f9f5b0dfa4 | ||
|
|
8e98180951 | ||
|
|
b78d7d6cbe | ||
|
|
f754ef1ad3 | ||
|
|
06108e4b6f | ||
|
|
2e96001654 | ||
|
|
4965851238 | ||
|
|
d7e5557d61 | ||
|
|
71b3c3e19f | ||
|
|
ed5be80f08 | ||
|
|
9b2696fac5 | ||
|
|
302bf17c31 | ||
|
|
d68a85bbc5 | ||
|
|
ee407befb5 | ||
|
|
5c6b703276 | ||
|
|
fb257a544f | ||
|
|
1f58461326 | ||
|
|
2e021c8b9e | ||
|
|
163299504a | ||
|
|
a3a49077b5 | ||
|
|
092176ea1a | ||
|
|
d560f9ade4 | ||
|
|
0052307686 | ||
|
|
b7ee0cdcf8 | ||
|
|
3bf9ad1c42 | ||
|
|
91c539fb8d | ||
|
|
be19c0ad8d | ||
|
|
b4f6992918 | ||
|
|
7b5edb7c65 | ||
|
|
76aad36e84 | ||
|
|
2d675aa35d | ||
|
|
1e53748ae3 | ||
|
|
6f9976ebad | ||
|
|
bd2eaa9e97 | ||
|
|
363213ccf7 | ||
|
|
66dc0cc657 | ||
|
|
6a507553d1 | ||
|
|
68d783192d | ||
|
|
e792e8d79d | ||
|
|
e0da7f09ca | ||
|
|
75a2fca8bb | ||
|
|
405f49c490 | ||
|
|
7292c44082 | ||
|
|
a8bbc39bfd | ||
|
|
168e347a82 | ||
|
|
739f629996 | ||
|
|
b47ec3b64d | ||
|
|
9e7b7eed27 | ||
|
|
1eaf0d0bc4 | ||
|
|
e1f5b9b6c3 | ||
|
|
c7fb56f95f | ||
|
|
723a97e3af | ||
|
|
e41ceff0be | ||
|
|
66c0b1c951 | ||
|
|
ef1af8259e | ||
|
|
96f1d73e35 | ||
|
|
21adc68e28 | ||
|
|
261ce67cee | ||
|
|
22c01ed11a | ||
|
|
b18bb9502a | ||
|
|
177b4925a1 | ||
|
|
91412ff821 | ||
|
|
d13082a8ca | ||
|
|
86759125a9 | ||
|
|
82d9668c9b | ||
|
|
6fd8705990 | ||
|
|
ac077dfc13 | ||
|
|
b50a884af9 | ||
|
|
c2c6a95170 | ||
|
|
ad5c9e91ae | ||
|
|
27ebb5114c | ||
|
|
b5e33c15f6 | ||
|
|
d3ef76324f | ||
|
|
f4f2ec380c | ||
|
|
1af0e03eeb | ||
|
|
6e6c8ee779 | ||
|
|
190b50cbaf | ||
|
|
5d8a6626bb | ||
|
|
6819e65160 | ||
|
|
ca678b56d1 | ||
|
|
f6f9216162 | ||
|
|
2034034c1b | ||
|
|
0f6b3f231a | ||
|
|
bc9ffe1d31 | ||
|
|
703f7ff3d7 | ||
|
|
f233a8c8d6 | ||
|
|
36dd25826b | ||
|
|
5aac24d2b9 | ||
|
|
71b0c66631 | ||
|
|
433408dddb | ||
|
|
1ee31c1689 | ||
|
|
ecf819ca61 | ||
|
|
b64123a9aa | ||
|
|
bac10b91ff | ||
|
|
9b5fe6dd83 | ||
|
|
442399b268 | ||
|
|
1eb34c7830 | ||
|
|
e1becca659 | ||
|
|
dd80f5187a | ||
|
|
a851e6aa20 | ||
|
|
0867c5bd05 | ||
|
|
6e106d646b | ||
|
|
39b38e3c80 | ||
|
|
0f44a3482c | ||
|
|
7ef5f03eb3 | ||
|
|
a5999a3e9c | ||
|
|
559a170957 | ||
|
|
f237851e42 | ||
|
|
6a03ff1bf6 | ||
|
|
529fd13668 | ||
|
|
26bb0ac268 | ||
|
|
68367db214 | ||
|
|
7be0dabf87 | ||
|
|
859980af02 | ||
|
|
c8ee1e487f | ||
|
|
f85589d208 | ||
|
|
30874c681f | ||
|
|
dffd7a9746 | ||
|
|
8e35501954 | ||
|
|
5693ec0302 | ||
|
|
640a77ec6c | ||
|
|
b65a437102 | ||
|
|
574e9ca58b | ||
|
|
df65b7a9c8 | ||
|
|
c9534d8fac | ||
|
|
aad604a589 | ||
|
|
b38be1d953 | ||
|
|
6fbf7cbc94 | ||
|
|
8fd6bc10c1 | ||
|
|
90cc5a9f5d | ||
|
|
6db800f286 | ||
|
|
5df1822217 | ||
|
|
f486c15d32 | ||
|
|
8da91783b1 | ||
|
|
6a66170677 | ||
|
|
ebba3c0eef | ||
|
|
062adefb99 | ||
|
|
9988ed85df | ||
|
|
c303d3040d | ||
|
|
c7dfd43daa | ||
|
|
9147ab0ec7 | ||
|
|
4438e81e37 | ||
|
|
2cb8c271a8 | ||
|
|
0ceab721e9 | ||
|
|
d877febcb8 | ||
|
|
8a35f8c40b | ||
|
|
6ed61ea9f1 | ||
|
|
7abdfe27cb | ||
|
|
9bec1df52f | ||
|
|
9043e20646 | ||
|
|
1420c60486 | ||
|
|
44da54c418 | ||
|
|
61a9133855 | ||
|
|
d661b6f44c | ||
|
|
3f784f4294 | ||
|
|
faa8c0e6dd | ||
|
|
34de3d53ad | ||
|
|
8f07afce83 | ||
|
|
95b8f39ea5 | ||
|
|
141f34f817 | ||
|
|
c02991a5c2 | ||
|
|
d14a0be2c8 | ||
|
|
87d0001569 | ||
|
|
8dbd52da54 | ||
|
|
34ed8788a4 | ||
|
|
a71bf6c62b | ||
|
|
83298a2d47 | ||
|
|
a5e4f35c32 | ||
|
|
0da8ebe1c1 | ||
|
|
f66d20e039 | ||
|
|
c23c56d46c | ||
|
|
0b41474968 | ||
|
|
40b0de1d51 | ||
|
|
7ec4892d73 | ||
|
|
f2a1263198 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -408,4 +408,6 @@ FodyWeavers.xsd
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
*.sln.iml
|
||||
# Stale editor backups
|
||||
*.bak
|
||||
|
||||
@@ -9,6 +9,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.UnitTests", "SVSim.Un
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.Bootstrap", "SVSim.Bootstrap\SVSim.Bootstrap.csproj", "{666786D9-9A4D-49EA-A759-39055C57F9AA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleNode", "SVSim.BattleNode\SVSim.BattleNode.csproj", "{F4549DD3-566A-4155-8D52-3A4D2A7072F7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine", "SVSim.BattleEngine\SVSim.BattleEngine.csproj", "{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine.Tests", "SVSim.BattleEngine.Tests\SVSim.BattleEngine.Tests.csproj", "{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -31,5 +37,17 @@ Global
|
||||
{666786D9-9A4D-49EA-A759-39055C57F9AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{666786D9-9A4D-49EA-A759-39055C57F9AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{666786D9-9A4D-49EA-A759-39055C57F9AA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
7
SVSim.BattleEngine.Tests/AssemblyAttributes.cs
Normal file
7
SVSim.BattleEngine.Tests/AssemblyAttributes.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
// Each engine-state fixture wraps its tests in a TestBattleScope, so AsyncLocal ambient
|
||||
// isolates per-test state (mgr/GameMgr/IsForecast/IsRandomDraw/RecoveryInfo/etc.). The
|
||||
// residual process-globals (Unity Resources shim cache, Wizard.LocalLog accumulators) are
|
||||
// now thread-safe (ConcurrentDictionary / static lock), so fixtures can run in parallel.
|
||||
using NUnit.Framework;
|
||||
|
||||
[assembly: Parallelizable(ParallelScope.Fixtures)]
|
||||
246
SVSim.BattleEngine.Tests/BattleAmbientTests.cs
Normal file
246
SVSim.BattleEngine.Tests/BattleAmbientTests.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
#nullable enable
|
||||
using SVSim.BattleEngine.Ambient;
|
||||
using NUnit.Framework;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests;
|
||||
|
||||
[TestFixture, Parallelizable(ParallelScope.Self)]
|
||||
public class BattleAmbientTests
|
||||
{
|
||||
[Test]
|
||||
public void Current_IsNull_WhenNoScope()
|
||||
{
|
||||
Assert.That(BattleAmbient.Current, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Require_Throws_WhenNoScope()
|
||||
{
|
||||
Assert.Throws<System.InvalidOperationException>(() => BattleAmbient.Require());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Enter_SetsCurrent_RestoresOnDispose()
|
||||
{
|
||||
var ctx = new BattleAmbientContext { ViewerId = 42 };
|
||||
Assert.That(BattleAmbient.Current, Is.Null);
|
||||
using (var _ = BattleAmbient.Enter(ctx))
|
||||
{
|
||||
Assert.That(BattleAmbient.Current, Is.SameAs(ctx));
|
||||
Assert.That(BattleAmbient.Require().ViewerId, Is.EqualTo(42));
|
||||
}
|
||||
Assert.That(BattleAmbient.Current, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Enter_Nested_RestoresPriorOnDispose()
|
||||
{
|
||||
var outer = new BattleAmbientContext { ViewerId = 1 };
|
||||
var inner = new BattleAmbientContext { ViewerId = 2 };
|
||||
using (var _o = BattleAmbient.Enter(outer))
|
||||
{
|
||||
Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(1));
|
||||
using (var _i = BattleAmbient.Enter(inner))
|
||||
{
|
||||
Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(2));
|
||||
}
|
||||
Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Enter_FlowsAcrossAwait()
|
||||
{
|
||||
var ctx = new BattleAmbientContext { ViewerId = 99 };
|
||||
using (var _ = BattleAmbient.Enter(ctx))
|
||||
{
|
||||
await Task.Yield();
|
||||
Assert.That(BattleAmbient.Current, Is.SameAs(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Enter_IsolatedBetweenConcurrentTasks()
|
||||
{
|
||||
var ctxA = new BattleAmbientContext { ViewerId = 100 };
|
||||
var ctxB = new BattleAmbientContext { ViewerId = 200 };
|
||||
|
||||
var taskA = Task.Run(async () => {
|
||||
using var _ = BattleAmbient.Enter(ctxA);
|
||||
await Task.Delay(20);
|
||||
return BattleAmbient.Current!.ViewerId;
|
||||
});
|
||||
var taskB = Task.Run(async () => {
|
||||
using var _ = BattleAmbient.Enter(ctxB);
|
||||
await Task.Delay(20);
|
||||
return BattleAmbient.Current!.ViewerId;
|
||||
});
|
||||
|
||||
var results = await Task.WhenAll(taskA, taskB);
|
||||
Assert.That(results[0], Is.EqualTo(100));
|
||||
Assert.That(results[1], Is.EqualTo(200));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsForecast_ReadsAmbient_WhenScopeActive()
|
||||
{
|
||||
var ctx = new BattleAmbientContext { IsForecast = false };
|
||||
using var _ = BattleAmbient.Enter(ctx);
|
||||
Assert.That(BattleManagerBase.IsForecast, Is.False);
|
||||
ctx.IsForecast = true;
|
||||
Assert.That(BattleManagerBase.IsForecast, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsForecast_WriteInsideScope_WritesAmbient_NotFallback()
|
||||
{
|
||||
var ctx = new BattleAmbientContext { IsForecast = false };
|
||||
using (var _ = BattleAmbient.Enter(ctx))
|
||||
{
|
||||
BattleManagerBase.IsForecast = true;
|
||||
Assert.That(ctx.IsForecast, Is.True);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsForecast_OutsideScope_GetAndSetThrow()
|
||||
{
|
||||
// Post-Task-8: fallback is gone. Both get and set go through BattleAmbient.Require(),
|
||||
// which throws when no scope is active. This is the forcing function — any unwrapped
|
||||
// engine code that touches IsForecast fails fast instead of silently writing a static.
|
||||
Assert.That(BattleAmbient.Current, Is.Null);
|
||||
Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsForecast; });
|
||||
Assert.Throws<System.InvalidOperationException>(() => BattleManagerBase.IsForecast = true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsRandomDraw_OutsideScope_GetAndSetThrow_InsideScope_Roundtrips()
|
||||
{
|
||||
// Post-Task-8: get/set both Require() a scope. Inside a scope, writes land on the ctx.
|
||||
Assert.That(BattleAmbient.Current, Is.Null);
|
||||
Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsRandomDraw; });
|
||||
Assert.Throws<System.InvalidOperationException>(() => BattleManagerBase.IsRandomDraw = true);
|
||||
|
||||
var ctx = new BattleAmbientContext { IsRandomDraw = false };
|
||||
using (var _ = BattleAmbient.Enter(ctx))
|
||||
{
|
||||
Assert.That(BattleManagerBase.IsRandomDraw, Is.False);
|
||||
BattleManagerBase.IsRandomDraw = true;
|
||||
Assert.That(ctx.IsRandomDraw, Is.True);
|
||||
}
|
||||
|
||||
// Scope disposed -> back to throwing on access.
|
||||
Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsRandomDraw; });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetIns_ReadsAmbient_WhenScopeActive()
|
||||
{
|
||||
var fakeMgr = (BattleManagerBase)System.Runtime.Serialization
|
||||
.FormatterServices.GetUninitializedObject(typeof(BattleManagerBase));
|
||||
var ctx = new BattleAmbientContext { Mgr = fakeMgr };
|
||||
using var _ = BattleAmbient.Enter(ctx);
|
||||
Assert.That(BattleManagerBase.GetIns(), Is.SameAs(fakeMgr));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetIns_OutsideScope_ReturnsNull()
|
||||
{
|
||||
// Post-Task-8: fallback is gone. GetIns() reads Current?.Mgr (soft, kept null-tolerant so
|
||||
// engine call sites that pattern `GetIns()?.Foo ?? default` still compose). With no scope
|
||||
// active, Current is null, so GetIns() returns null.
|
||||
Assert.That(BattleAmbient.Current, Is.Null);
|
||||
Assert.That(BattleManagerBase.GetIns(), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ViewerId_ReadsAmbient_WhenScopeActive()
|
||||
{
|
||||
var ctx = new BattleAmbientContext { ViewerId = 12345 };
|
||||
using var _ = BattleAmbient.Enter(ctx);
|
||||
Assert.That(Cute.Certification.ViewerId, Is.EqualTo(12345));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RealTimeNetworkAgent_ReadsAmbient_WhenScopeActive()
|
||||
{
|
||||
var ctx = new BattleAmbientContext();
|
||||
using var _ = BattleAmbient.Enter(ctx);
|
||||
Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.Null);
|
||||
var agent = (RealTimeNetworkAgent)System.Runtime.Serialization
|
||||
.FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent));
|
||||
ctx.NetworkAgent = agent;
|
||||
Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.SameAs(agent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetRealTimeNetworkBattle_InsideScope_WritesAmbient()
|
||||
{
|
||||
var ctx = new BattleAmbientContext();
|
||||
using var _ = BattleAmbient.Enter(ctx);
|
||||
var agent = (RealTimeNetworkAgent)System.Runtime.Serialization
|
||||
.FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent));
|
||||
Wizard.ToolboxGame.SetRealTimeNetworkBattle(agent);
|
||||
Assert.That(ctx.NetworkAgent, Is.SameAs(agent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BattleRecoveryInfo_ReadsAmbient_WhenScopeActive()
|
||||
{
|
||||
var info = (Wizard.BattleRecoveryInfo)System.Runtime.Serialization
|
||||
.FormatterServices.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo));
|
||||
var ctx = new BattleAmbientContext { RecoveryInfo = info };
|
||||
using var _ = BattleAmbient.Enter(ctx);
|
||||
Assert.That(Wizard.Data.BattleRecoveryInfo, Is.SameAs(info));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BattleRecoveryInfo_SetInsideScope_WritesAmbient()
|
||||
{
|
||||
var ctx = new BattleAmbientContext();
|
||||
using var _ = BattleAmbient.Enter(ctx);
|
||||
var info = (Wizard.BattleRecoveryInfo)System.Runtime.Serialization
|
||||
.FormatterServices.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo));
|
||||
Wizard.Data.BattleRecoveryInfo = info;
|
||||
Assert.That(ctx.RecoveryInfo, Is.SameAs(info));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GameMgr_GetIns_InsideScope_ReturnsScopeInstance()
|
||||
{
|
||||
var mgr = new GameMgr();
|
||||
var ctx = new BattleAmbientContext { GameMgr = mgr };
|
||||
using var _ = BattleAmbient.Enter(ctx);
|
||||
Assert.That(GameMgr.GetIns(), Is.SameAs(mgr));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GameMgr_GetIns_OutsideScope_Throws()
|
||||
{
|
||||
Assert.That(BattleAmbient.Current, Is.Null);
|
||||
Assert.Throws<System.InvalidOperationException>(() => GameMgr.GetIns());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GameMgr_GetIns_IsolatedBetweenConcurrentTasks()
|
||||
{
|
||||
var mgrA = new GameMgr();
|
||||
var mgrB = new GameMgr();
|
||||
|
||||
var taskA = Task.Run(async () => {
|
||||
using var _ = BattleAmbient.Enter(new BattleAmbientContext { GameMgr = mgrA });
|
||||
await Task.Delay(20);
|
||||
return GameMgr.GetIns();
|
||||
});
|
||||
var taskB = Task.Run(async () => {
|
||||
using var _ = BattleAmbient.Enter(new BattleAmbientContext { GameMgr = mgrB });
|
||||
await Task.Delay(20);
|
||||
return GameMgr.GetIns();
|
||||
});
|
||||
var results = await Task.WhenAll(taskA, taskB);
|
||||
Assert.That(results[0], Is.SameAs(mgrA));
|
||||
Assert.That(results[1], Is.SameAs(mgrB));
|
||||
}
|
||||
}
|
||||
56
SVSim.BattleEngine.Tests/BattleCardViewShimTests.cs
Normal file
56
SVSim.BattleEngine.Tests/BattleCardViewShimTests.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using Wizard.Battle.View;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// Regression for the in-play metamorphose NRE diagnosed 2026-06-07 (bid 283192092460).
|
||||
//
|
||||
// The IsRecovery card-create delegate (NetworkBattleManagerBase.cs:379) passes null for the
|
||||
// cardGameObject, which left BattleCardView.GameObject null. Skill_metamorphose.cs:147 in the
|
||||
// IsInplay branch then NRE'd on the unguarded
|
||||
// metamorphosedCard.BattleCardView.GameObject.transform.rotation = Quaternion.identity
|
||||
// — a purely cosmetic transform reset that has no corresponding state mutation, but tripped over
|
||||
// null-GameObject before the surrounding mutations (ReplaceInPlay, SetUpInplay,
|
||||
// FlagCardAsDestroyedBySkill, RemoveFromInPlay) could complete.
|
||||
//
|
||||
// Fix: ViewUiTouchStubs.cs's BattleCardView.GameObject is now lazily non-null (matches the
|
||||
// existing Component.gameObject pattern at UnityShim.cs:94). The shim materializes a no-op
|
||||
// GameObject on first read; the cosmetic touch resolves to a no-op assignment instead of NRE.
|
||||
[TestFixture]
|
||||
public class BattleCardViewShimTests
|
||||
{
|
||||
[Test]
|
||||
public void GameObject_is_lazily_non_null_so_unguarded_recovery_touches_no_op()
|
||||
{
|
||||
var view = new BattleCardView();
|
||||
|
||||
Assert.That(view.GameObject, Is.Not.Null,
|
||||
"BattleCardView.GameObject must be lazily non-null in the shim so unguarded " +
|
||||
"Unity touches on the IsRecovery card-create path (which passes null cardGameObject) " +
|
||||
"resolve to no-ops instead of NRE-ing.");
|
||||
|
||||
Assert.That(view.GameObject.transform, Is.Not.Null,
|
||||
"GameObject.transform must follow the shim's lazy non-null Component pattern (UnityShim.cs:94).");
|
||||
|
||||
Assert.DoesNotThrow(() => view.GameObject.transform.rotation = Quaternion.identity,
|
||||
"Skill_metamorphose.cs:147's cosmetic transform.rotation reset on the in-play branch must " +
|
||||
"not throw in the headless IsRecovery path (live bid 283192092460: A's Petrification " +
|
||||
"on B's in-play card).");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GameObject_is_stable_across_reads_so_a_set_followed_by_read_returns_the_same_instance()
|
||||
{
|
||||
// Lazy materialization caches the GameObject on first read, so subsequent reads return
|
||||
// the same instance — required for any code path that reads .GameObject, mutates it,
|
||||
// and reads again (e.g. follower position/rotation/scale set in sequence).
|
||||
var view = new BattleCardView();
|
||||
var first = view.GameObject;
|
||||
var second = view.GameObject;
|
||||
Assert.That(second, Is.SameAs(first),
|
||||
"lazy GameObject must cache; otherwise the second read returns a fresh instance " +
|
||||
"and any mutation on the first read is lost.");
|
||||
}
|
||||
}
|
||||
}
|
||||
105
SVSim.BattleEngine.Tests/BuffFollowerOracleTests.cs
Normal file
105
SVSim.BattleEngine.Tests/BuffFollowerOracleTests.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M4 (next-hardest deterministic card): a when_play SELF-BUFF follower resolves to correct
|
||||
// authoritative state HEADLESS via the same IsForecast/IsRecovery + ActionProcessor path the M2
|
||||
// vanilla follower and M3 fixed-damage spell proved (design §5 / DP4 + M3 resume recipe). The new
|
||||
// oracle dimension over M2/M3 is the PLAYED CARD'S OWN STAT DELTA: the fanfare `powerup`
|
||||
// `add_offense=1&add_life=1` to `target=self` must raise the follower's Atk and Life by exactly
|
||||
// those amounts over its CardCSVData base — a self-buff, so no target selection is involved.
|
||||
[TestFixture]
|
||||
public class BuffFollowerOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Self_buff_fanfare_raises_own_atk_and_life()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Minimal opponent/turn wiring (see M2/M3 oracles): opponent refs + active turn flag. The
|
||||
// self-buff's target resolver (`character=me&target=self`) reads the active player's own
|
||||
// in-play card, so the turn flag must be set before the fanfare sweeps.
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
// Seed leader life so neither leader reads as a 0-life game-over state that silently blocks
|
||||
// the play (M3 learning); this card deals no damage but the play-legality gate still checks it.
|
||||
HeadlessEngineEnv.InitLeaderLife(mgr);
|
||||
|
||||
// The card's fanfare is gated on `play_count>2` (cards.json skill_condition for 103111050).
|
||||
// The engine reads this from BattlePlayerBase.GetCurrentTurnPlayCount(); seed it past the
|
||||
// threshold via the public AddCurrentTrunPlayCount so the powerup actually fires. (Without
|
||||
// this the card resolves to the board but takes no buff — the delta-vs-base oracle is what
|
||||
// distinguishes "buff applied" from "fanfare silently gated out".)
|
||||
player.AddCurrentTrunPlayCount(5);
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.BuffFollowerId);
|
||||
|
||||
// Place the self-buff follower in the active player's hand with PP to spare; empty board.
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.BuffFollowerId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
// Pre-state snapshot.
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int inplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
int enemyHandBefore = enemy.HandCardList.Count;
|
||||
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
|
||||
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
|
||||
|
||||
// Resolve the play through the real engine.
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
|
||||
"ActionProcessor.PlayCard threw on a self-buff fanfare follower");
|
||||
|
||||
// Oracle: the own-stat delta is the new M4 dimension; the rest are the §5 follower invariants.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// Primary M4 assertion: the fanfare powerup raised the follower's own stats by exactly
|
||||
// the buff amounts over its CardCSVData base (1/1 -> 2/2).
|
||||
Assert.That(card.Atk, Is.EqualTo(cardParam.Atk + HeadlessEngineEnv.BuffAddOffense),
|
||||
"follower atk != base + fanfare add_offense");
|
||||
Assert.That(card.Life, Is.EqualTo(cardParam.Life + HeadlessEngineEnv.BuffAddLife),
|
||||
"follower life != base + fanfare add_life");
|
||||
// Cost paid.
|
||||
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
|
||||
// Follower moved hand -> board.
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(card), "card still in hand");
|
||||
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
|
||||
Assert.That(player.ClassAndInPlayCardList, Contains.Item(card), "card not in play");
|
||||
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(inplayBefore + 1), "in-play count not +1");
|
||||
// Opponent unchanged (the buff targets self, not the opponent).
|
||||
Assert.That(enemy.HandCardList.Count, Is.EqualTo(enemyHandBefore), "opponent hand changed");
|
||||
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board changed");
|
||||
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore), "opponent leader life changed");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
48
SVSim.BattleEngine.Tests/ConstructionProbeTests.cs
Normal file
48
SVSim.BattleEngine.Tests/ConstructionProbeTests.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M2 probe (go/no-go step 1): can BattleManagerBase / the two-player pair be constructed
|
||||
// HEADLESS at all? This drives the real practice init path
|
||||
// (`new SingleBattleMgr(StandardBattleMgrContentsCreator)`), which internally builds the
|
||||
// BattlePlayer + BattleEnemy pair, against the M1 shim — with NO Unity runtime.
|
||||
//
|
||||
// The point of this test is diagnostic: if construction throws, the stack trace tells us the
|
||||
// first shim gap on the *resolution* path (vs the compile path M1 already proved). We assert
|
||||
// success, but a failure here is the informative outcome we want surfaced.
|
||||
[TestFixture]
|
||||
public class ConstructionProbeTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
[Test]
|
||||
public void SingleBattleMgr_constructs_headless()
|
||||
{
|
||||
// Mirror the forecast flags the design pins (DP4 / §3): suppress VFX registration and
|
||||
// collapse wait delays. TestBattleScope already sets ctx.IsForecast=true; this line is a
|
||||
// belt-and-suspenders write through the ambient setter.
|
||||
BattleManagerBase.IsForecast = true;
|
||||
|
||||
SingleBattleMgr mgr = null;
|
||||
try
|
||||
{
|
||||
mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Fail(
|
||||
"Headless construction of SingleBattleMgr threw — first shim gap on the " +
|
||||
"resolution path:\n" + ex);
|
||||
}
|
||||
|
||||
Assert.That(mgr, Is.Not.Null);
|
||||
Assert.That(mgr.BattlePlayer, Is.Not.Null, "BattlePlayer (self) not created");
|
||||
Assert.That(mgr.BattleEnemy, Is.Not.Null, "BattleEnemy (opponent) not created");
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
}
|
||||
}
|
||||
}
|
||||
123
SVSim.BattleEngine.Tests/DrawSpellOracleTests.cs
Normal file
123
SVSim.BattleEngine.Tests/DrawSpellOracleTests.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M9 (the §5 draw oracle): a when_play DRAW spell resolves to correct authoritative state HEADLESS
|
||||
// via the same IsForecast/IsRecovery + ActionProcessor path M2-M8 proved. The NEW oracle dimension
|
||||
// is the HAND/DECK DELTA — the deck->hand transfer no prior milestone read: M3/M4/M6/M8 moved
|
||||
// stats, M2/M5/M7 the board, M3 the leader. The spell's `draw 1` must pull the single seeded deck
|
||||
// card into the caster's hand (deck -1, that exact card now in hand) while the spell itself pays
|
||||
// its cost and leaves to the cemetery.
|
||||
//
|
||||
// RNG is neutralized structurally (see HeadlessEngineEnv.DrawSpellId): every real draw selects from
|
||||
// the deck via a `random_count` filter, so the deck is seeded with EXACTLY ONE known card — a
|
||||
// single-card pool makes `random_count=1` deterministic regardless of the RandomSeed. This rides
|
||||
// the M5 prefab card-creation path (the deck card is engine-created off the null-view seam) the same
|
||||
// way the summon-token milestone did.
|
||||
[TestFixture]
|
||||
public class DrawSpellOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Draw_spell_moves_the_seeded_deck_card_into_hand()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Minimal opponent/turn wiring (see M2-M8 oracles): opponent refs + active turn flag. The
|
||||
// draw resolves onto the active player's own hand/deck (the skill filter is character=me).
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
// Seed leader life: this spell deals no damage, but the play-legality gate still rejects a
|
||||
// play when a leader reads as a 0-life game-over state (M3 learning).
|
||||
HeadlessEngineEnv.InitLeaderLife(mgr);
|
||||
|
||||
// Seed the card-template prefabs the internal (createNullView:false) creation path clones —
|
||||
// the draw VFX touches the drawn card's view layer, so keep the M5 prefab surface available.
|
||||
HeadlessEngineEnv.InitCardTemplates(mgr);
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.DrawSpellId);
|
||||
|
||||
// Seed EXACTLY ONE known card on the caster's deck (forces the random_count=1 selection),
|
||||
// and place the draw spell in hand with PP to spare.
|
||||
var deckCard = HeadlessEngineEnv.SeedDeck(mgr, HeadlessEngineEnv.DeckSeedCardId, index: 2, isPlayer: true);
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.DrawSpellId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
// Pre-state snapshot.
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int deckBefore = player.DeckCardList.Count;
|
||||
int cemeteryBefore = player.CemeteryList.Count;
|
||||
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
|
||||
|
||||
// Sanity: the to-be-drawn card starts in the deck, not the hand.
|
||||
Assert.That(player.DeckCardList, Does.Contain(deckCard), "seeded card not in deck pre-play");
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(deckCard), "seeded card already in hand pre-play");
|
||||
|
||||
// Resolve the play through the real engine.
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
|
||||
"ActionProcessor.PlayCard threw on a draw spell");
|
||||
|
||||
// Oracle: the deck->hand transfer is the new M9 dimension; the rest are the §5 spell-shaped
|
||||
// invariants proven by M3.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// Primary M9 assertion: the seeded deck card moved into the caster's hand...
|
||||
Assert.That(player.HandCardList, Does.Contain(deckCard),
|
||||
"drawn card did not land in hand");
|
||||
Assert.That(player.HandCardList.Any(c => c.CardId == HeadlessEngineEnv.DeckSeedCardId), Is.True,
|
||||
"no card with the seeded id is in hand");
|
||||
// ...and left the deck (deck -1, down to empty here).
|
||||
Assert.That(player.DeckCardList, Does.Not.Contain(deckCard), "drawn card still in deck");
|
||||
Assert.That(player.DeckCardList.Count, Is.EqualTo(deckBefore - 1), "deck count not -1");
|
||||
// The drawn card is the engine's OWN seeded deck object, not a fresh creation.
|
||||
Assert.That(deckCard.IsInHand, Is.True, "drawn card not marked in-hand");
|
||||
|
||||
// The spell itself: pays exactly its cost...
|
||||
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
|
||||
// ...leaves the hand (it is consumed, the drawn card replaces it -> net hand count flat)...
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
|
||||
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore), "hand count changed (spell -1 + draw +1 should net flat)");
|
||||
// ...resolves to the cemetery (a spell is not a follower; it never occupies the board).
|
||||
Assert.That(player.CemeteryList, Does.Contain(card), "spell did not resolve to the cemetery");
|
||||
Assert.That(player.CemeteryList.Count, Is.EqualTo(cemeteryBefore + 1), "cemetery count not +1");
|
||||
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
|
||||
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(deckCard), "drawn card wrongly placed on the board");
|
||||
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
|
||||
|
||||
// Opponent untouched (the draw is character=me).
|
||||
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board changed");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
142
SVSim.BattleEngine.Tests/DynamicValueSpellOracleTests.cs
Normal file
142
SVSim.BattleEngine.Tests/DynamicValueSpellOracleTests.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M10 (the first DYNAMIC `{}`-VALUE card — the first deliberate step beyond the four §5-named
|
||||
// oracle dimensions M2-M9 closed): a when_play spell whose effect MAGNITUDE is COMPUTED by the
|
||||
// engine from live game state, not carried as a literal. 112134010's sole skill is
|
||||
// `when_play damage={me.play_count}-1` to units; the `{}` resolves
|
||||
// (SkillOptionValue.ParseInt -> SkillFilterVariable.Parse -> SkillEnvironmentalPlayCount.Filtering)
|
||||
// to `GetCurrentTurnPlayCount() - 1`. That GetCurrentTurnPlayCount() is the SAME per-turn counter
|
||||
// M4 seeded via the public AddCurrentTrunPlayCount to drive a play_count GATE — M10 proves the seam
|
||||
// also feeds the effect VALUE.
|
||||
//
|
||||
// The new oracle dimension over every prior milestone is the ENGINE-COMPUTED VALUE: the asserted
|
||||
// damage is derived from the engine's OWN live play-count accessor (GetCurrentTurnPlayCount() - 1),
|
||||
// never a hardcoded literal. Per memory project_battle_relay_nontargeted_effects, a state-derived
|
||||
// value that the wire could NOT carry (spellboost cost) is exactly what desynced the PvP relay;
|
||||
// proving the engine resolves a `{}` value headless is the direct validation that the port (not a
|
||||
// relay) is the necessary path.
|
||||
//
|
||||
// Timing note (the M10 first-unknown, RESOLVED empirically by the first RED): the per-play
|
||||
// auto-increment AddCurrentTrunPlayCount(1) lives in ActionProcessor's OnBeforePlayCard
|
||||
// (BattlePlayerBase.cs:1400), which is subscribed by SetupActionProcessorEvent — and that is only
|
||||
// called on the OperateMgr / Prediction / OperationSimulator paths, NOT on the direct
|
||||
// `new ActionProcessor(pair).PlayCard` (DP4) path this harness uses. So the headless play does NOT
|
||||
// self-bump the per-turn play count: the skill reads EXACTLY the seeded GetCurrentTurnPlayCount()
|
||||
// and the damage == seeded - 1. (The first RED expected a +1 that this path never applies; the
|
||||
// state-derived primary assertion below was right regardless, and the concrete pins were corrected
|
||||
// to the observed no-bump behavior.)
|
||||
[TestFixture]
|
||||
public class DynamicValueSpellOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Dynamic_damage_spell_deals_engine_computed_play_count_value()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Minimal opponent/turn wiring (see M2-M6 oracles): opponent refs + active turn flag. The
|
||||
// spell's target resolver walks player -> opponent -> opponent's in-play units; the
|
||||
// `{me.play_count}` read keys on the active player's current turn.
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
// Seed leader life so neither leader reads as a 0-life game-over state (blocks plays, M3).
|
||||
HeadlessEngineEnv.InitLeaderLife(mgr);
|
||||
|
||||
// Put ONE vanilla follower on the ENEMY board. The spell is `character=both` (AoE over both
|
||||
// boards' units), but with no player-side units the only matched target is this enemy
|
||||
// follower; its base life (13) exceeds any seeded play count so it SURVIVES -> clean
|
||||
// life-delta read (no dependence on death/removal). card_type=unit excludes both leaders.
|
||||
var target = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.DynamicDamageTargetFollowerId, 0, isPlayer: false);
|
||||
|
||||
// Seed the live game state the `{}` value reads: the active player's current-turn play
|
||||
// count. This is the M4 seam (AddCurrentTrunPlayCount), here driving the VALUE not a gate.
|
||||
player.AddCurrentTrunPlayCount(HeadlessEngineEnv.DynamicSeededPlayCount);
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.DynamicDamageSpellId);
|
||||
|
||||
// Place the dynamic-value spell in the active player's hand with PP to spare.
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.DynamicDamageSpellId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
// Pre-state snapshot.
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
|
||||
int targetLifeBefore = target.Life;
|
||||
int playerLeaderLifeBefore = player.ClassAndInPlayCardList[0].Life;
|
||||
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
|
||||
|
||||
// Resolve the play through the real engine (auto-targeted AoE -> selectedCards: null).
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
|
||||
"ActionProcessor.PlayCard threw on a dynamic {}-value damage spell");
|
||||
|
||||
// The engine-computed value, derived from the engine's OWN live play-count accessor (the
|
||||
// direct-ActionProcessor path does not self-bump it, so this reads the seeded value) —
|
||||
// exactly the value the skill's `{me.play_count}-1` resolved against. NOT a hardcoded
|
||||
// literal: this is the M10 dimension (effect magnitude computed from state the wire can't
|
||||
// carry).
|
||||
int playCountAtResolution = player.GetCurrentTurnPlayCount();
|
||||
int expectedDamage = playCountAtResolution - 1;
|
||||
int actualDamage = targetLifeBefore - target.Life;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// PRIMARY M10 assertion: the damage dealt equals the engine-COMPUTED {me.play_count}-1,
|
||||
// read from live state — proving the engine resolved the `{}` expression, not a literal.
|
||||
Assert.That(actualDamage, Is.EqualTo(expectedDamage),
|
||||
"damage dealt did not equal the engine-computed {me.play_count}-1 value");
|
||||
// Concrete pins (catch a silent state-read failure where play_count would default to 0,
|
||||
// making damage -1 -> 0): the direct-ActionProcessor path applies no self-play bump, so
|
||||
// the resolution-time count is exactly the seeded value and the damage is seeded - 1.
|
||||
Assert.That(playCountAtResolution, Is.EqualTo(HeadlessEngineEnv.DynamicSeededPlayCount),
|
||||
"play count was not read as the seeded current-turn value");
|
||||
Assert.That(actualDamage, Is.EqualTo(HeadlessEngineEnv.DynamicSeededPlayCount - 1),
|
||||
"net damage did not equal seeded play_count - 1 ({me.play_count}-1 mis-resolved)");
|
||||
// Target survives (life > damage) and stays on the board; both leaders untouched
|
||||
// (card_type=unit excludes class cards).
|
||||
Assert.That(target.Life, Is.EqualTo(targetLifeBefore - expectedDamage), "target life delta wrong");
|
||||
Assert.That(enemy.ClassAndInPlayCardList, Does.Contain(target), "target unexpectedly left the board");
|
||||
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "enemy board count changed");
|
||||
Assert.That(player.ClassAndInPlayCardList[0].Life, Is.EqualTo(playerLeaderLifeBefore), "player leader damaged (unit-only AoE hit a leader)");
|
||||
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore), "enemy leader damaged (unit-only AoE hit a leader)");
|
||||
// §5 spell-shaped invariants: cost paid, spell leaves hand, does NOT occupy the board.
|
||||
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
|
||||
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
|
||||
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
|
||||
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
69
SVSim.BattleEngine.Tests/EmitPathReadOracleTests.cs
Normal file
69
SVSim.BattleEngine.Tests/EmitPathReadOracleTests.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M13 (hub O1, deterministic): the first headless observation of the EMIT path. Drive the proven M3
|
||||
// fixed-damage spell (900124030) through mgr.OperateMgr.PlayCard on a NetworkBattleManagerBase-derived
|
||||
// mgr and confirm the engine reaches its emission path (RealTimeNetworkAgent.OnEmit fires PlayActions)
|
||||
// without crashing, while the committed state still matches the M3 direct-ActionProcessor oracle.
|
||||
// Liveness only (E4); structural frame decoding + the RNG rand-list (M14) are deferred.
|
||||
[TestFixture]
|
||||
public class EmitPathReadOracleTests : NetworkEmitFixtureBase
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
// The process-global reset (IsForecast=true + clear injected agent) now lives in the shared
|
||||
// NetworkEmitFixtureBase.ResetNetworkEmitGlobals [TearDown], inherited here — see that file
|
||||
// for why the leak matters.
|
||||
|
||||
[Test]
|
||||
public void M3_spell_driven_via_OperateMgr_reaches_emit_without_crashing()
|
||||
{
|
||||
var (mgr, emitted) = HeadlessEngineEnv.NewNetworkEmitBattle();
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
int leaderLifeBefore = enemy.Class.Life;
|
||||
|
||||
var spell = HeadlessEngineEnv.CreateHeadlessHandCard(
|
||||
HeadlessEngineEnv.SpellId, index: 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(spell);
|
||||
int cost = spell.Cost;
|
||||
player.Pp = 10;
|
||||
|
||||
Assert.DoesNotThrow(
|
||||
() => mgr.OperateMgr.PlayCard(spell, isPlayer: true, selectCards: null),
|
||||
"OperateMgr.PlayCard threw driving the M3 spell through the emit path");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// Emit reached: OnEmit fired with PlayActions (the O1 liveness signal).
|
||||
Assert.That(emitted, Does.Contain(NetworkBattleDefine.NetworkBattleURI.PlayActions),
|
||||
"the engine did not reach a PlayActions emit");
|
||||
// State intact vs the M3 direct-path oracle.
|
||||
Assert.That(enemy.Class.Life, Is.EqualTo(leaderLifeBefore - 3), "enemy leader should take 3");
|
||||
Assert.That(player.Pp, Is.EqualTo(10 - cost), "PP should be paid");
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(spell), "spell should leave the hand");
|
||||
Assert.That(player.CemeteryList, Does.Contain(spell), "spell should land in the cemetery");
|
||||
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(spell), "a spell does not occupy the board");
|
||||
});
|
||||
|
||||
// Best-effort (F-E-7): with CurrentMatchingStatus seeded non-Disconnected (NewNetworkEmitBattle),
|
||||
// the flow reaches stockEmitMessageMgr.StockData(info); read it back. If the stock machinery is
|
||||
// not drivable headless this milestone, this assertion is DEFERRED to structural validation
|
||||
// (spec §6) — the OnEmit + no-throw + state checks above are the decisive O1 read on their own.
|
||||
var agent = Wizard.ToolboxGame.RealTimeNetworkAgent;
|
||||
var stocked = HeadlessEngineEnv.TryReadStockedEmitData(agent); // returns null if unreachable
|
||||
if (stocked != null)
|
||||
Assert.That(stocked, Is.Not.Empty, "the emitted dict should be stocked non-empty");
|
||||
else
|
||||
Assert.Inconclusive("payload-presence DEFERRED: stock-sequencer not drivable headless (spec §6)");
|
||||
}
|
||||
}
|
||||
}
|
||||
96
SVSim.BattleEngine.Tests/FixedDamageSpellOracleTests.cs
Normal file
96
SVSim.BattleEngine.Tests/FixedDamageSpellOracleTests.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M3 (next-hardest deterministic card): a FIXED-DAMAGE SPELL resolves to correct authoritative
|
||||
// state HEADLESS via the same IsForecast/IsRecovery + ActionProcessor path the M2 vanilla
|
||||
// follower proved (design §5 / DP4 + M3 resume recipe). The new oracle dimension over M2 is the
|
||||
// OPPONENT LEADER-LIFE DELTA: the spell's when_play `damage=3` to the enemy leader must reduce
|
||||
// that leader's Life by exactly 3, with the spell consuming its cost and NOT entering the board.
|
||||
[TestFixture]
|
||||
public class FixedDamageSpellOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
// The spell's sole skill is `damage=3` to the enemy leader (cards.json skill_option for 900124030).
|
||||
private const int ExpectedLeaderDamage = 3;
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fixed_damage_spell_reduces_opponent_leader_life()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Minimal opponent/turn wiring (see M2 oracle): opponent refs + active turn flag. The
|
||||
// spell's target resolver walks player -> opponent -> opponent's class card (the leader).
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
// Seed leader life (engine's InitializeClassLife subset) so the enemy leader is a live,
|
||||
// damageable target rather than a 0-life game-over state that blocks the play.
|
||||
HeadlessEngineEnv.InitLeaderLife(mgr);
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.SpellId);
|
||||
|
||||
// Place the spell in the active player's hand with PP to spare; empty board otherwise.
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.SpellId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
// Pre-state snapshot.
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
|
||||
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
|
||||
|
||||
// Resolve the play through the real engine.
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
|
||||
"ActionProcessor.PlayCard threw on a fixed-damage spell");
|
||||
|
||||
// Oracle: the leader-life delta is the new M3 dimension; the rest are the §5 spell-shaped invariants.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// Primary M3 assertion: opponent leader takes exactly the spell's fixed damage.
|
||||
Assert.That(enemy.ClassAndInPlayCardList[0].Life,
|
||||
Is.EqualTo(enemyLeaderLifeBefore - ExpectedLeaderDamage),
|
||||
"opponent leader life not reduced by the spell's fixed damage");
|
||||
// Cost paid.
|
||||
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
|
||||
// Spell leaves hand.
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
|
||||
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
|
||||
// A spell is not a follower: it must NOT occupy the board (resolves to graveyard).
|
||||
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
|
||||
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
|
||||
// Opponent board (leader card only) count unchanged — only its life moved.
|
||||
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board count changed");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
40
SVSim.BattleEngine.Tests/Fixtures/battle_test_cl1.ndjson
Normal file
40
SVSim.BattleEngine.Tests/Fixtures/battle_test_cl1.ndjson
Normal file
@@ -0,0 +1,40 @@
|
||||
{"ts":"2026-06-05T16:36:19.3503474Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:19.3573466Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"889788596105","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":508806643},"oppoInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":508806643,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":100314010},{"idx":2,"cardId":100314020},{"idx":3,"cardId":102324040},{"idx":4,"cardId":101324050},{"idx":5,"cardId":101024010},{"idx":6,"cardId":101314020},{"idx":7,"cardId":101311050},{"idx":8,"cardId":101311010},{"idx":9,"cardId":100314020},{"idx":10,"cardId":101321040},{"idx":11,"cardId":101024010},{"idx":12,"cardId":127011010},{"idx":13,"cardId":100314040},{"idx":14,"cardId":101314020},{"idx":15,"cardId":102331010},{"idx":16,"cardId":102324040},{"idx":17,"cardId":101334040},{"idx":18,"cardId":100321010},{"idx":19,"cardId":101324040},{"idx":20,"cardId":100314030},{"idx":21,"cardId":101324040},{"idx":22,"cardId":101311050},{"idx":23,"cardId":701341011},{"idx":24,"cardId":101324050},{"idx":25,"cardId":100314030},{"idx":26,"cardId":101311010},{"idx":27,"cardId":101321070},{"idx":28,"cardId":101024010},{"idx":29,"cardId":100314040},{"idx":30,"cardId":127011010},{"idx":31,"cardId":127011010},{"idx":32,"cardId":100314010},{"idx":33,"cardId":102334020},{"idx":34,"cardId":101334030},{"idx":35,"cardId":101341010},{"idx":36,"cardId":101321040},{"idx":37,"cardId":101314020},{"idx":38,"cardId":101321070},{"idx":39,"cardId":100321010},{"idx":40,"cardId":101334020}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:21.2805258Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":1,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:21.2820523Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:45.4884447Z","direction":"send","uri":"Swap","body":{"idxList":[3]}}
|
||||
{"ts":"2026-06-05T16:36:45.4909435Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":4}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.8360545Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":4}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"idxChangeSeed":857671914,"spin":243,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.9530582Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":5,"playSeq":6,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:49.0622004Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-05T16:36:53.9257769Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":6,"playSeq":7}}
|
||||
{"ts":"2026-06-05T16:36:53.9473080Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-05T16:36:54.4348349Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":7,"playSeq":8,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:54.4458360Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"143","key2":"17","key3":"0","key4":"141","key5":"170","key6":"0"}}}
|
||||
{"ts":"2026-06-05T16:36:54.4643354Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":7,"playSeq":9,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:54.5198350Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[23,14],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"avarice":1}}],"actionSeq":2}}
|
||||
{"ts":"2026-06-05T16:36:59.8031059Z","direction":"send","uri":"PlayActions","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[2,4,23,14],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"0"}},{"move":{"idx":[8],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"type":30}}
|
||||
{"ts":"2026-06-05T16:37:02.5213012Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,4,23,14,8],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}},{"trigger":{"isSelf":1,"avarice":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:03.0188508Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"141","key2":"175","key3":"0","key4":"141","key5":"170","key6":"0"},"type":0,"actionSeq":5,"cemetery":[1,0]}}
|
||||
{"ts":"2026-06-05T16:37:03.1346446Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":12,"playSeq":10,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:03.1561609Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:07.8849014Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":13,"playSeq":11,"playIdx":37,"type":30,"knownList":[{"idx":37,"cardId":101121020,"to":20,"spellboost":0,"attachTarget":""}]}}
|
||||
{"ts":"2026-06-05T16:37:08.1357329Z","direction":"send","uri":"Echo","body":{"playIdx":37,"orderList":[{"move":{"idx":[37],"isSelf":0,"from":10,"to":20}},{"playerParam":{"isSelf":0,"buffUnit":1}}],"type":30}}
|
||||
{"ts":"2026-06-05T16:37:09.1078628Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":14,"playSeq":12}}
|
||||
{"ts":"2026-06-05T16:37:09.6087702Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":15,"playSeq":13,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:11.0449391Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-05T16:37:11.4765571Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"141","key2":"175","key3":"0","key4":"143","key5":"170","key6":"101121070"}}}
|
||||
{"ts":"2026-06-05T16:37:11.4925578Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":15,"playSeq":14,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:11.5190593Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":8}}
|
||||
{"ts":"2026-06-05T16:37:25.1553015Z","direction":"send","uri":"PlayActions","body":{"playIdx":2,"targetList":[{"targetIdx":37,"isSelf":0,"selectSkillIndex":[1]}],"orderList":[{"move":{"idx":[2],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,23,14,8,24],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"3"}},{"move":{"idx":[37],"isSelf":0,"from":20,"to":30}},{"move":{"idx":[15],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}},{"trigger":{"isSelf":1,"avarice":1}}],"type":31}}
|
||||
{"ts":"2026-06-05T16:37:26.1829531Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[4,23,14,8,24,15],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}},{"trigger":{"isSelf":1,"avarice":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:26.6838102Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"142","key2":"334","key3":"0","key4":"145","key5":"170","key6":"0"},"type":0,"actionSeq":11,"cemetery":[2,1]}}
|
||||
{"ts":"2026-06-05T16:37:28.3338739Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":20,"playSeq":15,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:28.3556277Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[19],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-05T16:37:33.2699751Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":21,"playSeq":16}}
|
||||
{"ts":"2026-06-05T16:37:33.2873251Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,39,19],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:33.7738440Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":22,"playSeq":17,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:33.7898440Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"142","key2":"334","key3":"0","key4":"147","key5":"265","key6":"0"}}}
|
||||
{"ts":"2026-06-05T16:37:33.8063464Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":24,"playSeq":18,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:33.8323438Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":13}}
|
||||
{"ts":"2026-06-05T16:37:38.6691412Z","direction":"send","uri":"PlayActions","body":{"playIdx":14,"orderList":[{"move":{"idx":[14],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,23,8,24,15,37],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"6"}},{"move":{"idx":[36,18],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"avarice":1}}],"type":30}}
|
||||
38
SVSim.BattleEngine.Tests/Fixtures/battle_test_cl2.ndjson
Normal file
38
SVSim.BattleEngine.Tests/Fixtures/battle_test_cl2.ndjson
Normal file
@@ -0,0 +1,38 @@
|
||||
{"ts":"2026-06-05T16:36:19.3388464Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:19.3458471Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"889788596105","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":508806643},"oppoInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":508806643,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":100114010},{"idx":2,"cardId":101121080},{"idx":3,"cardId":101114010},{"idx":4,"cardId":113011010},{"idx":5,"cardId":101121020},{"idx":6,"cardId":100111010},{"idx":7,"cardId":102141010},{"idx":8,"cardId":102111060},{"idx":9,"cardId":100111070},{"idx":10,"cardId":113011010},{"idx":11,"cardId":101131050},{"idx":12,"cardId":101121080},{"idx":13,"cardId":100111010},{"idx":14,"cardId":102121010},{"idx":15,"cardId":701141011},{"idx":16,"cardId":100114010},{"idx":17,"cardId":101114050},{"idx":18,"cardId":102131020},{"idx":19,"cardId":102111060},{"idx":20,"cardId":100114010},{"idx":21,"cardId":102121030},{"idx":22,"cardId":102121030},{"idx":23,"cardId":101114050},{"idx":24,"cardId":100111070},{"idx":25,"cardId":100111020},{"idx":26,"cardId":101121110},{"idx":27,"cardId":102131030},{"idx":28,"cardId":113011010},{"idx":29,"cardId":102131010},{"idx":30,"cardId":100111020},{"idx":31,"cardId":101131020},{"idx":32,"cardId":101114050},{"idx":33,"cardId":101121010},{"idx":34,"cardId":101121080},{"idx":35,"cardId":101121110},{"idx":36,"cardId":101114010},{"idx":37,"cardId":101121020},{"idx":38,"cardId":100111020},{"idx":39,"cardId":102121010},{"idx":40,"cardId":101121010}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:21.2050506Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":0,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:21.2065539Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.8260552Z","direction":"send","uri":"Swap","body":{"idxList":[]}}
|
||||
{"ts":"2026-06-05T16:36:46.8285526Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.8295526Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":4}],"idxChangeSeed":224055814,"spin":243,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.9460536Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":0}}
|
||||
{"ts":"2026-06-05T16:36:53.9137786Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-05T16:36:54.4108350Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"141","key2":"170","key3":"0","key4":"143","key5":"17","key6":"0"},"type":0,"actionSeq":2,"cemetery":[0,0]}}
|
||||
{"ts":"2026-06-05T16:36:54.5258347Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":8,"playSeq":6,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:54.5523350Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[23,14],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"avarice":1}}]}}
|
||||
{"ts":"2026-06-05T16:36:59.8136078Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":9,"playSeq":7,"playIdx":1,"type":30,"knownList":[{"idx":1,"cardId":100314010,"to":30,"spellboost":0,"attachTarget":""}]}}
|
||||
{"ts":"2026-06-05T16:37:00.0026151Z","direction":"send","uri":"Echo","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[2,4,23,14],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"0"}},{"move":{"idx":[8],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}],"type":30}}
|
||||
{"ts":"2026-06-05T16:37:02.5313002Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":10,"playSeq":8}}
|
||||
{"ts":"2026-06-05T16:37:02.5503289Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,4,23,14,8],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}},{"trigger":{"isSelf":0,"avarice":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:03.0339655Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":11,"playSeq":9,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:03.0510647Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"141","key2":"170","key3":"0","key4":"141","key5":"175","key6":"0"}}}
|
||||
{"ts":"2026-06-05T16:37:03.0670774Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":11,"playSeq":10,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:03.1321443Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":5}}
|
||||
{"ts":"2026-06-05T16:37:07.8809043Z","direction":"send","uri":"PlayActions","body":{"playIdx":37,"orderList":[{"move":{"idx":[37],"isSelf":1,"from":10,"to":20}},{"playerParam":{"isSelf":1,"buffUnit":1}}],"type":30}}
|
||||
{"ts":"2026-06-05T16:37:09.0943648Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-05T16:37:09.5927718Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"143","key2":"170","key3":"101121070","key4":"141","key5":"175","key6":"0"},"type":0,"actionSeq":8,"cemetery":[0,1]}}
|
||||
{"ts":"2026-06-05T16:37:11.5305571Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":16,"playSeq":11,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:11.5519635Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:25.1769841Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":19,"playSeq":12,"playIdx":2,"type":31,"knownList":[{"idx":2,"cardId":100314020,"to":30,"spellboost":1,"attachTarget":""}],"oppoTargetList":[{"targetIdx":37,"isSelf":0}]}}
|
||||
{"ts":"2026-06-05T16:37:25.3675799Z","direction":"send","uri":"Echo","body":{"playIdx":2,"orderList":[{"move":{"idx":[2],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[4,23,14,8,24],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"3"}},{"move":{"idx":[37],"isSelf":1,"from":20,"to":30}},{"move":{"idx":[15],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}},{"trigger":{"isSelf":0,"avarice":1}}],"type":31}}
|
||||
{"ts":"2026-06-05T16:37:26.1899527Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":20,"playSeq":13}}
|
||||
{"ts":"2026-06-05T16:37:26.6913132Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":21,"playSeq":14,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:28.1438230Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[4,23,14,8,24,15],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}},{"trigger":{"isSelf":0,"avarice":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:28.2597994Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"145","key2":"170","key3":"0","key4":"142","key5":"334","key6":"0"}}}
|
||||
{"ts":"2026-06-05T16:37:28.2755229Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":19,"playSeq":15,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:28.3213347Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[19],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":11}}
|
||||
{"ts":"2026-06-05T16:37:33.2604742Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,39,19],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:33.7603450Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"147","key2":"265","key3":"0","key4":"142","key5":"334","key6":"0"},"type":0,"actionSeq":13,"cemetery":[1,2]}}
|
||||
{"ts":"2026-06-05T16:37:33.8438435Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":25,"playSeq":16,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:33.8648584Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:38.6786420Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":26,"playSeq":17,"playIdx":14,"type":30,"knownList":[{"idx":14,"cardId":101314020,"to":30,"spellboost":2,"attachTarget":""}]}}
|
||||
109
SVSim.BattleEngine.Tests/Fixtures/battle_test_fresh_cl1.ndjson
Normal file
109
SVSim.BattleEngine.Tests/Fixtures/battle_test_fresh_cl1.ndjson
Normal file
@@ -0,0 +1,109 @@
|
||||
{"ts":"2026-06-07T12:05:10.0824442Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:10.1134456Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"907324319325","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":742186477},"oppoInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":742186477,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":101324040},{"idx":2,"cardId":101321070},{"idx":3,"cardId":101321040},{"idx":4,"cardId":101324050},{"idx":5,"cardId":101334030},{"idx":6,"cardId":102334020},{"idx":7,"cardId":101024010},{"idx":8,"cardId":102331010},{"idx":9,"cardId":101324040},{"idx":10,"cardId":101314020},{"idx":11,"cardId":127011010},{"idx":12,"cardId":100314020},{"idx":13,"cardId":101024010},{"idx":14,"cardId":701341011},{"idx":15,"cardId":101311010},{"idx":16,"cardId":101311050},{"idx":17,"cardId":102324040},{"idx":18,"cardId":101341010},{"idx":19,"cardId":127011010},{"idx":20,"cardId":101311010},{"idx":21,"cardId":101314020},{"idx":22,"cardId":100321010},{"idx":23,"cardId":101321070},{"idx":24,"cardId":100314030},{"idx":25,"cardId":101314020},{"idx":26,"cardId":101311050},{"idx":27,"cardId":101024010},{"idx":28,"cardId":100314010},{"idx":29,"cardId":127011010},{"idx":30,"cardId":100314040},{"idx":31,"cardId":100321010},{"idx":32,"cardId":101334020},{"idx":33,"cardId":100314030},{"idx":34,"cardId":100314040},{"idx":35,"cardId":101321040},{"idx":36,"cardId":102324040},{"idx":37,"cardId":100314020},{"idx":38,"cardId":101334040},{"idx":39,"cardId":100314010},{"idx":40,"cardId":101324050}],"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:13.3684415Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":0,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:13.3699431Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:34.8570706Z","direction":"send","uri":"Swap","body":{"idxList":[2,3]}}
|
||||
{"ts":"2026-06-07T12:05:34.8895711Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":4},{"pos":2,"idx":5}],"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:34.8905684Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":4},{"pos":2,"idx":5}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"idxChangeSeed":1430655717,"spin":243,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:36.6990699Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[8],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":0}}
|
||||
{"ts":"2026-06-07T12:05:42.2485694Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,8],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-07T12:05:42.7450678Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"141","key2":"56","key3":"0","key4":"143","key5":"14","key6":"0"},"type":0,"actionSeq":2,"cemetery":[0,0]}}
|
||||
{"ts":"2026-06-07T12:05:42.8775704Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":8,"playSeq":6,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:42.9050694Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[10,16],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"avarice":1}}]}}
|
||||
{"ts":"2026-06-07T12:05:46.4670675Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":9,"playSeq":7}}
|
||||
{"ts":"2026-06-07T12:05:46.4855683Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,10,16],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}},{"trigger":{"isSelf":0,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:05:46.9690709Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":10,"playSeq":8,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:46.9860711Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"141","key2":"56","key3":"0","key4":"142","key5":"134","key6":"0"}}}
|
||||
{"ts":"2026-06-07T12:05:47.0020697Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":10,"playSeq":9,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:47.4990684Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[29],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":4}}
|
||||
{"ts":"2026-06-07T12:05:54.6460692Z","direction":"send","uri":"PlayActions","body":{"playIdx":8,"orderList":[{"move":{"idx":[8],"isSelf":1,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:05:55.7140680Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,29],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-07T12:05:56.2210693Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"143","key2":"140","key3":"102331036","key4":"142","key5":"134","key6":"0"},"type":0,"actionSeq":7,"cemetery":[0,0]}}
|
||||
{"ts":"2026-06-07T12:05:57.0875698Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":15,"playSeq":10,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:57.1090694Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[15],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-07T12:06:12.6924224Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":16,"playSeq":11,"playIdx":1,"type":30,"knownList":[{"idx":1,"cardId":102131030,"to":20,"spellboost":0,"attachTarget":"","cost":2,"clan":1,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:06:12.9394251Z","direction":"send","uri":"Echo","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":0,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:06:16.5024225Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":17,"playSeq":12}}
|
||||
{"ts":"2026-06-07T12:06:16.5194264Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,10,16,15],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-07T12:06:16.9874227Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":18,"playSeq":13,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:17.0039250Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"143","key2":"140","key3":"102331036","key4":"144","key5":"177","key6":"102131049"}}}
|
||||
{"ts":"2026-06-07T12:06:17.0209229Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":18,"playSeq":14,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:17.0494250Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[3],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":10}}
|
||||
{"ts":"2026-06-07T12:06:28.8094232Z","direction":"send","uri":"PlayActions","body":{"playIdx":3,"orderList":[{"move":{"idx":[3],"isSelf":1,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:06:29.8539237Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,29],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-07T12:06:30.3519249Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"145","key2":"140","key3":"203652104","key4":"144","key5":"177","key6":"102131049"},"type":0,"actionSeq":13,"cemetery":[0,0]}}
|
||||
{"ts":"2026-06-07T12:06:31.2029243Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":23,"playSeq":15,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:31.2239242Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-07T12:06:36.0499227Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":25,"playSeq":16,"playIdx":1,"type":10,"knownList":[{"idx":1,"cardId":102131030,"to":30,"spellboost":0,"attachTarget":"","cost":2,"clan":1,"tribe":"0"}],"oppoTargetList":[{"targetIdx":8,"isSelf":0}]}}
|
||||
{"ts":"2026-06-07T12:06:36.0879224Z","direction":"send","uri":"Echo","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":0,"from":20,"to":30}},{"move":{"idx":[8],"isSelf":1,"from":20,"to":30}}],"type":10}}
|
||||
{"ts":"2026-06-07T12:06:36.7079231Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":26,"playSeq":17}}
|
||||
{"ts":"2026-06-07T12:06:37.1924235Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":27,"playSeq":18,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:38.0604227Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,10,16,15,24],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-07T12:06:38.1769227Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"147","key2":"140","key3":"101321058","key4":"148","key5":"321","key6":"0"}}}
|
||||
{"ts":"2026-06-07T12:06:38.1919253Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":26,"playSeq":19,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:38.2194225Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":2}},{"move":{"idx":[19],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":16}}
|
||||
{"ts":"2026-06-07T12:06:46.5499241Z","direction":"send","uri":"PlayActions","body":{"playIdx":29,"keyAction":[{"type":1,"cardId":127011010,"selectCard":{"cardId":[121011010],"open":0}}],"orderList":[{"move":{"idx":[29],"isSelf":1,"from":10,"to":20}},{"add":{"idx":[41],"isSelf":1,"card":{"cardId":121011010},"isChoice":"1"}},{"move":{"idx":[41],"isSelf":1,"from":50,"to":10}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:06:50.3119230Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,19,41],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-07T12:06:50.8109234Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"149","key2":"305","key3":"228332150","key4":"148","key5":"321","key6":"0"},"type":0,"actionSeq":19,"cemetery":[1,1]}}
|
||||
{"ts":"2026-06-07T12:06:50.9109252Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":32,"playSeq":20,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:50.9319252Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[11],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-07T12:06:55.3344248Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":33,"playSeq":21,"playIdx":10,"type":30,"knownList":[{"idx":10,"cardId":101121080,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:06:55.5239284Z","direction":"send","uri":"Echo","body":{"playIdx":10,"orderList":[{"move":{"idx":[10],"isSelf":0,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:06:56.0979233Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":34,"playSeq":22}}
|
||||
{"ts":"2026-06-07T12:06:56.5964232Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":35,"playSeq":23,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:57.4474248Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,16,15,24,11],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":3}}]}}
|
||||
{"ts":"2026-06-07T12:06:57.5634280Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"149","key2":"305","key3":"228332150","key4":"150","key5":"302","key6":"101121116"}}}
|
||||
{"ts":"2026-06-07T12:06:57.5794253Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":36,"playSeq":24,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:57.6139259Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":22}}
|
||||
{"ts":"2026-06-07T12:07:02.6699249Z","direction":"send","uri":"PlayActions","body":{"playIdx":39,"orderList":[{"move":{"idx":[39],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[1,4,5,19,41],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"7"}},{"move":{"idx":[17],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}},{"trigger":{"isSelf":1,"avarice":1}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:10.2104225Z","direction":"send","uri":"PlayActions","body":{"playIdx":41,"orderList":[{"move":{"idx":[41],"isSelf":1,"from":10,"to":20}},{"move":{"idx":[6],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:17.7444250Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,19,17,6],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":3}},{"trigger":{"isSelf":1,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:07:18.2599231Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"147","key2":"221","key3":"349343345","key4":"150","key5":"302","key6":"101121116"},"type":0,"actionSeq":26,"cemetery":[2,1]}}
|
||||
{"ts":"2026-06-07T12:07:18.3594228Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":41,"playSeq":25,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:07:18.3874231Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[6],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-07T12:07:22.0834250Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":42,"playSeq":26,"playIdx":6,"type":30,"knownList":[{"idx":6,"cardId":113011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"uList":[{"idxList":[34],"from":0,"to":10,"isSelf":1,"skill":"6|19|0"}]}}
|
||||
{"ts":"2026-06-07T12:07:22.2814232Z","direction":"send","uri":"Echo","body":{"playIdx":6,"orderList":[{"move":{"idx":[6],"isSelf":0,"from":10,"to":20}},{"target":{"isSelf":0,"group":["g1"],"conditions":[{"state":0,"tribe":"eq7"}],"rand":[[0.739030951046865]]}},{"move":{"idx":"g1","isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}},{"trigger":{"isSelf":0,"avarice":1}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:25.6384231Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":43,"playSeq":27}}
|
||||
{"ts":"2026-06-07T12:07:25.6554259Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,16,15,24,11,34],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":3}},{"trigger":{"isSelf":0,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:07:26.1384241Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":44,"playSeq":28,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:07:26.1544243Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"147","key2":"221","key3":"349343345","key4":"149","key5":"540","key6":"214132162"}}}
|
||||
{"ts":"2026-06-07T12:07:26.1709251Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":45,"playSeq":29,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:07:26.2184224Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[32],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":29}}
|
||||
{"ts":"2026-06-07T12:07:34.2019228Z","direction":"send","uri":"PlayActions","body":{"playIdx":1,"targetList":[{"targetIdx":6,"isSelf":1,"selectSkillIndex":[1],"skillIndex":[1]}],"orderList":[{"move":{"idx":[1],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,5,19,17,6,32],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"20"}},{"alter":{"idx":[6],"isSelf":1,"type":"add","spellboost":"a2","attachTarget":"21"}},{"move":{"idx":[23],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}},{"trigger":{"isSelf":1,"avarice":1}}],"type":31}}
|
||||
{"ts":"2026-06-07T12:07:41.2306722Z","direction":"send","uri":"PlayActions","body":{"playIdx":17,"orderList":[{"move":{"idx":[17],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,5,19,6,32,23],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"23"}},{"add":{"idx":[42],"isSelf":1,"card":{"cardId":900311050}}},{"move":{"idx":[42],"isSelf":1,"from":50,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:46.6846799Z","direction":"send","uri":"PlayActions","body":{"playIdx":41,"targetList":[{"targetIdx":10,"isSelf":0}],"type":10}}
|
||||
{"ts":"2026-06-07T12:07:48.2356829Z","direction":"send","uri":"PlayActions","body":{"playIdx":29,"targetList":[{"targetIdx":10,"isSelf":0}],"orderList":[{"move":{"idx":[29],"isSelf":1,"from":20,"to":30}},{"move":{"idx":[10],"isSelf":0,"from":20,"to":30}}],"type":10}}
|
||||
{"ts":"2026-06-07T12:07:49.9904200Z","direction":"send","uri":"PlayActions","body":{"playIdx":3,"targetList":[{"targetIdx":6,"isSelf":0}],"orderList":[{"move":{"idx":[3],"isSelf":1,"from":20,"to":30}},{"move":{"idx":[6],"isSelf":0,"from":20,"to":30}}],"type":10}}
|
||||
{"ts":"2026-06-07T12:07:51.8734061Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[4,5,19,6,32,23],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":5}},{"trigger":{"isSelf":1,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:07:52.3726572Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"154","key2":"393","key3":"1021322270","key4":"153","key5":"540","key6":"0"},"type":0,"actionSeq":36,"cemetery":[6,3]}}
|
||||
{"ts":"2026-06-07T12:07:52.4729369Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":53,"playSeq":30,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:07:52.4946960Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":6}},{"move":{"idx":[18],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-07T12:07:57.1776003Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":54,"playSeq":31,"playIdx":34,"type":30,"knownList":[{"idx":34,"cardId":113011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"uList":[{"idxList":[5],"from":0,"to":10,"isSelf":1,"skill":"34|28|0"}]}}
|
||||
{"ts":"2026-06-07T12:07:57.2503917Z","direction":"send","uri":"Echo","body":{"playIdx":34,"orderList":[{"move":{"idx":[34],"isSelf":0,"from":10,"to":20}},{"target":{"isSelf":0,"group":["g1"],"conditions":[{"state":0,"tribe":"eq7"}],"rand":[[0.668529128501438]]}},{"move":{"idx":"g1","isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}},{"trigger":{"isSelf":0,"avarice":1}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:58.2623261Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":55,"playSeq":32,"playIdx":18,"type":30,"knownList":[{"idx":18,"cardId":100111010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:08:00.2645722Z","direction":"send","uri":"Echo","body":{"playIdx":18,"orderList":[{"move":{"idx":[18],"isSelf":0,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:02.7695981Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":56,"playSeq":33,"playIdx":5,"type":30,"knownList":[{"idx":5,"cardId":113011010,"to":20,"spellboost":0,"attachTarget":"","cost":2,"clan":0,"tribe":"7"}]}}
|
||||
{"ts":"2026-06-07T12:08:02.8451199Z","direction":"send","uri":"Echo","body":{"playIdx":5,"orderList":[{"move":{"idx":[5],"isSelf":0,"from":10,"to":20}},{"scan":{"idx":[4,7,8,9,12,13,14,17,19,20,21,22,23,25,26,27,28,29,30,31,32,33,35,36,37,38,39,40],"conditions":[{"tribe":"7"}]}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:05.7442862Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":57,"playSeq":34}}
|
||||
{"ts":"2026-06-07T12:08:05.7667846Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,16,15,24,11],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":1}},{"move":{"idx":[42],"isSelf":1,"from":20,"to":30,"hasGuard":[42]}},{"trigger":{"isSelf":0,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:08:06.2448192Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":58,"playSeq":35,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:08:06.2608181Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"156","key2":"393","key3":"121011060","key4":"152","key5":"302","key6":"326133205"}}}
|
||||
{"ts":"2026-06-07T12:08:06.2778185Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":64,"playSeq":36,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:08:06.3228189Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[38],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":41}}
|
||||
{"ts":"2026-06-07T12:08:17.8343721Z","direction":"send","uri":"PlayActions","body":{"playIdx":19,"keyAction":[{"type":1,"cardId":127011010,"selectCard":{"cardId":[120011010],"open":0}}],"orderList":[{"move":{"idx":[19],"isSelf":1,"from":10,"to":20}},{"add":{"idx":[43],"isSelf":1,"card":{"cardId":120011010},"isChoice":"1"}},{"move":{"idx":[43],"isSelf":1,"from":50,"to":10}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:21.3291075Z","direction":"send","uri":"PlayActions","body":{"playIdx":4,"targetList":[{"targetIdx":5,"isSelf":0,"selectSkillIndex":[1]}],"orderList":[{"move":{"idx":[4],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[5,6,32,23,38,43],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"33"}},{"metamorphose":{"idx":[5],"isSelf":0,"after":{"cardId":900311020}}}],"type":31}}
|
||||
{"ts":"2026-06-07T12:08:25.9578557Z","direction":"send","uri":"PlayActions","body":{"playIdx":5,"targetList":[{"targetIdx":34,"isSelf":0,"selectSkillIndex":[1]}],"orderList":[{"move":{"idx":[5],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[6,32,23,38,43],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"36"}},{"move":{"idx":[34],"isSelf":0,"from":20,"to":30}},{"add":{"idx":[44],"isSelf":1,"card":{"cardId":900334010}}},{"move":{"idx":[44],"isSelf":1,"from":50,"to":10}}],"type":31}}
|
||||
{"ts":"2026-06-07T12:08:29.5860517Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[6,32,23,38,43,44],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":3}}]}}
|
||||
{"ts":"2026-06-07T12:08:30.0854894Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"162","key2":"770","key3":"248022140","key4":"154","key5":"302","key6":"1000422107"},"type":0,"actionSeq":46,"cemetery":[9,4]}}
|
||||
{"ts":"2026-06-07T12:08:30.1853353Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":65,"playSeq":37,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:08:30.2078357Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[35],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-07T12:08:37.7255447Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":66,"playSeq":38,"playIdx":15,"type":30,"knownList":[{"idx":15,"cardId":101121110,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:08:37.9275599Z","direction":"send","uri":"Echo","body":{"playIdx":15,"orderList":[{"move":{"idx":[15],"isSelf":0,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:38.5997627Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":67,"playSeq":39}}
|
||||
{"ts":"2026-06-07T12:08:39.0994174Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":68,"playSeq":40,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:08:39.8688009Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,16,24,11,35],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":5}}]}}
|
||||
{"ts":"2026-06-07T12:08:39.9995393Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"162","key2":"770","key3":"248022140","key4":"156","key5":"417","key6":"1101543355"}}}
|
||||
{"ts":"2026-06-07T12:08:40.0160656Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":80,"playSeq":41,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:08:40.0427529Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[20],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":49}}
|
||||
{"ts":"2026-06-07T12:08:44.0590977Z","direction":"send","uri":"PlayActions","body":{"playIdx":20,"orderList":[{"move":{"idx":[20],"isSelf":1,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:49.2798814Z","direction":"send","uri":"PlayActions","body":{"playIdx":23,"orderList":[{"move":{"idx":[23],"isSelf":1,"from":10,"to":20}},{"playerParam":{"isSelf":1,"buffUnit":1}}],"type":30}}
|
||||
118
SVSim.BattleEngine.Tests/Fixtures/battle_test_fresh_cl2.ndjson
Normal file
118
SVSim.BattleEngine.Tests/Fixtures/battle_test_fresh_cl2.ndjson
Normal file
@@ -0,0 +1,118 @@
|
||||
{"ts":"2026-06-07T12:05:10.0764449Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:10.1264431Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"907324319325","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":742186477},"oppoInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":742186477,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":102131030},{"idx":2,"cardId":101121080},{"idx":3,"cardId":101131050},{"idx":4,"cardId":101114010},{"idx":5,"cardId":113011010},{"idx":6,"cardId":113011010},{"idx":7,"cardId":101121020},{"idx":8,"cardId":101121010},{"idx":9,"cardId":102141010},{"idx":10,"cardId":101121080},{"idx":11,"cardId":101114010},{"idx":12,"cardId":102111060},{"idx":13,"cardId":102131020},{"idx":14,"cardId":102131010},{"idx":15,"cardId":101121110},{"idx":16,"cardId":101121110},{"idx":17,"cardId":100111020},{"idx":18,"cardId":100111010},{"idx":19,"cardId":102121030},{"idx":20,"cardId":100111020},{"idx":21,"cardId":101121080},{"idx":22,"cardId":101121020},{"idx":23,"cardId":100111070},{"idx":24,"cardId":102111060},{"idx":25,"cardId":101131020},{"idx":26,"cardId":101114050},{"idx":27,"cardId":101114050},{"idx":28,"cardId":101121010},{"idx":29,"cardId":701141011},{"idx":30,"cardId":102121010},{"idx":31,"cardId":100111010},{"idx":32,"cardId":100114010},{"idx":33,"cardId":101114050},{"idx":34,"cardId":113011010},{"idx":35,"cardId":100114010},{"idx":36,"cardId":100111020},{"idx":37,"cardId":102121030},{"idx":38,"cardId":102121010},{"idx":39,"cardId":100114010},{"idx":40,"cardId":100111070}],"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:13.3624432Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":1,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:13.3644442Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:29.7550686Z","direction":"send","uri":"Swap","body":{"idxList":[]}}
|
||||
{"ts":"2026-06-07T12:05:29.7695695Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:34.8895711Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":4},{"pos":2,"idx":5}],"idxChangeSeed":661650374,"spin":243,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:36.7840686Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":5,"playSeq":6,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:37.9140709Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[8],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-07T12:05:42.3100693Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":6,"playSeq":7}}
|
||||
{"ts":"2026-06-07T12:05:42.3835692Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,8],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-07T12:05:42.7575705Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":7,"playSeq":8,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:42.7750675Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"143","key2":"14","key3":"0","key4":"141","key5":"56","key6":"0"}}}
|
||||
{"ts":"2026-06-07T12:05:42.7905712Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":7,"playSeq":9,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:42.8590737Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[10,16],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"avarice":1}}],"actionSeq":2}}
|
||||
{"ts":"2026-06-07T12:05:46.4565675Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,10,16],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}},{"trigger":{"isSelf":1,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:05:46.9540693Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"142","key2":"134","key3":"0","key4":"141","key5":"56","key6":"0"},"type":0,"actionSeq":4,"cemetery":[0,0]}}
|
||||
{"ts":"2026-06-07T12:05:47.5195696Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":11,"playSeq":10,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:47.5415707Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[29],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-07T12:05:54.7275709Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":12,"playSeq":11,"playIdx":8,"type":30,"knownList":[{"idx":8,"cardId":102331010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:05:54.9510692Z","direction":"send","uri":"Echo","body":{"playIdx":8,"orderList":[{"move":{"idx":[8],"isSelf":0,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:05:55.7230693Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":13,"playSeq":12}}
|
||||
{"ts":"2026-06-07T12:05:56.2255669Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":14,"playSeq":13,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:56.9100687Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,29],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-07T12:05:57.0275696Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"142","key2":"134","key3":"0","key4":"143","key5":"140","key6":"102331036"}}}
|
||||
{"ts":"2026-06-07T12:05:57.0415684Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":14,"playSeq":14,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:05:57.0740682Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[15],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":7}}
|
||||
{"ts":"2026-06-07T12:06:12.6129250Z","direction":"send","uri":"PlayActions","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":1,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:06:16.4794226Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,10,16,15],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-07T12:06:16.9789227Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"144","key2":"177","key3":"102131049","key4":"143","key5":"140","key6":"102331036"},"type":0,"actionSeq":10,"cemetery":[0,0]}}
|
||||
{"ts":"2026-06-07T12:06:17.0619236Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":19,"playSeq":15,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:17.0839228Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[3],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-07T12:06:28.8204242Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":20,"playSeq":16,"playIdx":3,"type":30,"knownList":[{"idx":3,"cardId":101321040,"to":20,"spellboost":0,"attachTarget":"","cost":2,"clan":3,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:06:29.0409223Z","direction":"send","uri":"Echo","body":{"playIdx":3,"orderList":[{"move":{"idx":[3],"isSelf":0,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:06:29.8804238Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":21,"playSeq":17}}
|
||||
{"ts":"2026-06-07T12:06:30.3639243Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":22,"playSeq":18,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:30.9664239Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,29],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-07T12:06:31.1154246Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"144","key2":"177","key3":"102131049","key4":"145","key5":"140","key6":"203652104"}}}
|
||||
{"ts":"2026-06-07T12:06:31.1309231Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":22,"playSeq":19,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:31.1914245Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":13}}
|
||||
{"ts":"2026-06-07T12:06:36.0239226Z","direction":"send","uri":"PlayActions","body":{"playIdx":1,"targetList":[{"targetIdx":8,"isSelf":0}],"orderList":[{"move":{"idx":[1],"isSelf":1,"from":20,"to":30}},{"move":{"idx":[8],"isSelf":0,"from":20,"to":30}}],"type":10}}
|
||||
{"ts":"2026-06-07T12:06:36.6854243Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,10,16,15,24],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-07T12:06:37.1859231Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"148","key2":"321","key3":"0","key4":"147","key5":"140","key6":"101321058"},"type":0,"actionSeq":16,"cemetery":[1,1]}}
|
||||
{"ts":"2026-06-07T12:06:38.2359235Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":27,"playSeq":20,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:38.2569229Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":2}},{"move":{"idx":[19],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-07T12:06:46.5794252Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":30,"playSeq":21,"playIdx":29,"type":30,"knownList":[{"idx":29,"cardId":127011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"keyAction":[{"type":1,"cardId":127011010}]}}
|
||||
{"ts":"2026-06-07T12:06:47.0374244Z","direction":"send","uri":"Echo","body":{"playIdx":29,"orderList":[{"move":{"idx":[29],"isSelf":0,"from":10,"to":20}},{"add":{"idx":[41],"isSelf":0,"card":{"candidates":[121011010,120011010]},"isChoice":"1"}},{"move":{"idx":[41],"isSelf":0,"from":50,"to":10}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:06:50.3279267Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":31,"playSeq":22}}
|
||||
{"ts":"2026-06-07T12:06:50.3444241Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,19,41],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-07T12:06:50.8274230Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":32,"playSeq":23,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:50.8434224Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"148","key2":"321","key3":"0","key4":"149","key5":"305","key6":"228332150"}}}
|
||||
{"ts":"2026-06-07T12:06:50.8594230Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":31,"playSeq":24,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:50.9024228Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[11],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":19}}
|
||||
{"ts":"2026-06-07T12:06:55.3169242Z","direction":"send","uri":"PlayActions","body":{"playIdx":10,"orderList":[{"move":{"idx":[10],"isSelf":1,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:06:56.0779247Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,16,15,24,11],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":3}}]}}
|
||||
{"ts":"2026-06-07T12:06:56.5774224Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"150","key2":"302","key3":"101121116","key4":"149","key5":"305","key6":"228332150"},"type":0,"actionSeq":22,"cemetery":[1,1]}}
|
||||
{"ts":"2026-06-07T12:06:57.6284227Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":37,"playSeq":25,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:06:57.6504253Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-07T12:07:02.6859240Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":38,"playSeq":26,"playIdx":39,"type":30,"knownList":[{"idx":39,"cardId":100314010,"to":30,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:07:02.8454236Z","direction":"send","uri":"Echo","body":{"playIdx":39,"orderList":[{"move":{"idx":[39],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[1,4,5,19,41],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"7"}},{"move":{"idx":[17],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}},{"trigger":{"isSelf":0,"avarice":1}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:10.2264230Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":39,"playSeq":27,"playIdx":41,"type":30,"knownList":[{"idx":41,"cardId":121011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:07:10.3164226Z","direction":"send","uri":"Echo","body":{"playIdx":41,"orderList":[{"move":{"idx":[41],"isSelf":0,"from":10,"to":20}},{"move":{"idx":[6],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:17.7599274Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":40,"playSeq":28}}
|
||||
{"ts":"2026-06-07T12:07:17.7789256Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,19,17,6],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":3}},{"trigger":{"isSelf":0,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:07:18.2769237Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":41,"playSeq":29,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:07:18.2949243Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"150","key2":"302","key3":"101121116","key4":"147","key5":"221","key6":"349343345"}}}
|
||||
{"ts":"2026-06-07T12:07:18.3089265Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":40,"playSeq":30,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:07:18.3409222Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[6],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":26}}
|
||||
{"ts":"2026-06-07T12:07:22.0604232Z","direction":"send","uri":"PlayActions","body":{"playIdx":6,"orderList":[{"move":{"idx":[6],"isSelf":1,"from":10,"to":20}},{"move":{"idx":[34],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}},{"trigger":{"isSelf":1,"avarice":1}}],"uList":[{"idxList":[34],"from":0,"to":10,"isSelf":1,"skill":"6|19|0"}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:25.6229734Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,16,15,24,11,34],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":3}},{"trigger":{"isSelf":1,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:07:26.1224220Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"149","key2":"540","key3":"214132162","key4":"147","key5":"221","key6":"349343345"},"type":0,"actionSeq":29,"cemetery":[1,2]}}
|
||||
{"ts":"2026-06-07T12:07:26.2219233Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":46,"playSeq":31,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:07:26.2444230Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[32],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-07T12:07:34.2504226Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":49,"playSeq":32,"playIdx":1,"type":31,"knownList":[{"idx":1,"cardId":101324040,"to":30,"spellboost":0,"attachTarget":"","cost":3,"clan":3,"tribe":"0"}],"oppoTargetList":[{"targetIdx":6,"isSelf":1}]}}
|
||||
{"ts":"2026-06-07T12:07:34.4124257Z","direction":"send","uri":"Echo","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[4,5,19,17,6,32],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"20"}},{"move":{"idx":[23],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}},{"trigger":{"isSelf":0,"avarice":1}}],"type":31}}
|
||||
{"ts":"2026-06-07T12:07:41.2491729Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":50,"playSeq":33,"playIdx":17,"type":30,"knownList":[{"idx":17,"cardId":102324040,"to":30,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:07:41.4554809Z","direction":"send","uri":"Echo","body":{"playIdx":17,"orderList":[{"move":{"idx":[17],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[4,5,19,6,32,23],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"23"}},{"add":{"idx":[42],"isSelf":0,"card":{"cardId":900311050}}},{"move":{"idx":[42],"isSelf":0,"from":50,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:46.6891818Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":52,"playSeq":34,"playIdx":41,"type":10,"oppoTargetList":[{"targetIdx":10,"isSelf":0}]}}
|
||||
{"ts":"2026-06-07T12:07:46.7161815Z","direction":"send","uri":"Echo","body":{"playIdx":41,"type":10}}
|
||||
{"ts":"2026-06-07T12:07:48.2401820Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":54,"playSeq":35,"playIdx":29,"type":10,"knownList":[{"idx":29,"cardId":127011010,"to":30,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"oppoTargetList":[{"targetIdx":10,"isSelf":0}]}}
|
||||
{"ts":"2026-06-07T12:07:48.3302904Z","direction":"send","uri":"Echo","body":{"playIdx":29,"orderList":[{"move":{"idx":[29],"isSelf":0,"from":20,"to":30}},{"move":{"idx":[10],"isSelf":1,"from":20,"to":30}}],"type":10}}
|
||||
{"ts":"2026-06-07T12:07:50.0089639Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":56,"playSeq":36,"playIdx":3,"type":10,"knownList":[{"idx":3,"cardId":101321040,"to":30,"spellboost":0,"attachTarget":"","cost":2,"clan":3,"tribe":"0"}],"oppoTargetList":[{"targetIdx":6,"isSelf":0}]}}
|
||||
{"ts":"2026-06-07T12:07:50.2322631Z","direction":"send","uri":"Echo","body":{"playIdx":3,"orderList":[{"move":{"idx":[3],"isSelf":0,"from":20,"to":30}},{"move":{"idx":[6],"isSelf":1,"from":20,"to":30}}],"type":10}}
|
||||
{"ts":"2026-06-07T12:07:51.8934054Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":57,"playSeq":37}}
|
||||
{"ts":"2026-06-07T12:07:52.0776073Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[4,5,19,6,32,23],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":5}},{"trigger":{"isSelf":0,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:07:52.3771546Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":58,"playSeq":38,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:07:52.3931550Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"153","key2":"540","key3":"0","key4":"154","key5":"393","key6":"1021322270"}}}
|
||||
{"ts":"2026-06-07T12:07:52.4097475Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":52,"playSeq":39,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:07:52.4689367Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":6}},{"move":{"idx":[18],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":36}}
|
||||
{"ts":"2026-06-07T12:07:57.1625968Z","direction":"send","uri":"PlayActions","body":{"playIdx":34,"orderList":[{"move":{"idx":[34],"isSelf":1,"from":10,"to":20}},{"move":{"idx":[5],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}},{"trigger":{"isSelf":1,"avarice":1}}],"uList":[{"idxList":[5],"from":0,"to":10,"isSelf":1,"skill":"34|28|0"}],"type":30}}
|
||||
{"ts":"2026-06-07T12:07:58.2473269Z","direction":"send","uri":"PlayActions","body":{"playIdx":18,"orderList":[{"move":{"idx":[18],"isSelf":1,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:02.7615951Z","direction":"send","uri":"PlayActions","body":{"playIdx":5,"orderList":[{"move":{"idx":[5],"isSelf":1,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:05.7352832Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,16,15,24,11],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":1}},{"move":{"idx":[42],"isSelf":0,"from":20,"to":30,"hasGuard":[42]}},{"trigger":{"isSelf":1,"avarice":0}}]}}
|
||||
{"ts":"2026-06-07T12:08:06.2301214Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"152","key2":"302","key3":"326133205","key4":"156","key5":"393","key6":"121011060"},"type":0,"actionSeq":41,"cemetery":[3,7]}}
|
||||
{"ts":"2026-06-07T12:08:06.3303197Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":65,"playSeq":40,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:08:06.3513355Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[38],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-07T12:08:17.8463749Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":68,"playSeq":41,"playIdx":19,"type":30,"knownList":[{"idx":19,"cardId":127011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"keyAction":[{"type":1,"cardId":127011010}]}}
|
||||
{"ts":"2026-06-07T12:08:17.9224312Z","direction":"send","uri":"Echo","body":{"playIdx":19,"orderList":[{"move":{"idx":[19],"isSelf":0,"from":10,"to":20}},{"add":{"idx":[43],"isSelf":0,"card":{"candidates":[121011010,120011010]},"isChoice":"1"}},{"move":{"idx":[43],"isSelf":0,"from":50,"to":10}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:21.3856074Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":71,"playSeq":42,"playIdx":4,"type":31,"knownList":[{"idx":4,"cardId":101324050,"to":30,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"oppoTargetList":[{"targetIdx":5,"isSelf":0}]}}
|
||||
{"ts":"2026-06-07T12:08:21.5844099Z","direction":"send","uri":"Echo","body":{"playIdx":4,"orderList":[{"move":{"idx":[4],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[5,6,32,23,38,43],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"33"}},{"metamorphose":{"idx":[5],"isSelf":1,"after":{"cardId":900311020}}}],"type":31}}
|
||||
{"ts":"2026-06-07T12:08:25.9743530Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":74,"playSeq":43,"playIdx":5,"type":31,"knownList":[{"idx":5,"cardId":101334030,"to":30,"spellboost":2,"attachTarget":"","cost":3,"clan":3,"tribe":"0"}],"oppoTargetList":[{"targetIdx":34,"isSelf":0}]}}
|
||||
{"ts":"2026-06-07T12:08:26.1638091Z","direction":"send","uri":"Echo","body":{"playIdx":5,"orderList":[{"move":{"idx":[5],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[6,32,23,38,43],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"36"}},{"move":{"idx":[34],"isSelf":1,"from":20,"to":30}},{"add":{"idx":[44],"isSelf":0,"card":{"cardId":900334010}}},{"move":{"idx":[44],"isSelf":0,"from":50,"to":10}}],"type":31}}
|
||||
{"ts":"2026-06-07T12:08:29.6025555Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":75,"playSeq":44}}
|
||||
{"ts":"2026-06-07T12:08:29.6190527Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[6,32,23,38,43,44],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":3}}]}}
|
||||
{"ts":"2026-06-07T12:08:30.1015223Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":76,"playSeq":45,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:08:30.1180409Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"154","key2":"302","key3":"1000422107","key4":"162","key5":"770","key6":"248022140"}}}
|
||||
{"ts":"2026-06-07T12:08:30.1345601Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":64,"playSeq":46,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:08:30.1768361Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[35],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":46}}
|
||||
{"ts":"2026-06-07T12:08:37.7130477Z","direction":"send","uri":"PlayActions","body":{"playIdx":15,"orderList":[{"move":{"idx":[15],"isSelf":1,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:38.5902629Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,16,24,11,35],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":5}}]}}
|
||||
{"ts":"2026-06-07T12:08:39.0894170Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"156","key2":"417","key3":"1101543355","key4":"162","key5":"770","key6":"248022140"},"type":0,"actionSeq":49,"cemetery":[4,9]}}
|
||||
{"ts":"2026-06-07T12:08:40.0572510Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":81,"playSeq":47,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-07T12:08:40.0784574Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[20],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-07T12:08:44.0705950Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":82,"playSeq":48,"playIdx":20,"type":30,"knownList":[{"idx":20,"cardId":101311010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:08:44.2734716Z","direction":"send","uri":"Echo","body":{"playIdx":20,"orderList":[{"move":{"idx":[20],"isSelf":0,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:08:49.2868793Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":83,"playSeq":49,"playIdx":23,"type":30,"knownList":[{"idx":23,"cardId":101321070,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
|
||||
{"ts":"2026-06-07T12:08:49.5008504Z","direction":"send","uri":"Echo","body":{"playIdx":23,"orderList":[{"move":{"idx":[23],"isSelf":0,"from":10,"to":20}}],"type":30}}
|
||||
{"ts":"2026-06-07T12:09:11.1269227Z","direction":"receive","uri":null,"body":{"uri":"BattleFinish","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"result":201,"resultCode":1}}
|
||||
151
SVSim.BattleEngine.Tests/GatedConditionalOracleTests.cs
Normal file
151
SVSim.BattleEngine.Tests/GatedConditionalOracleTests.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M11 (the GATE itself is the oracle): every prior milestone either had no skill_condition or
|
||||
// seeded its gate TRUE so the effect fires (M4 seeded play_count>2; M10 seeded a play_count
|
||||
// VALUE). None proved the engine SUPPRESSES an effect when a skill_condition evaluates FALSE —
|
||||
// the dual of "effect fires". M11 proves conditional BRANCHING resolves headless by asserting
|
||||
// BOTH directions of the SAME gated card in ONE fixture (design "M11 — NEXT" resume guide):
|
||||
//
|
||||
// * gate TRUE (play_count > 2, seeded via the public AddCurrentTrunPlayCount seam M4/M10 use)
|
||||
// -> the when_play powerup fires -> the follower is buffed over its base stats.
|
||||
// * gate FALSE (play_count <= 2, the bare-construction default)
|
||||
// -> the powerup is a NO-OP: zero stat delta, BUT the card still pays its cost
|
||||
// and still leaves hand -> board (the gate suppresses the EFFECT, not the PLAY).
|
||||
//
|
||||
// Card: 103111050 — the M4 self-buff follower (ELF clan-1 cost-1 base 1/1, sole non-evo skill
|
||||
// `when_play` `powerup` `add_offense=1&add_life=1` to `character=me&target=self`), whose
|
||||
// skill_condition is `character=me&target=self&play_count>2` (verified in cards.json). The gate
|
||||
// reads BattlePlayerBase.GetCurrentTurnPlayCount(), seedable past/below the threshold via the
|
||||
// public AddCurrentTrunPlayCount. Reusing the M4-proven buff DIMENSION means the only NEW thing
|
||||
// under test is the CONDITIONAL — exactly the resume-guide's "proven effect dimension, gate is
|
||||
// the oracle" prescription.
|
||||
//
|
||||
// Why one fixture, both branches, ONE card is decisive: the two assertions are jointly
|
||||
// satisfiable ONLY by a correctly-gating engine. An "always-buffs" engine fails the FALSE branch
|
||||
// (would buff with play_count=0); a "never-buffs" engine fails the TRUE branch (M4's gate seed
|
||||
// wouldn't fire). M4 already demonstrated this split as a manual load-bearing probe (remove the
|
||||
// seed -> buff vanishes); M11 promotes it to the PRIMARY assertion.
|
||||
[TestFixture]
|
||||
public class GatedConditionalOracleTests
|
||||
{
|
||||
// A clearly super-threshold seed (play_count 5 > 2): the gate evaluates TRUE, fanfare fires.
|
||||
private const int GateTrueSeed = 5;
|
||||
// The bare-construction default is play_count 0 (<= 2 -> gate FALSE); we seed nothing for the
|
||||
// FALSE branch, exactly as M4's load-bearing probe did when it removed its seed.
|
||||
private const int GateFalseSeed = 0;
|
||||
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
// Resolve the gated self-buff follower on a FRESH battle with the per-turn play count seeded
|
||||
// to `seededPlayCount`, and report the play's outcome. A fresh mgr per branch is required:
|
||||
// play_count is per-mgr state and a resolved play mutates the board, so the two branches must
|
||||
// not share a battle. Mirrors the M4 BuffFollowerOracleTests setup verbatim, parameterized on
|
||||
// the seed (which is the only thing M11 varies between branches).
|
||||
private (BattleCardBase card, CardParameter param, int ppBefore, int ppAfter,
|
||||
int handBefore, bool inHandAfter, int inplayBefore, bool onBoardAfter, int inplayAfter)
|
||||
PlayGatedSelfBuff(int seededPlayCount)
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr; // route GetIns() to this branch's mgr
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Minimal opponent/turn wiring (M2/M3/M4 oracles): opponent refs + active turn flag. The
|
||||
// self-buff target resolver (`character=me&target=self`) reads the active player's own
|
||||
// in-play card, so the turn flag must be set before the fanfare sweeps.
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
// Seed leader life so neither leader reads as a 0-life game-over that silently blocks the
|
||||
// play (M3 learning). This card deals no damage but the play-legality gate still checks it.
|
||||
HeadlessEngineEnv.InitLeaderLife(mgr);
|
||||
|
||||
// THE GATE SEED — the one knob M11 turns between branches. The skill_condition
|
||||
// `play_count>2` reads BattlePlayerBase.GetCurrentTurnPlayCount(); seed it via the public
|
||||
// AddCurrentTrunPlayCount (M4/M10 seam). For the FALSE branch we leave the bare default 0.
|
||||
if (seededPlayCount > 0) player.AddCurrentTrunPlayCount(seededPlayCount);
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.BuffFollowerId);
|
||||
|
||||
// Place the gated self-buff follower in the active player's hand with PP to spare; empty board.
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.BuffFollowerId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int inplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
|
||||
$"ActionProcessor.PlayCard threw on the gated self-buff (seed={seededPlayCount})");
|
||||
|
||||
return (card, cardParam, ppBefore, player.Pp,
|
||||
handBefore, player.HandCardList.Contains(card),
|
||||
inplayBefore, player.ClassAndInPlayCardList.Contains(card),
|
||||
player.ClassAndInPlayCardList.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Gated_fanfare_fires_when_seeded_true_and_is_suppressed_when_false()
|
||||
{
|
||||
// ----- Branch 1: gate TRUE (play_count 5 > 2) -> the fanfare FIRES (M4 dimension). -----
|
||||
var t = PlayGatedSelfBuff(GateTrueSeed);
|
||||
|
||||
// ----- Branch 2: gate FALSE (play_count 0 <= 2) -> the fanfare is SUPPRESSED. -----
|
||||
var f = PlayGatedSelfBuff(GateFalseSeed);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// PRIMARY M11 assertion — the gate itself: SAME card, opposite stat outcomes driven
|
||||
// ONLY by the seeded condition.
|
||||
// TRUE -> buffed: base 1/1 + 1/1 = 2/2.
|
||||
Assert.That(t.card.Atk, Is.EqualTo(t.param.Atk + HeadlessEngineEnv.BuffAddOffense),
|
||||
"[gate TRUE] atk != base + add_offense (fanfare should have fired)");
|
||||
Assert.That(t.card.Life, Is.EqualTo(t.param.Life + HeadlessEngineEnv.BuffAddLife),
|
||||
"[gate TRUE] life != base + add_life (fanfare should have fired)");
|
||||
// FALSE -> unbuffed: stays at the CardCSVData base 1/1 (effect suppressed).
|
||||
Assert.That(f.card.Atk, Is.EqualTo(f.param.Atk),
|
||||
"[gate FALSE] atk != base (fanfare should have been gated out)");
|
||||
Assert.That(f.card.Life, Is.EqualTo(f.param.Life),
|
||||
"[gate FALSE] life != base (fanfare should have been gated out)");
|
||||
|
||||
// The gate suppresses the EFFECT, not the PLAY: in BOTH branches the card still pays
|
||||
// its cost and still moves hand -> board like any follower.
|
||||
// TRUE branch:
|
||||
Assert.That(t.ppAfter, Is.EqualTo(t.ppBefore - t.param.Cost), "[gate TRUE] PP not reduced by cost");
|
||||
Assert.That(t.inHandAfter, Is.False, "[gate TRUE] card still in hand");
|
||||
Assert.That(t.onBoardAfter, Is.True, "[gate TRUE] card not on board");
|
||||
Assert.That(t.inplayAfter, Is.EqualTo(t.inplayBefore + 1), "[gate TRUE] in-play count not +1");
|
||||
// FALSE branch — the M11 crux: cost STILL paid + card STILL resolves despite the no-op effect.
|
||||
Assert.That(f.ppAfter, Is.EqualTo(f.ppBefore - f.param.Cost), "[gate FALSE] PP not reduced by cost");
|
||||
Assert.That(f.inHandAfter, Is.False, "[gate FALSE] card still in hand");
|
||||
Assert.That(f.onBoardAfter, Is.True, "[gate FALSE] card not on board");
|
||||
Assert.That(f.inplayAfter, Is.EqualTo(f.inplayBefore + 1), "[gate FALSE] in-play count not +1");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
115
SVSim.BattleEngine.Tests/HeadlessCardMaster.cs
Normal file
115
SVSim.BattleEngine.Tests/HeadlessCardMaster.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using Wizard;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// Populates the engine's static CardMaster headless, from the loader's cards.json dump
|
||||
// (serialized CardCSVData objects). We bypass the network/Resources init path
|
||||
// (CardMaster.InitializeCardMaster) and the private ctor/field via reflection — CardMaster
|
||||
// exposes no public injection seam. Class cards (id < 100) resolve via the ctor's
|
||||
// _classCardParam, so an empty load still satisfies construction; pass real ids for the oracle.
|
||||
public static class HeadlessCardMaster
|
||||
{
|
||||
private static readonly string CardsJsonPath =
|
||||
Path.Combine(AppContext.BaseDirectory, "Data", "cards.json");
|
||||
|
||||
// Every id ever requested this process. Load is CUMULATIVE: each call rebuilds the master from
|
||||
// the union, so a later Load(subset) never evicts cards an earlier Load (e.g. EnsureProcessGlobals's
|
||||
// oracle set) installed. Without this, the static CardMaster is shared mutable state across the
|
||||
// whole NUnit run and a Load(deck) in one test silently breaks an oracle test that runs after.
|
||||
private static readonly HashSet<int> _everLoaded = new();
|
||||
// Serialise Load: assembly-level Parallelizable(Fixtures) means concurrent fixtures race here,
|
||||
// and HashSet<int>.Add + the static CardMaster install are not thread-safe.
|
||||
private static readonly object _loadGate = new object();
|
||||
|
||||
// Load the given card ids (empty = none) into a CardMaster registered as Default, MERGED with all
|
||||
// previously-loaded ids.
|
||||
public static void Load(params int[] cardIds)
|
||||
{
|
||||
lock (_loadGate)
|
||||
{
|
||||
LoadCore(cardIds);
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadCore(int[] cardIds)
|
||||
{
|
||||
foreach (var id in cardIds) _everLoaded.Add(id);
|
||||
var want = new HashSet<int>(_everLoaded);
|
||||
var rows = new List<CardCSVData>();
|
||||
if (want.Count > 0)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(CardsJsonPath));
|
||||
int sort = 0;
|
||||
foreach (var el in doc.RootElement.EnumerateArray())
|
||||
{
|
||||
if (!el.TryGetProperty("card_id", out var idEl)) continue;
|
||||
if (!int.TryParse(idEl.GetString(), out var id) || !want.Contains(id)) continue;
|
||||
rows.Add(BuildCardCsvData(el, sort++));
|
||||
}
|
||||
var missing = want.Except(rows.Select(r => int.Parse(r.card_id))).ToArray();
|
||||
if (missing.Length > 0)
|
||||
throw new InvalidOperationException(
|
||||
"cards.json missing requested ids: " + string.Join(",", missing));
|
||||
}
|
||||
|
||||
var cm = NewCardMaster(rows);
|
||||
InjectAsDefault(cm);
|
||||
}
|
||||
|
||||
// Construct a CardCSVData without running its CSV ctor; set each member from the JSON object
|
||||
// by exact name match (cards.json keys == CardCSVData member names).
|
||||
private static CardCSVData BuildCardCsvData(JsonElement el, int sortIndex)
|
||||
{
|
||||
var c = (CardCSVData)FormatterServices.GetUninitializedObject(typeof(CardCSVData));
|
||||
const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
foreach (var prop in el.EnumerateObject())
|
||||
{
|
||||
string val = prop.Value.ValueKind == JsonValueKind.Null ? null : prop.Value.ToString();
|
||||
var f = typeof(CardCSVData).GetField(prop.Name, bf);
|
||||
if (f != null) { SetMember(f.FieldType, val, v => f.SetValue(c, v)); continue; }
|
||||
var p = typeof(CardCSVData).GetProperty(prop.Name, bf);
|
||||
if (p != null && p.CanWrite) SetMember(p.PropertyType, val, v => p.SetValue(c, v));
|
||||
}
|
||||
// SortIndex is normally set by the ctor; mirror it.
|
||||
var si = typeof(CardCSVData).GetProperty("SortIndex", bf);
|
||||
if (si != null && si.CanWrite) si.SetValue(c, sortIndex);
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void SetMember(Type t, string val, Action<object> set)
|
||||
{
|
||||
if (t == typeof(string)) set(val);
|
||||
else if (t == typeof(int)) set(int.TryParse(val, out var i) ? i : 0);
|
||||
else if (t == typeof(bool)) set(val == "1" || string.Equals(val, "true", StringComparison.OrdinalIgnoreCase));
|
||||
// other types left at default
|
||||
}
|
||||
|
||||
private static CardMaster NewCardMaster(List<CardCSVData> rows)
|
||||
{
|
||||
var ctor = typeof(CardMaster).GetConstructor(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic, null,
|
||||
new[] { typeof(List<CardCSVData>) }, null);
|
||||
if (ctor == null) throw new InvalidOperationException("CardMaster(List<CardCSVData>) ctor not found");
|
||||
return (CardMaster)ctor.Invoke(new object[] { rows });
|
||||
}
|
||||
|
||||
private static void InjectAsDefault(CardMaster cm)
|
||||
{
|
||||
var idType = typeof(CardMaster).GetNestedType("CardMasterId");
|
||||
var defaultId = Enum.Parse(idType, "Default");
|
||||
var dictType = typeof(Dictionary<,>).MakeGenericType(idType, typeof(CardMaster));
|
||||
var dict = (System.Collections.IDictionary)Activator.CreateInstance(dictType);
|
||||
dict[defaultId] = cm;
|
||||
var fld = typeof(CardMaster).GetField("_dictCardMaster",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
fld.SetValue(null, dict);
|
||||
}
|
||||
}
|
||||
}
|
||||
557
SVSim.BattleEngine.Tests/HeadlessFixture.cs
Normal file
557
SVSim.BattleEngine.Tests/HeadlessFixture.cs
Normal file
@@ -0,0 +1,557 @@
|
||||
using System.Reflection;
|
||||
using SVSim.BattleEngine.Rng;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
using Wizard.Battle.Phase;
|
||||
using Wizard.Battle.Recovery;
|
||||
using Wizard.Battle.Replay;
|
||||
using Wizard.Battle.Resource;
|
||||
using Wizard.Battle.View.Vfx;
|
||||
using Wizard.BattleMgr;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// Initializes the global engine state a headless battle assumes exists. In the real client this
|
||||
// is populated from /load/index at login; here we author the minimum the resolution path reads.
|
||||
public static class HeadlessEngineEnv
|
||||
{
|
||||
// Simplest zero-skill vanilla follower in cards.json: neutral (clan 0), cost 1, 1/2, no skill.
|
||||
public const int FollowerId = 100011010;
|
||||
|
||||
// M3 next-hardest deterministic card: a fixed-damage spell. 900124030 is an ELF (clan 1, matches
|
||||
// PlayerClassId) cost-3 spell whose sole skill is `when_play` `damage=3` to `card_type=class`
|
||||
// (the enemy leader) — auto-targeted (no select_count), no RNG. Deterministic burn to the face.
|
||||
public const int SpellId = 900124030;
|
||||
|
||||
// M4 next-hardest deterministic card: a when_play SELF-BUFF follower. 103111050 is an ELF
|
||||
// (clan 1) cost-1 1/1 whose sole non-evo skill is `when_play` `powerup` `add_offense=1&add_life=1`
|
||||
// with skill_target `character=me&target=self` — it buffs ITSELF, so no target selection (the
|
||||
// fanfare auto-resolves). Fixed +1/+1 => a deterministic stat-delta oracle. The skill is gated on
|
||||
// `play_count>2`; the headless harness seeds that via the public AddCurrentTrunPlayCount (see the
|
||||
// oracle test). Base 1/1 -> 2/2 after the fanfare.
|
||||
public const int BuffFollowerId = 103111050;
|
||||
public const int BuffAddOffense = 1;
|
||||
public const int BuffAddLife = 1;
|
||||
|
||||
// M5 next-hardest deterministic card: a when_play SUMMON_TOKEN spell. 800134010 is an ELF
|
||||
// (clan 1) cost-1 spell whose sole skill is `when_play` `summon_token=100011020` with
|
||||
// `skill_target=none` and an UNGATED condition (`character=me`, trivially the caster): it
|
||||
// summons exactly ONE neutral 2/2 follower TOKEN onto the caster's board — no target
|
||||
// selection, no RNG (Skill_summon_token's random branch is `num >= 0 && !IsForecast`, and
|
||||
// this option carries no `random_count`, so num=-1 => the deterministic literal-id path).
|
||||
// The new oracle dimension over M2/M3/M4 is a BOARD-COUNT DELTA from a SKILL-CREATED card:
|
||||
// a token that was never in the hand/deck appears in play. This is also the first headless
|
||||
// exercise of the PUBLIC prefab card-creation path (CardCreatorBase.CreateCard,
|
||||
// createNullView:false, via BattlePlayerBase.CreateNextIndexCard) — class-card construction
|
||||
// hits `default: return null` and the M2-M4 hand cards used the private null-view seam, so
|
||||
// the view-building creation path is genuinely new here.
|
||||
public const int TokenSpellId = 800134010;
|
||||
public const int SummonedTokenId = 100011020; // neutral 2/2 follower token
|
||||
public const int SummonedTokenAtk = 2;
|
||||
public const int SummonedTokenLife = 2;
|
||||
|
||||
// M6 next milestone: the first card requiring TARGET SELECTION — exercises the selectedCards
|
||||
// path of ActionProcessor.PlayCard (dormant through M2-M5, all of which played
|
||||
// selectedCards: null). 800134020 is an ELF (clan 1) cost-1 SPELL whose sole skill is
|
||||
// `when_play` `damage=5` to a SELECTED enemy follower
|
||||
// (skill_target=character=op&target=inplay&card_type=unit&select_count=1), ungated
|
||||
// (character=me), no RNG, no dynamic `{}` value. The new oracle dimension is SELECTION
|
||||
// ROUTING: with TWO followers on the enemy board and ONE passed as selectedCards, only the
|
||||
// selected follower takes the 5 damage and the un-selected one is untouched.
|
||||
public const int TargetSpellId = 800134020;
|
||||
public const int TargetSpellDamage = 5;
|
||||
|
||||
// Two zero-skill vanilla NEUTRAL followers placed on the ENEMY board. Both have life > the
|
||||
// 5 damage so they SURVIVE — this gives a differential life-delta oracle (selected -5,
|
||||
// un-selected -0) that reads the authoritative damage path M3 already proved, without
|
||||
// depending on follower death/board-removal timing (a separate, unproven mechanic). Distinct
|
||||
// base life (13 vs 7) so the two post-states can't coincidentally match.
|
||||
public const int SelectTargetFollowerId = 900041010; // neutral 13/13
|
||||
public const int UnselectTargetFollowerId = 102011010; // neutral 6/7
|
||||
|
||||
// M7 next milestone: targeted DESTROY — the first card proving follower DEATH / board-removal
|
||||
// resolves in the AUTHORITATIVE (committed) part of PlayCard headless, not the cosmetic
|
||||
// post-Process tail. 800144120 is an ELF (clan 1) cost-0 SPELL whose sole skill is `when_play`
|
||||
// `destroy` of a SELECTED enemy follower
|
||||
// (skill_target=character=op&target=inplay&card_type=unit&select_count=1), ungated
|
||||
// (skill_condition=character=me), no RNG, no dynamic value. `destroy` is UNCONDITIONAL removal
|
||||
// (vs `damage` needing a >=life amount), so the oracle is the cleanest possible "card left the
|
||||
// board": selected follower gone + enemy board count -1 + selected card in CemeteryList, while
|
||||
// the un-selected follower stays (routing, M6's lesson, confirmed load-bearing by swapping the
|
||||
// selection). Reuses the two M2/M6 vanilla followers as the target board (destroy is
|
||||
// unconditional so their stats are irrelevant — distinct ids only so selected vs un-selected
|
||||
// can't be confused). InitCardTemplates is NOT needed (destroy creates no card).
|
||||
public const int DestroySpellId = 800144120;
|
||||
public const int DestroyTargetFollowerId = FollowerId; // neutral 1/2 (the selected, destroyed one)
|
||||
public const int DestroyOtherFollowerId = UnselectTargetFollowerId; // neutral 6/7 (the un-selected survivor)
|
||||
|
||||
// M8 next milestone: LETHAL damage — proves follower DEATH VIA COMBAT MATH (damage >= life ->
|
||||
// 0 life -> the same RemoveInplayCard/cemetery death path M7 lit up via `destroy`, but reached
|
||||
// through the dominant real-card mechanic: "deal N damage"). Reuses the M6 damage=5 spell
|
||||
// (800134020) but with target followers STRADDLING 5 life so the SAME spell kills one and merely
|
||||
// chips the other in a single oracle: the SELECTED target has life <= 5 and dies (board -1 +
|
||||
// cemetery +1, the M7 assertions), while the UN-SELECTED control has life > 5 and survives at
|
||||
// reduced life (the M6 life-delta assertion). This combines M7's removal dimension with M6's
|
||||
// life-delta + routing, and distinguishes death-via-damage from the unconditional `destroy`.
|
||||
public const int LethalDamageSpellId = TargetSpellId; // 800134020, when_play damage=5
|
||||
public const int LethalDamage = TargetSpellDamage; // 5
|
||||
public const int LethalTargetFollowerId = FollowerId; // neutral 1/2 (life 2 <= 5 -> dies)
|
||||
public const int SurvivorTargetFollowerId = UnselectTargetFollowerId; // neutral 6/7 (life 7 > 5 -> survives at 2)
|
||||
|
||||
// M9 next milestone: when_play DRAW — proves the HAND/DECK DELTA dimension (design §5's draw
|
||||
// oracle): the last deterministic, non-RNG card-effect class no prior milestone touched (M3/M4/
|
||||
// M6/M8 moved stats, M2/M5/M7 the board, M3 the leader — none read the deck->hand transfer).
|
||||
// 800114010 is an ELF (clan 1) cost-1 SPELL whose sole skill is `when_play` `draw` of ONE card
|
||||
// from the caster's own deck (skill_target=character=me&target=deck&card_type=all&random_count=1),
|
||||
// ungated (skill_condition=character=me), no evo skill, no preprocess, no dynamic `{}` value.
|
||||
//
|
||||
// ADAPTATION FROM THE RESUME-GUIDE SHAPE: the guide asked for a `skill_target=none` draw with
|
||||
// "no RNG", but no such card exists in cards.json — EVERY draw selects from the deck via a
|
||||
// `random_count=N` target filter (skill_option is always literally `none`; the count lives in
|
||||
// skill_target). The RNG is neutralized structurally instead: seed the deck with EXACTLY ONE
|
||||
// known card, so `random_count=1` over a single-card pool is deterministic regardless of the
|
||||
// RandomSeed. This keeps the oracle decisive (drawn id is forced) while exercising the real
|
||||
// draw path. Like the summon token, a drawn card is engine-CREATED off the deck the M5 prefab
|
||||
// way; unlike summon, the card already exists (we seed it) and the skill only MOVES it deck->hand.
|
||||
public const int DrawSpellId = 800114010;
|
||||
public const int DeckSeedCardId = FollowerId; // the single known deck card (neutral 1/2 vanilla)
|
||||
|
||||
// M10 next milestone: the first DYNAMIC `{}`-VALUE card — proves the engine COMPUTES an effect
|
||||
// magnitude from live game state (a value the wire can't carry; per memory
|
||||
// project_battle_relay_nontargeted_effects this state-derived-value problem is exactly what
|
||||
// broke the PvP relay, so proving the engine resolves it headless is the direct validation that
|
||||
// the port — not a relay — is the necessary path). Still non-RNG: a seeded state makes the value
|
||||
// deterministic. 112134010 is an ELF (clan 1) cost-2 SPELL whose sole skill is `when_play`
|
||||
// `damage={me.play_count}-1` to `character=both&target=inplay&card_type=unit` (with a
|
||||
// `base_card_id!=900111010|900111020` exclusion) — an AoE over BOTH boards' units, auto-targeted
|
||||
// (no select_count, so selectedCards: null like M2-M5), ungated (skill_condition=character=me).
|
||||
//
|
||||
// The `{}` value resolves (SkillOptionValue.ParseInt) as
|
||||
// `_filterVariable.Parse("me.play_count") - 1`, where Parse routes to
|
||||
// SkillEnvironmentalPlayCount.Filtering -> playerInfo.GetCurrentTurnPlayCount() (the
|
||||
// `isPrePlay=false` resolution path). That is the SAME per-turn counter the public
|
||||
// AddCurrentTrunPlayCount feeds (M4 proved this seam drove the play_count>2 GATE; M10 proves it
|
||||
// also feeds the `{}` VALUE). The per-play auto-increment AddCurrentTrunPlayCount(1) lives in
|
||||
// ActionProcessor's OnBeforePlayCard (BattlePlayerBase.cs:1400), subscribed by
|
||||
// SetupActionProcessorEvent — which is ONLY called on the OperateMgr/Prediction/OperationSimulator
|
||||
// paths, NOT on the direct `new ActionProcessor(pair).PlayCard` (DP4) path this harness uses. So
|
||||
// the headless play does NOT self-bump the per-turn count: the skill reads EXACTLY the seeded
|
||||
// GetCurrentTurnPlayCount() and the damage == seeded - 1. The oracle derives the expected
|
||||
// magnitude from the engine's OWN live GetCurrentTurnPlayCount(), not from a hardcoded literal,
|
||||
// which is the M10 dimension (engine-computed value, not a wire-carried constant).
|
||||
//
|
||||
// The target is the M6 vanilla NEUTRAL 13/13 follower (SelectTargetFollowerId, already loaded):
|
||||
// life 13 > any reasonable seeded count, so it SURVIVES for a clean life-delta read (reusing the
|
||||
// M3/M6/M8 damage->life path), and `card_type=unit` excludes both leaders (asserted untouched).
|
||||
public const int DynamicDamageSpellId = 112134010;
|
||||
public const int DynamicDamageTargetFollowerId = SelectTargetFollowerId; // neutral 13/13 (survives, clean delta)
|
||||
// A deliberately non-trivial seeded per-turn play count so the computed damage (== this value)
|
||||
// is an obvious state read, not a coincidence with a small literal. The load-bearing probe
|
||||
// (M4/M6/M8 discipline) varies this and watches the damage track it.
|
||||
public const int DynamicSeededPlayCount = 4;
|
||||
|
||||
// M12 (the design §5 RNG oracle): reuse the M9 draw spell (800114010, when_play `draw` 1 from the
|
||||
// caster's deck via a random_count=1 filter) but over a MULTI-card deck with IsRandomDraw=true.
|
||||
// M9 passed only because IsRandomDraw=false takes BattlePlayerBase.LotteryRandomDrawCard's
|
||||
// top-of-deck `else` branch (BattlePlayerBase.cs:3174-3185) — a 1-card pool made index 0 the only
|
||||
// card. With IsRandomDraw=true the selection runs through SkillRandomSelectFilter.Filtering, which
|
||||
// calls BattleManagerBase.GetIns().StableRandom(poolCount) per pick (SkillRandomSelectFilter.cs:42,
|
||||
// gated on IsRandomDraw) — the chokepoint HeadlessBattleMgr overrides. So the scripted source picks
|
||||
// exactly which deck card is drawn, proving a GENUINE multi-outcome roll (the dimension M9's
|
||||
// one-card pool deliberately avoided).
|
||||
//
|
||||
// Three distinguishable deck cards seeded at consecutive indices; SkillRandomSelectFilter orders
|
||||
// the pool by Index (line 34), so the pick index maps to position in this order:
|
||||
// index 0 -> RngDeckCardA (100011010), index 1 -> RngDeckCardB (103111050), index 2 -> RngDeckCardC (100011020)
|
||||
// All three are already loaded by HeadlessCardMaster.Load via EnsureInitialized (FollowerId,
|
||||
// BuffFollowerId, SummonedTokenId), so no Load change is needed.
|
||||
public const int RngDrawSpellId = DrawSpellId; // 800114010, when_play draw 1 (random_count=1)
|
||||
public const int RngDeckCardA = FollowerId; // neutral 1/2 -> Index-order position 0
|
||||
public const int RngDeckCardB = BuffFollowerId; // ELF 1/1 -> Index-order position 1
|
||||
public const int RngDeckCardC = SummonedTokenId; // neutral 2/2 -> Index-order position 2
|
||||
|
||||
private static bool _done;
|
||||
private static readonly object _processGlobalsGate = new object();
|
||||
|
||||
// Process-globals only: load card master, install master data, seed LoadDetail/Crossover,
|
||||
// seed Certification.udid. Per-battle/per-test state (IsForecast, chara ids on the DataMgr,
|
||||
// NetworkUserInfoData) is now seeded inside TestBattleScope's ctor against the per-scope
|
||||
// GameMgr — calling it here would crash because GameMgr.GetIns() Requires an ambient scope.
|
||||
// Thread-safe (assembly-level Parallelizable(Fixtures) means many fixtures' [SetUp] race here).
|
||||
public static void EnsureProcessGlobals()
|
||||
{
|
||||
if (_done) return;
|
||||
lock (_processGlobalsGate)
|
||||
{
|
||||
if (_done) return;
|
||||
EnsureProcessGlobalsCore();
|
||||
_done = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureProcessGlobalsCore()
|
||||
{
|
||||
// Wizard.Data.Load: static /load/index snapshot. The ctor's CreateBackgroundId reads
|
||||
// Data.Load.data._userTutorial (LoadDetail self-inits _userTutorial). Suppress VFX too.
|
||||
Wizard.Data.Load = new Load { data = new LoadDetail() };
|
||||
// CardParameter(CardCSVData) reads Data.Crossover.RestrictedCard for deck-limit calc;
|
||||
// an empty Crossover returns the default count (no restriction). Private setter -> reflect.
|
||||
typeof(Wizard.Data).GetProperty("Crossover",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)
|
||||
.SetValue(null, new Wizard.Crossover());
|
||||
// CardMaster must be non-null before construction (the leader/class card looks up id 0).
|
||||
// Load the M2 vanilla follower + the M3 fixed-damage spell + the M4 self-buff follower +
|
||||
// the M5 summon-token spell AND the token it summons so each oracle can create + look up
|
||||
// real stats. The summoned token id must be present: Skill_summon_token resolves it
|
||||
// through CardMaster.GetCardParameterFromId during creation.
|
||||
HeadlessCardMaster.Load(FollowerId, SpellId, BuffFollowerId, TokenSpellId, SummonedTokenId,
|
||||
TargetSpellId, SelectTargetFollowerId, UnselectTargetFollowerId, DestroySpellId, DrawSpellId,
|
||||
DynamicDamageSpellId);
|
||||
// Master reference data (class-character list) for leader/class card resolution.
|
||||
HeadlessMasterData.Install();
|
||||
|
||||
// The network emit path's payload builder (RealTimeNetworkAgent.CreateEmitData) reads
|
||||
// Cute.Certification.Udid (RealTimeNetworkAgent.cs:1407). The Udid getter lazily decodes from
|
||||
// Toolbox.SavedataManager (Certification.cs:35), which is null headless. Seed the private static
|
||||
// backing field with a non-empty placeholder so the getter short-circuits before touching the
|
||||
// savedata manager. The value is opaque to the engine (it's just echoed into the emit dict).
|
||||
typeof(Cute.Certification)
|
||||
.GetField("udid", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)
|
||||
.SetValue(null, "headless-udid");
|
||||
}
|
||||
|
||||
// Simple deterministic 40-card deck for multi-instance tests: every slot is the same vanilla
|
||||
// FollowerId. Card 100011010 is loaded as part of EnsureProcessGlobals' HeadlessCardMaster.Load
|
||||
// batch so SessionBattleEngine.Setup resolves each entry without re-loading. Kept a single
|
||||
// shape — the multi-instance property being verified (per-session ambient isolation across
|
||||
// parallel battles) is driven by distinct masterSeeds on the engines, not by deck variation.
|
||||
public static long[] SampleDeck()
|
||||
{
|
||||
var deck = new long[40];
|
||||
for (int i = 0; i < 40; i++) deck[i] = FollowerId;
|
||||
return deck;
|
||||
}
|
||||
|
||||
// Per-ambient seeder: writes the player/enemy chara ids onto the AMBIENT GameMgr's DataMgr.
|
||||
// Called by TestBattleScope after the scope is entered so GameMgr.GetIns() routes to the
|
||||
// per-test GameMgr, not whichever one happened to be ambient last.
|
||||
public static void SeedCharaIdsOnCurrentAmbient()
|
||||
{
|
||||
// Player/enemy leaders (chara ids must map to a ClassCharacterMasterData in Master).
|
||||
// Set the backing fields directly: the public SetPlayerCharaId() also pulls MyRotation/
|
||||
// AvatarBattle info (more null statics) which the resolution path doesn't need (the
|
||||
// TryGet* accessors are null-tolerant).
|
||||
var dm = GameMgr.GetIns().GetDataMgr();
|
||||
SetField(dm, "_playerCharaId", HeadlessMasterData.PlayerCharaId);
|
||||
SetField(dm, "_enemyCharaId", HeadlessMasterData.EnemyCharaId);
|
||||
}
|
||||
|
||||
// Per-ambient seeder: installs a no-op NetworkUserInfoData on the AMBIENT GameMgr so
|
||||
// NetworkBattleManagerBase.CreateBackgroundId()'s GetNetworkUserInfoData().GetFieldId() call
|
||||
// resolves (M13). Field id 1 == ForestField, a valid background.
|
||||
public static void SeedNetUserOnCurrentAmbient()
|
||||
{
|
||||
// NetworkBattleManagerBase.CreateBackgroundId() (M13) reads
|
||||
// GameMgr.GetIns().GetNetworkUserInfoData().GetFieldId() when the RecoveryManager yields no
|
||||
// bg id (NullRecoveryManager.BackGroundId == -1). In production RealTimeNetworkAgent seeds
|
||||
// this NetworkUserInfoData at match start; the bare construction path leaves GameMgr's
|
||||
// _netUser null (no lazy init, unlike the other GodObject getters). Seed a no-op instance
|
||||
// whose _selfInfo carries just "fieldId" (GetFieldId reads _selfInfo["fieldId"]); field id 1
|
||||
// == ForestField, a valid background. Nothing here drives game state — it only satisfies the
|
||||
// network mgr's background lookup, a background lookup the single-battle path
|
||||
// (`SingleBattleMgr`) never performs.
|
||||
var netUser = new NetworkUserInfoData();
|
||||
netUser.SetSelfInfo(
|
||||
new System.Collections.Generic.Dictionary<string, object> { ["fieldId"] = 1 },
|
||||
isWatchReplayRecovery: false);
|
||||
GameMgr.GetIns().SetNetworkUserInfoData(netUser);
|
||||
}
|
||||
|
||||
// Seed each leader's starting life on a freshly-constructed mgr. The engine does this in
|
||||
// BattleManagerBase.SetupInitialGameState -> InitializeClassLife (InitBaseMaxLife per leader),
|
||||
// but the full SetupInitialGameState also cascades into rotation/avatar/turn-panel UI init
|
||||
// that is irrelevant (and hostile) to a headless resolution test, so apply just the
|
||||
// InitializeClassLife subset. Without this a leader's BaseMaxLife defaults to 0 — which reads
|
||||
// as already-dead/game-over and silently blocks any card play (the M2 follower oracle never
|
||||
// noticed because it only asserted leader life *unchanged*, and 0 == 0).
|
||||
public const int DefaultLeaderLife = 20;
|
||||
|
||||
public static void InitLeaderLife(BattleManagerBase mgr, int life = DefaultLeaderLife)
|
||||
{
|
||||
((ClassBattleCardBase)mgr.BattlePlayer.Class).InitBaseMaxLife(life);
|
||||
((ClassBattleCardBase)mgr.BattleEnemy.Class).InitBaseMaxLife(life);
|
||||
}
|
||||
|
||||
// The PUBLIC prefab card-creation path (CardCreatorBase.CreateCard, createNullView:false) —
|
||||
// used by anything the engine creates INTERNALLY (summons, token-draws, etc.), as opposed to
|
||||
// the test's direct private null-view seam for hand cards — clones card-template prefabs held
|
||||
// on BattleManagerBase.SBattleLoad. The real async battle load (CoLoad) builds these; the bare
|
||||
// `new SingleBattleMgr(...)` construction path leaves SBattleLoad null (the M2 NRE was here).
|
||||
// Seed it with non-null no-op CardTemplates: their `.gameObject` is a lazy shim no-op, and the
|
||||
// shim's CloneObjectToParent + self-consistent object graph carry the rest. Nothing here
|
||||
// computes game state — the token's authoritative stats come from CardCSVData, not the view.
|
||||
public static void InitCardTemplates(BattleManagerBase mgr)
|
||||
{
|
||||
mgr.SBattleLoad = new SBattleLoad
|
||||
{
|
||||
UnitCardTemplate = new CardTemplate(),
|
||||
SpellCardTemplate = new CardTemplate(),
|
||||
FieldCardTemplate = new CardTemplate(),
|
||||
};
|
||||
// The created card's transform is positioned/parented under the battle's 3D scene-graph
|
||||
// containers (CardCreatorBase.CreateCardTypeBuildInfo reads ins.CardHolder/ECardHolder/
|
||||
// PCardPlace/Battle3DContainer). The real battle load instantiates these; seed non-null
|
||||
// no-op GameObjects so the positioning resolves (no-op transforms; nothing rendered).
|
||||
mgr.Battle3DContainer = new GameObject();
|
||||
mgr.CardHolder = new GameObject();
|
||||
mgr.ECardHolder = new GameObject();
|
||||
mgr.PCardPlace = new GameObject();
|
||||
mgr.ChoiceCardHolder = new GameObject();
|
||||
mgr.EvolveCardHolder = new GameObject();
|
||||
}
|
||||
|
||||
// The shared headless card-creation primitive. CardCreatorBase.CreateCardWithoutResources is
|
||||
// the engine's own null-view creation path (CreateBase -> new *BattleCard(buildInfo).Setup(
|
||||
// createNullView:true)); it's private, so reflect it rather than reimplement the 14-arg
|
||||
// BuildInfo wiring. The public CardCreatorBase.CreateCard goes through prefab cloning.
|
||||
//
|
||||
// The engine's CreateCard also calls owner.SetupCardEvent(card); the raw
|
||||
// CreateCardWithoutResources seam skips it, so we fold it in here. SetupCardEvent wires the
|
||||
// per-card play events (BattlePlayerBase.cs:1452): for a SPELL/amulet it attaches
|
||||
// OnPlay -> RemoveSpellCardFromHand and OnFinishWhenPlaySkill -> AddSpellCardToCemetery, which
|
||||
// are how a non-follower leaves the hand at all (a follower's hand->field move is intrinsic to
|
||||
// SetUpInplay, not event-driven). For a follower SetupCardEvent only attaches an OnEvolve hook
|
||||
// that never fires on a vanilla play, so folding it in is a no-op there — making this a single
|
||||
// primitive both follower and non-follower oracles can share.
|
||||
public static BattleCardBase CreateHeadlessHandCard(int cardId, int index, bool isPlayer, BattleManagerBase mgr)
|
||||
{
|
||||
var io = mgr.CreatePlayerInnerOptionsBuilder();
|
||||
var m = typeof(CardCreatorBase).GetMethod("CreateCardWithoutResources",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var card = (BattleCardBase)m.Invoke(null, new object[] { cardId, index, isPlayer, mgr, io });
|
||||
BattlePlayerBase owner = isPlayer ? (BattlePlayerBase)mgr.BattlePlayer : mgr.BattleEnemy;
|
||||
owner.SetupCardEvent(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
// Put a follower DIRECTLY onto a player's board headless (vs as a side-effect of PlayCard),
|
||||
// for setting up a target board state. Create it through the shared null-view seam, then drive
|
||||
// the engine's own hand->field move: HandCardToField requires the card to be in HandCardList,
|
||||
// then AddInplayCards it + removes it from hand (BattlePlayerBase.cs:2568). For a vanilla
|
||||
// follower the OnAddPlayCard/StopBattleHandCard/OnSummonAfter events it fires are no-ops (no
|
||||
// fanfare), so the follower lands on the board at its CardCSVData base stats. M2 proved the
|
||||
// hand->field placement path resolves headless.
|
||||
public static BattleCardBase PutFollowerInPlay(BattleManagerBase mgr, int cardId, int index, bool isPlayer)
|
||||
{
|
||||
var card = CreateHeadlessHandCard(cardId, index, isPlayer, mgr);
|
||||
BattlePlayerBase owner = isPlayer ? (BattlePlayerBase)mgr.BattlePlayer : mgr.BattleEnemy;
|
||||
owner.HandCardList.Add(card);
|
||||
owner.HandCardToField(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
// Push a known card onto a player's DECK headless (the M9 draw oracle's setup primitive). The
|
||||
// bare `new SingleBattleMgr(...)` construction leaves DeckCardList non-null-but-empty (ctor at
|
||||
// BattlePlayerBase.cs:1050), and a card's deck membership IS its `IsInDeck` (BattleCardBase.cs:970
|
||||
// `=> SelfBattlePlayer.DeckCardList.Contains(this)`) — so no separate "in deck" flag is needed.
|
||||
// Create the card through the same null-view seam hand/board cards use, then drive the engine's
|
||||
// own AddToDeck (BattlePlayerBase.cs:3038): for a vanilla follower it is just DeckCardList.Add
|
||||
// (HasDeckSelfSkill is false; the XorShiftRandom/IsMulliganEnd reshuffle bookkeeping short-
|
||||
// circuits on the null/inactive headless RNG). The drawn card is then the engine's own deck
|
||||
// object, so the oracle can assert deck->hand identity by reference, not just by id.
|
||||
public static BattleCardBase SeedDeck(BattleManagerBase mgr, int cardId, int index, bool isPlayer)
|
||||
{
|
||||
var card = CreateHeadlessHandCard(cardId, index, isPlayer, mgr);
|
||||
BattlePlayerBase owner = isPlayer ? (BattlePlayerBase)mgr.BattlePlayer : mgr.BattleEnemy;
|
||||
owner.AddToDeck(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
// Build a headless battle wired for AUTHORITATIVE RNG: real rolls under IsForecast (via the
|
||||
// injected source on HeadlessBattleMgr) AND IsRandomDraw=true (the second gate — without it the
|
||||
// random-select filters bypass the roll and pick index 0; BattleManagerBase.cs:415,
|
||||
// SkillRandomSelectFilter.cs:42). Mirrors the opponent/turn/leader-life wiring every oracle does.
|
||||
// Returns the constructed HeadlessBattleMgr; the caller seeds hands/decks/boards and plays.
|
||||
public static HeadlessBattleMgr NewAuthoritativeBattle(IRandomSource rng)
|
||||
{
|
||||
EnsureProcessGlobals(); // sets IsForecast = true among other globals
|
||||
BattleManagerBase.IsRandomDraw = true; // the second RNG gate (F-RNG-2)
|
||||
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator(), rng);
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
SetField(player, "_opponentBattlePlayer", enemy);
|
||||
SetField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
InitLeaderLife(mgr); // a 0-life leader reads as game-over and blocks plays
|
||||
InitCardTemplates(mgr); // the draw VFX touches the drawn card's view layer
|
||||
return mgr;
|
||||
}
|
||||
|
||||
// M13 emit-path read. Builds a HeadlessNetworkBattleMgr (the emitting twin of the
|
||||
// HeadlessBattleMgr NewAuthoritativeBattle returns) and stands up the OnEmit capture seam: the
|
||||
// engine's own RealTimeNetworkAgent.OnEmit event (RealTimeNetworkAgent.cs:1270) fires the played
|
||||
// URI before both emit guards, so capturing it needs no Engine/shim edit — just an injected agent.
|
||||
// Returns (mgr, emitted-URI list). The caller seeds the hand and drives mgr.OperateMgr.PlayCard.
|
||||
public static (HeadlessNetworkBattleMgr mgr, System.Collections.Generic.List<NetworkBattleDefine.NetworkBattleURI> emitted)
|
||||
NewNetworkEmitBattle(IRandomSource rng = null)
|
||||
{
|
||||
EnsureProcessGlobals(); // sets IsForecast = true among other globals
|
||||
var mgr = new HeadlessNetworkBattleMgr(new HeadlessContentsCreator(), rng);
|
||||
// NOTE: IsRecovery is left FALSE here (unlike the solo NewAuthoritativeBattle). The network
|
||||
// emit path is gated on !IsRecovery in BOTH places: NetworkStandardBattleMgr.SendPlayCard
|
||||
// (NetworkStandardBattleMgr.cs:155) and the OnSetCardComplete->SendPlayCard subscription in
|
||||
// SetUpNetworkOperateEvent (NetworkBattleManagerBase.cs:927, which early-returns under
|
||||
// IsRecovery). With IsRecovery=true the play would resolve state but never emit. (The solo
|
||||
// NewAuthoritativeBattle uses IsRecovery=true only to collapse VFX wait delays; here the no-op
|
||||
// view shims absorb the real view layer instead — see the IsForecast=false block below.)
|
||||
|
||||
// IsForecast MUST be false on the network emit path. BattleManagerBase.IsVirtualBattle is
|
||||
// `=> IsForecast` (BattleManagerBase.cs:657), and NetworkStandardBattleMgr.SendPlayCard is gated
|
||||
// on `!IsVirtualBattle` (NetworkStandardBattleMgr.cs:155) — under IsForecast=true the play
|
||||
// resolves state but the emit is suppressed. EnsureInitialized leaves IsForecast=true (correct
|
||||
// for the direct-ActionProcessor solo oracles, where it suppresses VFX); clear it here so the
|
||||
// genuine emit fires. The cost is that VFX registration is no longer short-circuited, so the
|
||||
// play exercises the real view layer — those view touches are satisfied by the no-op view shims
|
||||
// (InitCardTemplates, the HandView/DetailPanel fills below). M3's damage is literal, immune to
|
||||
// any play-count bump the OperateMgr path adds vs the direct path.
|
||||
BattleManagerBase.IsForecast = false;
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
SetField(player, "_opponentBattlePlayer", enemy);
|
||||
SetField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
InitLeaderLife(mgr); // a 0-life leader reads as game-over and blocks plays
|
||||
InitCardTemplates(mgr); // play/draw VFX touches the card view layer
|
||||
// The OperateMgr emit path runs SetupActionProcessorEvent (skipped by the direct-ActionProcessor
|
||||
// solo oracles), which subscribes BattleMgr.DetailMgr.DetailPanelControl.UpdateCardDescriptionOnEvent
|
||||
// to OnPlayComplete (BattlePlayerBase.cs:1431). DetailMgr is created in CreateManager but its
|
||||
// DetailPanelControl (a UI control) is null headless. Seed the engine's own NullDetailPanelControl
|
||||
// no-op so the play-complete event resolves without touching the UI.
|
||||
mgr.DetailMgr.DetailPanelControl = new NullDetailPanelControl();
|
||||
|
||||
// Inject a headless RealTimeNetworkAgent so NetworkBattleSender's ToolboxGame.RealTimeNetworkAgent
|
||||
// .* calls resolve, and subscribe OnEmit. GetUninitializedObject skips the MonoBehaviour Awake.
|
||||
var agent = (RealTimeNetworkAgent)System.Runtime.Serialization.FormatterServices
|
||||
.GetUninitializedObject(typeof(RealTimeNetworkAgent));
|
||||
// CurrentMatchingStatus has a protected setter; seed it non-Disconnected so EmitMsgPack does not
|
||||
// early-return at RealTimeNetworkAgent.cs:1272 (needed only for the best-effort payload read, Task 4;
|
||||
// OnEmit fires regardless). The default on the uninitialized object is OffLine (0), which clears the
|
||||
// SetCurrentMatchingStatus guards; the only side effect is a static-StringBuilder trace log, so the
|
||||
// public setter runs cleanly headless. Prepared (50) is the real enum member (RealTimeNetworkAgent.cs:35).
|
||||
agent.SetCurrentMatchingStatus(RealTimeNetworkAgent.MatchingStatus.Prepared);
|
||||
|
||||
// EmitMsgPack -> AddActionSequence (RealTimeNetworkAgent.cs:1773, fired for the PlayActions URI)
|
||||
// does `_gungnir._actionSequenceNum++` and `NetworkLogger.LogInfo(...)`. On the
|
||||
// GetUninitializedObject agent both are null (the real ctor builds them at :289/:301). Seed an
|
||||
// uninitialized Gungnir (its ctor news a ConnectionReporter + Ticks — unneeded; AddActionSequence
|
||||
// only touches the int counter) and the engine's own NetworkNullLogger no-op so the action-seq
|
||||
// bookkeeping runs without crashing. Neither drives game state.
|
||||
SetField(agent, "_gungnir",
|
||||
System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(Gungnir)));
|
||||
SetProperty(agent, "NetworkLogger", new NetworkNullLogger());
|
||||
|
||||
// Suppress the actual socket transmission. After OnEmit fires (RealTimeNetworkAgent.cs:1270, the
|
||||
// O1 liveness signal), EmitMsgPack -> EmitMsgUriPack reaches the stockEmitMessageMgr / _manager.Socket
|
||||
// network I/O (RealTimeNetworkAgent.cs:1444+/1487) — none of which exists headless. The engine's
|
||||
// OWN _notEmit flag (set in recovery/replay) short-circuits EmitMsgUriPack at :1438 BEFORE any of
|
||||
// that, so the emit stays genuine (OnEmit already fired through the real send path) while the
|
||||
// byte-push is skipped. This is the only honest way to terminate the path headless: we are NOT
|
||||
// faking OnEmit, only declining to open a socket we cannot open.
|
||||
SetField(agent, "_notEmit", true);
|
||||
|
||||
var emitted = new System.Collections.Generic.List<NetworkBattleDefine.NetworkBattleURI>();
|
||||
agent.OnEmit += uri => emitted.Add(uri);
|
||||
Wizard.ToolboxGame.SetRealTimeNetworkBattle(agent);
|
||||
|
||||
return (mgr, emitted);
|
||||
}
|
||||
|
||||
// M13 Task 4 best-effort: read the emit payload back out of the agent's stock sequencer. With
|
||||
// _notEmit=true (NewNetworkEmitBattle terminates the path that way), EmitMsgUriPack short-circuits
|
||||
// BEFORE stockEmitMessageMgr.StockData (RealTimeNetworkAgent.cs:1438 vs :1461), so the stock is
|
||||
// expected to be null/empty — return null on any null/throw so the test degrades to Inconclusive
|
||||
// rather than failing. Field `stockEmitMessageMgr` (:103) + `GetSequenceAllData()`
|
||||
// (StockEmitMgr.cs:81, returns List<Dictionary<string,object>>) verified against the copied engine.
|
||||
// Precondition: this is expected-null ONLY while NewNetworkEmitBattle sets _notEmit=true and leaves
|
||||
// stockEmitMessageMgr unconstructed. If that harness setup changes, revisit — a non-null stock should
|
||||
// then make the test ASSERT on the payload rather than defer to Inconclusive.
|
||||
public static System.Collections.IList TryReadStockedEmitData(RealTimeNetworkAgent agent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var f = typeof(RealTimeNetworkAgent).GetField("stockEmitMessageMgr",
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
var stock = f?.GetValue(agent);
|
||||
if (stock == null) return null;
|
||||
var m = stock.GetType().GetMethod("GetSequenceAllData");
|
||||
return m?.Invoke(stock, null) as System.Collections.IList;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static void SetField(object obj, string name, object value)
|
||||
{
|
||||
var f = obj.GetType().GetField(name,
|
||||
System.Reflection.BindingFlags.Instance |
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Public);
|
||||
if (f == null) throw new System.InvalidOperationException(
|
||||
$"{obj.GetType().Name} has no field '{name}'");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
// Set a property whose setter is non-public (e.g. RealTimeNetworkAgent.NetworkLogger has a
|
||||
// protected setter). Walks the type hierarchy because the declaring type may be a base class.
|
||||
private static void SetProperty(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
System.Reflection.PropertyInfo p = null;
|
||||
while (t != null && p == null)
|
||||
{
|
||||
p = t.GetProperty(name,
|
||||
System.Reflection.BindingFlags.Instance |
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Public);
|
||||
t = t.BaseType;
|
||||
}
|
||||
if (p == null) throw new System.InvalidOperationException(
|
||||
$"{obj.GetType().Name} has no property '{name}'");
|
||||
p.SetValue(obj, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Test-side replica of the engine's own StandardBattleMgrContentsCreator (the practice/solo
|
||||
// init path: GameMgr.cs:244 `new SingleBattleMgr(new StandardBattleMgrContentsCreator(null, null))`).
|
||||
// Authored here (not copied) so we control the seed deterministically; uses the real engine
|
||||
// managers verbatim. The real StandardBattleMgrContentsCreator + SingleBattlePhaseCreator were
|
||||
// cut from the M1 copy set (entry-point constructors), so we reproduce them minimally.
|
||||
public sealed class HeadlessContentsCreator : IBattleMgrContentsCreator
|
||||
{
|
||||
public int RandomSeed => 12345; // fixed; vanilla follower has no RNG so value is irrelevant
|
||||
|
||||
// No-op managers (vs the practice path's file-backed SingleBattleRecoveryRecordManager):
|
||||
// the ctor's FirstRecoverySetting/FirstReplaySetting dereference these, and recovery/replay
|
||||
// recording is irrelevant to the M2 oracle, so use the engine's own null implementations.
|
||||
public IRecoveryManager RecoveryManager { get; } = new NullRecoveryManager();
|
||||
public IRecoveryRecordManager RecoveryRecordManager { get; } = new NullRecoveryRecordManager();
|
||||
public IReplayRecordManager ReplayRecordManager { get; } = new NullReplayRecordManager();
|
||||
|
||||
public IBattleResourceMgr CreateResourceMgr() => new BattleResourceMgr();
|
||||
public VfxMgr CreateVfxMgr() => new VfxMgr();
|
||||
public IPhaseCreator CreatePhaseCreator(BattleManagerBase battleMgr) =>
|
||||
new HeadlessPhaseCreator(battleMgr);
|
||||
}
|
||||
|
||||
// Equivalent of the engine's SingleBattlePhaseCreator: inherits PhaseCreatorBase wholesale.
|
||||
public sealed class HeadlessPhaseCreator : PhaseCreatorBase
|
||||
{
|
||||
public HeadlessPhaseCreator(BattleManagerBase battleMgr) : base(battleMgr) { }
|
||||
}
|
||||
}
|
||||
64
SVSim.BattleEngine.Tests/HeadlessHandViewStubTests.cs
Normal file
64
SVSim.BattleEngine.Tests/HeadlessHandViewStubTests.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using NUnit.Framework;
|
||||
using Wizard.Battle.View;
|
||||
using Wizard.Battle.View.Vfx;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// Regression for the Heal-triggered Skill_heal NRE diagnosed 2026-06-07 (bid 799755786270).
|
||||
//
|
||||
// A follower with a `when_spell_play` Heal trigger fires on a spell play and routes through
|
||||
// Skill_heal.Start → ClassBattleCardBase.ApplyHealing → CreatePullHandInVfx
|
||||
// → HandViewBase.HandUnfocus (HandViewBase.cs:124-131)
|
||||
// The base implementation does `_handControl.SetHandState(HandControl.HandState.Unfocus)`.
|
||||
// HeadlessHandViewStub.CreateHandControl returns null in headless, so `_handControl` is null
|
||||
// and the base method NREs unconditionally — even when the heal amount is 0.
|
||||
//
|
||||
// The fix overrides HandUnfocus/HandFocus/FocusRearrangeHandHand on the stub to return
|
||||
// NullVfx without touching `_handControl`. These are PURE PRESENTATION methods (visual
|
||||
// ease-in/ease-out of the hand cards) — no game-state implications — so no-op'ing them
|
||||
// headless is safe; the surrounding state mutations in ApplyHealing (HealLife, skill triggers)
|
||||
// still run.
|
||||
//
|
||||
// Pattern parity with the metamorphose-NRE shim fix in ViewUiTouchStubs.cs (BattleCardView.GameObject
|
||||
// lazy non-null): production Unity touches that the headless engine must no-op rather than throw.
|
||||
[TestFixture]
|
||||
public class HeadlessHandViewStubTests
|
||||
{
|
||||
[Test]
|
||||
public void HandUnfocus_does_not_throw_and_returns_non_null_vfx()
|
||||
{
|
||||
var stub = HeadlessHandViewStub.Instance;
|
||||
|
||||
VfxBase vfx = null;
|
||||
Assert.DoesNotThrow(() => vfx = stub.HandUnfocus(),
|
||||
"HandUnfocus must no-op headlessly — the live regression (bid 799755786270) crashed " +
|
||||
"Skill_heal.Start when a when_spell_play Heal trigger fired with heal:0 because the " +
|
||||
"base HandUnfocus dereferences a null _handControl.");
|
||||
Assert.That(vfx, Is.Not.Null, "must return a non-null Vfx (caller registers it on a sequential player).");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandFocus_does_not_throw_and_returns_non_null_vfx()
|
||||
{
|
||||
var stub = HeadlessHandViewStub.Instance;
|
||||
|
||||
VfxBase vfx = null;
|
||||
Assert.DoesNotThrow(() => vfx = stub.HandFocus(),
|
||||
"HandFocus is the sister cosmetic touch (called from CreatePullHandOutVfx on the " +
|
||||
"OWNER's turn). Same null _handControl, same headless no-op required.");
|
||||
Assert.That(vfx, Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FocusRearrangeHandHand_does_not_throw_and_returns_non_null_vfx()
|
||||
{
|
||||
var stub = HeadlessHandViewStub.Instance;
|
||||
|
||||
VfxBase vfx = null;
|
||||
Assert.DoesNotThrow(() => vfx = stub.FocusRearrangeHandHand(),
|
||||
"FocusRearrangeHandHand reads _handControl.IsHandStateFocus() before dispatching to " +
|
||||
"HandFocus or HandUnfocus; the base implementation would NRE on the read.");
|
||||
Assert.That(vfx, Is.Not.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
SVSim.BattleEngine.Tests/HeadlessMasterData.cs
Normal file
95
SVSim.BattleEngine.Tests/HeadlessMasterData.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using Wizard;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// Builds the minimal Data.Master reference context a headless battle reads. In the client this
|
||||
// comes from the /load/index master section; here we author just enough for the resolution path
|
||||
// (currently: ClassCharacterList, so the leader/class card can resolve player/enemy class_id).
|
||||
// Entries are constructed without their CSV ctor (private setters set via reflection).
|
||||
public static class HeadlessMasterData
|
||||
{
|
||||
public const int PlayerCharaId = 1;
|
||||
public const int EnemyCharaId = 2;
|
||||
public const int PlayerClassId = 1; // ClanType -> class card clan
|
||||
public const int EnemyClassId = 2;
|
||||
|
||||
public static void Install()
|
||||
{
|
||||
var master = (Master)FormatterServices.GetUninitializedObject(typeof(Master));
|
||||
// The resolution path reads many Master.* collections (e.g. WhenPlayEffectKeywordMaster)
|
||||
// and calls LINQ on them unguarded. Default every collection member to an empty instance
|
||||
// so those touches no-op instead of NRE; then override the ones we need with content.
|
||||
EnsureEmptyCollections(master);
|
||||
var list = new List<ClassCharacterMasterData>
|
||||
{
|
||||
NewChara(PlayerCharaId, PlayerClassId),
|
||||
NewChara(EnemyCharaId, EnemyClassId),
|
||||
};
|
||||
SetMember(master, "ClassCharacterList", list);
|
||||
Data.Master = master;
|
||||
}
|
||||
|
||||
// Initialize every List<>/array/Dictionary<> field/auto-property on the object to an empty
|
||||
// non-null instance (only if currently null).
|
||||
private static void EnsureEmptyCollections(object obj)
|
||||
{
|
||||
const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
foreach (var f in obj.GetType().GetFields(bf))
|
||||
{
|
||||
if (f.GetValue(obj) != null) continue;
|
||||
var empty = EmptyOf(f.FieldType);
|
||||
if (empty != null) f.SetValue(obj, empty);
|
||||
}
|
||||
}
|
||||
|
||||
private static object EmptyOf(Type t)
|
||||
{
|
||||
if (t.IsArray) return Array.CreateInstance(t.GetElementType(), 0);
|
||||
if (t.IsGenericType)
|
||||
{
|
||||
var def = t.GetGenericTypeDefinition();
|
||||
if (def == typeof(List<>) || def == typeof(Dictionary<,>) ||
|
||||
def == typeof(HashSet<>) || def == typeof(IList<>) ||
|
||||
def == typeof(IDictionary<,>) || def == typeof(ICollection<>) ||
|
||||
def == typeof(IEnumerable<>))
|
||||
{
|
||||
var concrete = def == typeof(List<>) || def == typeof(IList<>) ||
|
||||
def == typeof(ICollection<>) || def == typeof(IEnumerable<>)
|
||||
? typeof(List<>).MakeGenericType(t.GetGenericArguments())
|
||||
: def == typeof(HashSet<>)
|
||||
? typeof(HashSet<>).MakeGenericType(t.GetGenericArguments())
|
||||
: typeof(Dictionary<,>).MakeGenericType(t.GetGenericArguments());
|
||||
return Activator.CreateInstance(concrete);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ClassCharacterMasterData NewChara(int charaId, int classId)
|
||||
{
|
||||
var c = (ClassCharacterMasterData)FormatterServices.GetUninitializedObject(typeof(ClassCharacterMasterData));
|
||||
SetMember(c, "chara_id", charaId);
|
||||
SetMember(c, "class_id", classId);
|
||||
SetMember(c, "skin_id", charaId);
|
||||
SetMember(c, "is_usable", true);
|
||||
return c;
|
||||
}
|
||||
|
||||
// Set a member (auto-property backing field or field) by name, tolerating private setters.
|
||||
private static void SetMember(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
var p = t.GetProperty(name, bf);
|
||||
if (p != null && p.SetMethod != null) { p.SetValue(obj, value); return; }
|
||||
var f = t.GetField(name, bf)
|
||||
?? t.GetField($"<{name}>k__BackingField", bf);
|
||||
if (f != null) { f.SetValue(obj, value); return; }
|
||||
throw new InvalidOperationException($"{t.Name} has no settable member '{name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
134
SVSim.BattleEngine.Tests/LethalDamageSpellOracleTests.cs
Normal file
134
SVSim.BattleEngine.Tests/LethalDamageSpellOracleTests.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M8 (death VIA COMBAT MATH): a when_play TARGETED-DAMAGE spell whose amount is >= the target
|
||||
// follower's life resolves to correct authoritative state HEADLESS via the same IsForecast/
|
||||
// IsRecovery + ActionProcessor + selectedCards path M6/M7 proved. M3 proved `damage` to the LEADER
|
||||
// (life-delta, no death). M7 proved board-removal via UNCONDITIONAL `destroy`. M8 closes the gap
|
||||
// between them: the follower dies as a CONSEQUENCE of damage -> life<=0 -> the dead-check + the same
|
||||
// RemoveInplayCard/cemetery path M7 lit up — the dominant real-card removal mechanic (most "deal N
|
||||
// damage" cards), reached through combat math rather than a `destroy` skill.
|
||||
//
|
||||
// The spell is select_count=1 (proven in M6 — it hits ONLY the selected target), so the oracle is:
|
||||
// with two followers on the enemy board STRADDLING the 5 damage and the LETHAL one passed as
|
||||
// `selectedCards`, the selected follower (life 2 <= 5) DIES from combat math (enemy board -1, gone,
|
||||
// in CemeteryList — the M7 removal assertions, but reached via damage not `destroy`), while the
|
||||
// un-selected control (life 7 > 5) is UNTOUCHED (life unchanged, still on board — the M6 routing
|
||||
// assertion). The STRADDLE is what makes death-via-combat-math falsifiable: the load-bearing probe
|
||||
// (swap the selection to the 6/7) makes that follower SURVIVE at 2 (7-5) and NOBODY die — proving
|
||||
// the removal is gated on the SELECTED follower's life reaching <= 0 (combat math), not on
|
||||
// "selected gets removed" (which would be M7's unconditional `destroy`) or a blanket wipe.
|
||||
[TestFixture]
|
||||
public class LethalDamageSpellOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Lethal_damage_spell_kills_the_selected_follower_and_chips_the_survivor()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Minimal opponent/turn wiring (see M2-M7 oracles): opponent refs + active turn flag. The
|
||||
// spell's target resolver walks player -> opponent -> opponent's in-play followers.
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
// Seed leader life so neither leader reads as a 0-life game-over state (blocks plays, M3).
|
||||
HeadlessEngineEnv.InitLeaderLife(mgr);
|
||||
|
||||
// Put TWO vanilla followers on the ENEMY board STRADDLING the 5 damage: the SELECTED target
|
||||
// has life 2 (<= 5) so it dies; the un-selected control has life 7 (> 5) and, being a
|
||||
// select_count=1 spell's non-target, is untouched. (The straddle powers the load-bearing
|
||||
// probe: selecting the 6/7 instead makes it survive at 2 and nobody die.)
|
||||
var selected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.LethalTargetFollowerId, 0, isPlayer: false);
|
||||
var survivor = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.SurvivorTargetFollowerId, 1, isPlayer: false);
|
||||
|
||||
// Sanity: the chosen ids actually straddle the damage (one lethal, one not) at setup.
|
||||
Assert.That(selected.Life, Is.LessThanOrEqualTo(HeadlessEngineEnv.LethalDamage),
|
||||
"selected follower's life is not <= the spell damage (it would not die)");
|
||||
Assert.That(survivor.Life, Is.GreaterThan(HeadlessEngineEnv.LethalDamage),
|
||||
"survivor follower's life is not > the spell damage (it would not survive)");
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.LethalDamageSpellId);
|
||||
|
||||
// Place the lethal-damage spell in the active player's hand with PP to spare.
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.LethalDamageSpellId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
// Pre-state snapshot.
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
|
||||
int enemyCemeteryBefore = enemy.CemeteryList.Count;
|
||||
int survivorLifeBefore = survivor.Life;
|
||||
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
|
||||
|
||||
// Resolve the play through the real engine, passing the chosen (lethal) target via selectedCards.
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: new List<BattleCardBase> { selected }),
|
||||
"ActionProcessor.PlayCard threw on a lethal targeted-damage spell");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// PRIMARY M8 — death via combat math: the SELECTED follower (life <= damage) is removed
|
||||
// from the enemy board and lands in the cemetery (the M7 removal dimension, reached
|
||||
// through damage rather than `destroy`).
|
||||
Assert.That(enemy.ClassAndInPlayCardList, Does.Not.Contain(selected),
|
||||
"lethal-damaged follower still on the enemy board (death-via-damage did not remove it)");
|
||||
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore - 1),
|
||||
"enemy board count not -1 (lethal damage did not commit a removal, or hit the wrong count)");
|
||||
Assert.That(enemy.CemeteryList, Contains.Item(selected),
|
||||
"lethal-damaged follower not in the enemy CemeteryList");
|
||||
Assert.That(enemy.CemeteryList.Count, Is.EqualTo(enemyCemeteryBefore + 1),
|
||||
"enemy cemetery count not +1");
|
||||
|
||||
// PRIMARY M8 — routing: the UN-SELECTED control (life > damage) is UNTOUCHED and stays on
|
||||
// the board (the M6 routing assertion; select_count=1 hits only the selected target, so
|
||||
// this proves the lethal removal was routed to the selection and is not a blanket wipe).
|
||||
Assert.That(enemy.ClassAndInPlayCardList, Contains.Item(survivor),
|
||||
"un-selected follower was removed (effect not routed, or a blanket wipe)");
|
||||
Assert.That(survivor.Life, Is.EqualTo(survivorLifeBefore),
|
||||
"un-selected follower took damage (effect not routed to the selection)");
|
||||
|
||||
// Leader untouched (the spell targets a follower, not the face).
|
||||
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore),
|
||||
"opponent leader life changed (damage hit the leader, not the selected follower)");
|
||||
|
||||
// Cost paid; spell leaves hand and (being a spell) does NOT occupy the board.
|
||||
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
|
||||
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
|
||||
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
|
||||
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
99
SVSim.BattleEngine.Tests/MultiInstanceEngineTests.cs
Normal file
99
SVSim.BattleEngine.Tests/MultiInstanceEngineTests.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleEngine.Ambient;
|
||||
using SVSim.BattleNode.Sessions.Engine;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests;
|
||||
|
||||
/// <summary>The forcing-function tests for the multi-instancing migration (Task 8). Each engine
|
||||
/// instance carries its OWN <see cref="BattleAmbientContext"/> internally (SessionBattleEngine
|
||||
/// constructs a per-session ctx in its field initializer and enters it on every Setup/Receive/
|
||||
/// read), so two engines on two tasks must resolve independently — no shared "current mgr",
|
||||
/// "current GameMgr", or "current viewer id" state. The stress test pins
|
||||
/// parallel-equals-sequential to catch any residual contamination (which would manifest as a
|
||||
/// life/PP/hand-count mismatch between the parallel and sequential runs).</summary>
|
||||
[TestFixture, Parallelizable(ParallelScope.All)]
|
||||
public class MultiInstanceEngineTests
|
||||
{
|
||||
[OneTimeSetUp]
|
||||
public void OneTimeSetUp() => HeadlessEngineEnv.EnsureProcessGlobals();
|
||||
|
||||
[Test]
|
||||
public async Task TwoBattles_ResolveIndependently_OnDifferentTasks()
|
||||
{
|
||||
var engineA = new SessionBattleEngine();
|
||||
var engineB = new SessionBattleEngine();
|
||||
engineA.Setup(masterSeed: 111, HeadlessEngineEnv.SampleDeck(), HeadlessEngineEnv.SampleDeck(),
|
||||
seatAClass: 1, seatBClass: 2);
|
||||
engineB.Setup(masterSeed: 222, HeadlessEngineEnv.SampleDeck(), HeadlessEngineEnv.SampleDeck(),
|
||||
seatAClass: 5, seatBClass: 7);
|
||||
|
||||
var taskA = Task.Run(() => DriveBasicTurns(engineA));
|
||||
var taskB = Task.Run(() => DriveBasicTurns(engineB));
|
||||
await Task.WhenAll(taskA, taskB);
|
||||
|
||||
// Pin the engines' post-Setup state to concrete starting values: LeaderLife=20 (InitLeaderLife's
|
||||
// DefaultLeaderLife, applied by SessionBattleEngine.Setup), Pp=0 (pre-first-turn, no PP refill
|
||||
// has run), HandCount=0 (Setup builds the deck/leader graph but doesn't deal an opening hand —
|
||||
// mulligan/draw happens once a turn-start phase runs, which DriveBasicTurns doesn't trigger).
|
||||
// Both engines must report the SAME starting state regardless of distinct masterSeeds, which is
|
||||
// the cross-contamination property under test: ambient isolation means neither engine's reads
|
||||
// can leak into the other's seat lookups.
|
||||
Assert.That(engineA.LeaderLife(true), Is.EqualTo(20));
|
||||
Assert.That(engineB.LeaderLife(true), Is.EqualTo(20));
|
||||
Assert.That(engineA.Pp(true), Is.EqualTo(0));
|
||||
Assert.That(engineB.Pp(true), Is.EqualTo(0));
|
||||
Assert.That(engineA.HandCount(true), Is.EqualTo(0));
|
||||
Assert.That(engineB.HandCount(true), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task StressN_BaselineMatches([Values(4, 8, 16)] int n)
|
||||
{
|
||||
var inputs = new (int seed, long[] deckA, long[] deckB)[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
inputs[i] = (1000 + i, HeadlessEngineEnv.SampleDeck(), HeadlessEngineEnv.SampleDeck());
|
||||
|
||||
// Setup AND Drive both parallelize: the residual decomp-origin static accumulators
|
||||
// (Wizard.LocalLog._lastTraceLogStringBuilder etc.) and the Unity Resources shim
|
||||
// cache are now thread-safe (static lock / ConcurrentDictionary), so two engines
|
||||
// constructing in parallel no longer corrupts shared scratch state. The full
|
||||
// construct-then-read pipeline runs concurrently per task and the result still
|
||||
// pins to the sequential baseline — that is the cross-contamination property
|
||||
// under test (ambient isolation + safe shared statics).
|
||||
var parallel = await Task.WhenAll(inputs.Select(input => Task.Run(() =>
|
||||
{
|
||||
var e = new SessionBattleEngine();
|
||||
e.Setup(input.seed, input.deckA, input.deckB);
|
||||
DriveBasicTurns(e);
|
||||
return e.LeaderLife(true);
|
||||
})));
|
||||
|
||||
var sequential = new int[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var e = new SessionBattleEngine();
|
||||
e.Setup(inputs[i].seed, inputs[i].deckA, inputs[i].deckB);
|
||||
DriveBasicTurns(e);
|
||||
sequential[i] = e.LeaderLife(true);
|
||||
}
|
||||
|
||||
Assert.That(parallel, Is.EqualTo(sequential));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GameMgr_GetIns_WithoutScope_Throws()
|
||||
{
|
||||
Assert.That(BattleAmbient.Current, Is.Null);
|
||||
Assert.Throws<System.InvalidOperationException>(() => GameMgr.GetIns());
|
||||
}
|
||||
|
||||
private static void DriveBasicTurns(SessionBattleEngine e)
|
||||
{
|
||||
_ = e.LeaderLife(true);
|
||||
_ = e.Pp(true);
|
||||
_ = e.HandCount(true);
|
||||
}
|
||||
}
|
||||
19
SVSim.BattleEngine.Tests/NetworkEmitFixtureBase.cs
Normal file
19
SVSim.BattleEngine.Tests/NetworkEmitFixtureBase.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// Shared base for every network-emit test fixture (M13 EmitPathReadOracleTests, the
|
||||
// construction-probe's OnEmit seam test, and any M14+ network fixture to come).
|
||||
//
|
||||
// POST-TASK-8 (multi-instancing migration): now empty. The historical hygiene gap this class
|
||||
// closed (HeadlessEngineEnv.NewNetworkEmitBattle leaving IsForecast=false + a stray injected
|
||||
// agent visible to a later solo fixture) was a PROCESS-GLOBAL leak via the now-deleted
|
||||
// BattleManagerBase._isForecastFallback + ToolboxGame._realTimeNetworkAgentFallback statics.
|
||||
// Both fields are gone: IsForecast/RealTimeNetworkAgent live on the per-test ambient context
|
||||
// (TestBattleScope's BattleAmbientContext), so scope Dispose drops them. A later fixture's
|
||||
// new TestBattleScope starts a fresh ctx with IsForecast=true and a null NetworkAgent by
|
||||
// default — exactly the EnsureInitialized invariant the old TearDown manually restored.
|
||||
//
|
||||
// Kept as a marker base class so derived fixtures don't churn; can be deleted in Task 9.
|
||||
public abstract class NetworkEmitFixtureBase
|
||||
{
|
||||
}
|
||||
}
|
||||
41
SVSim.BattleEngine.Tests/NetworkMgrConstructionProbeTests.cs
Normal file
41
SVSim.BattleEngine.Tests/NetworkMgrConstructionProbeTests.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleEngine.Rng;
|
||||
using Wizard.BattleMgr;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M13 step 1 (the M2 ConstructionProbe pattern): can a NetworkBattleManagerBase-derived mgr be
|
||||
// built headless at all? NetworkBattleManagerSetup constructs NetworkTouchControl(this,
|
||||
// _battleCamera, _backGround) + RegisterActionManager + OperateReceive — the largest new shim
|
||||
// surface since M5's prefab path. Isolate "ctor runs" before any play is driven.
|
||||
[TestFixture]
|
||||
public class NetworkMgrConstructionProbeTests : NetworkEmitFixtureBase
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
[Test]
|
||||
public void HeadlessNetworkBattleMgr_constructs_headless()
|
||||
{
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
var mgr = new HeadlessNetworkBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
Assert.That(mgr, Is.Not.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnEmit_capture_seam_is_wired_via_injected_agent()
|
||||
{
|
||||
var (mgr, emitted) = HeadlessEngineEnv.NewNetworkEmitBattle();
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
Assert.That(mgr, Is.Not.Null);
|
||||
Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.Not.Null,
|
||||
"agent must be injected so NetworkBattleSender's ToolboxGame.RealTimeNetworkAgent.* calls resolve");
|
||||
Assert.That(emitted, Is.Empty, "no emit yet — only the seam is wired");
|
||||
}
|
||||
}
|
||||
}
|
||||
84
SVSim.BattleEngine.Tests/RandomDrawOracleTests.cs
Normal file
84
SVSim.BattleEngine.Tests/RandomDrawOracleTests.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleEngine.Rng;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M12: the first card whose outcome is a GENUINE RNG roll. The M9 draw spell over a 3-card deck with
|
||||
// IsRandomDraw=true selects via SkillRandomSelectFilter -> GetIns().StableRandom(poolCount), which
|
||||
// HeadlessBattleMgr routes to the injected ScriptedRandomSource. The oracle asserts the engine drew
|
||||
// EXACTLY the card the scripted roll selects, and (load-bearing) that the pick TRACKS the script:
|
||||
// a different scripted unit draws a different card. This is the multi-outcome roll M9's one-card pool
|
||||
// deliberately neutralized — it requires the F2 decoupling (real rolls under IsForecast) AND the
|
||||
// IsRandomDraw=true second gate, both delivered by NewAuthoritativeBattle.
|
||||
[TestFixture]
|
||||
public class RandomDrawOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
|
||||
[TearDown]
|
||||
public void ResetRandomDrawGate()
|
||||
{
|
||||
// NewAuthoritativeBattle sets the process-global BattleManagerBase.IsRandomDraw = true; reset it
|
||||
// so this fixture doesn't leak that state into later-running fixtures (which expect the default
|
||||
// false / top-of-deck draw behavior). Prevents order-dependent flakes as more RNG oracles land.
|
||||
// (Now an ambient write inside the scope; harmless either way.)
|
||||
BattleManagerBase.IsRandomDraw = false;
|
||||
_scope?.Dispose();
|
||||
_scope = null;
|
||||
}
|
||||
|
||||
// Draw with a single scripted unit; return (drawnCardId, deckCountAfter). The deck is seeded with
|
||||
// three distinguishable cards at indices 2,3,4 -> Index-order positions 0,1,2 map to
|
||||
// RngDeckCardA/B/C. The draw makes one StableRandom(3) call -> index = floor(3*unit).
|
||||
private (int drawnId, int deckAfter) DrawWith(double unit)
|
||||
{
|
||||
var mgr = HeadlessEngineEnv.NewAuthoritativeBattle(new ScriptedRandomSource(new[] { unit }));
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
var player = mgr.BattlePlayer;
|
||||
|
||||
HeadlessEngineEnv.SeedDeck(mgr, HeadlessEngineEnv.RngDeckCardA, index: 2, isPlayer: true);
|
||||
HeadlessEngineEnv.SeedDeck(mgr, HeadlessEngineEnv.RngDeckCardB, index: 3, isPlayer: true);
|
||||
HeadlessEngineEnv.SeedDeck(mgr, HeadlessEngineEnv.RngDeckCardC, index: 4, isPlayer: true);
|
||||
|
||||
var spell = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.RngDrawSpellId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(spell);
|
||||
player.Pp = 10;
|
||||
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(spell, selectedCards: null), "PlayCard threw on the random draw");
|
||||
|
||||
// The drawn card is the new hand entry that is not the spell.
|
||||
var drawn = player.HandCardList.Single(c => c.CardId != HeadlessEngineEnv.RngDrawSpellId);
|
||||
return (drawn.CardId, player.DeckCardList.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Random_draw_picks_the_scripted_card()
|
||||
{
|
||||
// unit 0.5 -> floor(3*0.5)=1 -> Index-order position 1 -> RngDeckCardB.
|
||||
var (drawnId, deckAfter) = DrawWith(0.5);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(drawnId, Is.EqualTo(HeadlessEngineEnv.RngDeckCardB),
|
||||
"scripted roll 0.5 should draw the middle (Index-order position 1) deck card");
|
||||
Assert.That(deckAfter, Is.EqualTo(2), "deck should be 3 -> 2 after drawing one");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Random_draw_pick_tracks_the_scripted_roll()
|
||||
{
|
||||
// Load-bearing: varying the scripted unit must move the pick across all three positions.
|
||||
// floor(3*0.0)=0 -> A ; floor(3*0.5)=1 -> B ; floor(3*0.9)=2 -> C.
|
||||
Assert.That(DrawWith(0.0).drawnId, Is.EqualTo(HeadlessEngineEnv.RngDeckCardA), "0.0 -> position 0");
|
||||
Assert.That(DrawWith(0.5).drawnId, Is.EqualTo(HeadlessEngineEnv.RngDeckCardB), "0.5 -> position 1");
|
||||
Assert.That(DrawWith(0.9).drawnId, Is.EqualTo(HeadlessEngineEnv.RngDeckCardC), "0.9 -> position 2");
|
||||
}
|
||||
}
|
||||
}
|
||||
105
SVSim.BattleEngine.Tests/RngSeamTests.cs
Normal file
105
SVSim.BattleEngine.Tests/RngSeamTests.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleEngine.Rng;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class RngSeamTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
// RandomSourceBridge.Range must mirror the engine's exact roll arithmetic:
|
||||
// BattleManagerBase.StableRandom does `(int)Math.Floor((double)val * unit)`.
|
||||
[Test]
|
||||
public void Bridge_Range_mirrors_engine_floor_arithmetic()
|
||||
{
|
||||
Assert.That(RandomSourceBridge.Range(7, 0.0), Is.EqualTo(0)); // floor(7*0) = 0
|
||||
Assert.That(RandomSourceBridge.Range(7, 0.999), Is.EqualTo(6)); // floor(6.993) = 6 (never == val)
|
||||
Assert.That(RandomSourceBridge.Range(3, 0.5), Is.EqualTo(1)); // floor(1.5) = 1 (middle of 3)
|
||||
Assert.That(RandomSourceBridge.Range(1, 0.5), Is.EqualTo(0)); // floor(0.5) = 0
|
||||
}
|
||||
|
||||
// SeededRandomSource(seed) must reproduce the engine's own generators EXACTLY: BattleManagerBase
|
||||
// seeds both _stableRandom and _stableRandomOnlySelf as `new System.Random(RandomSeed)`
|
||||
// (BattleManagerBase.cs:721-722). NextUnit() == synced.NextDouble(); NextSelf(max) == self.Next(max).
|
||||
[Test]
|
||||
public void SeededSource_reproduces_two_System_Random_streams()
|
||||
{
|
||||
const int seed = 12345;
|
||||
var src = new SeededRandomSource(seed);
|
||||
|
||||
var refSynced = new System.Random(seed); // mirrors _stableRandom
|
||||
var refSelf = new System.Random(seed); // mirrors _stableRandomOnlySelf (separate stream)
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
Assert.That(src.NextUnit(), Is.EqualTo(refSynced.NextDouble()), $"NextUnit drift at {i}");
|
||||
for (int i = 0; i < 8; i++)
|
||||
Assert.That(src.NextSelf(100), Is.EqualTo(refSelf.Next(100)), $"NextSelf drift at {i}");
|
||||
}
|
||||
|
||||
// ScriptedRandomSource feeds a known sequence (the oracle's control + the Phase-3 replay seam).
|
||||
// It MUST throw on overrun, not wrap: an unexpected extra roll should fail loudly so a test
|
||||
// surfaces a miscount of engine RNG calls rather than silently reusing a value.
|
||||
[Test]
|
||||
public void ScriptedSource_returns_sequence_then_throws_on_overrun()
|
||||
{
|
||||
var src = new ScriptedRandomSource(new[] { 0.1, 0.5 }, new[] { 3 });
|
||||
|
||||
Assert.That(src.NextUnit(), Is.EqualTo(0.1));
|
||||
Assert.That(src.NextUnit(), Is.EqualTo(0.5));
|
||||
Assert.That(() => src.NextUnit(), Throws.InvalidOperationException, "should throw on unit overrun");
|
||||
|
||||
Assert.That(src.NextSelf(99), Is.EqualTo(3));
|
||||
Assert.That(() => src.NextSelf(99), Throws.InvalidOperationException, "should throw on self overrun");
|
||||
}
|
||||
|
||||
// The decoupling (F2): the override must roll REAL values even though IsForecast == true (which
|
||||
// forces the un-overridden engine methods to return 0). A ScriptedRandomSource proves the value
|
||||
// came from the injected source, not the engine's zeroing.
|
||||
[Test]
|
||||
public void Override_rolls_real_values_under_IsForecast()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // would zero the un-overridden engine RNG
|
||||
|
||||
// 3 units; with RandomSourceBridge.Range(val, unit) = floor(val*unit):
|
||||
// StableRandom(7) with 0.5 -> floor(3.5) = 3
|
||||
// StableRandomDouble() -> 0.25
|
||||
// StableRandomOnlySelf(10) -> scripted self pick 4
|
||||
var src = new ScriptedRandomSource(new[] { 0.5, 0.25 }, new[] { 4 });
|
||||
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator(), src);
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
|
||||
Assert.That(mgr.StableRandom(7), Is.EqualTo(3), "StableRandom did not use the injected source");
|
||||
Assert.That(mgr.randomResult, Is.EqualTo(0.5), "StableRandom must set randomResult to the rolled unit");
|
||||
Assert.That(mgr.StableRandomDouble(), Is.EqualTo(0.25), "StableRandomDouble did not use the injected source");
|
||||
Assert.That(mgr.randomResult, Is.EqualTo(0.25), "StableRandomDouble must set randomResult");
|
||||
Assert.That(mgr.StableRandomOnlySelf(10), Is.EqualTo(4), "StableRandomOnlySelf did not use the injected source");
|
||||
}
|
||||
|
||||
// Parity: with the DEFAULT (seeded) source, HeadlessBattleMgr.StableRandom must equal what the
|
||||
// verbatim engine would compute — floor(val * new System.Random(seed).NextDouble()) — pinning the
|
||||
// re-authored RandomSourceBridge arithmetic to the engine's own formula+generator. (The default
|
||||
// source seeds from HeadlessContentsCreator.RandomSeed == 12345.)
|
||||
[Test]
|
||||
public void Default_source_matches_engine_generator_and_formula()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true;
|
||||
|
||||
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator()); // default SeededRandomSource(12345)
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
var reference = new System.Random(12345);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
int expected = (int)System.Math.Floor(7 * reference.NextDouble());
|
||||
Assert.That(mgr.StableRandom(7), Is.EqualTo(expected), $"parity drift at roll {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
SVSim.BattleEngine.Tests/SVSim.BattleEngine.Tests.csproj
Normal file
39
SVSim.BattleEngine.Tests/SVSim.BattleEngine.Tests.csproj
Normal file
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<!-- Match the engine: decompiled types are not nullable-clean and use explicit usings. -->
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<!-- Pinned to 12.0 (net8.0 default) to match SVSim.BattleEngine; see the rationale there
|
||||
(the vendored decompiled engine breaks under C# 14's 'field' contextual keyword). -->
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SVSim.BattleEngine\SVSim.BattleEngine.csproj" />
|
||||
<ProjectReference Include="..\SVSim.BattleNode\SVSim.BattleNode.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Captured PvP battle (both clients) replayed through the engine in the N1 shadow test. -->
|
||||
<None Include="Fixtures\**\*.ndjson" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The loader's card-master dump (serialized CardCSVData objects). The headless fixture
|
||||
reflects these into CardMaster so the resolution path can look up real card stats. -->
|
||||
<Content Include="..\SVSim.Bootstrap\Data\cards.json" Link="Data\cards.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
103
SVSim.BattleEngine.Tests/SessionEngine/CaptureReplay.cs
Normal file
103
SVSim.BattleEngine.Tests/SessionEngine/CaptureReplay.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
internal sealed record CapturedFrame(DateTime Ts, string Direction, string Uri, MsgEnvelope Env, string RawBody);
|
||||
|
||||
/// <summary>Parses a battle_test ndjson capture into MsgEnvelopes the engine can ingest.
|
||||
///
|
||||
/// Capture quirk (verified against data_dumps/captures/battle_test): the authoritative URI lives at
|
||||
/// the TOP LEVEL for SEND frames (the body omits uri/viewerId/uuid and carries only the play
|
||||
/// payload) and in the BODY for RECEIVE frames (top-level uri is null). We resolve uri as
|
||||
/// top ?? body, then normalize the body into a full envelope (injecting the fields a send-frame body
|
||||
/// lacks) so MsgEnvelope.FromJson — which requires uri/viewerId/uuid — succeeds for both.</summary>
|
||||
internal static class CaptureReplay
|
||||
{
|
||||
public static IReadOnlyList<CapturedFrame> Load(string fixtureFileName)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", fixtureFileName);
|
||||
var frames = new List<CapturedFrame>();
|
||||
foreach (var line in File.ReadLines(path))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
using var doc = JsonDocument.Parse(line);
|
||||
var root = doc.RootElement;
|
||||
var direction = root.TryGetProperty("direction", out var dEl) ? dEl.GetString() ?? "" : "";
|
||||
var ts = root.TryGetProperty("ts", out var tsEl) && tsEl.ValueKind == JsonValueKind.String
|
||||
? DateTime.Parse(tsEl.GetString()!, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind)
|
||||
: default;
|
||||
if (!root.TryGetProperty("body", out var bodyEl) || bodyEl.ValueKind != JsonValueKind.Object)
|
||||
continue;
|
||||
|
||||
string uri =
|
||||
root.TryGetProperty("uri", out var tu) && tu.ValueKind == JsonValueKind.String
|
||||
? tu.GetString()!
|
||||
: bodyEl.TryGetProperty("uri", out var bu) && bu.ValueKind == JsonValueKind.String
|
||||
? bu.GetString()!
|
||||
: "None";
|
||||
|
||||
// Normalize: send-frame bodies are bare payloads (no envelope fields). Inject the keys
|
||||
// FromJson requires; set the resolved uri.
|
||||
var obj = JsonNode.Parse(bodyEl.GetRawText())!.AsObject();
|
||||
obj["uri"] = uri;
|
||||
if (!obj.ContainsKey("viewerId")) obj["viewerId"] = 0L;
|
||||
if (!obj.ContainsKey("uuid")) obj["uuid"] = "";
|
||||
var normalized = obj.ToJsonString();
|
||||
|
||||
MsgEnvelope env;
|
||||
try { env = MsgEnvelope.FromJson(normalized); }
|
||||
catch { continue; } // out-of-model / unparseable line
|
||||
frames.Add(new CapturedFrame(ts, direction, uri, env, normalized));
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
/// <summary>Both clients' SENT frames interleaved in capture (ts) order, each tagged with its
|
||||
/// seat: cl1 == seat A == player (true), cl2 == seat B == opponent (false). This is the node's
|
||||
/// both-clients-sends ingest order — the same ts ordering the N1 shadow-replay test uses, here
|
||||
/// extended to merge both sides' sends rather than replaying one client's full receive stream.</summary>
|
||||
public static IEnumerable<(MsgEnvelope Env, bool Seat)> InterleavedSends(
|
||||
IReadOnlyList<CapturedFrame> cl1, IReadOnlyList<CapturedFrame> cl2)
|
||||
{
|
||||
return cl1.Where(f => f.Direction == "send").Select(f => (f, Seat: true))
|
||||
.Concat(cl2.Where(f => f.Direction == "send").Select(f => (f, Seat: false)))
|
||||
.OrderBy(x => x.f.Ts)
|
||||
.Select(x => (x.f.Env, x.Seat));
|
||||
}
|
||||
|
||||
/// <summary>The selfDeck idx->cardId order from the Matched frame (the order the node also
|
||||
/// computed and handed the client). This is the deck the engine seats for that side.</summary>
|
||||
public static IReadOnlyList<long> SelfDeckFrom(IEnumerable<CapturedFrame> frames)
|
||||
{
|
||||
var matched = frames.FirstOrDefault(f => f.Uri == nameof(NetworkBattleUri.Matched));
|
||||
if (matched is null) return Array.Empty<long>();
|
||||
using var doc = JsonDocument.Parse(matched.RawBody);
|
||||
if (!doc.RootElement.TryGetProperty("selfDeck", out var deck)) return Array.Empty<long>();
|
||||
return deck.EnumerateArray()
|
||||
.OrderBy(e => e.GetProperty("idx").GetInt32())
|
||||
.Select(e => e.GetProperty("cardId").GetInt64())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>The per-battle master seed the capture carries (Matched.selfInfo.seed) — the seed the
|
||||
/// node generated and both clients used (F-N-5). Falls back to 0 if absent.</summary>
|
||||
public static int SeedFrom(IEnumerable<CapturedFrame> frames)
|
||||
{
|
||||
var matched = frames.FirstOrDefault(f => f.Uri == nameof(NetworkBattleUri.Matched));
|
||||
if (matched is null) return 0;
|
||||
using var doc = JsonDocument.Parse(matched.RawBody);
|
||||
if (doc.RootElement.TryGetProperty("selfInfo", out var si)
|
||||
&& si.TryGetProperty("seed", out var seed)
|
||||
&& seed.TryGetInt32(out var v))
|
||||
return v;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Sessions.Engine;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// PHASE 4 — DECISIVE VERIFICATION (TEST-ONLY, no production fix, no Engine/*.cs edits).
|
||||
///
|
||||
/// QUESTION: does feeding the headless shadow engine the FULL client inputs (server-authored
|
||||
/// Deal/Swap/Ready setup frames for BOTH seats + the real per-seat <c>idxChangeSeed</c>) make its
|
||||
/// recovery-mode draw recompute faithful, so the "Target card was not found in hand cards"
|
||||
/// divergences vanish?
|
||||
///
|
||||
/// This builds the explicit 2x2 {setup-frames ingested: yes/no} x {real seed: yes/no} divergence
|
||||
/// table over the SAME fresh battle (907324319325, battle_test_fresh_cl1/cl2.ndjson), and — at the
|
||||
/// FIRST remaining divergence — dumps the engine's hand indices/ids vs the wire's <c>playIdx</c>.
|
||||
///
|
||||
/// SEEDING MECHANISM (clean, both seats): the seat-B <c>Ready</c> ingest throws an NRE headless (the
|
||||
/// recovery deal path isn't headless-clean for the opponent seat), so the wire <c>Ready</c> cannot be
|
||||
/// relied on to seat seat B's XorShift. To inject the real seed FAITHFULLY for BOTH seats without
|
||||
/// depending on the throwing Ready, we call the test seam <see cref="SessionBattleEngine"/>.
|
||||
/// <c>DebugSeedIdxChange(self, oppo)</c> (-> <c>BattleManagerBase.CreateXorShift</c>) BEFORE the
|
||||
/// mulligan-end frame, with the real per-seat seeds (seat A = cl1's Ready idxChangeSeed = 1430655717,
|
||||
/// seat B = cl2's = 661650374). We ASSERT both <c>SelfXorShiftActive</c> and <c>OppoXorShiftActive</c>
|
||||
/// are true after.
|
||||
///
|
||||
/// SETUP-FRAME INGEST: identical mechanism to <see cref="CaptureReplayReshuffleRootCauseTests"/> — a
|
||||
/// single <c>Deal</c> (cl1's receive Deal seats BOTH hands), each seat's <c>Swap</c> (its mulligan),
|
||||
/// each seat's <c>Ready</c> (mulligan-end). The {no-setup-frames} row SKIPS Deal/Swap/Ready entirely:
|
||||
/// the engine's autonomous Setup hand stands, and we replay only the plays.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public class CaptureReplayFullInputDivergenceExperimentTests
|
||||
{
|
||||
// Real per-seat idxChangeSeed carried by each client's Ready frame (given in the experiment brief;
|
||||
// re-confirmed below against the captures).
|
||||
private const int SeatASeed = 1430655717; // cl1 / seat A / player
|
||||
private const int SeatBSeed = 661650374; // cl2 / seat B / opponent
|
||||
|
||||
private static readonly HashSet<string> SkipUris = new()
|
||||
{
|
||||
nameof(NetworkBattleUri.Echo),
|
||||
nameof(NetworkBattleUri.ChatStamp),
|
||||
nameof(NetworkBattleUri.Gungnir),
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> MulliganUris = new()
|
||||
{
|
||||
nameof(NetworkBattleUri.Deal),
|
||||
nameof(NetworkBattleUri.Swap),
|
||||
nameof(NetworkBattleUri.Ready),
|
||||
};
|
||||
|
||||
private sealed record HandDump(string Seat, int PlayIdx, string Uri, string Reason,
|
||||
IReadOnlyList<(int Index, int CardId)> SelfHand,
|
||||
IReadOnlyList<(int Index, int CardId)> OppoHand,
|
||||
bool PlayIdxInSelfHand, bool PlayIdxInOppoHand);
|
||||
|
||||
private sealed record Cell(
|
||||
bool SetupFrames, bool RealSeed,
|
||||
int Divergences, bool SelfXorActive, bool OppoXorActive,
|
||||
HandDump? FirstNotFoundDump);
|
||||
|
||||
private static int ReadPlayIdx(string rawBody)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(rawBody);
|
||||
return doc.RootElement.TryGetProperty("playIdx", out var p) && p.TryGetInt32(out var v) ? v : -1;
|
||||
}
|
||||
|
||||
// Snapshot a seat's hand as (engine Index, CardId) pairs. Reads through the SessionBattleEngine
|
||||
// oracle accessors (HandCount/HandCardIndex/HandCardId).
|
||||
private static List<(int, int)> HandSnapshot(SessionBattleEngine engine, bool seat)
|
||||
{
|
||||
var list = new List<(int, int)>();
|
||||
int n = engine.HandCount(seat);
|
||||
for (int i = 0; i < n; i++)
|
||||
list.Add((engine.HandCardIndex(seat, i), engine.HandCardId(seat, i)));
|
||||
return list;
|
||||
}
|
||||
|
||||
private static Cell Run(bool setupFrames, bool realSeed)
|
||||
{
|
||||
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
|
||||
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
|
||||
|
||||
var deckA = CaptureReplay.SelfDeckFrom(cl1);
|
||||
var deckB = CaptureReplay.SelfDeckFrom(cl2);
|
||||
Assert.That(deckA, Is.Not.Empty);
|
||||
Assert.That(deckB, Is.Not.Empty);
|
||||
|
||||
var engine = new SessionBattleEngine();
|
||||
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
|
||||
Assert.That(engine.IsReady, Is.True);
|
||||
|
||||
// Inject the real per-seat seed BEFORE mulligan-end (Ready). Clean both-seat activation via the
|
||||
// CreateXorShift seam, sidestepping the seat-B Ready NRE.
|
||||
if (realSeed)
|
||||
engine.DebugSeedIdxChange(SeatASeed, SeatBSeed);
|
||||
|
||||
int divergences = 0;
|
||||
HandDump? firstNotFound = null;
|
||||
|
||||
void Ingest(MsgEnvelope env, bool seat, string uri, string rawBody)
|
||||
{
|
||||
var r = engine.Receive(env, isPlayerSeat: seat);
|
||||
if (!r.Diverged) return;
|
||||
divergences++;
|
||||
if (firstNotFound is null && (r.RejectReason ?? "").Contains("not found in hand"))
|
||||
{
|
||||
int playIdx = ReadPlayIdx(rawBody);
|
||||
var self = HandSnapshot(engine, seat);
|
||||
var oppo = HandSnapshot(engine, !seat);
|
||||
firstNotFound = new HandDump(
|
||||
seat ? "A" : "B", playIdx, uri, Trim(r.RejectReason),
|
||||
self, oppo,
|
||||
self.Any(h => h.Item1 == playIdx), oppo.Any(h => h.Item1 == playIdx));
|
||||
}
|
||||
}
|
||||
|
||||
CapturedFrame Receive(IReadOnlyList<CapturedFrame> frames, string uri) =>
|
||||
frames.First(f => f.Direction == "receive" && f.Uri == uri);
|
||||
|
||||
// --- Phase 1: setup frames (optional) ---------------------------------------------------------
|
||||
if (setupFrames)
|
||||
{
|
||||
var deal = Receive(cl1, nameof(NetworkBattleUri.Deal));
|
||||
Ingest(deal.Env, seat: true, nameof(NetworkBattleUri.Deal), deal.RawBody);
|
||||
foreach (var (frames, seat) in new[] { (cl1, true), (cl2, false) })
|
||||
{
|
||||
var swap = Receive(frames, nameof(NetworkBattleUri.Swap));
|
||||
Ingest(swap.Env, seat, nameof(NetworkBattleUri.Swap), swap.RawBody);
|
||||
var ready = Receive(frames, nameof(NetworkBattleUri.Ready));
|
||||
Ingest(ready.Env, seat, nameof(NetworkBattleUri.Ready), ready.RawBody);
|
||||
}
|
||||
}
|
||||
|
||||
bool selfActive = engine.SelfXorShiftActive;
|
||||
bool oppoActive = engine.OppoXorShiftActive;
|
||||
|
||||
// Snapshot the engine's post-setup hands (after Deal/Swap/Ready) for the full-inputs cell, so the
|
||||
// report can compare the engine's mulligan-resolved hand against the wire's Swap/Ready move list.
|
||||
if (setupFrames && realSeed)
|
||||
{
|
||||
TestContext.WriteLine(" [post-setup] engine SELF (seat A) hand: " +
|
||||
string.Join(" ", HandSnapshot(engine, true).Select(h => $"(idx={h.Item1},cid={h.Item2})")));
|
||||
TestContext.WriteLine(" [post-setup] engine OPPO (seat B) hand: " +
|
||||
string.Join(" ", HandSnapshot(engine, false).Select(h => $"(idx={h.Item1},cid={h.Item2})")));
|
||||
}
|
||||
|
||||
// --- Phase 2: replay both clients' interleaved SENDS (the plays) ------------------------------
|
||||
var sends = SendsWithRawBody(cl1, cl2)
|
||||
.Where(x => !SkipUris.Contains(x.Frame.Uri))
|
||||
.ToList();
|
||||
foreach (var x in sends)
|
||||
Ingest(x.Frame.Env, x.Seat, x.Frame.Uri, x.Frame.RawBody);
|
||||
|
||||
return new Cell(setupFrames, realSeed, divergences, selfActive, oppoActive, firstNotFound);
|
||||
}
|
||||
|
||||
private static IEnumerable<(CapturedFrame Frame, bool Seat)> SendsWithRawBody(
|
||||
IReadOnlyList<CapturedFrame> cl1, IReadOnlyList<CapturedFrame> cl2)
|
||||
{
|
||||
return cl1.Where(f => f.Direction == "send").Select(f => (f, Seat: true))
|
||||
.Concat(cl2.Where(f => f.Direction == "send").Select(f => (f, Seat: false)))
|
||||
.OrderBy(x => x.f.Ts)
|
||||
.Select(x => (x.f, x.Seat));
|
||||
}
|
||||
|
||||
private static string Trim(string? s) => (s ?? "").Split(" @ ")[0];
|
||||
|
||||
[Test]
|
||||
public void Full_input_2x2_divergence_table_and_first_remaining_divergence_dump()
|
||||
{
|
||||
// Confirm the brief's per-seat seeds match the captures' Ready frames before relying on them.
|
||||
ConfirmReadySeeds();
|
||||
|
||||
var cells = new[]
|
||||
{
|
||||
Run(setupFrames: false, realSeed: false), // baseline-ish: autonomous Setup hand, seed -1
|
||||
Run(setupFrames: false, realSeed: true),
|
||||
Run(setupFrames: true, realSeed: false),
|
||||
Run(setupFrames: true, realSeed: true), // FULL INPUTS
|
||||
};
|
||||
|
||||
TestContext.WriteLine("=== 2x2 DIVERGENCE TABLE (setup-frames x real-seed) ===");
|
||||
TestContext.WriteLine("setupFrames | realSeed | divergences | selfXor | oppoXor");
|
||||
foreach (var c in cells)
|
||||
TestContext.WriteLine(
|
||||
$" {(c.SetupFrames ? "YES" : "no ")} | {(c.RealSeed ? "YES" : "no ")} | {c.Divergences,2} | {c.SelfXorActive,-5} | {c.OppoXorActive,-5}");
|
||||
|
||||
var full = cells.Single(c => c.SetupFrames && c.RealSeed);
|
||||
TestContext.WriteLine("");
|
||||
TestContext.WriteLine($"FULL-INPUTS cell: setupFrames=YES realSeed=YES -> divergences={full.Divergences} " +
|
||||
$"selfXorActive={full.SelfXorActive} oppoXorActive={full.OppoXorActive}");
|
||||
|
||||
if (full.FirstNotFoundDump is { } d)
|
||||
{
|
||||
TestContext.WriteLine("");
|
||||
TestContext.WriteLine("=== FIRST 'not found in hand' DIVERGENCE (full-inputs cell) ===");
|
||||
TestContext.WriteLine($" seat={d.Seat} uri={d.Uri} wire playIdx={d.PlayIdx} reason={d.Reason}");
|
||||
TestContext.WriteLine($" playIdx in self hand? {d.PlayIdxInSelfHand} in oppo hand? {d.PlayIdxInOppoHand}");
|
||||
TestContext.WriteLine($" engine SELF (seat {d.Seat}) hand [{d.SelfHand.Count}]: " +
|
||||
string.Join(" ", d.SelfHand.Select(h => $"(idx={h.Index},cid={h.CardId})")));
|
||||
TestContext.WriteLine($" engine OPPO hand [{d.OppoHand.Count}]: " +
|
||||
string.Join(" ", d.OppoHand.Select(h => $"(idx={h.Index},cid={h.CardId})")));
|
||||
}
|
||||
else
|
||||
{
|
||||
TestContext.WriteLine("");
|
||||
TestContext.WriteLine("FULL-INPUTS cell produced NO 'not found in hand' divergence.");
|
||||
}
|
||||
|
||||
// EVIDENCE ASSERTIONS (pin the experiment's reproducibility, not a desired fix outcome):
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// The seed seam activates BOTH seats' XorShift in every realSeed cell.
|
||||
foreach (var c in cells.Where(c => c.RealSeed))
|
||||
{
|
||||
Assert.That(c.SelfXorActive, Is.True,
|
||||
$"realSeed cell (setup={c.SetupFrames}) must activate self XorShift");
|
||||
Assert.That(c.OppoXorActive, Is.True,
|
||||
$"realSeed cell (setup={c.SetupFrames}) must activate oppo XorShift");
|
||||
}
|
||||
// With NO seed seam AND NO setup frames (the live shadow's effective state — never
|
||||
// ingests the seed-bearing Ready), BOTH seats' XorShift stay inactive.
|
||||
var bare = cells.Single(c => !c.RealSeed && !c.SetupFrames);
|
||||
Assert.That(bare.SelfXorActive, Is.False, "no-seed/no-setup leaves self XorShift inactive");
|
||||
Assert.That(bare.OppoXorActive, Is.False, "no-seed/no-setup leaves oppo XorShift inactive");
|
||||
|
||||
// With setup frames but no seam, the seat-A Ready frame's own idxChangeSeed activates the
|
||||
// SELF XorShift (seat B's Ready NREs before it can seat oppo) — so self is active, oppo isn't.
|
||||
var setupNoSeam = cells.Single(c => !c.RealSeed && c.SetupFrames);
|
||||
Assert.That(setupNoSeam.SelfXorActive, Is.True,
|
||||
"setup-frames cell: seat-A Ready idxChangeSeed activates self XorShift");
|
||||
Assert.That(setupNoSeam.OppoXorActive, Is.False,
|
||||
"setup-frames cell: seat-B Ready NREs before seating oppo XorShift");
|
||||
|
||||
// THE DECISIVE FINDING: full inputs (setup frames + real seed, both seats' XorShift active)
|
||||
// do NOT eliminate the divergences — they stay at the 14 baseline.
|
||||
var full2 = cells.Single(c => c.SetupFrames && c.RealSeed);
|
||||
Assert.That(full2.SelfXorActive && full2.OppoXorActive, Is.True,
|
||||
"full-inputs cell has both seats' XorShift active");
|
||||
Assert.That(full2.Divergences, Is.GreaterThan(0),
|
||||
"REFUTED: full inputs do NOT make the recovery recompute faithful — divergences remain");
|
||||
});
|
||||
}
|
||||
|
||||
// Re-confirm the brief's per-seat seeds against the captured Ready frames (fail loudly if the
|
||||
// fixtures ever drift from the assumed seeds).
|
||||
private static void ConfirmReadySeeds()
|
||||
{
|
||||
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
|
||||
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
|
||||
int a = ReadReadySeed(cl1);
|
||||
int b = ReadReadySeed(cl2);
|
||||
TestContext.WriteLine($"Confirmed Ready idxChangeSeed: cl1(seatA)={a} cl2(seatB)={b}");
|
||||
Assert.That(a, Is.EqualTo(SeatASeed), "cl1 Ready idxChangeSeed must equal the brief's seat-A seed");
|
||||
Assert.That(b, Is.EqualTo(SeatBSeed), "cl2 Ready idxChangeSeed must equal the brief's seat-B seed");
|
||||
}
|
||||
|
||||
private static int ReadReadySeed(IReadOnlyList<CapturedFrame> frames)
|
||||
{
|
||||
var ready = frames.First(f => f.Direction == "receive" && f.Uri == nameof(NetworkBattleUri.Ready));
|
||||
var obj = JsonNode.Parse(ready.RawBody)!.AsObject();
|
||||
return (int)obj["idxChangeSeed"]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Sessions.Engine;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// PHASE 4 — DRAW-RECOMPUTE ROOT-CAUSE VALIDATION (TEST-ONLY; no production fix; no Engine/*.cs edits).
|
||||
///
|
||||
/// HYPOTHESIS (from the experiment brief): the shadow diverges ("Target card was not found in hand
|
||||
/// cards", post-mulligan) because the per-turn network DRAW is a SEEDED-RANDOM pick from the deck via
|
||||
/// <c>mgr.StableRandom(...)</c> (SkillRandomSelectFilter.Filtering:49/58), gated by the process-global
|
||||
/// <c>BattleManagerBase.IsRandomDraw</c> — which the real match-load sets true via
|
||||
/// <c>StartOpening → SetupInitialGameState(areCardsRandomlyDrawn:true)</c> (BattleManagerBase.cs:1098/1110).
|
||||
/// The headless <see cref="SessionBattleEngine"/>.Setup never runs SetupInitialGameState, so IsRandomDraw
|
||||
/// stays FALSE and the shadow draws TOP-OF-DECK while the clients draw seeded-random → mismatch.
|
||||
/// AND the shared <c>_stableRandom</c> stream must be advanced by the wire <c>spin</c> pre-roll the Ready
|
||||
/// frame carries (spin=243), which <c>OperateReceive.StartOperate:80-83</c> applies but the shadow never
|
||||
/// ingests — so without it the stream is offset.
|
||||
///
|
||||
/// ISOLATION MATRIX (this is the report's headline): setup frames + real seed are held CONSTANT (the
|
||||
/// faithful baseline the prior FullInput experiment pinned at 14); the two NEW variables are toggled:
|
||||
/// • {IsRandomDraw=false, no spin} = baseline (top-of-deck draws; the live shadow's effective state)
|
||||
/// • {IsRandomDraw=true, no spin} = random-draw active but stream MIS-aligned (expect WORSE)
|
||||
/// • {IsRandomDraw=true, +spin} = random-draw active AND stream aligned (the hypothesised fix)
|
||||
///
|
||||
/// SPIN APPLICATION: spin=243 appears on the Ready frame in BOTH captures (each client applies its own
|
||||
/// once). Our shadow shares ONE <c>_stableRandom</c> across both seats (seated as both players), and a
|
||||
/// single client's stream sits 243 draws in after ITS Ready — so we apply spin=243 ONCE, after the
|
||||
/// Deal/Swap/Ready setup frames and before the plays, exactly where the real client's StartOperate would.
|
||||
/// (A scan of both fixtures confirms Ready is the ONLY frame carrying a non-zero spin.)
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public class CaptureReplayRandomDrawSpinRootCauseTests
|
||||
{
|
||||
private const int SeatASeed = 1430655717; // cl1 / seat A / player (Ready idxChangeSeed)
|
||||
private const int SeatBSeed = 661650374; // cl2 / seat B / opponent
|
||||
private const int WireSpin = 243; // both captures' Ready frame spin
|
||||
|
||||
private static readonly HashSet<string> SkipUris = new()
|
||||
{
|
||||
nameof(NetworkBattleUri.Echo),
|
||||
nameof(NetworkBattleUri.ChatStamp),
|
||||
nameof(NetworkBattleUri.Gungnir),
|
||||
};
|
||||
|
||||
private sealed record HandDump(string Seat, int PlayIdx, string Uri, string Reason,
|
||||
int StableRandomCount,
|
||||
IReadOnlyList<(int Index, int CardId)> SelfHand,
|
||||
IReadOnlyList<(int Index, int CardId)> OppoHand,
|
||||
bool PlayIdxInSelfHand, bool PlayIdxInOppoHand);
|
||||
|
||||
private sealed record Cell(bool RandomDraw, bool Spin, int Divergences, HandDump? FirstNotFound);
|
||||
|
||||
private static int ReadPlayIdx(string rawBody)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(rawBody);
|
||||
return doc.RootElement.TryGetProperty("playIdx", out var p) && p.TryGetInt32(out var v) ? v : -1;
|
||||
}
|
||||
|
||||
private static List<(int, int)> HandSnapshot(SessionBattleEngine engine, bool seat)
|
||||
{
|
||||
var list = new List<(int, int)>();
|
||||
int n = engine.HandCount(seat);
|
||||
for (int i = 0; i < n; i++)
|
||||
list.Add((engine.HandCardIndex(seat, i), engine.HandCardId(seat, i)));
|
||||
return list;
|
||||
}
|
||||
|
||||
private static CapturedFrame Receive(IReadOnlyList<CapturedFrame> frames, string uri) =>
|
||||
frames.First(f => f.Direction == "receive" && f.Uri == uri);
|
||||
|
||||
private static Cell Run(bool randomDraw, bool spin)
|
||||
{
|
||||
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
|
||||
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
|
||||
|
||||
var deckA = CaptureReplay.SelfDeckFrom(cl1);
|
||||
var deckB = CaptureReplay.SelfDeckFrom(cl2);
|
||||
Assert.That(deckA, Is.Not.Empty);
|
||||
Assert.That(deckB, Is.Not.Empty);
|
||||
|
||||
var engine = new SessionBattleEngine();
|
||||
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
|
||||
Assert.That(engine.IsReady, Is.True);
|
||||
|
||||
// CONSTANT across all cells: faithful seed seam (both seats' XorShift active), sidestepping the
|
||||
// seat-B Ready NRE — identical to the FullInput experiment's full-inputs cell.
|
||||
engine.DebugSeedIdxChange(SeatASeed, SeatBSeed);
|
||||
|
||||
// NEW VARIABLE 1: the IsRandomDraw gate. Set BEFORE any draw (deal is the first draw).
|
||||
engine.DebugSetRandomDraw(randomDraw);
|
||||
|
||||
int divergences = 0;
|
||||
HandDump? firstNotFound = null;
|
||||
|
||||
void Ingest(MsgEnvelope env, bool seat, string uri, string rawBody)
|
||||
{
|
||||
var r = engine.Receive(env, isPlayerSeat: seat);
|
||||
if (!r.Diverged) return;
|
||||
divergences++;
|
||||
if (firstNotFound is null && (r.RejectReason ?? "").Contains("not found in hand"))
|
||||
{
|
||||
var self = HandSnapshot(engine, seat);
|
||||
var oppo = HandSnapshot(engine, !seat);
|
||||
int playIdx = ReadPlayIdx(rawBody);
|
||||
firstNotFound = new HandDump(
|
||||
seat ? "A" : "B", playIdx, uri, Trim(r.RejectReason),
|
||||
engine.DebugStableRandomCount, self, oppo,
|
||||
self.Any(h => h.Item1 == playIdx), oppo.Any(h => h.Item1 == playIdx));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Phase 1: setup frames (CONSTANT: Deal once + each seat's Swap + Ready) -------------------
|
||||
var deal = Receive(cl1, nameof(NetworkBattleUri.Deal));
|
||||
Ingest(deal.Env, seat: true, nameof(NetworkBattleUri.Deal), deal.RawBody);
|
||||
foreach (var (frames, seat) in new[] { (cl1, true), (cl2, false) })
|
||||
{
|
||||
var swap = Receive(frames, nameof(NetworkBattleUri.Swap));
|
||||
Ingest(swap.Env, seat, nameof(NetworkBattleUri.Swap), swap.RawBody);
|
||||
var ready = Receive(frames, nameof(NetworkBattleUri.Ready));
|
||||
Ingest(ready.Env, seat, nameof(NetworkBattleUri.Ready), ready.RawBody);
|
||||
}
|
||||
|
||||
// NEW VARIABLE 2: the spin pre-roll, applied at mulligan-end (after Ready, before the first
|
||||
// turn-start draw) — where OperateReceive.StartOperate applies the Ready's spin in production.
|
||||
// ONE application of 243 (shared stream, one client's worth of advance).
|
||||
if (spin)
|
||||
engine.DebugSpinPreroll(WireSpin);
|
||||
|
||||
// --- Phase 2: replay both clients' interleaved SENDS (the plays) ------------------------------
|
||||
var sends = SendsWithRawBody(cl1, cl2)
|
||||
.Where(x => !SkipUris.Contains(x.Frame.Uri))
|
||||
.ToList();
|
||||
foreach (var x in sends)
|
||||
Ingest(x.Frame.Env, x.Seat, x.Frame.Uri, x.Frame.RawBody);
|
||||
|
||||
return new Cell(randomDraw, spin, divergences, firstNotFound);
|
||||
}
|
||||
|
||||
private static IEnumerable<(CapturedFrame Frame, bool Seat)> SendsWithRawBody(
|
||||
IReadOnlyList<CapturedFrame> cl1, IReadOnlyList<CapturedFrame> cl2)
|
||||
{
|
||||
return cl1.Where(f => f.Direction == "send").Select(f => (f, Seat: true))
|
||||
.Concat(cl2.Where(f => f.Direction == "send").Select(f => (f, Seat: false)))
|
||||
.OrderBy(x => x.f.Ts)
|
||||
.Select(x => (x.f, x.Seat));
|
||||
}
|
||||
|
||||
private static string Trim(string? s) => (s ?? "").Split(" @ ")[0];
|
||||
|
||||
[Test]
|
||||
public void IsRandomDraw_plus_spin_preroll_isolation_matrix()
|
||||
{
|
||||
try
|
||||
{
|
||||
ConfirmSpin();
|
||||
|
||||
var baseline = Run(randomDraw: false, spin: false);
|
||||
var rdOnly = Run(randomDraw: true, spin: false);
|
||||
var rdSpin = Run(randomDraw: true, spin: true);
|
||||
|
||||
TestContext.WriteLine("=== ISOLATION MATRIX (setup-frames + real-seed held CONSTANT) ===");
|
||||
TestContext.WriteLine("IsRandomDraw | spin | divergences");
|
||||
TestContext.WriteLine($" false | no | {baseline.Divergences}");
|
||||
TestContext.WriteLine($" true | no | {rdOnly.Divergences}");
|
||||
TestContext.WriteLine($" true | +243 | {rdSpin.Divergences}");
|
||||
|
||||
DumpFirst("baseline {false,no}", baseline);
|
||||
DumpFirst("rd-only {true,no}", rdOnly);
|
||||
DumpFirst("rd+spin {true,+243}", rdSpin);
|
||||
|
||||
Assert.Pass(
|
||||
$"MATRIX baseline={baseline.Divergences} rdOnly={rdOnly.Divergences} rdSpin={rdSpin.Divergences}");
|
||||
}
|
||||
catch (SuccessException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
TestContext.WriteLine("EXPERIMENT THREW: " + ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DumpFirst(string label, Cell c)
|
||||
{
|
||||
if (c.FirstNotFound is not { } d)
|
||||
{
|
||||
TestContext.WriteLine($"[{label}] no 'not found in hand' divergence.");
|
||||
return;
|
||||
}
|
||||
TestContext.WriteLine($"[{label}] FIRST 'not found in hand': seat={d.Seat} uri={d.Uri} " +
|
||||
$"wire playIdx={d.PlayIdx} stableRandomCount={d.StableRandomCount} reason={d.Reason}");
|
||||
TestContext.WriteLine($" playIdx in self hand? {d.PlayIdxInSelfHand} in oppo hand? {d.PlayIdxInOppoHand}");
|
||||
TestContext.WriteLine($" SELF (seat {d.Seat}) hand [{d.SelfHand.Count}]: " +
|
||||
string.Join(" ", d.SelfHand.Select(h => $"(idx={h.Index},cid={h.CardId})")));
|
||||
TestContext.WriteLine($" OPPO hand [{d.OppoHand.Count}]: " +
|
||||
string.Join(" ", d.OppoHand.Select(h => $"(idx={h.Index},cid={h.CardId})")));
|
||||
}
|
||||
|
||||
/// <summary>STEP 4 (payoff check): with the hypothesised fix applied {IsRandomDraw=true, +spin},
|
||||
/// does the engine reach and RESOLVE cl1's spellboost play so PlayedCardSpellboost/PlayedCardCost
|
||||
/// return real (non-zero) values? cl1's deck carries the spellboost-scaling follower 101314020 at
|
||||
/// deck idx 10/21/25. We replay the {true,+243} cell and, after each accepted seat-A PlayActions,
|
||||
/// probe whether any in-play/cemetery card has that id with a resolved cost/spellboost. We report
|
||||
/// whether the spellboost play was ever reached at all.</summary>
|
||||
[Test]
|
||||
public void Spellboost_play_resolution_under_random_draw_plus_spin()
|
||||
{
|
||||
const int SpellboostCardId = 101314020;
|
||||
|
||||
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
|
||||
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
|
||||
var deckA = CaptureReplay.SelfDeckFrom(cl1);
|
||||
var deckB = CaptureReplay.SelfDeckFrom(cl2);
|
||||
|
||||
var engine = new SessionBattleEngine();
|
||||
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
|
||||
engine.DebugSeedIdxChange(SeatASeed, SeatBSeed);
|
||||
engine.DebugSetRandomDraw(true);
|
||||
|
||||
// setup frames
|
||||
engine.Receive(Receive(cl1, nameof(NetworkBattleUri.Deal)).Env, isPlayerSeat: true);
|
||||
foreach (var (frames, seat) in new[] { (cl1, true), (cl2, false) })
|
||||
{
|
||||
engine.Receive(Receive(frames, nameof(NetworkBattleUri.Swap)).Env, isPlayerSeat: seat);
|
||||
engine.Receive(Receive(frames, nameof(NetworkBattleUri.Ready)).Env, isPlayerSeat: seat);
|
||||
}
|
||||
engine.DebugSpinPreroll(WireSpin);
|
||||
|
||||
int acceptedSeatAPlays = 0, divergedBeforeFirstPlay = 0;
|
||||
bool spellboostResolved = false;
|
||||
int sbCost = -999, sbCharge = -999;
|
||||
|
||||
var sends = SendsWithRawBody(cl1, cl2).Where(x => !SkipUris.Contains(x.Frame.Uri)).ToList();
|
||||
bool sawFirstPlay = false;
|
||||
foreach (var x in sends)
|
||||
{
|
||||
bool isPlay = x.Frame.Uri == nameof(NetworkBattleUri.PlayActions);
|
||||
var r = engine.Receive(x.Frame.Env, isPlayerSeat: x.Seat);
|
||||
if (isPlay && !sawFirstPlay) { sawFirstPlay = true; if (r.Diverged) divergedBeforeFirstPlay++; }
|
||||
if (isPlay && x.Seat && !r.Diverged)
|
||||
{
|
||||
acceptedSeatAPlays++;
|
||||
int playIdx = ReadPlayIdx(x.Frame.RawBody);
|
||||
long id = engine.PlayedCardId(true, playIdx, 0);
|
||||
if (id == SpellboostCardId)
|
||||
{
|
||||
spellboostResolved = true;
|
||||
sbCost = engine.PlayedCardCost(true, playIdx, -1);
|
||||
sbCharge = engine.PlayedCardSpellboost(true, playIdx, -1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestContext.WriteLine($"[spellboost payoff] acceptedSeatAPlays={acceptedSeatAPlays} " +
|
||||
$"divergedAtFirstPlay={divergedBeforeFirstPlay} spellboostResolved={spellboostResolved} " +
|
||||
$"cost={sbCost} charge={sbCharge}");
|
||||
|
||||
// The replay diverges at the FIRST seat-A play (matrix shows playIdx=8 not in hand), so the
|
||||
// engine never advances to the later spellboost play — the visible spellboost symptom is NOT
|
||||
// fixed by {IsRandomDraw+spin} because the prerequisite (aligned draws) is not met.
|
||||
Assert.That(divergedBeforeFirstPlay, Is.EqualTo(1),
|
||||
"the FIRST seat-A play already diverges under {IsRandomDraw=true,+spin}");
|
||||
Assert.That(spellboostResolved, Is.False,
|
||||
"the spellboost play is never reached because the replay diverges at the first play");
|
||||
}
|
||||
|
||||
private static void ConfirmSpin()
|
||||
{
|
||||
foreach (var fn in new[] { "battle_test_fresh_cl1.ndjson", "battle_test_fresh_cl2.ndjson" })
|
||||
{
|
||||
var frames = CaptureReplay.Load(fn);
|
||||
var ready = Receive(frames, nameof(NetworkBattleUri.Ready));
|
||||
var obj = JsonNode.Parse(ready.RawBody)!.AsObject();
|
||||
int spin = obj.TryGetPropertyValue("spin", out var s) ? (int)s! : 0;
|
||||
TestContext.WriteLine($"Confirmed {fn} Ready spin={spin}");
|
||||
Assert.That(spin, Is.EqualTo(WireSpin), $"{fn} Ready spin must equal {WireSpin}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Sessions.Engine;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// PHASE 4 STEP 1 — Tier 2 capture-replay root-cause VERIFICATION (NOT a fix).
|
||||
///
|
||||
/// Replays the FRESH smoke captures (battle 907324319325) — battle_test_fresh_cl1/cl2.ndjson —
|
||||
/// through a <see cref="SessionBattleEngine"/>, then measures whether the per-seat <c>idxChangeSeed</c>
|
||||
/// the real Ready frame carries is what controls the "Target card was not found in hand cards"
|
||||
/// divergence symptom.
|
||||
///
|
||||
/// FAITHFUL SETUP (the live ShadowIngest only feeds client SENDS, which contain NO Deal/Ready, so a
|
||||
/// bare send-only replay can't even seat a hand — that conflates "missing Deal" with "missing
|
||||
/// reshuffle"). To ISOLATE the reshuffle/seed effect we seat each seat's hand from its OWN client's
|
||||
/// RECEIVE Deal + Swap + Ready (the frames that establish the hand and reach mulligan-end), then replay
|
||||
/// both clients' interleaved SENDS (the plays). The Ready frame natively carries the real per-seat
|
||||
/// idxChangeSeed (cl1=1430655717, cl2=661650374), and the engine's recovery receiver calls
|
||||
/// <c>CreateXorShift</c> from it (NetworkBattleReceiver.cs:1125-1126). The A/B is then:
|
||||
/// • WITH-SEED: ingest the Ready frame verbatim (idxChangeSeed present) -> XorShift active;
|
||||
/// • SEED-STRIPPED: ingest the SAME Ready frame with idxChangeSeed forced to -1 -> XorShift inactive
|
||||
/// (this is exactly the live shadow's effective state, since it never ingests the seed-bearing Ready).
|
||||
/// The ONLY difference between the two runs is whether the seed reaches CreateXorShift.
|
||||
///
|
||||
/// DECK SETUP MECHANISM (feasibility crux, RESOLVED): each side's deck is reconstructed from the
|
||||
/// capture's <c>Matched.selfDeck</c> (idx->cardId, the exact shuffled order the node also handed the
|
||||
/// client) via <see cref="CaptureReplay.SelfDeckFrom"/>; the master seed from <c>Matched.selfInfo.seed</c>.
|
||||
/// The deck IS in the socket capture — no external fixture needed.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public class CaptureReplayReshuffleRootCauseTests
|
||||
{
|
||||
private static readonly HashSet<string> SkipUris = new()
|
||||
{
|
||||
nameof(NetworkBattleUri.Echo),
|
||||
nameof(NetworkBattleUri.ChatStamp),
|
||||
nameof(NetworkBattleUri.Gungnir),
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> MulliganUris = new()
|
||||
{
|
||||
nameof(NetworkBattleUri.Deal),
|
||||
nameof(NetworkBattleUri.Swap),
|
||||
nameof(NetworkBattleUri.Ready),
|
||||
};
|
||||
|
||||
private sealed record ReplayOutcome(
|
||||
int FrameCount, List<string> Divergences, bool AllDivergencesPostMulligan, bool SelfXorShiftActive);
|
||||
|
||||
// Re-parse a captured frame, overriding the Ready body's idxChangeSeed (and oppoIdxChangeSeed if
|
||||
// present). Used to STRIP the seed (-1) to model the live shadow's seed-less state.
|
||||
private static MsgEnvelope OverrideReadySeed(CapturedFrame f, int newSeed)
|
||||
{
|
||||
var obj = JsonNode.Parse(f.RawBody)!.AsObject();
|
||||
obj["idxChangeSeed"] = newSeed;
|
||||
if (obj.ContainsKey("oppoIdxChangeSeed")) obj["oppoIdxChangeSeed"] = newSeed;
|
||||
return MsgEnvelope.FromJson(obj.ToJsonString());
|
||||
}
|
||||
|
||||
/// <summary>Seat both hands from each client's receive Deal+Swap+Ready, then replay both clients'
|
||||
/// interleaved SENDS. <paramref name="stripSeed"/> forces the Ready idxChangeSeed to -1 (the live
|
||||
/// shadow's effective state). Returns divergences + the post-setup self XorShift state.</summary>
|
||||
private static ReplayOutcome Replay(bool stripSeed)
|
||||
{
|
||||
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
|
||||
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
|
||||
|
||||
var deckA = CaptureReplay.SelfDeckFrom(cl1);
|
||||
var deckB = CaptureReplay.SelfDeckFrom(cl2);
|
||||
Assert.That(deckA, Is.Not.Empty, "cl1 Matched.selfDeck must reconstruct seat A's deck");
|
||||
Assert.That(deckB, Is.Not.Empty, "cl2 Matched.selfDeck must reconstruct seat B's deck");
|
||||
|
||||
var engine = new SessionBattleEngine();
|
||||
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
|
||||
Assert.That(engine.IsReady, Is.True, "engine must seat from the captured decks + seed");
|
||||
|
||||
var divergences = new List<string>();
|
||||
bool sawMulliganEnd = false;
|
||||
bool anyDivergencePreMulligan = false;
|
||||
|
||||
void Ingest(MsgEnvelope env, bool seat, string uri)
|
||||
{
|
||||
if (MulliganUris.Contains(uri)) sawMulliganEnd = true;
|
||||
var r = engine.Receive(env, isPlayerSeat: seat);
|
||||
if (r.Diverged)
|
||||
{
|
||||
divergences.Add($"seat={(seat ? "A" : "B")} {uri}: {Trim(r.RejectReason)}");
|
||||
if (!sawMulliganEnd) anyDivergencePreMulligan = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Phase 1: seat both hands from the receive setup frames ----------------------------------
|
||||
// A single Deal seats BOTH opening hands (cl1's receive Deal carries self=A + oppo=B), so we
|
||||
// ingest Deal ONCE (as seat A) — ingesting both clients' Deals would double-deal (NRE / "Sequence
|
||||
// contains more than one"). Each seat's Swap then applies that seat's mulligan, and each seat's
|
||||
// Ready carries THAT seat's idxChangeSeed (cl1's for A, cl2's for B; the recovery receiver consumes
|
||||
// only the SELF seed per ingest, NetworkBattleReceiver.cs:1126), reaching mulligan-end per seat.
|
||||
CapturedFrame Receive(IReadOnlyList<CapturedFrame> frames, string uri) =>
|
||||
frames.First(f => f.Direction == "receive" && f.Uri == uri);
|
||||
|
||||
// Deal once (seat A's receive Deal seats both hands).
|
||||
Ingest(Receive(cl1, nameof(NetworkBattleUri.Deal)).Env, seat: true, nameof(NetworkBattleUri.Deal));
|
||||
// Each seat's mulligan swap, then each seat's Ready (its own seed).
|
||||
foreach (var (frames, seat) in new[] { (cl1, true), (cl2, false) })
|
||||
{
|
||||
Ingest(Receive(frames, nameof(NetworkBattleUri.Swap)).Env, seat, nameof(NetworkBattleUri.Swap));
|
||||
var ready = Receive(frames, nameof(NetworkBattleUri.Ready));
|
||||
var readyEnv = stripSeed ? OverrideReadySeed(ready, -1) : ready.Env;
|
||||
Ingest(readyEnv, seat, nameof(NetworkBattleUri.Ready));
|
||||
}
|
||||
|
||||
bool selfActive = engine.SelfXorShiftActive;
|
||||
|
||||
// --- Phase 2: replay both clients' interleaved SENDS (the plays / turn ops) -------------------
|
||||
var sends = CaptureReplay.InterleavedSends(cl1, cl2)
|
||||
.Where(x => !SkipUris.Contains(x.Env.Uri.ToString()))
|
||||
.ToList();
|
||||
foreach (var (env, seat) in sends)
|
||||
Ingest(env, seat, env.Uri.ToString());
|
||||
|
||||
return new ReplayOutcome(
|
||||
FrameCount: sends.Count, divergences, !anyDivergencePreMulligan, selfActive);
|
||||
}
|
||||
|
||||
private static string Trim(string? s) =>
|
||||
(s ?? "").Split(" @ ")[0];
|
||||
|
||||
[Test]
|
||||
public void Capture_replay_reproduces_post_mulligan_divergence_and_pins_what_the_seed_does_not_fix()
|
||||
{
|
||||
var withSeed = Replay(stripSeed: false);
|
||||
var stripped = Replay(stripSeed: true);
|
||||
|
||||
TestContext.WriteLine($"WITH-SEED (Ready idxChangeSeed present): selfXorShiftActive={withSeed.SelfXorShiftActive} " +
|
||||
$"playFrames={withSeed.FrameCount} divergences={withSeed.Divergences.Count}");
|
||||
foreach (var d in withSeed.Divergences) TestContext.WriteLine(" DIVERGE " + d);
|
||||
TestContext.WriteLine($"SEED-STRIPPED (idxChangeSeed=-1, the live shadow state): selfXorShiftActive={stripped.SelfXorShiftActive} " +
|
||||
$"playFrames={stripped.FrameCount} divergences={stripped.Divergences.Count}");
|
||||
foreach (var d in stripped.Divergences) TestContext.WriteLine(" DIVERGE " + d);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// (1) The reported symptom reproduces DETERMINISTICALLY from the captures: the replay diverges,
|
||||
// including the verbatim "Target card was not found in hand cards" exception.
|
||||
Assert.That(withSeed.Divergences, Is.Not.Empty,
|
||||
"the capture replay must reproduce the divergence symptom");
|
||||
Assert.That(withSeed.Divergences.Any(d => d.Contains("not found in hand")), Is.True,
|
||||
"the reported 'Target card was not found in hand cards' symptom must reproduce");
|
||||
|
||||
// (2) All divergences occur AFTER the mulligan barrier — consistent with a post-mulligan cause.
|
||||
Assert.That(withSeed.AllDivergencesPostMulligan, Is.True, "with-seed divergences are post-mulligan");
|
||||
Assert.That(stripped.AllDivergencesPostMulligan, Is.True, "stripped divergences are post-mulligan");
|
||||
|
||||
// (3) The wire seed DOES drive the engine's XorShift gate (NetworkBattleReceiver.cs:1126):
|
||||
// present -> active, stripped (the live shadow's state) -> inactive.
|
||||
Assert.That(withSeed.SelfXorShiftActive, Is.True,
|
||||
"ingesting the real Ready (idxChangeSeed present) activates the engine's XorShift");
|
||||
Assert.That(stripped.SelfXorShiftActive, Is.False,
|
||||
"stripping idxChangeSeed (the live shadow's state) leaves the XorShift inactive");
|
||||
|
||||
// (4) THE KEY VERIFICATION FINDING — activating the XorShift via the wire seed does NOT, on its
|
||||
// own, change the divergence count. The engine's recovery/watch RECEIVE path never performs
|
||||
// the post-mulligan full-deck reshuffle the live client does: the XorShift's GetChangeInt is
|
||||
// consumed ONLY by AddToDeckCardIndexChange (BattlePlayerBase.cs:3079) for cards added to the
|
||||
// deck AFTER mulligan-end, and the per-turn draw is engine-computed off the (un-reshuffled)
|
||||
// deck order, not driven by the wire's `move idx`. So "feed the seed" alone does NOT fix the
|
||||
// desync headless — the eventual fix must also make the engine reshuffle the deck post-
|
||||
// mulligan to match the client (or drive the draw from the wire idx). We PIN this here.
|
||||
Assert.That(stripped.Divergences.Count, Is.EqualTo(withSeed.Divergences.Count),
|
||||
"VERIFIED: activating the XorShift via the wire seed alone does NOT change the divergence " +
|
||||
"count — the engine's receive path does not reshuffle the deck, so the seed is necessary " +
|
||||
"but NOT sufficient (the fix needs the reshuffle too, not just the seed)");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Sessions.Engine;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// PHASE 4 — OPTION-A VIABILITY PROBE (TEST-ONLY; no production fix; no Engine/*.cs edits).
|
||||
///
|
||||
/// QUESTION: can a per-seat RNG router in the headless engine reliably attribute each StableRandom roll
|
||||
/// to the correct seat — so two seats can draw from two independent same-seeded sub-streams (mirroring
|
||||
/// two real clients, each with its OWN _stableRandom)?
|
||||
///
|
||||
/// METHOD: replay battle_test_fresh_cl1/cl2 through a <see cref="SessionBattleEngine"/> whose mgr RNG is
|
||||
/// a logging source. On EVERY roll it records (a) the seat signals the mgr can read from its own state
|
||||
/// (GetBattlePlayer(true/false).IsSelfTurn — the richest seat signal a mgr-level StableRandom override
|
||||
/// sees; there is NO "current operating seat" field on the mgr), and (b) the live call stack (where the
|
||||
/// acting seat is actually visible: MulliganCtrl._battlePlayer / BattlePlayerBase.LotteryRandomDrawCard /
|
||||
/// OperateReceive.StartOperate spin pre-roll). We dump the rolls for the mulligan lotteries, the first
|
||||
/// turn draws, and the spin pre-roll, and classify each — reporting whether the seat is UNAMBIGUOUS from
|
||||
/// mgr STATE vs only from the STACK.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public class CaptureReplayRngSeatAttributionProbeTests
|
||||
{
|
||||
private const int SeatASeed = 1430655717; // cl1 Ready idxChangeSeed
|
||||
private const int SeatBSeed = 661650374; // cl2 Ready idxChangeSeed
|
||||
private const int WireSpin = 243;
|
||||
|
||||
private static readonly HashSet<string> SkipUris = new()
|
||||
{
|
||||
nameof(NetworkBattleUri.Echo),
|
||||
nameof(NetworkBattleUri.ChatStamp),
|
||||
nameof(NetworkBattleUri.Gungnir),
|
||||
};
|
||||
|
||||
private static CapturedFrame Receive(IReadOnlyList<CapturedFrame> frames, string uri) =>
|
||||
frames.First(f => f.Direction == "receive" && f.Uri == uri);
|
||||
|
||||
private static IEnumerable<(CapturedFrame Frame, bool Seat)> SendsInTsOrder(
|
||||
IReadOnlyList<CapturedFrame> cl1, IReadOnlyList<CapturedFrame> cl2) =>
|
||||
cl1.Where(f => f.Direction == "send").Select(f => (f, Seat: true))
|
||||
.Concat(cl2.Where(f => f.Direction == "send").Select(f => (f, Seat: false)))
|
||||
.OrderBy(x => x.f.Ts)
|
||||
.Select(x => (x.f, x.Seat));
|
||||
|
||||
[Test]
|
||||
public void Roll_log_reveals_whether_acting_seat_is_attributable_from_state_or_only_stack()
|
||||
{
|
||||
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
|
||||
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
|
||||
|
||||
var deckA = CaptureReplay.SelfDeckFrom(cl1);
|
||||
var deckB = CaptureReplay.SelfDeckFrom(cl2);
|
||||
|
||||
// (5) seeds
|
||||
int seedA = CaptureReplay.SeedFrom(cl1);
|
||||
int seedB = CaptureReplay.SeedFrom(cl2);
|
||||
TestContext.WriteLine($"=== SEEDS (Matched.selfInfo.seed) ===");
|
||||
TestContext.WriteLine($" cl1 seed = {seedA}");
|
||||
TestContext.WriteLine($" cl2 seed = {seedB}");
|
||||
TestContext.WriteLine($" SAME? {seedA == seedB} (Ready idxChangeSeed cl1={SeatASeed} cl2={SeatBSeed} — DIFFERENT)");
|
||||
TestContext.WriteLine("");
|
||||
|
||||
var engine = new SessionBattleEngine();
|
||||
var log = engine.DebugSetupWithRollLog(masterSeed: seedA, seatADeck: deckA, seatBDeck: deckB);
|
||||
Assert.That(engine.IsReady, Is.True);
|
||||
|
||||
engine.DebugSeedIdxChange(SeatASeed, SeatBSeed);
|
||||
engine.DebugSetRandomDraw(true); // the gate that makes draws actually ROLL
|
||||
|
||||
// mark roll-log boundaries so we can bucket the rolls by phase
|
||||
int Mark() => log.Count;
|
||||
|
||||
int beforeDeal = Mark();
|
||||
engine.Receive(Receive(cl1, nameof(NetworkBattleUri.Deal)).Env, isPlayerSeat: true);
|
||||
int afterDeal = Mark();
|
||||
|
||||
// seat A mulligan (Swap+Ready) then seat B mulligan
|
||||
engine.Receive(Receive(cl1, nameof(NetworkBattleUri.Swap)).Env, isPlayerSeat: true);
|
||||
int afterSwapA = Mark();
|
||||
engine.Receive(Receive(cl1, nameof(NetworkBattleUri.Ready)).Env, isPlayerSeat: true);
|
||||
int afterReadyA = Mark();
|
||||
engine.Receive(Receive(cl2, nameof(NetworkBattleUri.Swap)).Env, isPlayerSeat: false);
|
||||
int afterSwapB = Mark();
|
||||
engine.Receive(Receive(cl2, nameof(NetworkBattleUri.Ready)).Env, isPlayerSeat: false);
|
||||
int afterReadyB = Mark();
|
||||
|
||||
// spin pre-roll (one client's 243 advance, applied once on the shared stream)
|
||||
engine.DebugSpinPreroll(WireSpin);
|
||||
int afterSpin = Mark();
|
||||
|
||||
// replay both clients' interleaved sends (the plays + turn ops -> turn-start draws fire here)
|
||||
var sends = SendsInTsOrder(cl1, cl2).Where(x => !SkipUris.Contains(x.Frame.Uri)).ToList();
|
||||
foreach (var x in sends)
|
||||
engine.Receive(x.Frame.Env, isPlayerSeat: x.Seat);
|
||||
int afterSends = Mark();
|
||||
|
||||
TestContext.WriteLine("=== ROLL-COUNT BY PHASE (IsRandomDraw=true) ===");
|
||||
TestContext.WriteLine($" Deal : {afterDeal - beforeDeal}");
|
||||
TestContext.WriteLine($" Swap A : {afterSwapA - afterDeal}");
|
||||
TestContext.WriteLine($" Ready A (mulligan): {afterReadyA - afterSwapA}");
|
||||
TestContext.WriteLine($" Swap B : {afterSwapB - afterReadyA}");
|
||||
TestContext.WriteLine($" Ready B (mulligan): {afterReadyB - afterSwapB}");
|
||||
TestContext.WriteLine($" spin pre-roll : {afterSpin - afterReadyB} (expected {WireSpin})");
|
||||
TestContext.WriteLine($" all sends/plays : {afterSends - afterSpin}");
|
||||
TestContext.WriteLine($" TOTAL : {log.Count}");
|
||||
TestContext.WriteLine("");
|
||||
|
||||
DumpRange("DEAL", log, beforeDeal, afterDeal);
|
||||
DumpRange("SWAP A (mulligan lottery, seat A)", log, afterDeal, afterSwapA);
|
||||
DumpRange("READY A (mulligan, seat A)", log, afterSwapA, afterReadyA);
|
||||
DumpRange("SWAP B (mulligan lottery, seat B)", log, afterReadyA, afterSwapB);
|
||||
DumpRange("READY B (mulligan, seat B)", log, afterSwapB, afterReadyB);
|
||||
DumpSpinSummary("SPIN PRE-ROLL", log, afterReadyB, afterSpin);
|
||||
// first ~12 of the play phase covers the early turn-start draws for both seats
|
||||
DumpRange("FIRST PLAY-PHASE ROLLS (turn draws + effects)", log, afterSpin,
|
||||
System.Math.Min(afterSpin + 12, afterSends));
|
||||
|
||||
// === STATE-vs-STACK attribution analysis ===
|
||||
AnalyzeAttribution(log, afterSpin);
|
||||
|
||||
Assert.Pass($"probe complete: {log.Count} rolls logged; see TestContext output for attribution analysis");
|
||||
}
|
||||
|
||||
private static void DumpRange(string label, IReadOnlyList<SessionBattleEngine.RollEntry> log, int from, int to)
|
||||
{
|
||||
TestContext.WriteLine($"--- {label} [rolls {from}..{to - 1}] ({to - from} rolls) ---");
|
||||
for (int i = from; i < to; i++)
|
||||
{
|
||||
var e = log[i];
|
||||
TestContext.WriteLine($" #{e.Index} {e.Api}(arg={e.Arg}) | mgrState: self.IsSelfTurn={e.SelfIsSelfTurn} oppo.IsSelfTurn={e.OppoIsSelfTurn} | classify={Classify(e)}");
|
||||
TestContext.WriteLine($" stack: {e.Stack}");
|
||||
}
|
||||
if (to - from == 0) TestContext.WriteLine(" (none)");
|
||||
TestContext.WriteLine("");
|
||||
}
|
||||
|
||||
private static void DumpSpinSummary(string label, IReadOnlyList<SessionBattleEngine.RollEntry> log, int from, int to)
|
||||
{
|
||||
TestContext.WriteLine($"--- {label} [rolls {from}..{to - 1}] ({to - from} rolls) ---");
|
||||
if (to - from > 0)
|
||||
{
|
||||
var first = log[from];
|
||||
TestContext.WriteLine($" first spin roll #{first.Index}: self.IsSelfTurn={first.SelfIsSelfTurn} oppo.IsSelfTurn={first.OppoIsSelfTurn}");
|
||||
TestContext.WriteLine($" stack: {first.Stack}");
|
||||
bool allStateIdentical = log.Skip(from).Take(to - from)
|
||||
.All(e => e.SelfIsSelfTurn == first.SelfIsSelfTurn && e.OppoIsSelfTurn == first.OppoIsSelfTurn);
|
||||
bool allViaStartOperate = log.Skip(from).Take(to - from).All(e => e.Stack.Contains("StartOperate"));
|
||||
TestContext.WriteLine($" all {to - from} spin rolls have identical mgr seat-state? {allStateIdentical}");
|
||||
TestContext.WriteLine($" all {to - from} spin rolls routed via OperateReceive.StartOperate? {allViaStartOperate}");
|
||||
}
|
||||
TestContext.WriteLine("");
|
||||
}
|
||||
|
||||
// Best-effort classification from the STACK (the ground truth of who is rolling).
|
||||
private static string Classify(SessionBattleEngine.RollEntry e)
|
||||
{
|
||||
string s = e.Stack;
|
||||
if (s.Contains("StartOperate")) return "SPIN-PREROLL";
|
||||
if (s.Contains("_LotMulliganCardIndex") || s.Contains("MulliganCtrl")) return "MULLIGAN-LOTTERY";
|
||||
if (s.Contains("LotteryRandomDrawCard") || s.Contains("RandomCardDraw")) return "TURN/EFFECT-DRAW";
|
||||
if (s.Contains("SkillRandomSelectFilter")) return "SKILL-FILTER-DRAW";
|
||||
return "OTHER-EFFECT";
|
||||
}
|
||||
|
||||
private static void AnalyzeAttribution(IReadOnlyList<SessionBattleEngine.RollEntry> log, int playPhaseStart)
|
||||
{
|
||||
TestContext.WriteLine("=== STATE-vs-STACK ATTRIBUTION ANALYSIS ===");
|
||||
|
||||
// 1) Does mgr-state (IsSelfTurn flags) ever change across the whole replay? If both flags are
|
||||
// pinned at setup values (self=true/oppo=false) the entire time, mgr-state CANNOT distinguish
|
||||
// seats — every roll looks identical from mgr state.
|
||||
var distinctStates = log
|
||||
.Select(e => (e.SelfIsSelfTurn, e.OppoIsSelfTurn))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
TestContext.WriteLine($" distinct mgr seat-states observed across ALL {log.Count} rolls: {distinctStates.Count}");
|
||||
foreach (var st in distinctStates)
|
||||
TestContext.WriteLine($" (self.IsSelfTurn={st.Item1}, oppo.IsSelfTurn={st.Item2})");
|
||||
|
||||
// 2) For the mulligan lotteries: seat A's 6 rolls then seat B's 6 rolls happen back-to-back. Are
|
||||
// their mgr-states distinguishable? (They should NOT be — IsSelfTurn isn't toggled during
|
||||
// mulligan; both lotteries run with the same setup-time flags.)
|
||||
var mulliganRolls = log.Where(e => Classify(e) == "MULLIGAN-LOTTERY").ToList();
|
||||
var mulliganStates = mulliganRolls.Select(e => (e.SelfIsSelfTurn, e.OppoIsSelfTurn)).Distinct().Count();
|
||||
TestContext.WriteLine($" mulligan-lottery rolls: {mulliganRolls.Count}; distinct mgr seat-states among them: {mulliganStates}");
|
||||
TestContext.WriteLine($" -> seat attributable from mgr STATE alone? {(mulliganStates >= 2 ? "MAYBE" : "NO (state identical for both seats' lotteries)")}");
|
||||
bool mulliganSeatInStack = mulliganRolls.All(e => e.Stack.Contains("Mulligan") || e.Stack.Contains("_LotMulligan"));
|
||||
TestContext.WriteLine($" -> mulligan rolls carry a MulliganCtrl frame on the stack? {mulliganSeatInStack}");
|
||||
|
||||
// 3) For the play-phase draws: are turn-start draws present at all, and do their mgr-states track
|
||||
// the acting seat (i.e. does IsSelfTurn flip to identify whose turn/draw it is)?
|
||||
var drawRolls = log.Skip(playPhaseStart)
|
||||
.Where(e => Classify(e) is "TURN/EFFECT-DRAW" or "SKILL-FILTER-DRAW")
|
||||
.ToList();
|
||||
TestContext.WriteLine($" play-phase draw/filter rolls: {drawRolls.Count}");
|
||||
if (drawRolls.Count > 0)
|
||||
{
|
||||
var drawStates = drawRolls.Select(e => (e.SelfIsSelfTurn, e.OppoIsSelfTurn)).Distinct().Count();
|
||||
TestContext.WriteLine($" distinct mgr seat-states among draw rolls: {drawStates}");
|
||||
}
|
||||
|
||||
TestContext.WriteLine("");
|
||||
TestContext.WriteLine(" INTERPRETATION:");
|
||||
TestContext.WriteLine(" * If distinct mgr seat-states == 1 for a phase, the StableRandom override CANNOT");
|
||||
TestContext.WriteLine(" attribute that phase's rolls to a seat from mgr state — only the call STACK");
|
||||
TestContext.WriteLine(" (MulliganCtrl._battlePlayer / BattlePlayerBase 'this' / OperateReceive._isPlayer)");
|
||||
TestContext.WriteLine(" names the acting seat.");
|
||||
}
|
||||
}
|
||||
}
|
||||
27
SVSim.BattleEngine.Tests/SessionEngine/CaptureReplayTests.cs
Normal file
27
SVSim.BattleEngine.Tests/SessionEngine/CaptureReplayTests.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
[TestFixture]
|
||||
public class CaptureReplayTests
|
||||
{
|
||||
[Test]
|
||||
public void Load_parses_frames_and_extracts_self_deck()
|
||||
{
|
||||
var frames = CaptureReplay.Load("battle_test_cl1.ndjson");
|
||||
Assert.That(frames, Is.Not.Empty);
|
||||
|
||||
var deck = CaptureReplay.SelfDeckFrom(frames);
|
||||
Assert.That(deck, Is.Not.Empty, "Matched.selfDeck should parse");
|
||||
Assert.That(deck.Count, Is.EqualTo(40), "a standard deck is 40 cards");
|
||||
|
||||
// Send PlayActions carry their URI at the top level (body.uri == None); the helper must
|
||||
// resolve it correctly, not drop it to None.
|
||||
Assert.That(frames.Any(f => f.Direction == "send" && f.Uri == "PlayActions"),
|
||||
Is.True, "send PlayActions URI resolved from top level");
|
||||
|
||||
Assert.That(CaptureReplay.SeedFrom(frames), Is.GreaterThan(0), "Matched.selfInfo.seed parsed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Sessions.Engine;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
[TestFixture]
|
||||
public class SessionEngineConstructionTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
[Test]
|
||||
public void SessionBattleEngine_instantiates_and_is_not_ready_before_setup()
|
||||
{
|
||||
var engine = new SessionBattleEngine();
|
||||
Assert.That(engine.IsReady, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Setup_builds_two_seat_network_battle_headless()
|
||||
{
|
||||
// Load every card id the two test decks reference so CardMaster can resolve them.
|
||||
var deckA = Enumerable.Repeat(100011010L, 40).ToList(); // vanilla 1/2 follower x40
|
||||
var deckB = Enumerable.Repeat(100011010L, 40).ToList();
|
||||
HeadlessCardMaster.Load(100011010);
|
||||
|
||||
var engine = new SessionBattleEngine();
|
||||
Assert.DoesNotThrow(() => engine.Setup(masterSeed: 12345, seatADeck: deckA, seatBDeck: deckB));
|
||||
Assert.That(engine.IsReady, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Receive_one_playactions_resolves_headless()
|
||||
{
|
||||
// SUPERSEDED by the node-native oracle (SVSim.UnitTests HeadlessConductorTests). This test
|
||||
// predates the M-HC-0b view-untangle: before it, the receive conductor resolved NOTHING
|
||||
// headless (every InstantVfx the conductor fused the mutation into was no-op'd by the shared
|
||||
// VfxMgr, and OperateReceive.OnReceiveDeal was never wired), so a play "ingested" without
|
||||
// touching state and trivially did not reject. Now the conductor RESOLVES (HeadlessConductor
|
||||
// VfxMgr runs the InstantVfx; the deal seats the hand). This test feeds the first captured
|
||||
// `send PlayActions` WITHOUT first replaying the capture's Deal/mulligan, so the played card
|
||||
// is not in the seated hand and the now-live resolution correctly rejects
|
||||
// (RemoveSpellCardFromHand: not found). Replaying the capture's Deal first does NOT fix it:
|
||||
// the seated deck order can't reproduce the capture's post-mulligan idx references (the
|
||||
// documented capture-replay draw-misalignment artifact — see memory
|
||||
// project_battle_headless_conductor: "validate via node-native battles"). The valid headless
|
||||
// play oracle is now HeadlessConductorTests.Vanilla_play_resolves_on_engine_state_headless.
|
||||
Assert.Ignore("Superseded by node-native HeadlessConductorTests (M-HC-0b). Capture-replay " +
|
||||
"draw-misalignment makes a captured play unresolvable against a node-seated deck; the " +
|
||||
"node-native harness is the post-M-HC-0b oracle. Revive if capture-replay alignment lands.");
|
||||
|
||||
var cl1 = CaptureReplay.Load("battle_test_cl1.ndjson");
|
||||
var deck = CaptureReplay.SelfDeckFrom(cl1);
|
||||
// Load ALL deck ids in ONE call: HeadlessCardMaster.Load replaces the static CardMaster each
|
||||
// call, so a per-id loop would leave only the last card resolvable.
|
||||
HeadlessCardMaster.Load(deck.Select(x => (int)x).Distinct().ToArray());
|
||||
|
||||
var engine = new SessionBattleEngine();
|
||||
engine.Setup(CaptureReplay.SeedFrom(cl1), seatADeck: deck, seatBDeck: deck);
|
||||
|
||||
var firstPlay = cl1.First(f => f.Direction == "send" && f.Uri == "PlayActions");
|
||||
var result = engine.Receive(firstPlay.Env, isPlayerSeat: true);
|
||||
|
||||
Assert.That(result.RejectReason, Is.Null, $"ingest threw/rejected: {result.RejectReason}");
|
||||
Assert.That(result.Accepted, Is.True);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Sessions.Engine;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
[TestFixture]
|
||||
public class SessionEngineShadowReplayTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
// Frames that are transport/keepalive, not game actions — not ingested.
|
||||
private static readonly HashSet<string> SkipUris = new()
|
||||
{
|
||||
nameof(NetworkBattleUri.Echo),
|
||||
nameof(NetworkBattleUri.ChatStamp),
|
||||
nameof(NetworkBattleUri.Gungnir),
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void Shadow_replay_of_captured_battle_tracks_state_without_desync()
|
||||
{
|
||||
// SUPERSEDED by the node-native oracle (SVSim.UnitTests HeadlessConductorTests). This test's
|
||||
// "0 rejects" used to pass VACUOUSLY: before the M-HC-0b view-untangle the receive conductor
|
||||
// resolved NOTHING headless (InstantVfx mutations no-op'd; OnReceiveDeal unwired), so no
|
||||
// captured frame could diverge because none was applied. The retracted "shadow tracks the
|
||||
// capture" claim is documented in memory project_battle_node_engine_shadow / _headless_conductor.
|
||||
// Now that the conductor RESOLVES, replaying a captured stream against a node-seated deck hits
|
||||
// the documented capture-replay draw-misalignment: the seated deck order can't reproduce the
|
||||
// capture's post-mulligan idx references, so played cards aren't in the seated hand
|
||||
// (HandCardToField/RemoveSpellCardFromHand: not found). The decision (memory
|
||||
// project_battle_headless_conductor) is to validate headless resolution via NODE-NATIVE
|
||||
// battles, not capture replay. The node-native oracle now covers Deal+Play.
|
||||
Assert.Ignore("Superseded by node-native HeadlessConductorTests (M-HC-0b). Capture-replay " +
|
||||
"against a node-seated deck hits the documented draw-misalignment artifact once the " +
|
||||
"receive path actually resolves. Revive if a capture-replay alignment path lands.");
|
||||
|
||||
var cl1 = CaptureReplay.Load("battle_test_cl1.ndjson");
|
||||
var cl2 = CaptureReplay.Load("battle_test_cl2.ndjson");
|
||||
var deckA = CaptureReplay.SelfDeckFrom(cl1);
|
||||
var deckB = CaptureReplay.SelfDeckFrom(cl2);
|
||||
// One Load call with every id — Load replaces the static master each call.
|
||||
HeadlessCardMaster.Load(deckA.Concat(deckB).Select(x => (int)x).Distinct().ToArray());
|
||||
|
||||
var engine = new SessionBattleEngine();
|
||||
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
|
||||
|
||||
// Single-client full-stream replay (cl1 as the player seat): cl1's SENT frames are its own
|
||||
// actions (seat=true); its RECEIVED frames are the opponent/server actions (seat=false),
|
||||
// incl. the Deal that establishes both hands. This is exactly the stream cl1's receiver
|
||||
// processed, in capture (ts) order. (The node-side both-clients-sends model is exercised
|
||||
// live in Task 7; here we validate engine tracking against ground truth.)
|
||||
var stream = cl1.Where(f => !SkipUris.Contains(f.Uri))
|
||||
.OrderBy(f => f.Ts)
|
||||
.ToList();
|
||||
|
||||
var rejects = new List<string>();
|
||||
var violations = new List<string>();
|
||||
|
||||
foreach (var f in stream)
|
||||
{
|
||||
bool seat = f.Direction == "send";
|
||||
var r = engine.Receive(f.Env, isPlayerSeat: seat);
|
||||
if (r.RejectReason is not null)
|
||||
rejects.Add($"{f.Direction} {f.Uri}: {r.RejectReason}");
|
||||
|
||||
if (f.Uri == nameof(NetworkBattleUri.TurnEnd))
|
||||
CheckInvariants(engine, violations, atUri: f.Uri);
|
||||
}
|
||||
|
||||
foreach (var line in rejects) TestContext.WriteLine("REJECT " + line);
|
||||
foreach (var line in violations) TestContext.WriteLine("VIOLATION " + line);
|
||||
TestContext.WriteLine($"frames={stream.Count} rejects={rejects.Count} violations={violations.Count}");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(rejects, Is.Empty, "engine diverged / rejected a captured frame");
|
||||
Assert.That(violations, Is.Empty, "engine state left a structural invariant");
|
||||
});
|
||||
}
|
||||
|
||||
private static void CheckInvariants(SessionBattleEngine engine, List<string> violations, string atUri)
|
||||
{
|
||||
foreach (var seat in new[] { true, false })
|
||||
{
|
||||
int life = engine.LeaderLife(seat), pp = engine.Pp(seat);
|
||||
int board = engine.BoardCount(seat), hand = engine.HandCount(seat);
|
||||
if (life is < 0 or > 20) violations.Add($"{atUri} seat={seat} life={life}");
|
||||
if (pp is < 0 or > 10) violations.Add($"{atUri} seat={seat} pp={pp}");
|
||||
if (board is < 0 or > 7) violations.Add($"{atUri} seat={seat} board={board}");
|
||||
if (hand is < 0 or > 9) violations.Add($"{atUri} seat={seat} hand={hand}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Sessions.Engine;
|
||||
using System.Linq;
|
||||
using SVSim.BattleEngine.Tests;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine;
|
||||
|
||||
[TestFixture]
|
||||
public class SessionEngineSpellboostTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
[Test]
|
||||
public void EngineGlobalInit_makes_a_fresh_engine_ready()
|
||||
{
|
||||
EngineGlobalInit.EnsureInitialized();
|
||||
var cl1 = CaptureReplay.Load("battle_test_cl1.ndjson");
|
||||
var cl2 = CaptureReplay.Load("battle_test_cl2.ndjson");
|
||||
var deckA = CaptureReplay.SelfDeckFrom(cl1);
|
||||
var deckB = CaptureReplay.SelfDeckFrom(cl2);
|
||||
// Belt-and-suspenders (matches the sibling tests): load the decks into the harness master so
|
||||
// this test never depends on global card-master contents. EnsureInitialized() above still
|
||||
// proves EngineGlobalInit's own path works.
|
||||
foreach (var id in deckA.Concat(deckB).Distinct()) HeadlessCardMaster.Load((int)id);
|
||||
var engine = new SessionBattleEngine();
|
||||
Assert.DoesNotThrow(() => engine.Setup(masterSeed: 12345, seatADeck: deckA, seatBDeck: deckB));
|
||||
Assert.That(engine.IsReady, Is.True, "engine must be ready after EngineGlobalInit (carried-risk fix)");
|
||||
}
|
||||
|
||||
}
|
||||
111
SVSim.BattleEngine.Tests/SummonTokenOracleTests.cs
Normal file
111
SVSim.BattleEngine.Tests/SummonTokenOracleTests.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M5 (next-hardest deterministic card): a when_play SUMMON_TOKEN spell resolves to correct
|
||||
// authoritative state HEADLESS via the same IsForecast/IsRecovery + ActionProcessor path the
|
||||
// M2 vanilla follower / M3 fixed-damage spell / M4 self-buff follower proved (design §5 / DP4 +
|
||||
// M3+ resume recipe). The new oracle dimension over M2-M4 is a BOARD-COUNT DELTA from a
|
||||
// SKILL-CREATED card: the spell's `summon_token=100011020` must place exactly one NEW follower
|
||||
// token (id 100011020, a neutral 2/2) onto the caster's board — a card that was never in the
|
||||
// hand or deck. This is the first headless run of the PUBLIC prefab card-creation path
|
||||
// (CardCreatorBase.CreateCard, createNullView:false), so it stresses the view shim in a way the
|
||||
// earlier null-view-seam milestones did not.
|
||||
[TestFixture]
|
||||
public class SummonTokenOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Summon_token_spell_places_a_new_token_on_the_board()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Minimal opponent/turn wiring (see M2-M4 oracles): opponent refs + active turn flag.
|
||||
// The summon resolves onto the active player's own board (`summon_side` defaults to self).
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
// Seed leader life: this spell deals no damage, but the play-legality gate still rejects a
|
||||
// play when a leader reads as a 0-life game-over state (M3 learning).
|
||||
HeadlessEngineEnv.InitLeaderLife(mgr);
|
||||
|
||||
// Seed the card-template prefabs the engine's internal (createNullView:false) summon
|
||||
// creation path clones — the bare construction path leaves SBattleLoad null.
|
||||
HeadlessEngineEnv.InitCardTemplates(mgr);
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.TokenSpellId);
|
||||
|
||||
// Place the summon-token spell in the active player's hand with PP to spare; empty board.
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.TokenSpellId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
// Pre-state snapshot. ClassAndInPlayCardList holds the leader (index 0) on an empty board.
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
int enemyHandBefore = enemy.HandCardList.Count;
|
||||
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
|
||||
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
|
||||
|
||||
// Resolve the play through the real engine.
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
|
||||
"ActionProcessor.PlayCard threw on a summon-token spell");
|
||||
|
||||
// Oracle: the board-count delta + summoned token identity is the new M5 dimension; the rest
|
||||
// are the §5 spell-shaped invariants proven by M3.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// Primary M5 assertion: exactly one NEW card is on the player's board (the summoned
|
||||
// token), and it is the token id with its CardCSVData base stats — proving the skill
|
||||
// CREATED a card, not just moved the played one.
|
||||
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore + 1),
|
||||
"player board count not +1 (the summoned token did not land)");
|
||||
var token = player.ClassAndInPlayCardList
|
||||
.SingleOrDefault(c => c.CardId == HeadlessEngineEnv.SummonedTokenId);
|
||||
Assert.That(token, Is.Not.Null, "summoned token (id 100011020) not found on the board");
|
||||
Assert.That(token.Atk, Is.EqualTo(HeadlessEngineEnv.SummonedTokenAtk), "token atk != base");
|
||||
Assert.That(token.Life, Is.EqualTo(HeadlessEngineEnv.SummonedTokenLife), "token life != base");
|
||||
// The summoned token is NOT the played card.
|
||||
Assert.That(token, Is.Not.SameAs(card), "summoned token is the played spell itself");
|
||||
// Cost paid.
|
||||
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
|
||||
// The spell leaves hand and (being a spell) does NOT itself occupy the board.
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
|
||||
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
|
||||
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
|
||||
// Opponent unchanged (the summon targets the caster's own board).
|
||||
Assert.That(enemy.HandCardList.Count, Is.EqualTo(enemyHandBefore), "opponent hand changed");
|
||||
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board changed");
|
||||
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore), "opponent leader life changed");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
109
SVSim.BattleEngine.Tests/TargetedDamageSpellOracleTests.cs
Normal file
109
SVSim.BattleEngine.Tests/TargetedDamageSpellOracleTests.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M6 (the first TARGET-SELECTION card): a when_play TARGETED-DAMAGE spell resolves to correct
|
||||
// authoritative state HEADLESS via the same IsForecast/IsRecovery + ActionProcessor path the
|
||||
// M2-M5 cards proved — but for the FIRST time exercising the `selectedCards` path of
|
||||
// ActionProcessor.PlayCard (Engine/Wizard.Battle/ActionProcessor.cs:401, dormant until now;
|
||||
// M2-M5 all passed selectedCards: null). The new oracle dimension is SELECTION ROUTING: with
|
||||
// TWO followers on the enemy board and ONE passed as `selectedCards`, the spell's `damage=5`
|
||||
// must hit the SELECTED follower and leave the un-selected one untouched. A plain "a follower
|
||||
// took damage" assertion would false-pass; reading the differential (selected -5, un-selected 0)
|
||||
// is what proves the selectedCards path routes the effect to the chosen target. Load-bearing is
|
||||
// confirmed by swapping which follower is selected and watching the damage follow the selection.
|
||||
[TestFixture]
|
||||
public class TargetedDamageSpellOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Targeted_damage_spell_hits_only_the_selected_enemy_follower()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Minimal opponent/turn wiring (see M2-M5 oracles): opponent refs + active turn flag. The
|
||||
// spell's target resolver walks player -> opponent -> opponent's in-play followers.
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
// Seed leader life so neither leader reads as a 0-life game-over state (blocks plays, M3).
|
||||
HeadlessEngineEnv.InitLeaderLife(mgr);
|
||||
|
||||
// Put TWO vanilla followers on the ENEMY board (the new M6 setup). Both survive the 5
|
||||
// damage, so the oracle reads a differential life-delta rather than depending on death.
|
||||
var selected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.SelectTargetFollowerId, 0, isPlayer: false);
|
||||
var unselected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.UnselectTargetFollowerId, 1, isPlayer: false);
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.TargetSpellId);
|
||||
|
||||
// Place the targeted-damage spell in the active player's hand with PP to spare.
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.TargetSpellId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
// Pre-state snapshot.
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
|
||||
int selectedLifeBefore = selected.Life;
|
||||
int unselectedLifeBefore = unselected.Life;
|
||||
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
|
||||
|
||||
// Resolve the play through the real engine, passing the chosen target via selectedCards
|
||||
// (the M6 first — every prior milestone passed null).
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: new List<BattleCardBase> { selected }),
|
||||
"ActionProcessor.PlayCard threw on a targeted-damage spell");
|
||||
|
||||
// Oracle: selection routing is the new M6 dimension; the rest are the §5 spell-shaped invariants.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// PRIMARY M6 assertions: the SELECTED follower takes exactly the spell's damage...
|
||||
Assert.That(selected.Life, Is.EqualTo(selectedLifeBefore - HeadlessEngineEnv.TargetSpellDamage),
|
||||
"selected follower did not take the spell's damage");
|
||||
// ...and the UN-SELECTED follower is untouched (proves routing, not a blanket hit).
|
||||
Assert.That(unselected.Life, Is.EqualTo(unselectedLifeBefore),
|
||||
"un-selected follower was damaged (effect not routed to the selection)");
|
||||
// Both followers survive => still on the enemy board; leader unchanged.
|
||||
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore),
|
||||
"enemy board count changed (a target unexpectedly left the board)");
|
||||
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore),
|
||||
"opponent leader life changed (damage hit the leader, not the selected follower)");
|
||||
// Cost paid.
|
||||
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
|
||||
// Spell leaves hand and (being a spell) does NOT occupy the board.
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
|
||||
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
|
||||
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
|
||||
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
115
SVSim.BattleEngine.Tests/TargetedDestroySpellOracleTests.cs
Normal file
115
SVSim.BattleEngine.Tests/TargetedDestroySpellOracleTests.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M7 (the first card to prove follower DEATH / board-removal): a when_play TARGETED-DESTROY spell
|
||||
// resolves to correct authoritative state HEADLESS via the same IsForecast/IsRecovery +
|
||||
// ActionProcessor + selectedCards path M6 proved — but for the FIRST time exercising a mechanic
|
||||
// that REMOVES a card from the board. M2-M6 only ever ADDED to / mutated stats of cards already in
|
||||
// play; none proved the engine commits board REMOVAL inside the authoritative part of PlayCard
|
||||
// (rather than the cosmetic post-Process tail the prior docs flag). The new oracle dimension is
|
||||
// BOARD REMOVAL: with TWO followers on the enemy board and ONE passed as `selectedCards`, the
|
||||
// `destroy` must remove exactly the SELECTED follower (enemy board count -1, selected gone, landed
|
||||
// in the enemy CemeteryList) while leaving the un-selected follower on the board. The un-selected-
|
||||
// survives assertion is load-bearing the same way M4's delta-vs-base and M6's differential were:
|
||||
// it distinguishes "the destroy was routed to the selection" from "a blanket board wipe" — and is
|
||||
// confirmed by the routing already proven in M6 (the effect follows the selectedCards entry).
|
||||
[TestFixture]
|
||||
public class TargetedDestroySpellOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var t = obj.GetType();
|
||||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Targeted_destroy_spell_removes_only_the_selected_enemy_follower()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Minimal opponent/turn wiring (see M2-M6 oracles): opponent refs + active turn flag. The
|
||||
// destroy's target resolver walks player -> opponent -> opponent's in-play followers.
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
// Seed leader life so neither leader reads as a 0-life game-over state (blocks plays, M3).
|
||||
HeadlessEngineEnv.InitLeaderLife(mgr);
|
||||
|
||||
// Put TWO vanilla followers on the ENEMY board (the M6 setup). destroy is unconditional, so
|
||||
// their stats are irrelevant — distinct ids only so the selected vs un-selected can't be
|
||||
// confused. The selected one is destroyed; the un-selected one must survive.
|
||||
var selected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.DestroyTargetFollowerId, 0, isPlayer: false);
|
||||
var unselected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.DestroyOtherFollowerId, 1, isPlayer: false);
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.DestroySpellId);
|
||||
|
||||
// Place the targeted-destroy spell in the active player's hand with PP to spare.
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.DestroySpellId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
// Pre-state snapshot.
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
|
||||
int enemyCemeteryBefore = enemy.CemeteryList.Count;
|
||||
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
|
||||
|
||||
// Resolve the play through the real engine, passing the chosen target via selectedCards.
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: new List<BattleCardBase> { selected }),
|
||||
"ActionProcessor.PlayCard threw on a targeted-destroy spell");
|
||||
|
||||
// Oracle: board removal is the new M7 dimension; the rest are the §5 spell-shaped invariants.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// PRIMARY M7 assertions: the SELECTED follower is removed from the enemy board...
|
||||
Assert.That(enemy.ClassAndInPlayCardList, Does.Not.Contain(selected),
|
||||
"selected follower still on the enemy board (destroy did not remove it)");
|
||||
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore - 1),
|
||||
"enemy board count not -1 (a destroy did not commit, or hit the wrong number of cards)");
|
||||
// ...and it landed in the enemy's CemeteryList (the engine's destroy/death path).
|
||||
Assert.That(enemy.CemeteryList, Contains.Item(selected),
|
||||
"destroyed follower not in the enemy CemeteryList");
|
||||
Assert.That(enemy.CemeteryList.Count, Is.EqualTo(enemyCemeteryBefore + 1),
|
||||
"enemy cemetery count not +1");
|
||||
// ...while the UN-SELECTED follower stays on the board (proves routing, not a board wipe).
|
||||
Assert.That(enemy.ClassAndInPlayCardList, Contains.Item(unselected),
|
||||
"un-selected follower was destroyed (effect not routed to the selection)");
|
||||
// Leader untouched (destroy targets a follower, not the face).
|
||||
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore),
|
||||
"opponent leader life changed (destroy hit the leader, not the selected follower)");
|
||||
// Cost paid.
|
||||
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
|
||||
// Spell leaves hand and (being a spell) does NOT occupy the board.
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
|
||||
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
|
||||
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
|
||||
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
50
SVSim.BattleEngine.Tests/TestBattleScope.cs
Normal file
50
SVSim.BattleEngine.Tests/TestBattleScope.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using SVSim.BattleEngine.Ambient;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests;
|
||||
|
||||
/// <summary>Per-test ambient scope. Each test that touches engine statics wraps its body
|
||||
/// in `using var scope = new TestBattleScope();` (or with an explicit Mgr/ViewerId).
|
||||
///
|
||||
/// The constructor enters a fresh <see cref="BattleAmbientContext"/> (carrying a brand-new
|
||||
/// <see cref="GameMgr"/> so per-test mgr/DataMgr writes never bleed across tests), then
|
||||
/// runs the per-ambient seeders that <see cref="HeadlessEngineEnv.EnsureProcessGlobals"/>
|
||||
/// no longer does (chara ids on DataMgr, NetworkUserInfoData). Process-globals
|
||||
/// (card master, LoadDetail, Crossover, Certification.udid) come from
|
||||
/// <see cref="HeadlessEngineEnv.EnsureProcessGlobals"/> which runs once per process.
|
||||
///
|
||||
/// Public surface (vs. internal) so SVSim.UnitTests can reuse it via the same project
|
||||
/// reference in Task 7.</summary>
|
||||
public sealed class TestBattleScope : IDisposable
|
||||
{
|
||||
private readonly BattleAmbient.Scope _scope;
|
||||
public BattleAmbientContext Ctx { get; }
|
||||
|
||||
public TestBattleScope(BattleManagerBase? mgr = null, int viewerId = 1001)
|
||||
{
|
||||
// Make sure process-globals are seeded before we enter; idempotent + cheap after first call.
|
||||
HeadlessEngineEnv.EnsureProcessGlobals();
|
||||
|
||||
Ctx = new BattleAmbientContext
|
||||
{
|
||||
Mgr = mgr,
|
||||
GameMgr = new GameMgr(),
|
||||
ViewerId = viewerId,
|
||||
IsForecast = true,
|
||||
IsRandomDraw = true,
|
||||
RecoveryInfo = (Wizard.BattleRecoveryInfo)FormatterServices
|
||||
.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo)),
|
||||
};
|
||||
_scope = BattleAmbient.Enter(Ctx);
|
||||
|
||||
// Per-ambient seeders MUST run AFTER scope entry so GameMgr.GetIns() resolves to this
|
||||
// scope's GameMgr (not a stray one). EnsureProcessGlobals used to do these writes against
|
||||
// the global GameMgr; now they're scoped.
|
||||
HeadlessEngineEnv.SeedCharaIdsOnCurrentAmbient();
|
||||
HeadlessEngineEnv.SeedNetUserOnCurrentAmbient();
|
||||
}
|
||||
|
||||
public void Dispose() => _scope.Dispose();
|
||||
}
|
||||
93
SVSim.BattleEngine.Tests/VanillaFollowerOracleTests.cs
Normal file
93
SVSim.BattleEngine.Tests/VanillaFollowerOracleTests.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests
|
||||
{
|
||||
// M2 first-green (go/no-go step 2): a single zero-skill vanilla follower resolves to correct
|
||||
// authoritative state HEADLESS via the proven IsForecast/IsRecovery + ActionProcessor path
|
||||
// (design §5 / DP4). No Unity runtime, no VFX clock.
|
||||
[TestFixture]
|
||||
public class VanillaFollowerOracleTests
|
||||
{
|
||||
private TestBattleScope _scope;
|
||||
|
||||
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
|
||||
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
|
||||
|
||||
private static void SetPrivateField(object obj, string name, object value)
|
||||
{
|
||||
var f = obj.GetType().GetField(name,
|
||||
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
// Walk up the hierarchy if declared on a base type.
|
||||
var t = obj.GetType();
|
||||
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
|
||||
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
|
||||
f.SetValue(obj, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Vanilla_follower_resolves_to_correct_state()
|
||||
{
|
||||
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
|
||||
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
|
||||
_scope.Ctx.Mgr = mgr;
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
|
||||
// Wire the opponent links + active turn. The full BattlePlayerBase.Setup(opponent) does
|
||||
// this but cascades into UI/manager init irrelevant to the resolution path, so set the
|
||||
// minimal state directly: each player's opponent ref, and the active player's turn flag
|
||||
// (the on-enter-play skill sweep reads opponent.IsSelfTurn / IsGameFirst).
|
||||
SetPrivateField(player, "_opponentBattlePlayer", enemy);
|
||||
SetPrivateField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.FollowerId);
|
||||
|
||||
// Place the follower in the active player's hand with PP to spare; empty board otherwise.
|
||||
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.FollowerId, 1, isPlayer: true, mgr);
|
||||
player.HandCardList.Add(card);
|
||||
player.Pp = 10;
|
||||
|
||||
// Pre-state snapshot.
|
||||
int ppBefore = player.Pp;
|
||||
int handBefore = player.HandCardList.Count;
|
||||
int inplayBefore = player.ClassAndInPlayCardList.Count;
|
||||
int enemyHandBefore = enemy.HandCardList.Count;
|
||||
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
|
||||
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
|
||||
|
||||
// Resolve the play through the real engine.
|
||||
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
|
||||
var ap = new ActionProcessor(pair);
|
||||
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
|
||||
"ActionProcessor.PlayCard threw on a vanilla follower");
|
||||
|
||||
// Oracle (§5 invariants).
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
|
||||
Assert.That(player.HandCardList, Does.Not.Contain(card), "card still in hand");
|
||||
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
|
||||
Assert.That(player.ClassAndInPlayCardList, Contains.Item(card), "card not in play");
|
||||
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(inplayBefore + 1), "in-play count not +1");
|
||||
Assert.That(card.Atk, Is.EqualTo(cardParam.Atk), "follower atk != CardCSVData base");
|
||||
Assert.That(card.Life, Is.EqualTo(cardParam.Life), "follower life != CardCSVData base");
|
||||
// Opponent unchanged.
|
||||
Assert.That(enemy.HandCardList.Count, Is.EqualTo(enemyHandBefore), "opponent hand changed");
|
||||
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board changed");
|
||||
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore), "opponent leader life changed");
|
||||
// §5 "zero VFX registered with VfxMgr": structural here — the shim VfxMgr is a pure
|
||||
// no-op (RegisterImmediate/SequentialVfx do nothing) and IsForecast suppresses
|
||||
// registration in the real engine, so no VFX is ever played headless. Covered by the
|
||||
// DoesNotThrow above; there is no meaningful count to assert against the no-op shim.
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
3314
SVSim.BattleEngine/COPIED.manifest.tsv
Normal file
3314
SVSim.BattleEngine/COPIED.manifest.tsv
Normal file
File diff suppressed because it is too large
Load Diff
2
SVSim.BattleEngine/Engine/.gitattributes
vendored
Normal file
2
SVSim.BattleEngine/Engine/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Verbatim engine copies: never normalize line endings (keeps sha256 manifest valid).
|
||||
*.cs -text
|
||||
0
SVSim.BattleEngine/Engine/.gitkeep
Normal file
0
SVSim.BattleEngine/Engine/.gitkeep
Normal file
6
SVSim.BattleEngine/Engine/AISendIntervalTrigger.cs
Normal file
6
SVSim.BattleEngine/Engine/AISendIntervalTrigger.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
public class AISendIntervalTrigger : SendIntervalTrigger
|
||||
{
|
||||
public override void SendDataCheck(NetworkBattleManagerBase networkBattleManager, NetworkBattleDefine.NetworkBattleURI sendUri)
|
||||
{
|
||||
}
|
||||
}
|
||||
58
SVSim.BattleEngine/Engine/AITurnControl.cs
Normal file
58
SVSim.BattleEngine/Engine/AITurnControl.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Cute;
|
||||
using Wizard;
|
||||
|
||||
public class AITurnControl
|
||||
{
|
||||
private DateTime _startTime;
|
||||
|
||||
private bool _isStartTimer;
|
||||
|
||||
public AITurnControl()
|
||||
{
|
||||
_isStartTimer = false;
|
||||
}
|
||||
|
||||
public void StartTurnTimer()
|
||||
{
|
||||
_isStartTimer = true;
|
||||
_startTime = TimeUtil.GetAbsoluteTime();
|
||||
}
|
||||
|
||||
public void SetAndStartTurnTimer(DateTime time)
|
||||
{
|
||||
_isStartTimer = true;
|
||||
_startTime = time;
|
||||
}
|
||||
|
||||
public void Update(IEnemyAI ai)
|
||||
{
|
||||
if (ToolboxGame.RealTimeNetworkAgent != null && ToolboxGame.RealTimeNetworkAgent.PlayerNetworkStatus.IsAlive && !ai.IsConnectNetwork)
|
||||
{
|
||||
ai.Reconnect();
|
||||
}
|
||||
if (!_isStartTimer || !ToolboxGame.RealTimeNetworkAgent.PlayerNetworkStatus.IsAlive)
|
||||
{
|
||||
if (_isStartTimer && ai.IsConnectNetwork)
|
||||
{
|
||||
ai.Disconnect();
|
||||
}
|
||||
}
|
||||
else if ((float)NetworkUtility.GetTimeSpanSecond(_startTime.Ticks) >= 90f)
|
||||
{
|
||||
if (ai.IsStackAction)
|
||||
{
|
||||
ai.CleanupStackedAction();
|
||||
}
|
||||
if (!ai.IsStackAction)
|
||||
{
|
||||
ai.TurnEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void StopTurnTimer()
|
||||
{
|
||||
_isStartTimer = false;
|
||||
}
|
||||
}
|
||||
166
SVSim.BattleEngine/Engine/AchievedInfo.cs
Normal file
166
SVSim.BattleEngine/Engine/AchievedInfo.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System.Collections.Generic;
|
||||
using LitJson;
|
||||
using Wizard;
|
||||
using Wizard.Lottery;
|
||||
|
||||
public class AchievedInfo
|
||||
{
|
||||
private const string ACHIEVEMENT = "achieved_achievement_list";
|
||||
|
||||
private const string MISSION = "achieved_mission_list";
|
||||
|
||||
private const string REWARD = "achieved_mission_reward_list";
|
||||
|
||||
private const string VICTORY_REWARD = "win_reward_list";
|
||||
|
||||
private const string GRAND_MASTER_REWARD = "grand_master_reward_list";
|
||||
|
||||
private const string MISSION_START = "mission_start_data";
|
||||
|
||||
private const string BEGINNER_MISSION_REWARD = "achieved_beginner_mission_reward_list";
|
||||
|
||||
private const string BEGINNER_MISSION_REWARD_MESSAGE = "achieved_beginner_mission_list";
|
||||
|
||||
private const string BATTLE_PASS_REWARD_LIST = "battle_pass_reward_list";
|
||||
|
||||
private const string BATTLE_PASS_MESSAGE_LIST = "battle_pass_message_list";
|
||||
|
||||
private const long DONT_NOTIFY_IF_SMALLER_THAN_SECONDS = 10L;
|
||||
|
||||
public List<UserMission> _missions;
|
||||
|
||||
public List<UserAchievement> _achievements;
|
||||
|
||||
public List<ReceivedReward> _rewards;
|
||||
|
||||
public List<ReceivedReward> _victoryRewards;
|
||||
|
||||
public LotteryApplyData _lotteryData = LotteryApplyData.EmptyData();
|
||||
|
||||
public AchievedInfo()
|
||||
{
|
||||
_missions = new List<UserMission>();
|
||||
_achievements = new List<UserAchievement>();
|
||||
_rewards = new List<ReceivedReward>();
|
||||
_victoryRewards = new List<ReceivedReward>();
|
||||
}
|
||||
|
||||
public AchievedInfo(JsonData data)
|
||||
: this()
|
||||
{
|
||||
Read(data);
|
||||
}
|
||||
|
||||
public void Read(JsonData data)
|
||||
{
|
||||
if (data.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (data.Keys.Contains("achieved_mission_list"))
|
||||
{
|
||||
JsonData jsonData = data["achieved_mission_list"];
|
||||
if (jsonData != null)
|
||||
{
|
||||
for (int i = 0; i < jsonData.Count; i++)
|
||||
{
|
||||
_missions.Add(UserMission.CreateAchievedMission(jsonData[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.Keys.Contains("achieved_achievement_list"))
|
||||
{
|
||||
JsonData jsonData2 = data["achieved_achievement_list"];
|
||||
if (jsonData2 != null)
|
||||
{
|
||||
for (int j = 0; j < jsonData2.Count; j++)
|
||||
{
|
||||
UserAchievement userAchievement = UserAchievement.CreateCompletedAchievement(jsonData2[j]);
|
||||
if (!string.IsNullOrEmpty(userAchievement.OsId))
|
||||
{
|
||||
AchievementImpl.instance.ReleaseAchievement(userAchievement.OsId);
|
||||
}
|
||||
_achievements.Add(userAchievement);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.Keys.Contains("grand_master_reward_list"))
|
||||
{
|
||||
JsonData jsonData3 = data["grand_master_reward_list"];
|
||||
if (jsonData3 != null)
|
||||
{
|
||||
for (int k = 0; k < jsonData3.Count; k++)
|
||||
{
|
||||
_rewards.Add(ReceivedReward.CreateFromBattleResultGrandMaster(jsonData3[k]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.Keys.Contains("achieved_mission_reward_list"))
|
||||
{
|
||||
JsonData jsonData4 = data["achieved_mission_reward_list"];
|
||||
if (jsonData4 != null)
|
||||
{
|
||||
for (int l = 0; l < jsonData4.Count; l++)
|
||||
{
|
||||
_rewards.Add(ReceivedReward.CreateFromBattleResult(jsonData4[l]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.Keys.Contains("win_reward_list"))
|
||||
{
|
||||
JsonData jsonData5 = data["win_reward_list"];
|
||||
if (jsonData5 != null)
|
||||
{
|
||||
for (int m = 0; m < jsonData5.Count; m++)
|
||||
{
|
||||
_victoryRewards.Add(ReceivedReward.CreateVictoryReward(jsonData5[m]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.Keys.Contains("achieved_beginner_mission_reward_list"))
|
||||
{
|
||||
JsonData jsonData6 = data["achieved_beginner_mission_reward_list"];
|
||||
if (jsonData6 != null)
|
||||
{
|
||||
for (int n = 0; n < jsonData6.Count; n++)
|
||||
{
|
||||
_rewards.Add(ReceivedReward.CreateFromBeginnerMissionReward(jsonData6[n]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.Keys.Contains("achieved_beginner_mission_list"))
|
||||
{
|
||||
JsonData jsonData7 = data["achieved_beginner_mission_list"];
|
||||
if (jsonData7 != null)
|
||||
{
|
||||
for (int num = 0; num < jsonData7.Count; num++)
|
||||
{
|
||||
_missions.Add(UserMission.CreateAchievedMission(jsonData7[num]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.Keys.Contains("battle_pass_reward_list"))
|
||||
{
|
||||
JsonData jsonData8 = data["battle_pass_reward_list"];
|
||||
if (jsonData8 != null)
|
||||
{
|
||||
for (int num2 = 0; num2 < jsonData8.Count; num2++)
|
||||
{
|
||||
_rewards.Add(ReceivedReward.CreateFromBattlePassReward(jsonData8[num2]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.Keys.Contains("battle_pass_message_list"))
|
||||
{
|
||||
JsonData jsonData9 = data["battle_pass_message_list"];
|
||||
if (jsonData9 != null)
|
||||
{
|
||||
for (int num3 = 0; num3 < jsonData9.Count; num3++)
|
||||
{
|
||||
_missions.Add(UserMission.CreateAchievedMission(jsonData9[num3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
_lotteryData = LotteryApplyData.Parse(data);
|
||||
}
|
||||
}
|
||||
4
SVSim.BattleEngine/Engine/AchievementInfo.cs
Normal file
4
SVSim.BattleEngine/Engine/AchievementInfo.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
public class AchievementInfo : HeaderData
|
||||
{
|
||||
public AchievementInfoDetail data;
|
||||
}
|
||||
6
SVSim.BattleEngine/Engine/AchievementInfoDetail.cs
Normal file
6
SVSim.BattleEngine/Engine/AchievementInfoDetail.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class AchievementInfoDetail
|
||||
{
|
||||
public List<UserAchievement> user_achievement_list;
|
||||
}
|
||||
777
SVSim.BattleEngine/Engine/AchievementWindowBase.cs
Normal file
777
SVSim.BattleEngine/Engine/AchievementWindowBase.cs
Normal file
@@ -0,0 +1,777 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cute;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
using Wizard.Bingo;
|
||||
using Wizard.Lottery;
|
||||
using Wizard.Scripts.Network.Data.TableData;
|
||||
|
||||
public class AchievementWindowBase : MonoBehaviour
|
||||
{
|
||||
public enum AchievementType
|
||||
{
|
||||
None,
|
||||
Reward,
|
||||
Nonattainment,
|
||||
AlreadyReceived,
|
||||
PointRunning,
|
||||
PointClear,
|
||||
PointReceived
|
||||
}
|
||||
|
||||
public GameObject goButtonReward;
|
||||
|
||||
public UITexture achievementIconTexture;
|
||||
|
||||
public UILabel labelAchievementTitle;
|
||||
|
||||
public UILabel labelAchievementData;
|
||||
|
||||
public UILabel labelAchievementCount;
|
||||
|
||||
public UILabel _missionWaitLabel;
|
||||
|
||||
public UILabel _labelMissionPeriod;
|
||||
|
||||
public UILabel _labelMissionNotice;
|
||||
|
||||
public UISprite _titleLine;
|
||||
|
||||
public UILabel alreadyReceived;
|
||||
|
||||
[NonSerialized]
|
||||
public int type;
|
||||
|
||||
[NonSerialized]
|
||||
public int iType = -1;
|
||||
|
||||
[NonSerialized]
|
||||
public int level;
|
||||
|
||||
[NonSerialized]
|
||||
public string strAchievementData = "3種類のスリーブを使う。";
|
||||
|
||||
[SerializeField]
|
||||
private UILabel LabelDetailBtn;
|
||||
|
||||
[SerializeField]
|
||||
private UITable StarTable;
|
||||
|
||||
[SerializeField]
|
||||
private UISprite StarOriginal;
|
||||
|
||||
[SerializeField]
|
||||
private GameObject MailReceive;
|
||||
|
||||
[SerializeField]
|
||||
private UIGauge GaugeUI;
|
||||
|
||||
[SerializeField]
|
||||
private UILabel GaugeLabel;
|
||||
|
||||
[SerializeField]
|
||||
private UISprite _Separator;
|
||||
|
||||
[SerializeField]
|
||||
private UIWidget _StarsWidget;
|
||||
|
||||
[SerializeField]
|
||||
private UILabel _labelTopRight;
|
||||
|
||||
[SerializeField]
|
||||
private UILabel _missionStartTime;
|
||||
|
||||
[SerializeField]
|
||||
private UILabel _missionTimeOver;
|
||||
|
||||
[SerializeField]
|
||||
private UILabel _applyFinish;
|
||||
|
||||
private const int ACHIEVEMENT_STARS_MAX = 5;
|
||||
|
||||
private ResourceHandler _resourceHandler;
|
||||
|
||||
private const string SPRITE_PREFIX_BUTTON_BLUE = "btn_common_02_s_";
|
||||
|
||||
private int _viewMailId;
|
||||
|
||||
private QuestRewardInfo _questRewardInfo;
|
||||
|
||||
private Action _onReceivceAchievementSuccess;
|
||||
|
||||
private CrossoverRewardInfo _crossoverRewardInfo;
|
||||
|
||||
private const int BINGO_MISSION_SPRITE_WIDTH = 752;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
LabelDetailBtn.text = Data.SystemText.Get("Common_0022");
|
||||
}
|
||||
|
||||
public void SetType(AchievementType typeBase)
|
||||
{
|
||||
labelAchievementCount.gameObject.SetActive(typeBase != AchievementType.AlreadyReceived);
|
||||
alreadyReceived.gameObject.SetActive(typeBase == AchievementType.AlreadyReceived || typeBase == AchievementType.PointReceived);
|
||||
goButtonReward.SetActive(typeBase != AchievementType.AlreadyReceived && typeBase != AchievementType.PointReceived);
|
||||
UIManager.SetObjectToGrey(goButtonReward, typeBase == AchievementType.Nonattainment || typeBase == AchievementType.PointRunning);
|
||||
}
|
||||
|
||||
public void SetActiveGaugeUI(bool isActive)
|
||||
{
|
||||
GaugeUI.gameObject.SetActive(isActive);
|
||||
}
|
||||
|
||||
public void OnRewardClick()
|
||||
{
|
||||
AchievementReceiveRewardTask achievementReceiveRewardTask = new AchievementReceiveRewardTask();
|
||||
achievementReceiveRewardTask.SetParameter(type, level);
|
||||
StartCoroutine(Toolbox.NetworkManager.Connect(achievementReceiveRewardTask, OnRequestRewardAchievement, BaseTask.OnRequestFailed, BaseTask.OnFailedErrorCode));
|
||||
}
|
||||
|
||||
public void OnDetail()
|
||||
{
|
||||
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
|
||||
dialogBase.SetTitleLabel(Data.SystemText.Get("Mission_0007"));
|
||||
dialogBase.SetText(strAchievementData);
|
||||
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.CloseBtn);
|
||||
}
|
||||
|
||||
private void OnRequestRewardAchievement(NetworkTask.ResultCode error)
|
||||
{
|
||||
OnRequestReward(error, Data.MissionInfo.data.total_reward_list);
|
||||
_onReceivceAchievementSuccess.Call();
|
||||
}
|
||||
|
||||
private void OnRequestReward(NetworkTask.ResultCode error, List<ReceivedReward> rewards)
|
||||
{
|
||||
base.transform.parent.gameObject.AddMissingComponent<ReceiveReward>().ShowReadDialog(rewards, MailReceive, base.gameObject, _resourceHandler);
|
||||
MyPageMenu.Instance.UpdateMissionCount();
|
||||
}
|
||||
|
||||
private void SetAchievementCommon(UserAchievement achi)
|
||||
{
|
||||
bool num = achi.reward_type == 4;
|
||||
strAchievementData = achi.achievement_name;
|
||||
SystemText systemText = Data.SystemText;
|
||||
labelAchievementTitle.text = systemText.Get("Mission_0023") + strAchievementData;
|
||||
labelAchievementTitle.rightAnchor.target = _StarsWidget.transform;
|
||||
labelAchievementTitle.rightAnchor.relative = 0f;
|
||||
if (num)
|
||||
{
|
||||
ReceiveReward.SetTicket(achi.RewardUserGoodsId, achi.reward_number, achievementIconTexture, labelAchievementData, _resourceHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReceiveReward.SetTexture((UserGoods.Type)achi.reward_type, achievementIconTexture, _resourceHandler);
|
||||
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)achi.reward_type, achi.RewardUserGoodsId, achi.reward_number);
|
||||
}
|
||||
GaugeUI.gameObject.SetActive(value: true);
|
||||
int num2 = ((achi.total_count > achi.require_number) ? achi.require_number : achi.total_count);
|
||||
int require_number = achi.require_number;
|
||||
labelAchievementCount.gameObject.SetActive(value: false);
|
||||
GaugeLabel.text = num2 + "/" + require_number;
|
||||
if (num2 != 0 && require_number != 0)
|
||||
{
|
||||
float value = (float)num2 / (float)require_number;
|
||||
GaugeUI.Value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
GaugeUI.Value = 0f;
|
||||
}
|
||||
goButtonReward.GetComponent<UIButton>().GetComponentInChildren<UILabel>().text = systemText.Get("Mail_0023");
|
||||
}
|
||||
|
||||
private void SetRunning(UserAchievement achi)
|
||||
{
|
||||
SetType(AchievementType.Nonattainment);
|
||||
SetAchievementCommon(achi);
|
||||
SetAchievementStars(achi, cleared: false);
|
||||
}
|
||||
|
||||
private void SetAchievementStars(UserAchievement achi, bool cleared)
|
||||
{
|
||||
int num = achi._maxLevel;
|
||||
int num2 = achi.level;
|
||||
if (achi._maxLevel > 5)
|
||||
{
|
||||
num = 5;
|
||||
num2 = ((achi.level == achi._maxLevel) ? 5 : ((achi.level <= 0 || achi.level % 5 != 0) ? (achi.level % 5) : 5));
|
||||
}
|
||||
for (int i = 0; i < num; i++)
|
||||
{
|
||||
UISprite uISprite = UnityEngine.Object.Instantiate(StarOriginal, StarOriginal.transform.localPosition, StarOriginal.transform.localRotation);
|
||||
uISprite.transform.parent = StarTable.transform;
|
||||
uISprite.transform.localPosition = Vector3.zero;
|
||||
uISprite.transform.localScale = Vector3.one;
|
||||
if ((num2 == i + 1 && cleared) || num2 > i + 1)
|
||||
{
|
||||
uISprite.spriteName = "achievement_star_02";
|
||||
}
|
||||
else
|
||||
{
|
||||
uISprite.spriteName = "achievement_star_01";
|
||||
}
|
||||
uISprite.gameObject.SetActive(value: true);
|
||||
}
|
||||
StarTable.repositionNow = true;
|
||||
}
|
||||
|
||||
private void SetCanReceive(UserAchievement achi)
|
||||
{
|
||||
SetType(AchievementType.Reward);
|
||||
SetAchievementCommon(achi);
|
||||
SetAchievementStars(achi, cleared: false);
|
||||
UIButton component = goButtonReward.GetComponent<UIButton>();
|
||||
component.onClick.Clear();
|
||||
component.onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
|
||||
OnRewardClick();
|
||||
}));
|
||||
}
|
||||
|
||||
private void SetAlreadyReceived(UserAchievement achi)
|
||||
{
|
||||
SetType(AchievementType.AlreadyReceived);
|
||||
SetAchievementCommon(achi);
|
||||
SetAchievementStars(achi, cleared: true);
|
||||
}
|
||||
|
||||
public void SetAchievement(UserAchievement achi, ResourceHandler resourceHandler, Action onReceivceAchievementSuccess)
|
||||
{
|
||||
_resourceHandler = resourceHandler;
|
||||
_onReceivceAchievementSuccess = onReceivceAchievementSuccess;
|
||||
switch (achi.achievement_status)
|
||||
{
|
||||
case 0:
|
||||
SetRunning(achi);
|
||||
break;
|
||||
case 1:
|
||||
SetCanReceive(achi);
|
||||
break;
|
||||
case 2:
|
||||
SetAlreadyReceived(achi);
|
||||
break;
|
||||
default:
|
||||
UnityEngine.Debug.LogError("unkown achievement status");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCrossoverReward(CrossoverRewardInfo reward, AchievementType type, ResourceHandler resourceHandler, Action onReceiveReward)
|
||||
{
|
||||
_crossoverRewardInfo = reward;
|
||||
_resourceHandler = resourceHandler;
|
||||
SetType(type);
|
||||
string texName = UserGoods.GetUserGoodsImageName((UserGoods.Type)reward.RewardType, reward.RewardDetailId);
|
||||
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(texName, ResourcesManager.AssetLoadPathType.Item);
|
||||
_resourceHandler.Add(assetTypePath, delegate
|
||||
{
|
||||
if (reward.RewardDetailId == _crossoverRewardInfo.RewardDetailId)
|
||||
{
|
||||
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(texName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
|
||||
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
|
||||
}
|
||||
});
|
||||
RankInfo rankInfo = Data.Load.data.GetRankInfo(Format.Crossover, reward.Rank);
|
||||
labelAchievementTitle.text = Data.SystemText.Get("Profile_0042", Data.SystemText.Get(rankInfo.rank_name));
|
||||
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)reward.RewardType, reward.RewardDetailId, reward.RewardCount);
|
||||
GaugeUI.gameObject.SetActive(value: false);
|
||||
UIButton component = goButtonReward.GetComponent<UIButton>();
|
||||
component.GetComponentInChildren<UILabel>().text = Data.SystemText.Get("Mail_0023");
|
||||
goButtonReward.gameObject.SetActive(type != AchievementType.PointReceived);
|
||||
UIManager.SetObjectToGrey(goButtonReward, type != AchievementType.PointClear);
|
||||
component.onClick.Clear();
|
||||
component.onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
|
||||
ReceiveCrossoverReward(reward.RewardId, onReceiveReward);
|
||||
}));
|
||||
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
|
||||
}
|
||||
|
||||
private void ReceiveCrossoverReward(int rewardId, Action onReceiveReward)
|
||||
{
|
||||
CrossoverReceiveRankRewardTask task = new CrossoverReceiveRankRewardTask();
|
||||
task.SetParameter(rewardId);
|
||||
StartCoroutine(Toolbox.NetworkManager.Connect(task, delegate
|
||||
{
|
||||
onReceiveReward.Call();
|
||||
DialogCreator.CreateRewardReceiveDialog(task.ReceivedRewardList);
|
||||
}));
|
||||
}
|
||||
|
||||
public void SetQuestPoint(QuestRewardInfo reward, AchievementType type, ResourceHandler resourceHandler, Action onRequestRewardPointCallBack)
|
||||
{
|
||||
_questRewardInfo = reward;
|
||||
strAchievementData = reward.Point.ToString();
|
||||
labelAchievementTitle.text = Data.SystemText.Get("Quest_0019", strAchievementData);
|
||||
_resourceHandler = resourceHandler;
|
||||
SetType(type);
|
||||
string texName = UserGoods.GetUserGoodsImageName((UserGoods.Type)reward.RewardType, reward.RewardDetailId);
|
||||
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(texName, ResourcesManager.AssetLoadPathType.Item);
|
||||
_resourceHandler.Add(assetTypePath, delegate
|
||||
{
|
||||
if (reward.RewardDetailId == _questRewardInfo.RewardDetailId)
|
||||
{
|
||||
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(texName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
|
||||
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
|
||||
}
|
||||
});
|
||||
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)reward.RewardType, reward.RewardDetailId, reward.RewardCount);
|
||||
GaugeUI.gameObject.SetActive(value: false);
|
||||
UIButton component = goButtonReward.GetComponent<UIButton>();
|
||||
component.GetComponentInChildren<UILabel>().text = Data.SystemText.Get("Mail_0023");
|
||||
goButtonReward.gameObject.SetActive(type != AchievementType.PointReceived);
|
||||
UIManager.SetObjectToGrey(goButtonReward, type != AchievementType.PointClear);
|
||||
component.onClick.Clear();
|
||||
component.onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
|
||||
OnQuestPointReceive(reward.Id, onRequestRewardPointCallBack);
|
||||
}));
|
||||
GetComponent<UISprite>().spriteName = string.Empty;
|
||||
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
|
||||
}
|
||||
|
||||
public void SetMission(UserMission mission, ResourceHandler resourceHandler, bool canChangeMissions, bool enableSeparator, bool displayChange, Action onChangeMissionSuccess = null)
|
||||
{
|
||||
_resourceHandler = resourceHandler;
|
||||
_Separator.gameObject.SetActive(enableSeparator);
|
||||
if (mission.mission_status == 0 && SetMissionWait(mission))
|
||||
{
|
||||
return;
|
||||
}
|
||||
SystemText systemText = Data.SystemText;
|
||||
if (mission.reward_type == 4)
|
||||
{
|
||||
ReceiveReward.SetTicket(mission.RewardUserGoodsId, mission.reward_number, achievementIconTexture, labelAchievementData, _resourceHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReceiveReward.SetTexture((UserGoods.Type)mission.reward_type, mission.RewardUserGoodsId, achievementIconTexture, _resourceHandler);
|
||||
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)mission.reward_type, mission.RewardUserGoodsId, mission.reward_number);
|
||||
}
|
||||
labelAchievementTitle.text = mission.mission_name;
|
||||
int require_number = mission.require_number;
|
||||
bool flag = require_number > 0;
|
||||
GaugeUI.gameObject.SetActive(flag);
|
||||
if (flag)
|
||||
{
|
||||
int num = ((mission.total_count > mission.require_number) ? mission.require_number : mission.total_count);
|
||||
GaugeLabel.text = num + "/" + require_number;
|
||||
if (num != 0)
|
||||
{
|
||||
float value = (float)num / (float)require_number;
|
||||
GaugeUI.Value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
GaugeUI.Value = 0f;
|
||||
}
|
||||
}
|
||||
UIButton component = goButtonReward.GetComponent<UIButton>();
|
||||
component.normalSprite = "btn_common_02_s_off";
|
||||
component.pressedSprite = "btn_common_02_s_on";
|
||||
component.GetComponentInChildren<UILabel>().text = systemText.Get("Mission_0029");
|
||||
component.onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
|
||||
ChangeMission(mission.id, mission.mission_name, onChangeMissionSuccess);
|
||||
}));
|
||||
goButtonReward.SetActive(displayChange && !mission.default_flag);
|
||||
UIManager.SetObjectToGrey(goButtonReward, !canChangeMissions);
|
||||
labelAchievementCount.gameObject.SetActive(value: false);
|
||||
SetMissionPeriodLabel(mission);
|
||||
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
|
||||
}
|
||||
|
||||
public void SetBttlePassMonthlyMission(BattlePassMonthlyMission.MissionDetail mission, ResourceHandler resourceHandler)
|
||||
{
|
||||
_Separator.gameObject.SetActive(value: false);
|
||||
_labelMissionPeriod.gameObject.SetActive(value: false);
|
||||
labelAchievementCount.gameObject.SetActive(value: false);
|
||||
goButtonReward.gameObject.SetActive(value: false);
|
||||
_resourceHandler = resourceHandler;
|
||||
labelAchievementTitle.text = mission.Name;
|
||||
alreadyReceived.gameObject.SetActive(mission.IsCleared);
|
||||
BattlePassMonthlyMission.MissionDetail.RewardInfo reward = mission.Reward;
|
||||
if (reward == null)
|
||||
{
|
||||
_resourceHandler.Add(Toolbox.ResourcesManager.GetAssetTypePath("thumbnail_battle_pass_point", ResourcesManager.AssetLoadPathType.BattlePass), delegate
|
||||
{
|
||||
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath("thumbnail_battle_pass_point", ResourcesManager.AssetLoadPathType.BattlePass, isfetch: true);
|
||||
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath);
|
||||
});
|
||||
labelAchievementData.text = string.Empty;
|
||||
}
|
||||
else if (reward.UserGoods.GoodsType == UserGoods.Type.Item)
|
||||
{
|
||||
ReceiveReward.SetTicket(reward.UserGoods.Id, reward.Number, achievementIconTexture, labelAchievementData, _resourceHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReceiveReward.SetTexture(reward.UserGoods.GoodsType, achievementIconTexture, _resourceHandler);
|
||||
labelAchievementData.text = ReceiveReward.getTitle(reward.UserGoods.GoodsType, reward.UserGoods.Id, reward.Number);
|
||||
}
|
||||
SetViewBattlePassPointText(mission.BattlePassPoint);
|
||||
int requireNumber = mission.RequireNumber;
|
||||
bool flag = requireNumber > 0;
|
||||
GaugeUI.gameObject.SetActive(flag);
|
||||
if (flag)
|
||||
{
|
||||
int num = ((mission.DoneNumber > mission.RequireNumber) ? mission.RequireNumber : mission.DoneNumber);
|
||||
GaugeLabel.text = num + "/" + requireNumber;
|
||||
if (num != 0)
|
||||
{
|
||||
float value = (float)num / (float)requireNumber;
|
||||
GaugeUI.Value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
GaugeUI.Value = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetViewBattlePassPointText(int point)
|
||||
{
|
||||
string text = " ";
|
||||
if (labelAchievementData.text == string.Empty)
|
||||
{
|
||||
labelAchievementData.text = Data.SystemText.Get("BattlePass_0010", point.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
UILabel uILabel = labelAchievementData;
|
||||
uILabel.text = uILabel.text + text + Data.SystemText.Get("BattlePass_0010", point.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private bool SetMissionWait(UserMission mission)
|
||||
{
|
||||
MissionInfoTask missionInfoTask = GameMgr.GetIns().GetMissionInfoTask();
|
||||
long num = (long)Time.realtimeSinceStartup - missionInfoTask.RequestTime;
|
||||
long num2 = missionInfoTask.ServerTime + num;
|
||||
TimeSpan timeSpan = TimeSpan.FromSeconds(mission.start_time - num2).Add(new TimeSpan(0, 1, 0));
|
||||
int num3 = timeSpan.Hours;
|
||||
int num4 = timeSpan.Minutes;
|
||||
if (timeSpan.TotalHours >= 24.0)
|
||||
{
|
||||
num3 = 24;
|
||||
num4 = 0;
|
||||
}
|
||||
else if (num3 <= 0 && num4 <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (mission.IsGemMission())
|
||||
{
|
||||
_missionWaitLabel.text = Data.SystemText.Get("Mission_0073", num3.ToString("00"), num4.ToString("00"));
|
||||
_labelMissionNotice.gameObject.SetActive(value: true);
|
||||
_labelMissionNotice.text = Data.SystemText.Get("Mission_0074");
|
||||
}
|
||||
else
|
||||
{
|
||||
_missionWaitLabel.text = Data.SystemText.Get("Mission_0041", num3.ToString("00"), num4.ToString("00"));
|
||||
}
|
||||
labelAchievementTitle.gameObject.SetActive(value: false);
|
||||
labelAchievementCount.gameObject.SetActive(value: false);
|
||||
labelAchievementData.gameObject.SetActive(value: false);
|
||||
labelAchievementData.gameObject.SetActive(value: false);
|
||||
goButtonReward.gameObject.SetActive(value: false);
|
||||
GaugeUI.gameObject.SetActive(value: false);
|
||||
_titleLine.gameObject.SetActive(value: false);
|
||||
_missionWaitLabel.gameObject.SetActive(value: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetMissionPeriodLabel(UserMission mission)
|
||||
{
|
||||
if (mission.end_time <= 0 || mission.IsGemMission())
|
||||
{
|
||||
_labelMissionPeriod.gameObject.SetActive(value: false);
|
||||
return;
|
||||
}
|
||||
long nowUnixTime = GameMgr.GetIns().GetMissionInfoTask().NowUnixTime();
|
||||
string remainingTime = ConvertTime.GetRemainingTime(TimeSpan.FromSeconds(mission.GetMissionPeriodSec(nowUnixTime)));
|
||||
goButtonReward.gameObject.SetActive(value: false);
|
||||
_labelMissionPeriod.gameObject.SetActive(value: true);
|
||||
_labelMissionPeriod.text = remainingTime;
|
||||
}
|
||||
|
||||
private void ChangeMission(int id, string content, Action onChangeMissionSuccess)
|
||||
{
|
||||
SystemText systemText = Data.SystemText;
|
||||
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
|
||||
dialogBase.SetTitleLabel(systemText.Get("Mission_0033"));
|
||||
dialogBase.SetText(systemText.Get("Mission_0030", content));
|
||||
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.DecisionBtn);
|
||||
dialogBase.onPushButton1 = delegate
|
||||
{
|
||||
MissionRetireTask missionRetireTask = new MissionRetireTask();
|
||||
missionRetireTask.SetParameter(id);
|
||||
StartCoroutine(Toolbox.NetworkManager.Connect(missionRetireTask, delegate
|
||||
{
|
||||
onChangeMissionSuccess.Call();
|
||||
}, BaseTask.OnRequestFailed, BaseTask.OnFailedErrorCode));
|
||||
};
|
||||
}
|
||||
|
||||
private void OnQuestPointReceive(int rewardId, Action onRequestRewardPointCallBack)
|
||||
{
|
||||
QuestRewardReceiveTask task = new QuestRewardReceiveTask();
|
||||
task.SetParameter(rewardId);
|
||||
StartCoroutine(Toolbox.NetworkManager.Connect(task, delegate
|
||||
{
|
||||
onRequestRewardPointCallBack.Call();
|
||||
DialogCreator.CreateRewardReceiveDialog(task.ReceiveRewardList);
|
||||
}, BaseTask.OnRequestFailed, BaseTask.OnFailedErrorCode));
|
||||
}
|
||||
|
||||
public void SetHistoryItem(ItemAcquireHistory item, bool enableSeparator, ResourceHandler resourceHandler)
|
||||
{
|
||||
_resourceHandler = resourceHandler;
|
||||
SystemText systemText = Data.SystemText;
|
||||
if (item.RewardType == 4)
|
||||
{
|
||||
ReceiveReward.SetTicket(item.RewardUserGoodsId, item.RewardCount, achievementIconTexture, labelAchievementTitle, _resourceHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReceiveReward.SetTexture((UserGoods.Type)item.RewardType, achievementIconTexture, _resourceHandler);
|
||||
labelAchievementTitle.text = ReceiveReward.getTitle((UserGoods.Type)item.RewardType, item.RewardUserGoodsId, item.RewardCount);
|
||||
}
|
||||
labelAchievementData.text = item.Message;
|
||||
labelAchievementCount.text = systemText.Get("Mail_0043", ConvertTime.ToLocal(item.AcquireTime));
|
||||
GaugeUI.gameObject.SetActive(value: false);
|
||||
goButtonReward.SetActive(value: false);
|
||||
_Separator.gameObject.SetActive(enableSeparator);
|
||||
}
|
||||
|
||||
public void SetMail(MailData mail, Action<int, int> OnReadMail, ResourceHandler handler)
|
||||
{
|
||||
_resourceHandler = handler;
|
||||
_viewMailId = mail.mail_id;
|
||||
SetCommonMail(mail);
|
||||
TimeLeftUpdate timeLeftUpdate = base.gameObject.AddMissingComponent<TimeLeftUpdate>();
|
||||
timeLeftUpdate.mailData = mail;
|
||||
_labelTopRight.gameObject.SetActive(value: true);
|
||||
timeLeftUpdate.timeLeft = _labelTopRight;
|
||||
timeLeftUpdate.UpdateTime();
|
||||
SystemText systemText = Data.SystemText;
|
||||
labelAchievementCount.text = systemText.Get("Mail_0043", mail.create_time);
|
||||
goButtonReward.SetActive(value: true);
|
||||
goButtonReward.transform.Find("RewardLabel").GetComponent<UILabel>().text = systemText.Get("Mail_0023");
|
||||
UIButton component = goButtonReward.GetComponent<UIButton>();
|
||||
component.normalSprite = "btn_common_02_s_off";
|
||||
component.hoverSprite = "btn_common_02_s_off";
|
||||
component.pressedSprite = "btn_common_02_s_on";
|
||||
UIEventListener.Get(goButtonReward).onClick = delegate
|
||||
{
|
||||
OnReadMail(mail.mail_id, mail.mail_id);
|
||||
};
|
||||
}
|
||||
|
||||
public void SetHistoryMail(MailData mail, ResourceHandler handler)
|
||||
{
|
||||
_resourceHandler = handler;
|
||||
_viewMailId = mail.mail_id;
|
||||
SetCommonMail(mail);
|
||||
TimeLeftUpdate component = base.gameObject.GetComponent<TimeLeftUpdate>();
|
||||
if ((bool)component)
|
||||
{
|
||||
component.mailData = null;
|
||||
}
|
||||
_labelTopRight.gameObject.SetActive(value: false);
|
||||
labelAchievementCount.text = Data.SystemText.Get("Mail_0044", mail.create_time);
|
||||
goButtonReward.SetActive(value: false);
|
||||
}
|
||||
|
||||
private void SetCommonMail(MailData mailData)
|
||||
{
|
||||
GaugeUI.gameObject.SetActive(value: false);
|
||||
labelAchievementData.text = mailData.message;
|
||||
labelAchievementTitle.text = ReceiveReward.getTitle(mailData);
|
||||
string textureName = ReceiveReward.GetThumbnailName((UserGoods.Type)mailData.reward_type, mailData.RewardUserGoodsId);
|
||||
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item);
|
||||
_resourceHandler.Add(assetTypePath, delegate
|
||||
{
|
||||
if (mailData.mail_id == _viewMailId)
|
||||
{
|
||||
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
|
||||
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void CopyAnchor(UIRect.AnchorPoint original, UIRect.AnchorPoint destination)
|
||||
{
|
||||
destination.target = original.target;
|
||||
destination.relative = original.relative;
|
||||
destination.absolute = original.absolute;
|
||||
}
|
||||
|
||||
public void SetGetButtonToGreyOut()
|
||||
{
|
||||
UIManager.SetObjectToGrey(goButtonReward, b: true);
|
||||
}
|
||||
|
||||
public void SetLottery(LotteryMissionData lotteryData, bool needCeparator)
|
||||
{
|
||||
string userGoodsImageName = UserGoods.GetUserGoodsImageName(lotteryData.UserGoodsType, lotteryData.ItemId);
|
||||
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(userGoodsImageName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
|
||||
base.gameObject.GetComponent<UISprite>().width = 800;
|
||||
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath);
|
||||
_Separator.gameObject.SetActive(needCeparator);
|
||||
labelAchievementTitle.text = lotteryData.MissionTitle;
|
||||
if (lotteryData.UserGoodsType == UserGoods.Type.Item)
|
||||
{
|
||||
labelAchievementData.text = ReceiveReward.SetTicketTitle(lotteryData.ItemId, lotteryData.ItemCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
labelAchievementData.text = ReceiveReward.getTitle(lotteryData.UserGoodsType, lotteryData.ItemId, lotteryData.ItemCount);
|
||||
}
|
||||
goButtonReward.SetActive(value: false);
|
||||
if (lotteryData.StartTime.Second > 0)
|
||||
{
|
||||
_missionStartTime.text = Data.SystemText.Get("Mission_0077", lotteryData.StartTime.LocalTime);
|
||||
_missionStartTime.gameObject.SetActive(value: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_labelMissionPeriod.text = lotteryData.EndTime.GetShowText("Mission_0062", "Mission_0060", "Mission_0061");
|
||||
_labelMissionPeriod.gameObject.SetActive(value: true);
|
||||
}
|
||||
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
|
||||
_applyFinish.gameObject.SetActive(lotteryData.IsCleared);
|
||||
_labelMissionPeriod.gameObject.SetActive(!lotteryData.IsCleared && !lotteryData.IsTimeOver);
|
||||
_missionTimeOver.gameObject.SetActive(lotteryData.IsTimeOver);
|
||||
GaugeLabel.text = lotteryData.MissionCurrent + "/" + lotteryData.MissionMax;
|
||||
GaugeUI.Value = lotteryData.MissionRatio;
|
||||
bool active = true;
|
||||
if (lotteryData.IsCleared || lotteryData.MissionMax == 0)
|
||||
{
|
||||
active = false;
|
||||
}
|
||||
GaugeUI.gameObject.SetActive(active);
|
||||
}
|
||||
|
||||
public void SetBingoMission(BingoInfoTask.BingoMissionData missionData, bool needCeparator, ResourceHandler handler)
|
||||
{
|
||||
_resourceHandler = handler;
|
||||
base.gameObject.GetComponent<UISprite>().width = 752;
|
||||
base.gameObject.GetComponent<UISprite>().enabled = false;
|
||||
alreadyReceived.gameObject.SetActive(missionData.IsCleared);
|
||||
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)missionData.Reward.reward_type, missionData.Reward.rewardUserGoodsId, missionData.Reward.reward_count);
|
||||
string textureName = UserGoods.GetUserGoodsImageName((UserGoods.Type)missionData.Reward.reward_type, missionData.Reward.rewardUserGoodsId);
|
||||
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item);
|
||||
_resourceHandler.Add(assetTypePath, delegate
|
||||
{
|
||||
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
|
||||
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
|
||||
});
|
||||
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
|
||||
_titleLine.SetAnchor((GameObject)null);
|
||||
_titleLine.spriteName = "quest_line_05";
|
||||
_titleLine.SetDimensions(610, 2);
|
||||
_Separator.spriteName = "quest_line_02";
|
||||
_Separator.gameObject.SetActive(needCeparator);
|
||||
goButtonReward.SetActive(value: false);
|
||||
GaugeLabel.text = missionData.MissionCurrent + "/" + missionData.MissionMax;
|
||||
GaugeUI.Value = missionData.MissionRatio;
|
||||
labelAchievementTitle.text = missionData.MissionTitle;
|
||||
}
|
||||
|
||||
public void SetBingoRewardDetails(ReceivedReward reward, bool needCeparator, ResourceHandler handler)
|
||||
{
|
||||
_resourceHandler = handler;
|
||||
base.gameObject.GetComponent<UISprite>().width = 752;
|
||||
base.gameObject.GetComponent<UISprite>().enabled = false;
|
||||
goButtonReward.SetActive(value: false);
|
||||
_titleLine.SetAnchor((GameObject)null);
|
||||
_titleLine.spriteName = "quest_line_05";
|
||||
_titleLine.SetDimensions(610, 2);
|
||||
_Separator.spriteName = "quest_line_02";
|
||||
_Separator.gameObject.SetActive(needCeparator);
|
||||
labelAchievementTitle.text = string.Format(Data.SystemText.Get("Bingo_0004", reward.lineNum.ToString()));
|
||||
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)reward.reward_type, reward.rewardUserGoodsId, reward.reward_count);
|
||||
GaugeUI.gameObject.SetActive(value: false);
|
||||
string textureName = UserGoods.GetUserGoodsImageName((UserGoods.Type)reward.reward_type, reward.rewardUserGoodsId);
|
||||
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item);
|
||||
_resourceHandler.Add(assetTypePath, delegate
|
||||
{
|
||||
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
|
||||
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
|
||||
});
|
||||
}
|
||||
|
||||
public void SetBingoSideBarRewards(string lineNum, ReceivedReward reward, bool isCleared, bool needCeparator, ResourceHandler handler)
|
||||
{
|
||||
_resourceHandler = handler;
|
||||
_Separator.gameObject.SetActive(needCeparator);
|
||||
goButtonReward.SetActive(value: false);
|
||||
labelAchievementTitle.text = string.Format(Data.SystemText.Get("Bingo_0004", lineNum));
|
||||
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)reward.reward_type, reward.rewardUserGoodsId, reward.reward_count);
|
||||
alreadyReceived.gameObject.SetActive(isCleared);
|
||||
string textureName = UserGoods.GetUserGoodsImageName((UserGoods.Type)reward.reward_type, reward.rewardUserGoodsId);
|
||||
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item);
|
||||
_resourceHandler.Add(assetTypePath, delegate
|
||||
{
|
||||
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
|
||||
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
|
||||
});
|
||||
}
|
||||
|
||||
public void SetPracticePuzzleMission(PracticePuzzleMissionData mission, ResourceHandler resourceHandler, bool canChangeMissions, bool enableSeparator, bool displayChange, Action onChangeMissionSuccess = null)
|
||||
{
|
||||
_resourceHandler = resourceHandler;
|
||||
_Separator.gameObject.SetActive(enableSeparator);
|
||||
goButtonReward.SetActive(value: false);
|
||||
_ = Data.SystemText;
|
||||
if (mission.UserGoodsType == UserGoods.Type.Item)
|
||||
{
|
||||
ReceiveReward.SetTicket(mission.ItemId, mission.ItemCount, achievementIconTexture, labelAchievementData, _resourceHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReceiveReward.SetTexture(mission.UserGoodsType, mission.ItemId, achievementIconTexture, _resourceHandler);
|
||||
labelAchievementData.text = ReceiveReward.getTitle(mission.UserGoodsType, mission.ItemId, mission.ItemCount);
|
||||
}
|
||||
labelAchievementTitle.text = mission.Name;
|
||||
int totalMissionCount = mission.TotalMissionCount;
|
||||
bool flag = totalMissionCount > 0;
|
||||
GaugeUI.gameObject.SetActive(flag);
|
||||
if (flag)
|
||||
{
|
||||
int num = ((mission.TotalMissionCount > mission.CurrentClearCount) ? mission.CurrentClearCount : mission.TotalMissionCount);
|
||||
GaugeLabel.text = num + "/" + totalMissionCount;
|
||||
if (num != 0)
|
||||
{
|
||||
float value = (float)num / (float)totalMissionCount;
|
||||
GaugeUI.Value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
GaugeUI.Value = 0f;
|
||||
}
|
||||
}
|
||||
alreadyReceived.gameObject.SetActive(mission.IsCleared);
|
||||
labelAchievementCount.gameObject.SetActive(value: false);
|
||||
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
|
||||
}
|
||||
|
||||
public void SetRedEtherMission(RedEtherCampaignRewardData rewardData, ResourceHandler resourceHandler)
|
||||
{
|
||||
_resourceHandler = resourceHandler;
|
||||
goButtonReward.SetActive(value: false);
|
||||
ReceiveReward.SetTexture(rewardData.UserGoodsType, 0L, achievementIconTexture, _resourceHandler);
|
||||
labelAchievementData.text = ReceiveReward.getTitle(rewardData.UserGoodsType, 0L, rewardData.ItemCount);
|
||||
labelAchievementTitle.text = rewardData.MissionText;
|
||||
alreadyReceived.gameObject.SetActive(rewardData.IsCleared);
|
||||
GaugeUI.gameObject.SetActive(value: false);
|
||||
}
|
||||
}
|
||||
356
SVSim.BattleEngine/Engine/ActiveAnimation.cs
Normal file
356
SVSim.BattleEngine/Engine/ActiveAnimation.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using System.Collections.Generic;
|
||||
using AnimationOrTween;
|
||||
using UnityEngine;
|
||||
|
||||
[AddComponentMenu("NGUI/Internal/Active Animation")]
|
||||
public class ActiveAnimation : MonoBehaviour
|
||||
{
|
||||
public static ActiveAnimation current;
|
||||
|
||||
public List<EventDelegate> onFinished = new List<EventDelegate>();
|
||||
|
||||
[HideInInspector]
|
||||
public GameObject eventReceiver;
|
||||
|
||||
[HideInInspector]
|
||||
public string callWhenFinished;
|
||||
|
||||
private Animation mAnim;
|
||||
|
||||
private Direction mLastDirection;
|
||||
|
||||
private Direction mDisableDirection;
|
||||
|
||||
private bool mNotify;
|
||||
|
||||
private Animator mAnimator;
|
||||
|
||||
private string mClip = "";
|
||||
|
||||
private float playbackTime => Mathf.Clamp01(mAnimator.GetCurrentAnimatorStateInfo(0).normalizedTime);
|
||||
|
||||
public bool isPlaying
|
||||
{
|
||||
get
|
||||
{
|
||||
if (mAnim == null)
|
||||
{
|
||||
if (mAnimator != null)
|
||||
{
|
||||
if (mLastDirection == Direction.Reverse)
|
||||
{
|
||||
if (playbackTime == 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (playbackTime == 1f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
foreach (AnimationState item in mAnim)
|
||||
{
|
||||
if (!mAnim.IsPlaying(item.name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (mLastDirection == Direction.Forward)
|
||||
{
|
||||
if (item.time < item.length)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (mLastDirection == Direction.Reverse)
|
||||
{
|
||||
if (item.time > 0f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Finish()
|
||||
{
|
||||
if (mAnim != null)
|
||||
{
|
||||
foreach (AnimationState item in mAnim)
|
||||
{
|
||||
if (mLastDirection == Direction.Forward)
|
||||
{
|
||||
item.time = item.length;
|
||||
}
|
||||
else if (mLastDirection == Direction.Reverse)
|
||||
{
|
||||
item.time = 0f;
|
||||
}
|
||||
}
|
||||
mAnim.Sample();
|
||||
}
|
||||
else if (mAnimator != null)
|
||||
{
|
||||
mAnimator.Play(mClip, 0, (mLastDirection == Direction.Forward) ? 1f : 0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (mAnim != null)
|
||||
{
|
||||
foreach (AnimationState item in mAnim)
|
||||
{
|
||||
if (mLastDirection == Direction.Reverse)
|
||||
{
|
||||
item.time = item.length;
|
||||
}
|
||||
else if (mLastDirection == Direction.Forward)
|
||||
{
|
||||
item.time = 0f;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mAnimator != null)
|
||||
{
|
||||
mAnimator.Play(mClip, 0, (mLastDirection == Direction.Reverse) ? 1f : 0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (eventReceiver != null && EventDelegate.IsValid(onFinished))
|
||||
{
|
||||
eventReceiver = null;
|
||||
callWhenFinished = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
float deltaTime = RealTime.deltaTime;
|
||||
if (deltaTime == 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (mAnimator != null)
|
||||
{
|
||||
mAnimator.Update((mLastDirection == Direction.Reverse) ? (0f - deltaTime) : deltaTime);
|
||||
if (isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
mAnimator.enabled = false;
|
||||
base.enabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!(mAnim != null))
|
||||
{
|
||||
base.enabled = false;
|
||||
return;
|
||||
}
|
||||
bool flag = false;
|
||||
foreach (AnimationState item in mAnim)
|
||||
{
|
||||
if (!mAnim.IsPlaying(item.name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float num = item.speed * deltaTime;
|
||||
item.time += num;
|
||||
if (num < 0f)
|
||||
{
|
||||
if (item.time > 0f)
|
||||
{
|
||||
flag = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.time = 0f;
|
||||
}
|
||||
}
|
||||
else if (item.time < item.length)
|
||||
{
|
||||
flag = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.time = item.length;
|
||||
}
|
||||
}
|
||||
mAnim.Sample();
|
||||
if (flag)
|
||||
{
|
||||
return;
|
||||
}
|
||||
base.enabled = false;
|
||||
}
|
||||
if (!mNotify)
|
||||
{
|
||||
return;
|
||||
}
|
||||
mNotify = false;
|
||||
if (current == null)
|
||||
{
|
||||
current = this;
|
||||
EventDelegate.Execute(onFinished);
|
||||
if (eventReceiver != null && !string.IsNullOrEmpty(callWhenFinished))
|
||||
{
|
||||
eventReceiver.SendMessage(callWhenFinished, SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
current = null;
|
||||
}
|
||||
if (mDisableDirection != Direction.Toggle && mLastDirection == mDisableDirection)
|
||||
{
|
||||
NGUITools.SetActive(base.gameObject, state: false);
|
||||
}
|
||||
}
|
||||
|
||||
private void Play(string clipName, Direction playDirection)
|
||||
{
|
||||
if (playDirection == Direction.Toggle)
|
||||
{
|
||||
playDirection = ((mLastDirection != Direction.Forward) ? Direction.Forward : Direction.Reverse);
|
||||
}
|
||||
if (mAnim != null)
|
||||
{
|
||||
base.enabled = true;
|
||||
mAnim.enabled = false;
|
||||
if (string.IsNullOrEmpty(clipName))
|
||||
{
|
||||
if (!mAnim.isPlaying)
|
||||
{
|
||||
mAnim.Play();
|
||||
}
|
||||
}
|
||||
else if (!mAnim.IsPlaying(clipName))
|
||||
{
|
||||
mAnim.Play(clipName);
|
||||
}
|
||||
foreach (AnimationState item in mAnim)
|
||||
{
|
||||
if (string.IsNullOrEmpty(clipName) || item.name == clipName)
|
||||
{
|
||||
float num = Mathf.Abs(item.speed);
|
||||
item.speed = num * (float)playDirection;
|
||||
if (playDirection == Direction.Reverse && item.time == 0f)
|
||||
{
|
||||
item.time = item.length;
|
||||
}
|
||||
else if (playDirection == Direction.Forward && item.time == item.length)
|
||||
{
|
||||
item.time = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
mLastDirection = playDirection;
|
||||
mNotify = true;
|
||||
mAnim.Sample();
|
||||
}
|
||||
else if (mAnimator != null)
|
||||
{
|
||||
if (base.enabled && isPlaying && mClip == clipName)
|
||||
{
|
||||
mLastDirection = playDirection;
|
||||
return;
|
||||
}
|
||||
base.enabled = true;
|
||||
mNotify = true;
|
||||
mLastDirection = playDirection;
|
||||
mClip = clipName;
|
||||
mAnimator.Play(mClip, 0, (playDirection == Direction.Forward) ? 0f : 1f);
|
||||
}
|
||||
}
|
||||
|
||||
public static ActiveAnimation Play(Animation anim, string clipName, Direction playDirection, EnableCondition enableBeforePlay, DisableCondition disableCondition)
|
||||
{
|
||||
if (!NGUITools.GetActive(anim.gameObject))
|
||||
{
|
||||
if (enableBeforePlay != EnableCondition.EnableThenPlay)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
NGUITools.SetActive(anim.gameObject, state: true);
|
||||
UIPanel[] componentsInChildren = anim.gameObject.GetComponentsInChildren<UIPanel>();
|
||||
int i = 0;
|
||||
for (int num = componentsInChildren.Length; i < num; i++)
|
||||
{
|
||||
componentsInChildren[i].Refresh();
|
||||
}
|
||||
}
|
||||
ActiveAnimation activeAnimation = anim.GetComponent<ActiveAnimation>();
|
||||
if (activeAnimation == null)
|
||||
{
|
||||
activeAnimation = anim.gameObject.AddComponent<ActiveAnimation>();
|
||||
}
|
||||
activeAnimation.mAnim = anim;
|
||||
activeAnimation.mDisableDirection = (Direction)disableCondition;
|
||||
activeAnimation.onFinished.Clear();
|
||||
activeAnimation.Play(clipName, playDirection);
|
||||
if (activeAnimation.mAnim != null)
|
||||
{
|
||||
activeAnimation.mAnim.Sample();
|
||||
}
|
||||
else if (activeAnimation.mAnimator != null)
|
||||
{
|
||||
activeAnimation.mAnimator.Update(0f);
|
||||
}
|
||||
return activeAnimation;
|
||||
}
|
||||
|
||||
public static ActiveAnimation Play(Animation anim, string clipName, Direction playDirection)
|
||||
{
|
||||
return Play(anim, clipName, playDirection, EnableCondition.DoNothing, DisableCondition.DoNotDisable);
|
||||
}
|
||||
|
||||
public static ActiveAnimation Play(Animation anim, Direction playDirection)
|
||||
{
|
||||
return Play(anim, null, playDirection, EnableCondition.DoNothing, DisableCondition.DoNotDisable);
|
||||
}
|
||||
|
||||
public static ActiveAnimation Play(Animator anim, string clipName, Direction playDirection, EnableCondition enableBeforePlay, DisableCondition disableCondition)
|
||||
{
|
||||
if (enableBeforePlay != EnableCondition.IgnoreDisabledState && !NGUITools.GetActive(anim.gameObject))
|
||||
{
|
||||
if (enableBeforePlay != EnableCondition.EnableThenPlay)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
NGUITools.SetActive(anim.gameObject, state: true);
|
||||
UIPanel[] componentsInChildren = anim.gameObject.GetComponentsInChildren<UIPanel>();
|
||||
int i = 0;
|
||||
for (int num = componentsInChildren.Length; i < num; i++)
|
||||
{
|
||||
componentsInChildren[i].Refresh();
|
||||
}
|
||||
}
|
||||
ActiveAnimation activeAnimation = anim.GetComponent<ActiveAnimation>();
|
||||
if (activeAnimation == null)
|
||||
{
|
||||
activeAnimation = anim.gameObject.AddComponent<ActiveAnimation>();
|
||||
}
|
||||
activeAnimation.mAnimator = anim;
|
||||
activeAnimation.mDisableDirection = (Direction)disableCondition;
|
||||
activeAnimation.onFinished.Clear();
|
||||
activeAnimation.Play(clipName, playDirection);
|
||||
if (activeAnimation.mAnim != null)
|
||||
{
|
||||
activeAnimation.mAnim.Sample();
|
||||
}
|
||||
else if (activeAnimation.mAnimator != null)
|
||||
{
|
||||
activeAnimation.mAnimator.Update(0f);
|
||||
}
|
||||
return activeAnimation;
|
||||
}
|
||||
}
|
||||
22
SVSim.BattleEngine/Engine/AddDamageInfo.cs
Normal file
22
SVSim.BattleEngine/Engine/AddDamageInfo.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class AddDamageInfo : DamageModifier
|
||||
{
|
||||
public int AddDamage { get; protected set; }
|
||||
|
||||
public AddDamageInfo(int addDamage, string damageType, CardBasePrm.ClanType damageClan, bool isUseClass, int order)
|
||||
{
|
||||
AddDamage = addDamage;
|
||||
base.DamageType = new List<string>();
|
||||
base.DamageType.AddRange(damageType.Split(new string[1] { "_and_" }, StringSplitOptions.None));
|
||||
base.DamageClan = new List<CardBasePrm.ClanType> { damageClan };
|
||||
base.IsUseClass = isUseClass;
|
||||
base.OrderCount = order;
|
||||
}
|
||||
|
||||
public override int Calc(int damage)
|
||||
{
|
||||
return damage + AddDamage;
|
||||
}
|
||||
}
|
||||
20
SVSim.BattleEngine/Engine/AddHealModifierInfo.cs
Normal file
20
SVSim.BattleEngine/Engine/AddHealModifierInfo.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
public class AddHealModifierInfo : HealModifier
|
||||
{
|
||||
public int AddHealAmount { get; private set; }
|
||||
|
||||
public AddHealModifierInfo(int addHealAmount, int order, BattleCardBase owner)
|
||||
{
|
||||
AddHealAmount = addHealAmount;
|
||||
base.OrderCount = order;
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public override int Calc(int healAmount, BattleCardBase healOwner, BattleCardBase target)
|
||||
{
|
||||
if (healOwner.IsPlayer != _owner.IsPlayer)
|
||||
{
|
||||
return healAmount;
|
||||
}
|
||||
return healAmount + AddHealAmount;
|
||||
}
|
||||
}
|
||||
106
SVSim.BattleEngine/Engine/AddTargetInfo.cs
Normal file
106
SVSim.BattleEngine/Engine/AddTargetInfo.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Wizard;
|
||||
|
||||
public class AddTargetInfo
|
||||
{
|
||||
private BattleCardBase _ownerCard;
|
||||
|
||||
private ConditionSkillFilterCollection _conditionFilter;
|
||||
|
||||
private ApplySkillTargetFilterCollection _targetFilter;
|
||||
|
||||
private Func<SkillBase, bool> typeCheck;
|
||||
|
||||
private string _conditionFilterText;
|
||||
|
||||
private string _targetFilterText;
|
||||
|
||||
private string _skillTypeText;
|
||||
|
||||
private string _ownerCardtype;
|
||||
|
||||
private SkillCreator _skillCreator;
|
||||
|
||||
public AddTargetInfo(BattleCardBase ownerCard, string conditionFilterText, string targetFilterText, string skillTypeText, string ownerCardType, SkillBase skill)
|
||||
{
|
||||
_ownerCard = ownerCard;
|
||||
_conditionFilterText = conditionFilterText;
|
||||
_targetFilterText = targetFilterText;
|
||||
_skillTypeText = skillTypeText;
|
||||
_ownerCardtype = ownerCardType;
|
||||
_conditionFilter = new ConditionSkillFilterCollection();
|
||||
_targetFilter = new ApplySkillTargetFilterCollection();
|
||||
typeCheck = SetTypeCheck(_skillTypeText, _ownerCardtype);
|
||||
_skillCreator = _ownerCard.CreateSkillCreator(_ownerCard.SelfBattlePlayer, _ownerCard.OpponentBattlePlayer, _ownerCard.ResourceMgr);
|
||||
string[] array = _conditionFilterText.Split('&');
|
||||
List<SkillFilterCreator.ContentInfo> list = new List<SkillFilterCreator.ContentInfo>();
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
{
|
||||
SkillFilterCreator.ParseContentInfo(array[i], out var retParsedInfo);
|
||||
list.Add(retParsedInfo);
|
||||
}
|
||||
SkillCreator.SetupSkillConditionOld(_conditionFilter, list, _ownerCard, skill);
|
||||
string[] array2 = _targetFilterText.Split('&');
|
||||
List<SkillFilterCreator.ContentInfo> list2 = new List<SkillFilterCreator.ContentInfo>();
|
||||
for (int j = 0; j < array2.Length; j++)
|
||||
{
|
||||
SkillFilterCreator.ParseContentInfo(array2[j], out var retParsedInfo2);
|
||||
list2.Add(retParsedInfo2);
|
||||
}
|
||||
_skillCreator.SetupSkillTargetOld(_targetFilter, _ownerCard, list2, skill);
|
||||
}
|
||||
|
||||
public List<BattleCardBase> GetAddTargetCard(SkillBase skill, BattlePlayerReadOnlyInfoPair pair, SkillConditionCheckerOption checkerOption, SkillOptionValue optionValue)
|
||||
{
|
||||
if (typeCheck(skill) && FilterComparison(skill.ApplyFilterCollection))
|
||||
{
|
||||
return _targetFilter.Filtering(pair, checkerOption, optionValue).Cast<BattleCardBase>().ToList();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Func<SkillBase, bool> SetTypeCheck(string skillType, string ownerCardType)
|
||||
{
|
||||
if (skillType != null && skillType == "damage")
|
||||
{
|
||||
return (SkillBase skill) => CardTypeCheck(skill.SkillPrm.ownerCard, ownerCardType) && skill is Skill_damage;
|
||||
}
|
||||
return (SkillBase skill) => CardTypeCheck(skill.SkillPrm.ownerCard, ownerCardType) && skill is Skill_none;
|
||||
}
|
||||
|
||||
private bool CardTypeCheck(BattleCardBase card, string ownerCardType)
|
||||
{
|
||||
return ownerCardType switch
|
||||
{
|
||||
"all" => true,
|
||||
"unit" => card.IsUnit,
|
||||
"spell" => card.IsSpell,
|
||||
"field" => card.IsField,
|
||||
"chant_field" => card.IsChantField,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private bool FilterComparison(ApplySkillTargetFilterCollection ownerSkillFilter)
|
||||
{
|
||||
if (_conditionFilter.BattlePlayerFilter.GetType() == ownerSkillFilter.BattlePlayerFilter.GetType() && _conditionFilter.TargetFilter.GetType() == ownerSkillFilter.TargetFilter.GetType())
|
||||
{
|
||||
foreach (ISkillCardFilter cardType in _conditionFilter.CardFilterList)
|
||||
{
|
||||
if (!ownerSkillFilter.CardFilterList.Any((ISkillCardFilter s) => s.GetType() == cardType.GetType()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public AddTargetInfo Clone(BattleCardBase ownerCard)
|
||||
{
|
||||
return new AddTargetInfo(ownerCard, _conditionFilterText, _targetFilterText, _skillTypeText, _ownerCardtype, null);
|
||||
}
|
||||
}
|
||||
77
SVSim.BattleEngine/Engine/AlleyField.cs
Normal file
77
SVSim.BattleEngine/Engine/AlleyField.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class AlleyField : BackGroundBase
|
||||
{
|
||||
public override int FieldId => 22;
|
||||
|
||||
public override int FieldEffectId => 22;
|
||||
|
||||
public AlleyField(string bgmId = "NONE")
|
||||
: base(bgmId)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void BattleFieldBuild()
|
||||
{
|
||||
BattleCoroutine.GetInstance().StartCoroutine(BackGroundBase.ObjectChecker(0.5f, _str3DFieldPath, delegate
|
||||
{
|
||||
base.Field = GameObject.Find(_str3DFieldPath);
|
||||
base.Field.transform.parent = GameMgr.GetIns().m_GameManagerObj.transform;
|
||||
GimicAudioList = base.Field.GetComponent<AudioList>().GimicAudioList;
|
||||
_fieldModel = base.Field.transform.Find("md_bf_aley_root").gameObject;
|
||||
_fieldParticles = _fieldModel.transform.Find("Particles22").gameObject;
|
||||
List<string> list = new List<string>(_fieldObjDictionary.Keys);
|
||||
List<GameObject> list2 = new List<GameObject>();
|
||||
for (int i = 0; i < _fieldObjDictionary.Count; i++)
|
||||
{
|
||||
list2.Add(_fieldObjDictionary[list[i]]);
|
||||
}
|
||||
GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(list2, delegate
|
||||
{
|
||||
base.SetShaderGlobalColorBG = base.Field.transform.Find("SetMaterialColorBGManager").GetComponent<SetShaderGlobalColorBG>();
|
||||
base.IsLoadDone = true;
|
||||
}, isBattle: true, isField: true);
|
||||
}));
|
||||
}
|
||||
|
||||
public override void StartFieldSetEffect(Vector3 pos)
|
||||
{
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_FIELD_SET_22, pos);
|
||||
}
|
||||
|
||||
public override void StartFieldTapEffect(int areaId, Vector3 pos)
|
||||
{
|
||||
base.StartFieldTapEffect(areaId, pos);
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_FIELD_TAP_22_1, pos);
|
||||
}
|
||||
|
||||
protected override IEnumerator RunFieldOpening()
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySeByStr($"se_field_{_str3DFieldNo}_appear_1", "se_field_" + _str3DFieldNo, 0f, 0L);
|
||||
_battleCamera.Camera.transform.localPosition = new Vector3(2750f, -510f, -10f);
|
||||
_battleCamera.Camera.transform.localRotation = Quaternion.Euler(new Vector3(-10f, -53f, 84f));
|
||||
iTween.MoveTo(_battleCamera.Camera.gameObject, iTween.Hash("position", new Vector3(300f, -30f, -150f), "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutQuad));
|
||||
iTween.RotateTo(_battleCamera.Camera.gameObject, iTween.Hash("rotation", new Vector3(-11f, -100f, 92f), "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutQuad));
|
||||
yield return new WaitForSeconds(2f);
|
||||
iTween.MoveTo(_battleCamera.Camera.gameObject, iTween.Hash("position", _battleCamera.BattleCameraPos, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutExpo));
|
||||
iTween.RotateTo(_battleCamera.Camera.gameObject, iTween.Hash("rotation", _battleCamera.BattleCameraRot, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutExpo));
|
||||
yield return new WaitForSeconds(0f);
|
||||
}
|
||||
|
||||
protected override IEnumerator RunFieldGimic(GameObject obj)
|
||||
{
|
||||
string tag = obj.tag;
|
||||
if (tag != null && tag == "FieldGimic1")
|
||||
{
|
||||
_ = _gimicCntDictionary[obj.tag];
|
||||
}
|
||||
yield return new WaitForSeconds(0f);
|
||||
}
|
||||
|
||||
protected override IEnumerator RunFieldShake()
|
||||
{
|
||||
yield return new WaitForSeconds(0f);
|
||||
}
|
||||
}
|
||||
8
SVSim.BattleEngine/Engine/AnimationOrTween/Direction.cs
Normal file
8
SVSim.BattleEngine/Engine/AnimationOrTween/Direction.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace AnimationOrTween;
|
||||
|
||||
public enum Direction
|
||||
{
|
||||
Reverse = -1,
|
||||
Toggle,
|
||||
Forward
|
||||
}
|
||||
2651
SVSim.BattleEngine/Engine/ApiType.cs
Normal file
2651
SVSim.BattleEngine/Engine/ApiType.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
|
||||
public class ApplySkillTargetFilterCollection : SkillFilterCollectionBase
|
||||
{
|
||||
public List<ISkillCustomSelectFilter> ApplyCustomSelectFilterList { get; set; }
|
||||
|
||||
public List<ISkillExclutionFilter> ApplyExclutionFilterList { get; private set; }
|
||||
|
||||
public ISkillSelectFilter ApplySelectFilter { get; set; }
|
||||
|
||||
public List<ApplySkillTargetFilterCollection> ApplyAndFilter { get; set; }
|
||||
|
||||
public ApplySkillTargetFilterCollection()
|
||||
{
|
||||
ApplyCustomSelectFilterList = new List<ISkillCustomSelectFilter>();
|
||||
ApplyExclutionFilterList = new List<ISkillExclutionFilter>();
|
||||
ApplyAndFilter = new List<ApplySkillTargetFilterCollection>();
|
||||
}
|
||||
|
||||
public List<IReadOnlyBattleCardInfo> Filtering(BattlePlayerReadOnlyInfoPair pair, SkillConditionCheckerOption checkerOption, SkillOptionValue optionValue)
|
||||
{
|
||||
List<IReadOnlyBattleCardInfo> list = new List<IReadOnlyBattleCardInfo>();
|
||||
List<IReadOnlyBattleCardInfo> AndFilterTargets = new List<IReadOnlyBattleCardInfo>();
|
||||
IEnumerable<IBattlePlayerReadOnlyInfo> battlePlayerInfos = null;
|
||||
if (ApplyAndFilter.Count <= 0)
|
||||
{
|
||||
if (base.BattlePlayerFilter != null)
|
||||
{
|
||||
battlePlayerInfos = base.BattlePlayerFilter.Filtering(pair);
|
||||
}
|
||||
if (base.TargetFilter != null)
|
||||
{
|
||||
list = base.TargetFilter.Filtering(battlePlayerInfos, checkerOption).ToList();
|
||||
if (BattleManagerBase.GetIns().XorShiftRandom(isSelf: true) != null && BattleManagerBase.GetIns().XorShiftRandom(isSelf: false) == null && !pair.ReadOnlySelf.IsPlayer && (base.TargetFilter is SkillTargetInHandCardFilter || base.TargetFilter is SkillTargetReturnCardFilter || base.TargetFilter is SkillTargetTokenDrawCardFilter))
|
||||
{
|
||||
return list;
|
||||
}
|
||||
}
|
||||
foreach (ISkillCardFilter cardFilter in base.CardFilterList)
|
||||
{
|
||||
list = cardFilter.Filtering(list, optionValue).ToList();
|
||||
}
|
||||
int i = 0;
|
||||
for (int count = ApplyCustomSelectFilterList.Count; i < count; i++)
|
||||
{
|
||||
list = ApplyCustomSelectFilterList[i].Filtering(list, battlePlayerInfos, checkerOption).ToList();
|
||||
}
|
||||
for (int j = 0; j < ApplyExclutionFilterList.Count; j++)
|
||||
{
|
||||
list = ApplyExclutionFilterList[j].Filtering(list, battlePlayerInfos, checkerOption, optionValue).ToList();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int k = 0; k < ApplyAndFilter.Count; k++)
|
||||
{
|
||||
List<BattleCardBase> cards = ApplyAndFilter[k].Filtering(pair, checkerOption, optionValue).Cast<BattleCardBase>().ToList();
|
||||
List<IReadOnlyBattleCardInfo> collection = (from IReadOnlyBattleCardInfo x in ApplyAndFilter[k].SelectFilter.Filtering(cards, optionValue, checkerOption)
|
||||
where !AndFilterTargets.Contains(x)
|
||||
select x).ToList();
|
||||
AndFilterTargets.AddRange(collection);
|
||||
}
|
||||
}
|
||||
List<IReadOnlyBattleCardInfo> list2 = list.ToList();
|
||||
list2.AddRange(AndFilterTargets);
|
||||
return list2;
|
||||
}
|
||||
|
||||
public bool SimpleFiltering(IReadOnlyBattleCardInfo targetCard, BattlePlayerReadOnlyInfoPair pair, SkillConditionCheckerOption checkerOption, SkillOptionValue optionValue)
|
||||
{
|
||||
List<IReadOnlyBattleCardInfo> list = new List<IReadOnlyBattleCardInfo> { targetCard };
|
||||
IEnumerable<IBattlePlayerReadOnlyInfo> battlePlayerInfos = base.BattlePlayerFilter.Filtering(pair);
|
||||
for (int i = 0; i < base.CardFilterList.Count; i++)
|
||||
{
|
||||
list = base.CardFilterList[i].Filtering(list, optionValue).ToList();
|
||||
}
|
||||
for (int j = 0; j < ApplyCustomSelectFilterList.Count; j++)
|
||||
{
|
||||
list = ApplyCustomSelectFilterList[j].Filtering(list, battlePlayerInfos, checkerOption).ToList();
|
||||
}
|
||||
for (int k = 0; k < ApplyExclutionFilterList.Count; k++)
|
||||
{
|
||||
list = ApplyExclutionFilterList[k].Filtering(list, battlePlayerInfos, checkerOption, optionValue).ToList();
|
||||
}
|
||||
return list.Count() > 0;
|
||||
}
|
||||
}
|
||||
95
SVSim.BattleEngine/Engine/AreaBGInfo.cs
Normal file
95
SVSim.BattleEngine/Engine/AreaBGInfo.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class AreaBGInfo : MonoBehaviour
|
||||
{
|
||||
private readonly List<ChapterExtraData> _chapterExtraDatas = new List<ChapterExtraData>
|
||||
{
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 2,
|
||||
ExtraTextureChapter = 10,
|
||||
BGExtraEffectPath = "scn_map_change_1",
|
||||
SeType = Se.TYPE.SE_MAP_TREE_EFFECT
|
||||
},
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 2,
|
||||
ExtraTextureChapter = 11,
|
||||
ExtraTextureIndex = { 1, 2 },
|
||||
BGSuffix = "b"
|
||||
},
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 2,
|
||||
ExtraTextureChapter = 12,
|
||||
ExtraTextureIndex = { 1, 2 },
|
||||
BGSuffix = "b"
|
||||
},
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 1,
|
||||
ExtraTextureChapter = 4,
|
||||
BGExtraEffectPath = "scn_map_change_1",
|
||||
SeType = Se.TYPE.SE_MAP_TREE_EFFECT,
|
||||
ClanType = CardBasePrm.ClanType.NEMESIS
|
||||
},
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 1,
|
||||
ExtraTextureChapter = 5,
|
||||
ExtraTextureIndex = { 1, 2 },
|
||||
BGSuffix = "b",
|
||||
ClanType = CardBasePrm.ClanType.NEMESIS
|
||||
},
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 1,
|
||||
ExtraTextureChapter = 6,
|
||||
ExtraTextureIndex = { 1, 2 },
|
||||
BGSuffix = "b",
|
||||
ClanType = CardBasePrm.ClanType.NEMESIS
|
||||
},
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 9,
|
||||
ExtraTextureChapter = 1,
|
||||
BGSectionId = 4,
|
||||
BGExtraEffectPath = "scn_map_change_4",
|
||||
BGFirstClearEffectPath = "scn_map_change_2",
|
||||
FirstClearSeType = Se.TYPE.SE_MAP_SECTION9_CHAPTER1,
|
||||
SeType = Se.TYPE.SE_MAP_SECTION9_CHANGE_CHAPTER,
|
||||
ChapterMoveTime = 0f,
|
||||
FirstClearEffectDelayTime = 1.2f,
|
||||
FirstClearMoveOutDelayTime = 0.5f
|
||||
},
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 9,
|
||||
ExtraTextureChapter = 2,
|
||||
BGSectionId = 7,
|
||||
BGExtraEffectPath = "scn_map_change_4",
|
||||
BGFirstClearEffectPath = "scn_map_change_3",
|
||||
FirstClearSeType = Se.TYPE.SE_MAP_SECTION9_CHAPTER2,
|
||||
SeType = Se.TYPE.SE_MAP_SECTION9_CHANGE_CHAPTER,
|
||||
FirstClearEffectDelayTime = 1.2f,
|
||||
FirstClearMoveDelayTime = 1f
|
||||
},
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 9003,
|
||||
ExtraTextureIndex = { 1, 2 },
|
||||
BGSuffix = "c"
|
||||
}
|
||||
};
|
||||
|
||||
public List<ChapterExtraData> GetExtraChapters(int sectionId, int? selectStoryClassId)
|
||||
{
|
||||
List<ChapterExtraData> list = _chapterExtraDatas.FindAll((ChapterExtraData item) => item.SectionId == sectionId);
|
||||
if (sectionId == 20)
|
||||
{
|
||||
list.AddRange(AreaBGInfoSection20.GetChapterExtraDatas());
|
||||
}
|
||||
return list.FindAll((ChapterExtraData item) => (CardBasePrm.ClanType?)item.ClanType == (CardBasePrm.ClanType?)selectStoryClassId || item.ClanType == CardBasePrm.ClanType.NONE);
|
||||
}
|
||||
}
|
||||
127
SVSim.BattleEngine/Engine/AreaBGInfoSection20.cs
Normal file
127
SVSim.BattleEngine/Engine/AreaBGInfoSection20.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System.Collections.Generic;
|
||||
using Cute;
|
||||
|
||||
public class AreaBGInfoSection20
|
||||
{
|
||||
public const int SECTION_ID = 20;
|
||||
|
||||
public const int WERUSA_START = 10;
|
||||
|
||||
private const int WERUSA_END = 17;
|
||||
|
||||
private const int WERUSA_BG_SECTION_ID = 12;
|
||||
|
||||
private const int LEVIRU_START = 18;
|
||||
|
||||
private const int LEVIRU_END = 25;
|
||||
|
||||
private const int LEVIRU_BG_SECTION_ID = 10;
|
||||
|
||||
private const int IZUNIA_CHANGE_TEXTURE_START = 3;
|
||||
|
||||
private const int IZUNIA_END = 9;
|
||||
|
||||
private const int IZUNIA_BG_SECTION_ID = 2;
|
||||
|
||||
public const int NATERA_START = 26;
|
||||
|
||||
private const int NATERA_END = 33;
|
||||
|
||||
private const int NATERA_BG_SECTION_ID = 9;
|
||||
|
||||
private const int LAST_BATTLE_START = 34;
|
||||
|
||||
private const int LAST_BATTLE_END = 40;
|
||||
|
||||
private const int LAST_BATTLE_BG_SECTION_ID = 20;
|
||||
|
||||
public static List<ChapterExtraData> GetChapterExtraDatas()
|
||||
{
|
||||
List<ChapterExtraData> list = new List<ChapterExtraData>();
|
||||
list.AddRange(Chapter1_2());
|
||||
list.AddRange(Chapter3_9());
|
||||
list.AddRange(OtherWorldChapters(10, 17, 12, new List<int> { 1, 2, 6, 7 }, addTreeEffect: true));
|
||||
list.AddRange(OtherWorldChapters(18, 25, 10, new List<int> { 1, 2 }, addTreeEffect: true));
|
||||
list.AddRange(OtherWorldChapters(26, 33, 9, new List<int> { 2, 3, 4, 7, 8, 9 }, addTreeEffect: true));
|
||||
list.AddRange(OtherWorldChapters(34, 40, 20, null, addTreeEffect: false));
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<ChapterExtraData> OtherWorldChapters(int start, int end, int section, List<int> extraTextureIndex, bool addTreeEffect)
|
||||
{
|
||||
List<ChapterExtraData> list = new List<ChapterExtraData>();
|
||||
for (int i = start; i <= end; i++)
|
||||
{
|
||||
ChapterExtraData chapterExtraData = new ChapterExtraData
|
||||
{
|
||||
SectionId = 20,
|
||||
ExtraTextureChapter = i,
|
||||
BGSectionId = section
|
||||
};
|
||||
chapterExtraData.AddTreeEffect = addTreeEffect;
|
||||
if (i == 17 || i == 25 || i == 33)
|
||||
{
|
||||
chapterExtraData.BGExtraEffectPath = "scn_map_change_9";
|
||||
chapterExtraData.SeType = Se.TYPE.SE_MAP_SECTION9_CHANGE_CHAPTER;
|
||||
}
|
||||
if (extraTextureIndex.IsNotNullOrEmpty())
|
||||
{
|
||||
chapterExtraData.ExtraTextureIndex = extraTextureIndex;
|
||||
chapterExtraData.BGSuffix = "b";
|
||||
}
|
||||
list.Add(chapterExtraData);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<ChapterExtraData> Chapter1_2()
|
||||
{
|
||||
return new List<ChapterExtraData>
|
||||
{
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 20,
|
||||
ExtraTextureChapter = 1,
|
||||
BGExtraEffectPath = "scn_map_change_9",
|
||||
SeType = Se.TYPE.SE_MAP_SECTION20_CHANGE_CHAPTER1
|
||||
},
|
||||
new ChapterExtraData
|
||||
{
|
||||
SectionId = 20,
|
||||
ExtraTextureChapter = 2,
|
||||
BGExtraEffectPath = "scn_map_change_8",
|
||||
AttachExtraEffectToBgRoot = true,
|
||||
SeType = Se.TYPE.SE_MAP_TREE_EFFECT
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static List<ChapterExtraData> Chapter3_9()
|
||||
{
|
||||
List<ChapterExtraData> list = new List<ChapterExtraData>();
|
||||
for (int i = 3; i <= 9; i++)
|
||||
{
|
||||
ChapterExtraData chapterExtraData = new ChapterExtraData
|
||||
{
|
||||
SectionId = 20,
|
||||
ExtraTextureChapter = i,
|
||||
ExtraTextureIndex = { 1, 2, 6 },
|
||||
BGSectionId = 2,
|
||||
BGSuffix = "b"
|
||||
};
|
||||
if (i == 9)
|
||||
{
|
||||
chapterExtraData.BGExtraEffectPath = "scn_map_change_9";
|
||||
chapterExtraData.SeType = Se.TYPE.SE_MAP_SECTION9_CHANGE_CHAPTER;
|
||||
}
|
||||
list.Add(chapterExtraData);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static bool IsSpeedUpParticleTransition(int previousChapter, int nextChapter)
|
||||
{
|
||||
bool flag = previousChapter == 33 && nextChapter == 32;
|
||||
return previousChapter == 0 || flag;
|
||||
}
|
||||
}
|
||||
530
SVSim.BattleEngine/Engine/AreaSelInfo.cs
Normal file
530
SVSim.BattleEngine/Engine/AreaSelInfo.cs
Normal file
@@ -0,0 +1,530 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Cute;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
|
||||
public class AreaSelInfo : MonoBehaviour
|
||||
{
|
||||
private enum eTableCategory
|
||||
{
|
||||
CARD,
|
||||
SLEEVE,
|
||||
OTHER,
|
||||
MAX
|
||||
}
|
||||
|
||||
private const float LEFTSTAGEINFO_X_IN = 0f;
|
||||
|
||||
private const float LEFTSTAGEINFO_X_OUT = -1120f;
|
||||
|
||||
private const int CLEARPRESENT_MAX = 3;
|
||||
|
||||
private static readonly Vector3 CLEARPRESENT_CARD_COLLISIONSIZE = new Vector3(175f, 230f, 1f);
|
||||
|
||||
private const int CLEARPRESENT_CARD_DEPTHOFFSET = 50;
|
||||
|
||||
private const int CLEARPRESENT_RESOURCELIST_CAPACITY = 2;
|
||||
|
||||
private static readonly string[] CLEARPRESENT_NAME = new string[10] { "", "Common_0205", "", "Common_0201", "", "", "", "", "", "Common_0115" };
|
||||
|
||||
private static readonly string[] CLEARPRESENT_THUMBNAIL_SPRITENAME = new string[10] { "", "thumbnail_liquid", "", "thumbnail_crystal", "", "", "thumbnail_card", "thumbnail_emblem", "thumbnail_title", "thumbnail_rupy" };
|
||||
|
||||
private readonly Vector3 REWARD_TABLE_DEFAULT_POSITION = new Vector3(45f, -85.3f, 0f);
|
||||
|
||||
private readonly Vector3 REWARD_TABLE_CARD_POSITION = new Vector3(28.5f, -85.3f, 0f);
|
||||
|
||||
private const float TABLE_CONTAINS_CARD_REWARD_OFFSET_X = -16f;
|
||||
|
||||
private const int REWARD_BG_BASIC_WIDTH = 250;
|
||||
|
||||
private const int REWARD_BG_OFFSET_WIDTH_PER_GOODS = 90;
|
||||
|
||||
private readonly Dictionary<UserGoods.Type, float> REWARD_BG_OFFSET_MAGNIFICATION = new Dictionary<UserGoods.Type, float>
|
||||
{
|
||||
{
|
||||
UserGoods.Type.Degree,
|
||||
2f
|
||||
},
|
||||
{
|
||||
UserGoods.Type.Card,
|
||||
1.2f
|
||||
},
|
||||
{
|
||||
UserGoods.Type.Sleeve,
|
||||
1.2f
|
||||
},
|
||||
{
|
||||
UserGoods.Type.Skin,
|
||||
1.2f
|
||||
},
|
||||
{
|
||||
UserGoods.Type.RedEther,
|
||||
1f
|
||||
},
|
||||
{
|
||||
UserGoods.Type.Rupy,
|
||||
1f
|
||||
},
|
||||
{
|
||||
UserGoods.Type.Item,
|
||||
1f
|
||||
},
|
||||
{
|
||||
UserGoods.Type.Emblem,
|
||||
1f
|
||||
}
|
||||
};
|
||||
|
||||
private const float LABEL_ONLY_DEGREE_OFFSET_Y = -10f;
|
||||
|
||||
[SerializeField]
|
||||
private GameObject _clearRewardPrefab;
|
||||
|
||||
private List<AreaSelectClearReward> _clearRewardList = new List<AreaSelectClearReward>();
|
||||
|
||||
[SerializeField]
|
||||
private UITable _tableRoot;
|
||||
|
||||
[SerializeField]
|
||||
private UITable[] _tableRewardsCategory = new UITable[3];
|
||||
|
||||
[SerializeField]
|
||||
private GameObject _cardObjEvacuationRoot;
|
||||
|
||||
[SerializeField]
|
||||
private UISprite _spriteRewardBackground;
|
||||
|
||||
[SerializeField]
|
||||
private UILabel _labelAcquired;
|
||||
|
||||
private List<UIBase_CardManager.CardObjData> _cardObjList = new List<UIBase_CardManager.CardObjData>();
|
||||
|
||||
[SerializeField]
|
||||
private GameObject CardDetailRoot;
|
||||
|
||||
[SerializeField]
|
||||
private CardDetailUI CardDetailPrefab;
|
||||
|
||||
private CardDetailUI _cardDetail;
|
||||
|
||||
private List<string> _loadFileList = new List<string>();
|
||||
|
||||
private bool _isLoadEnd = true;
|
||||
|
||||
public void SetClearPresent(StoryChapterData chapterData)
|
||||
{
|
||||
if (chapterData == null || chapterData.Rewards == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (chapterData.Rewards.Length != 0)
|
||||
{
|
||||
base.gameObject.SetActive(value: true);
|
||||
bool isCleared = chapterData.IsCleared;
|
||||
for (int i = 0; i < _clearRewardList.Count; i++)
|
||||
{
|
||||
_clearRewardList[i].gameObject.SetActive(value: false);
|
||||
}
|
||||
List<long> list = new List<long>();
|
||||
for (int j = 0; j < chapterData.Rewards.Length; j++)
|
||||
{
|
||||
StoryChapterData.StoryReward storyReward = chapterData.Rewards[j];
|
||||
if (storyReward == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (j >= _clearRewardList.Count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (storyReward.RewardType == 5)
|
||||
{
|
||||
if (list.Contains(storyReward.RewardUserGoodsId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
list.Add(storyReward.RewardUserGoodsId);
|
||||
}
|
||||
_clearRewardList[j].gameObject.SetActive(value: true);
|
||||
_clearRewardList[j].ShowReward((UserGoods.Type)storyReward.RewardType, storyReward.RewardUserGoodsId, storyReward.RewardNumber, isCleared);
|
||||
}
|
||||
RepositionRewards();
|
||||
SetRewardBackgroundWidth();
|
||||
SetAcquiredLabel(isCleared);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.gameObject.SetActive(value: false);
|
||||
}
|
||||
}
|
||||
|
||||
private void RepositionRewards()
|
||||
{
|
||||
for (int i = 0; i < _tableRewardsCategory.Length; i++)
|
||||
{
|
||||
_tableRewardsCategory[i].gameObject.SetActive(value: false);
|
||||
}
|
||||
for (int j = 0; j < _clearRewardList.Count; j++)
|
||||
{
|
||||
if (_clearRewardList[j].gameObject.activeSelf)
|
||||
{
|
||||
int num = _clearRewardList[j].RewardGoodsType switch
|
||||
{
|
||||
UserGoods.Type.Card => 0,
|
||||
UserGoods.Type.Sleeve => 1,
|
||||
_ => 2,
|
||||
};
|
||||
Transform obj = _clearRewardList[j].gameObject.transform;
|
||||
obj.SetParent(_tableRewardsCategory[num].transform);
|
||||
obj.SetAsLastSibling();
|
||||
_tableRewardsCategory[num].gameObject.SetActive(value: true);
|
||||
}
|
||||
}
|
||||
for (int k = 0; k < _tableRewardsCategory.Length; k++)
|
||||
{
|
||||
if (_tableRewardsCategory[k].gameObject.activeInHierarchy)
|
||||
{
|
||||
_tableRewardsCategory[k].Reposition();
|
||||
}
|
||||
}
|
||||
_tableRoot.Reposition();
|
||||
if (_tableRewardsCategory[0].gameObject.activeInHierarchy)
|
||||
{
|
||||
_tableRoot.transform.localPosition = REWARD_TABLE_CARD_POSITION;
|
||||
for (int l = 0; l < _tableRewardsCategory.Length; l++)
|
||||
{
|
||||
if (l != 0)
|
||||
{
|
||||
Vector3 localPosition = _tableRewardsCategory[l].transform.localPosition;
|
||||
localPosition.x += -16f;
|
||||
_tableRewardsCategory[l].transform.localPosition = localPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_tableRoot.transform.localPosition = REWARD_TABLE_DEFAULT_POSITION;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetRewardBackgroundWidth()
|
||||
{
|
||||
float num = 0f;
|
||||
for (int i = 0; i < _clearRewardList.Count; i++)
|
||||
{
|
||||
if (_clearRewardList[i].gameObject.activeInHierarchy)
|
||||
{
|
||||
UserGoods.Type rewardGoodsType = _clearRewardList[i].RewardGoodsType;
|
||||
num += REWARD_BG_OFFSET_MAGNIFICATION[rewardGoodsType];
|
||||
}
|
||||
}
|
||||
int width = 250 + (int)(90f * num);
|
||||
_spriteRewardBackground.width = width;
|
||||
}
|
||||
|
||||
private void SetAcquiredLabel(bool isAcquired)
|
||||
{
|
||||
_labelAcquired.gameObject.SetActive(isAcquired);
|
||||
if (!isAcquired)
|
||||
{
|
||||
return;
|
||||
}
|
||||
float a = float.MaxValue;
|
||||
float num = float.MinValue;
|
||||
bool flag = true;
|
||||
for (int i = 0; i < _clearRewardList.Count; i++)
|
||||
{
|
||||
if (_clearRewardList[i].gameObject.activeInHierarchy)
|
||||
{
|
||||
Transform rewardTransform = _clearRewardList[i].GetRewardTransform();
|
||||
a = Mathf.Min(a, rewardTransform.position.x);
|
||||
num = Mathf.Max(num, rewardTransform.position.x);
|
||||
if (_clearRewardList[i].RewardGoodsType != UserGoods.Type.Degree)
|
||||
{
|
||||
flag = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vector3 position = _labelAcquired.transform.position;
|
||||
position.x = Mathf.Lerp(a, num, 0.5f);
|
||||
_labelAcquired.transform.position = position;
|
||||
Vector3 localPosition = _labelAcquired.transform.localPosition;
|
||||
localPosition.y = _tableRoot.transform.localPosition.y + (flag ? (-10f) : 0f);
|
||||
_labelAcquired.transform.localPosition = localPosition;
|
||||
}
|
||||
|
||||
public void LoadClearPresent(IReadOnlyList<StoryChapterData> stageDataList)
|
||||
{
|
||||
if (!_isLoadEnd)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isLoadEnd = false;
|
||||
ReleaseClearPresent();
|
||||
if (CardDetailPrefab != null)
|
||||
{
|
||||
_cardDetail = Object.Instantiate(CardDetailPrefab);
|
||||
_cardDetail.transform.parent = CardDetailRoot.transform;
|
||||
_cardDetail.transform.localPosition = Vector3.zero;
|
||||
_cardDetail.transform.localScale = Vector3.one;
|
||||
_cardDetail.OnClose = OnCardDetailClose;
|
||||
_cardDetail.gameObject.SetActive(value: false);
|
||||
_cardDetail.Initialize(_cardDetail.gameObject.layer, CardMaster.CardMasterId.Default);
|
||||
_cardDetail.IsShowFlavorTextButton = true;
|
||||
_cardDetail.IsShowVoiceButton = true;
|
||||
_cardDetail.IsShowEvolutionButton = true;
|
||||
}
|
||||
_clearRewardList.Clear();
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
AreaSelectClearReward component = NGUITools.AddChild(_tableRoot.gameObject, _clearRewardPrefab).GetComponent<AreaSelectClearReward>();
|
||||
_clearRewardList.Add(component);
|
||||
}
|
||||
List<int> list = new List<int>(2);
|
||||
List<string> loadPath = new List<string>();
|
||||
StoryChapterData.StoryReward storyReward = null;
|
||||
for (int j = 0; j < stageDataList.Count; j++)
|
||||
{
|
||||
for (int k = 0; k < stageDataList[j].Rewards.Length; k++)
|
||||
{
|
||||
string text = string.Empty;
|
||||
storyReward = stageDataList[j].Rewards[k];
|
||||
if (storyReward == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (storyReward.RewardType == 5)
|
||||
{
|
||||
if (!list.Contains((int)storyReward.RewardUserGoodsId))
|
||||
{
|
||||
list.Add((int)storyReward.RewardUserGoodsId);
|
||||
}
|
||||
}
|
||||
else if (storyReward.RewardType == 4)
|
||||
{
|
||||
string userGoodsImageName = UserGoods.GetUserGoodsImageName(UserGoods.Type.Item, storyReward.RewardUserGoodsId);
|
||||
text = Toolbox.ResourcesManager.GetAssetTypePath(userGoodsImageName, ResourcesManager.AssetLoadPathType.Item);
|
||||
}
|
||||
else if (storyReward.RewardType == 6)
|
||||
{
|
||||
long existingSleeveId = Toolbox.ResourcesManager.GetExistingSleeveId(storyReward.RewardUserGoodsId);
|
||||
text = Toolbox.ResourcesManager.GetAssetTypePath(existingSleeveId.ToString(), ResourcesManager.AssetLoadPathType.SleeveTexture);
|
||||
Sleeve sleeve = Data.Master.SleeveMgr.Get(existingSleeveId);
|
||||
if (sleeve.IsPremiumSleeve)
|
||||
{
|
||||
UIManager.GetInstance().getUIBase_CardManager().AddPremireSleevePath(ref loadPath, sleeve);
|
||||
}
|
||||
}
|
||||
else if (storyReward.RewardType == 8)
|
||||
{
|
||||
foreach (string degreeResource in DegreeHelper.GetDegreeResourceList(storyReward.RewardUserGoodsId, DegreeHelper.DegreeType.SMALL, isFetch: false))
|
||||
{
|
||||
if (!loadPath.Contains(degreeResource))
|
||||
{
|
||||
loadPath.Add(degreeResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (storyReward.RewardType == 7)
|
||||
{
|
||||
text = Toolbox.ResourcesManager.GetAssetTypePath(storyReward.RewardUserGoodsId.ToString(), ResourcesManager.AssetLoadPathType.Emblem_M);
|
||||
}
|
||||
else if (storyReward.RewardType == 10)
|
||||
{
|
||||
text = Toolbox.ResourcesManager.GetAssetTypePath(storyReward.RewardUserGoodsId.ToString(), ResourcesManager.AssetLoadPathType.ClassCharaSkinThumbnail);
|
||||
}
|
||||
if (text != string.Empty && !loadPath.Contains(text))
|
||||
{
|
||||
loadPath.Add(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (list.Count == 0 && loadPath.Count == 0)
|
||||
{
|
||||
_isLoadEnd = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
StartCoroutine(LoadClearPresentInner(list, loadPath));
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator LoadClearPresentInner(List<int> cardidlist, List<string> rewardPathList)
|
||||
{
|
||||
UIManager uiMgr = UIManager.GetInstance();
|
||||
UIBase_CardManager uiCardMgr = uiMgr.getUIBase_CardManager();
|
||||
bool isLoadRewardEnd = false;
|
||||
_loadFileList.Clear();
|
||||
bool isLoadCard = cardidlist.Count > 0;
|
||||
if (isLoadCard)
|
||||
{
|
||||
int layer = LayerMask.NameToLayer("FrontUI");
|
||||
uiMgr.CardLoadSelect(base.gameObject, cardidlist, layer, is2D: true);
|
||||
}
|
||||
StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(rewardPathList, delegate
|
||||
{
|
||||
_loadFileList.AddRange(rewardPathList);
|
||||
isLoadRewardEnd = true;
|
||||
}));
|
||||
while ((isLoadCard && (!uiCardMgr.getCreateEndFlag() || !uiCardMgr.isAssetAllReady)) || !isLoadRewardEnd)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
_cardObjList = uiMgr.getCardList2DObjs();
|
||||
if (_cardObjList != null)
|
||||
{
|
||||
for (int num = 0; num < _cardObjList.Count; num++)
|
||||
{
|
||||
GameObject cardObj = _cardObjList[num].CardObj;
|
||||
if (!(cardObj == null))
|
||||
{
|
||||
cardObj.SetActive(value: false);
|
||||
UITexture[] componentsInChildren = cardObj.GetComponentsInChildren<UITexture>(includeInactive: true);
|
||||
for (int num2 = 0; num2 < componentsInChildren.Length; num2++)
|
||||
{
|
||||
componentsInChildren[num2].depth += 50;
|
||||
}
|
||||
UILabel[] componentsInChildren2 = cardObj.GetComponentsInChildren<UILabel>(includeInactive: true);
|
||||
for (int num3 = 0; num3 < componentsInChildren2.Length; num3++)
|
||||
{
|
||||
componentsInChildren2[num3].depth += 50;
|
||||
}
|
||||
UISprite[] componentsInChildren3 = cardObj.GetComponentsInChildren<UISprite>(includeInactive: true);
|
||||
for (int num4 = 0; num4 < componentsInChildren3.Length; num4++)
|
||||
{
|
||||
componentsInChildren3[num4].depth += 50;
|
||||
}
|
||||
cardObj.GetComponent<CardListTemplate>().HideNum();
|
||||
cardObj.AddComponent<BoxCollider>().size = CLEARPRESENT_CARD_COLLISIONSIZE;
|
||||
cardObj.AddComponent<UIEventListener>().onClick = _cardDetail.OnPushCardDetailOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int num5 = 0; num5 < 3; num5++)
|
||||
{
|
||||
_clearRewardList[num5].Init(_cardObjList, _cardObjEvacuationRoot);
|
||||
}
|
||||
_isLoadEnd = true;
|
||||
}
|
||||
|
||||
public void ReleaseClearPresent()
|
||||
{
|
||||
if (_cardDetail != null)
|
||||
{
|
||||
Object.Destroy(_cardDetail.gameObject);
|
||||
_cardDetail = null;
|
||||
}
|
||||
if (_cardObjList != null)
|
||||
{
|
||||
for (int i = 0; i < _cardObjList.Count; i++)
|
||||
{
|
||||
Object.Destroy(_cardObjList[i].CardObj.gameObject);
|
||||
}
|
||||
_cardObjList.Clear();
|
||||
}
|
||||
Toolbox.ResourcesManager.RemoveAssetGroup(_loadFileList);
|
||||
_loadFileList.Clear();
|
||||
}
|
||||
|
||||
private void OnCardDetailClose()
|
||||
{
|
||||
}
|
||||
|
||||
public bool GetLoadEnd()
|
||||
{
|
||||
return _isLoadEnd;
|
||||
}
|
||||
|
||||
public void ResetInfoPosition()
|
||||
{
|
||||
base.gameObject.transform.localPosition = new Vector3(0f, base.gameObject.transform.localPosition.y, base.gameObject.transform.localPosition.z);
|
||||
}
|
||||
|
||||
public void MoveToScreen(bool isIn, bool isImmediate)
|
||||
{
|
||||
MoveToScreenObj(base.gameObject, isIn ? 0f : (-1120f), isImmediate ? 0f : 0.5f);
|
||||
}
|
||||
|
||||
public void MoveToScreenObj(GameObject target, float localPosX, float time)
|
||||
{
|
||||
if (Mathf.Approximately(time, 0f))
|
||||
{
|
||||
Vector3 localPosition = target.transform.localPosition;
|
||||
localPosition.x = localPosX;
|
||||
target.transform.localPosition = localPosition;
|
||||
return;
|
||||
}
|
||||
iTween.MoveTo(target, iTween.Hash("islocal", true, "x", localPosX, "time", time));
|
||||
}
|
||||
|
||||
public static string GetPresentItemName(int itemID, long userGoodsId)
|
||||
{
|
||||
switch ((UserGoods.Type)itemID)
|
||||
{
|
||||
case UserGoods.Type.RedEther:
|
||||
case UserGoods.Type.Rupy:
|
||||
return Data.SystemText.Get(CLEARPRESENT_NAME[itemID]);
|
||||
case UserGoods.Type.Item:
|
||||
{
|
||||
Item item = Data.Master.ItemList.Find((Item data) => data.UserGoodsId == userGoodsId);
|
||||
if (item == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return item.name;
|
||||
}
|
||||
case UserGoods.Type.Sleeve:
|
||||
{
|
||||
Sleeve sleeve = Data.Master.SleeveMgr.Get(userGoodsId);
|
||||
if (sleeve == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return sleeve.sleeve_name;
|
||||
}
|
||||
case UserGoods.Type.Emblem:
|
||||
{
|
||||
Emblem emblem = Data.Master.EmblemMgr.Get(userGoodsId);
|
||||
if (emblem == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return emblem._name;
|
||||
}
|
||||
case UserGoods.Type.Degree:
|
||||
{
|
||||
Degree degree = Data.Master.DegreeMgr.Get((int)userGoodsId);
|
||||
if (degree == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return degree._name;
|
||||
}
|
||||
case UserGoods.Type.Skin:
|
||||
{
|
||||
ClassCharacterMasterData charaPrmByCharaId = GameMgr.GetIns().GetDataMgr().GetCharaPrmByCharaId((int)userGoodsId);
|
||||
if (charaPrmByCharaId == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return charaPrmByCharaId.chara_name;
|
||||
}
|
||||
case UserGoods.Type.SpotCardPoint:
|
||||
return Data.SystemText.Get("Common_0161");
|
||||
case UserGoods.Type.MyPageBG:
|
||||
return Data.Master.MyPageCustomBGMaster[userGoodsId.ToString()].Name;
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetPresentItemSpriteName(int itemID)
|
||||
{
|
||||
if (itemID < 0 || itemID >= CLEARPRESENT_THUMBNAIL_SPRITENAME.Length)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return CLEARPRESENT_THUMBNAIL_SPRITENAME[itemID];
|
||||
}
|
||||
}
|
||||
489
SVSim.BattleEngine/Engine/AreaSelectBG.cs
Normal file
489
SVSim.BattleEngine/Engine/AreaSelectBG.cs
Normal file
@@ -0,0 +1,489 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cute;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class AreaSelectBG
|
||||
{
|
||||
private const float BGTEXTURE_WIDTH = 1024f;
|
||||
|
||||
private const float BGTEXTURE_HEIGHT = 1024f;
|
||||
|
||||
private const int BGTEXTURE_NUM_X = 4;
|
||||
|
||||
private const int BGTEXTURE_NUM_Y = 3;
|
||||
|
||||
private const float BGTEXTURE_WIDTH_HALF_LEFT = 2048f;
|
||||
|
||||
private const float BGTEXTURE_WIDTH_HALF_RIGHT = 2560f;
|
||||
|
||||
private const float BGTEXTURE_HEIGHT_HALF_UP = 1536f;
|
||||
|
||||
private const float BGTEXTURE_HEIGHT_HALF_BOTTOM = 1536f;
|
||||
|
||||
private const float BGTEXTURE_DRAG_MARGIN = 5f;
|
||||
|
||||
private const float BGTEXTURE_DRAG_DECELERATION = 0.875f;
|
||||
|
||||
private const float BGTEXTURE_DRAGARROW_ANIM_SPEED = 1f;
|
||||
|
||||
private const float BGDRAG_SEC_MAXSPEED = 0.25f;
|
||||
|
||||
private const float BGDRAG_SEC_RESETUPTOTIME_MAX = 0.5f;
|
||||
|
||||
private AreaSelectUI _areaSelectUI;
|
||||
|
||||
[SerializeField]
|
||||
private GameObject _BGRoot;
|
||||
|
||||
[SerializeField]
|
||||
private UITexture[] _BGTexture;
|
||||
|
||||
[SerializeField]
|
||||
private AreaBGInfo _areaBGInfo;
|
||||
|
||||
private ParticleSystem _bgEffect;
|
||||
|
||||
private AreaSelectEffectControlBase _bgEffectControl;
|
||||
|
||||
[SerializeField]
|
||||
private GameObject _BGDragCollision;
|
||||
|
||||
private Vector2 _BGDragDelta = Vector2.zero;
|
||||
|
||||
private bool _isBGDragEnable;
|
||||
|
||||
private float _BGDragSec;
|
||||
|
||||
private float _BGDragSecResetUpToTime;
|
||||
|
||||
private List<string> _loadedResources = new List<string>();
|
||||
|
||||
private bool _isLoadEndBGTexture = true;
|
||||
|
||||
private bool _isLoadEndParticle = true;
|
||||
|
||||
private StorySectionData _sectionData;
|
||||
|
||||
private int? _sectionClassId;
|
||||
|
||||
private List<ChapterExtraData> _extraChapters = new List<ChapterExtraData>();
|
||||
|
||||
private bool _isNormalBgSet = true;
|
||||
|
||||
private bool _changeChapterFirstCall = true;
|
||||
|
||||
private float _currentAspectRatio;
|
||||
|
||||
private Vector3 _currentBGScale = Vector3.one;
|
||||
|
||||
private Vector3 _currentParentPos = Vector3.zero;
|
||||
|
||||
private Vector2 _minMovablePos = Vector3.one;
|
||||
|
||||
private Vector2 _maxMovablePos = Vector3.one;
|
||||
|
||||
public ChapterExtraData ChapterExtraData { get; private set; }
|
||||
|
||||
public ChapterExtraData TransitionChapterExtraData { get; private set; }
|
||||
|
||||
public int BeforeChapterId { get; private set; }
|
||||
|
||||
public GameObject GetBGRoot()
|
||||
{
|
||||
return _BGRoot;
|
||||
}
|
||||
|
||||
public bool GetLoadEnd()
|
||||
{
|
||||
if (_isLoadEndBGTexture)
|
||||
{
|
||||
return _isLoadEndParticle;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetActiveBGEffect(bool isActive)
|
||||
{
|
||||
_bgEffect.gameObject.SetActive(isActive);
|
||||
if (_bgEffectControl != null)
|
||||
{
|
||||
_bgEffectControl.SetActiveBGEffect(isActive);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsBGDragEnable()
|
||||
{
|
||||
return _isBGDragEnable;
|
||||
}
|
||||
|
||||
public void Init(AreaSelectUI areaSelectUI)
|
||||
{
|
||||
_areaSelectUI = areaSelectUI;
|
||||
UIEventListener component = _BGDragCollision.GetComponent<UIEventListener>();
|
||||
if (null != component)
|
||||
{
|
||||
component.onDrag = OnDragBG;
|
||||
}
|
||||
SetBGDragEnable(enable: false);
|
||||
}
|
||||
|
||||
public void Term()
|
||||
{
|
||||
int i = 0;
|
||||
for (int num = _BGTexture.Length; i < num; i++)
|
||||
{
|
||||
_BGTexture[i].mainTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetBackGroundPath(int backGroundId, int index, bool isFetch = false)
|
||||
{
|
||||
return Toolbox.ResourcesManager.GetAssetTypePath("bg_story_" + backGroundId.ToString("00") + "_" + (index + 1).ToString("00"), ResourcesManager.AssetLoadPathType.Background, isFetch);
|
||||
}
|
||||
|
||||
private string GetExtraBackGroundPath(StorySectionData sectionData, int index, string suffix, bool isFetch = false)
|
||||
{
|
||||
return Toolbox.ResourcesManager.GetAssetTypePath("bg_story_" + sectionData.BackGroundId.ToString("00") + "_" + (index + 1).ToString("00") + suffix, ResourcesManager.AssetLoadPathType.Background, isFetch);
|
||||
}
|
||||
|
||||
private string GetExtraBackGroundPath(int backgroundId, int index, string suffix, bool isFetch = false)
|
||||
{
|
||||
return Toolbox.ResourcesManager.GetAssetTypePath("bg_story_" + backgroundId.ToString("00") + "_" + (index + 1).ToString("00") + suffix, ResourcesManager.AssetLoadPathType.Background, isFetch);
|
||||
}
|
||||
|
||||
private string GetMapEffectPath(int backGroundId, bool isFetch = false)
|
||||
{
|
||||
return Toolbox.ResourcesManager.GetAssetTypePath("scn_map_world_" + backGroundId, ResourcesManager.AssetLoadPathType.Effect2D, isFetch);
|
||||
}
|
||||
|
||||
private string GetTreeEffectPath(int backGroundId, bool isFetch = false)
|
||||
{
|
||||
return Toolbox.ResourcesManager.GetAssetTypePath("scn_map_world_tree_" + backGroundId, ResourcesManager.AssetLoadPathType.Effect2D, isFetch);
|
||||
}
|
||||
|
||||
public void LoadBG(StorySectionData sectionData, int? sectionClassId)
|
||||
{
|
||||
_sectionData = sectionData;
|
||||
_sectionClassId = sectionClassId;
|
||||
_extraChapters = _areaBGInfo.GetExtraChapters(_sectionData.Id, sectionClassId);
|
||||
_loadedResources.Add(GetMapEffectPath(sectionData.BackGroundId));
|
||||
_isLoadEndBGTexture = false;
|
||||
for (int i = 0; i < _BGTexture.Length; i++)
|
||||
{
|
||||
_loadedResources.Add(GetBackGroundPath(sectionData.BackGroundId, i));
|
||||
}
|
||||
foreach (ChapterExtraData extraChapter in _extraChapters)
|
||||
{
|
||||
int num = sectionData.BackGroundId;
|
||||
if (extraChapter.IsUseOtherSectionBG())
|
||||
{
|
||||
num = extraChapter.BGSectionId;
|
||||
for (int j = 0; j < _BGTexture.Length; j++)
|
||||
{
|
||||
_loadedResources.Add(GetBackGroundPath(num, j));
|
||||
}
|
||||
_loadedResources.Add(GetMapEffectPath(num));
|
||||
if (extraChapter.AddTreeEffect)
|
||||
{
|
||||
_loadedResources.Add(GetTreeEffectPath(num));
|
||||
}
|
||||
}
|
||||
foreach (int item in extraChapter.ExtraTextureIndex)
|
||||
{
|
||||
_loadedResources.Add(GetExtraBackGroundPath(num, item, extraChapter.BGSuffix));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(extraChapter.BGExtraEffectPath))
|
||||
{
|
||||
_loadedResources.Add(Toolbox.ResourcesManager.GetAssetTypePath(extraChapter.BGExtraEffectPath, ResourcesManager.AssetLoadPathType.Effect2D));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(extraChapter.BGFirstClearEffectPath))
|
||||
{
|
||||
_loadedResources.Add(Toolbox.ResourcesManager.GetAssetTypePath(extraChapter.BGFirstClearEffectPath, ResourcesManager.AssetLoadPathType.Effect2D));
|
||||
}
|
||||
}
|
||||
_areaSelectUI.StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(_loadedResources, _OnLoadEndBG));
|
||||
_isLoadEndParticle = false;
|
||||
}
|
||||
|
||||
public void UnLoadBG()
|
||||
{
|
||||
Toolbox.ResourcesManager.RemoveAssetGroup(_loadedResources);
|
||||
_loadedResources.Clear();
|
||||
_sectionData = null;
|
||||
_sectionClassId = null;
|
||||
}
|
||||
|
||||
private void _OnLoadEndBG()
|
||||
{
|
||||
List<GameObject> list = new List<GameObject>();
|
||||
_bgEffect = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(GetMapEffectPath(_sectionData.BackGroundId, isFetch: true)) as GameObject).GetComponent<ParticleSystem>();
|
||||
Vector3 localScale = _bgEffect.transform.localScale;
|
||||
_bgEffect.transform.parent = _BGRoot.transform;
|
||||
_bgEffect.transform.localScale = localScale;
|
||||
_bgEffect.transform.localPosition = Vector3.zero;
|
||||
_bgEffectControl = _bgEffect.gameObject.GetComponent<AreaSelectEffectControlBase>();
|
||||
list.Add(_bgEffect.gameObject);
|
||||
if (_bgEffectControl != null)
|
||||
{
|
||||
_bgEffectControl._backGroundEffects.Add(_bgEffectControl.BASE_EFFECT_INDEX, _bgEffect);
|
||||
_bgEffectControl._backGroundEffects.Add(_sectionData.BackGroundId, _bgEffect);
|
||||
}
|
||||
for (int i = 0; i < _BGTexture.Length; i++)
|
||||
{
|
||||
_BGTexture[i].mainTexture = Toolbox.ResourcesManager.LoadObject(GetBackGroundPath(_sectionData.BackGroundId, i, isFetch: true)) as Texture;
|
||||
}
|
||||
foreach (ChapterExtraData extraChapter in _extraChapters)
|
||||
{
|
||||
Dictionary<int, Texture> bGTexture = extraChapter.BGTexture;
|
||||
int num = _sectionData.BackGroundId;
|
||||
if (extraChapter.IsUseOtherSectionBG())
|
||||
{
|
||||
num = extraChapter.BGSectionId;
|
||||
for (int j = 0; j < _BGTexture.Length; j++)
|
||||
{
|
||||
bGTexture.Add(j, Toolbox.ResourcesManager.LoadObject(GetBackGroundPath(num, j, isFetch: true)) as Texture);
|
||||
}
|
||||
if (num != _sectionData.BackGroundId && !_bgEffectControl._backGroundEffects.ContainsKey(num))
|
||||
{
|
||||
ParticleSystem particleSystem = InitParticle(num, extraChapter.AddTreeEffect);
|
||||
_bgEffectControl._backGroundEffects.Add(num, particleSystem);
|
||||
list.Add(particleSystem.gameObject);
|
||||
}
|
||||
}
|
||||
foreach (int item in extraChapter.ExtraTextureIndex)
|
||||
{
|
||||
if (!bGTexture.ContainsKey(item))
|
||||
{
|
||||
bGTexture.Add(item, Toolbox.ResourcesManager.LoadObject(GetExtraBackGroundPath(num, item, extraChapter.BGSuffix, isFetch: true)) as Texture);
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(extraChapter.BGExtraEffectPath))
|
||||
{
|
||||
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(extraChapter.BGExtraEffectPath, ResourcesManager.AssetLoadPathType.Effect2D, isfetch: true);
|
||||
extraChapter.ExtraEffect = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(assetTypePath) as GameObject);
|
||||
if (extraChapter.AttachExtraEffectToBgRoot)
|
||||
{
|
||||
extraChapter.ExtraEffect.transform.parent = _BGRoot.transform;
|
||||
extraChapter.ExtraEffect.transform.localPosition = Vector3.zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
extraChapter.ExtraEffect.transform.parent = _areaSelectUI.transform;
|
||||
}
|
||||
extraChapter.ExtraEffect.SetActive(value: false);
|
||||
List<string> collection = GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(extraChapter.ExtraEffect, null);
|
||||
_loadedResources.AddRange(collection);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(extraChapter.BGFirstClearEffectPath))
|
||||
{
|
||||
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(extraChapter.BGFirstClearEffectPath, ResourcesManager.AssetLoadPathType.Effect2D, isfetch: true);
|
||||
extraChapter.FirstClearEffect = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(assetTypePath2) as GameObject);
|
||||
extraChapter.FirstClearEffect.transform.parent = _areaSelectUI.transform;
|
||||
extraChapter.FirstClearEffect.SetActive(value: false);
|
||||
List<string> collection2 = GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(extraChapter.FirstClearEffect, null);
|
||||
_loadedResources.AddRange(collection2);
|
||||
}
|
||||
}
|
||||
List<string> collection3 = GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(list, delegate
|
||||
{
|
||||
_isLoadEndParticle = true;
|
||||
});
|
||||
_loadedResources.AddRange(collection3);
|
||||
_isLoadEndBGTexture = true;
|
||||
}
|
||||
|
||||
private ParticleSystem InitParticle(int bgId, bool addTreeEffect = false)
|
||||
{
|
||||
Vector3 vector = default(Vector3);
|
||||
ParticleSystem component = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(GetMapEffectPath(bgId, isFetch: true)) as GameObject).GetComponent<ParticleSystem>();
|
||||
vector = component.transform.localScale;
|
||||
component.transform.parent = _BGRoot.transform;
|
||||
component.transform.localScale = vector;
|
||||
component.transform.localPosition = Vector3.zero;
|
||||
if (addTreeEffect)
|
||||
{
|
||||
GameObject gameObject = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(GetTreeEffectPath(bgId, isFetch: true)) as GameObject);
|
||||
gameObject.transform.parent = component.transform;
|
||||
gameObject.transform.localScale = Vector3.one;
|
||||
gameObject.transform.localPosition = Vector3.zero;
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
public void OnChangeSelectChapter(StoryChapterData chapterData, bool isFirstClear)
|
||||
{
|
||||
TransitionChapterExtraData = null;
|
||||
if (_extraChapters.Count > 0)
|
||||
{
|
||||
int intChapterId = int.Parse(chapterData.ChapterId);
|
||||
ChapterExtraData = _extraChapters.FirstOrDefault((ChapterExtraData n) => n.ExtraTextureChapter == intChapterId);
|
||||
}
|
||||
if (_bgEffectControl != null)
|
||||
{
|
||||
_bgEffectControl.OnChangeSelectChapter(this, _sectionData, _sectionClassId, chapterData, isFirstClear);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetExtraTexture(int chapterId)
|
||||
{
|
||||
if (_extraChapters.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ChapterExtraData chapterExtraData = _extraChapters.FirstOrDefault((ChapterExtraData n) => n.ExtraTextureChapter == chapterId || n.SectionId == 9003);
|
||||
if (chapterExtraData != null && chapterExtraData.IsChangeBG())
|
||||
{
|
||||
int num = _sectionData.BackGroundId;
|
||||
if (chapterExtraData.IsUseOtherSectionBG())
|
||||
{
|
||||
num = chapterExtraData.BGSectionId;
|
||||
for (int num2 = 0; num2 < _BGTexture.Length; num2++)
|
||||
{
|
||||
_BGTexture[num2].mainTexture = Toolbox.ResourcesManager.LoadObject(GetBackGroundPath(num, num2, isFetch: true)) as Texture;
|
||||
}
|
||||
}
|
||||
foreach (int item in chapterExtraData.ExtraTextureIndex)
|
||||
{
|
||||
_BGTexture[item].mainTexture = Toolbox.ResourcesManager.LoadObject(GetExtraBackGroundPath(num, item, chapterExtraData.BGSuffix, isFetch: true)) as Texture;
|
||||
}
|
||||
_isNormalBgSet = false;
|
||||
}
|
||||
else if (!_isNormalBgSet)
|
||||
{
|
||||
for (int num3 = 0; num3 < _BGTexture.Length; num3++)
|
||||
{
|
||||
_BGTexture[num3].mainTexture = Toolbox.ResourcesManager.LoadObject(GetBackGroundPath(_sectionData.BackGroundId, num3, isFetch: true)) as Texture;
|
||||
}
|
||||
_isNormalBgSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
public List<int> GetChaptersWithDifferentBackgroundFrom(int chapterId)
|
||||
{
|
||||
ChapterExtraData chapterExtra = _extraChapters.FirstOrDefault((ChapterExtraData n) => n.ExtraTextureChapter == chapterId);
|
||||
IEnumerable<ChapterExtraData> source = ((chapterExtra == null || !chapterExtra.IsUseOtherSectionBG()) ? _extraChapters.Where((ChapterExtraData c) => c.IsUseOtherSectionBG() && c.BGSectionId != _sectionData.BackGroundId) : _extraChapters.Where((ChapterExtraData c) => c.BGSectionId != chapterExtra.BGSectionId));
|
||||
return source.Select((ChapterExtraData s) => s.ExtraTextureChapter).ToList();
|
||||
}
|
||||
|
||||
public void SetExtraEffect(int chapterId, bool isFirstClear = false)
|
||||
{
|
||||
if (_extraChapters.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<ChapterExtraData> list = new List<ChapterExtraData>();
|
||||
list = ((BeforeChapterId >= chapterId) ? _extraChapters.FindAll((ChapterExtraData n) => n.ExtraTextureChapter != 0 && BeforeChapterId > n.ExtraTextureChapter && n.ExtraTextureChapter >= chapterId) : _extraChapters.FindAll((ChapterExtraData n) => n.ExtraTextureChapter != 0 && BeforeChapterId <= n.ExtraTextureChapter && n.ExtraTextureChapter < chapterId));
|
||||
BeforeChapterId = chapterId;
|
||||
TransitionChapterExtraData = list.FirstOrDefault((ChapterExtraData n) => n.ExtraEffect != null);
|
||||
if (isFirstClear)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (list.Count() == 0 || TransitionChapterExtraData == null)
|
||||
{
|
||||
_changeChapterFirstCall = false;
|
||||
return;
|
||||
}
|
||||
TransitionChapterExtraData = list.FirstOrDefault((ChapterExtraData n) => n.ExtraEffect != null);
|
||||
if (!_changeChapterFirstCall && TransitionChapterExtraData != null && TransitionChapterExtraData.ExtraEffect != null)
|
||||
{
|
||||
TransitionChapterExtraData.ExtraEffect.SetActive(value: false);
|
||||
TransitionChapterExtraData.ExtraEffect.SetActive(value: true);
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(TransitionChapterExtraData.SeType);
|
||||
}
|
||||
_changeChapterFirstCall = false;
|
||||
}
|
||||
|
||||
public IEnumerator SetClearEffect()
|
||||
{
|
||||
ChapterExtraData clearChapterExtraData = _extraChapters.FirstOrDefault((ChapterExtraData n) => n.ExtraTextureChapter == BeforeChapterId);
|
||||
if (clearChapterExtraData != null && clearChapterExtraData.FirstClearEffect != null)
|
||||
{
|
||||
yield return new WaitForSeconds(clearChapterExtraData.FirstClearEffectDelayTime);
|
||||
clearChapterExtraData.FirstClearEffect.SetActive(value: false);
|
||||
clearChapterExtraData.FirstClearEffect.SetActive(value: true);
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(clearChapterExtraData.FirstClearSeType);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetupEnd()
|
||||
{
|
||||
if (_bgEffectControl != null)
|
||||
{
|
||||
_bgEffectControl.SetupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDragBG(GameObject obj, Vector2 delta)
|
||||
{
|
||||
_BGDragSecResetUpToTime = 0.5f;
|
||||
_BGDragSec += Time.deltaTime;
|
||||
_BGDragSec = Mathf.Min(_BGDragSec, 0.25f);
|
||||
float t = Mathf.Clamp(_BGDragSec / 0.25f, 0f, 1f);
|
||||
_BGDragDelta = Vector2.Lerp(Vector2.zero, delta, t);
|
||||
}
|
||||
|
||||
public void UpdateBGDrag()
|
||||
{
|
||||
if (_BGDragSecResetUpToTime > 0f)
|
||||
{
|
||||
_BGDragSecResetUpToTime -= Time.deltaTime;
|
||||
if (_BGDragSecResetUpToTime < 0f)
|
||||
{
|
||||
_BGDragSecResetUpToTime = 0f;
|
||||
_BGDragSec = 0f;
|
||||
}
|
||||
}
|
||||
if (!Mathf.Approximately(_BGDragDelta.x, 0f) || !Mathf.Approximately(_BGDragDelta.y, 0f))
|
||||
{
|
||||
_BGDragDelta.x *= 0.875f;
|
||||
_BGDragDelta.y *= 0.875f;
|
||||
Vector3 localPosition = _BGRoot.transform.localPosition;
|
||||
localPosition.x += _BGDragDelta.x;
|
||||
localPosition.y += _BGDragDelta.y;
|
||||
UpdateMovableRange();
|
||||
localPosition.x = Mathf.Clamp(localPosition.x, _minMovablePos.x, _maxMovablePos.x);
|
||||
localPosition.y = Mathf.Clamp(localPosition.y, _minMovablePos.y, _maxMovablePos.y);
|
||||
_BGRoot.transform.localPosition = localPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBGDragEnable(bool enable)
|
||||
{
|
||||
_isBGDragEnable = enable;
|
||||
_BGDragCollision.SetActive(enable);
|
||||
_BGDragDelta = Vector2.zero;
|
||||
}
|
||||
|
||||
private void UpdateMovableRange()
|
||||
{
|
||||
float num = (float)Screen.width / (float)Screen.height;
|
||||
Vector3 localScale = _BGRoot.transform.localScale;
|
||||
Vector3 localPosition = _BGRoot.transform.parent.transform.localPosition;
|
||||
if (!(localScale == _currentBGScale) || num != _currentAspectRatio || !(localPosition == _currentParentPos))
|
||||
{
|
||||
float num2 = UIManager.GetInstance().UIManagerRoot.manualHeight;
|
||||
if (1.7777778f > num)
|
||||
{
|
||||
num2 *= 1.7777778f / num;
|
||||
}
|
||||
float num3 = num2 * num * 0.5f;
|
||||
float num4 = num2 * 0.5f;
|
||||
float num5 = 2048f * localScale.x;
|
||||
float num6 = 2560f * localScale.x;
|
||||
float num7 = 1536f * localScale.y;
|
||||
float num8 = 1536f * localScale.y;
|
||||
_minMovablePos.x = 0f - num6 + num3 + 5f - localPosition.x;
|
||||
_maxMovablePos.x = num5 - num3 - 5f - localPosition.x;
|
||||
_minMovablePos.y = 0f - num7 + num4 + 5f - localPosition.y;
|
||||
_maxMovablePos.y = num8 - num4 - 5f - localPosition.y;
|
||||
_currentAspectRatio = num;
|
||||
_currentBGScale = localScale;
|
||||
_currentParentPos = localPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
136
SVSim.BattleEngine/Engine/AreaSelectChapterEffect.cs
Normal file
136
SVSim.BattleEngine/Engine/AreaSelectChapterEffect.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Collections.Generic;
|
||||
using Cute;
|
||||
using UnityEngine;
|
||||
|
||||
public class AreaSelectChapterEffect
|
||||
{
|
||||
private static readonly Vector3 EFFECT_SCALE = new Vector3(320f, 320f, 320f);
|
||||
|
||||
private AreaSelectUI _areaSelectUI;
|
||||
|
||||
private List<string> _loadedResources = new List<string>();
|
||||
|
||||
private bool _isLoadEnd = true;
|
||||
|
||||
private Transform _effectParent;
|
||||
|
||||
private Dictionary<string, ParticleSystem> _effectList = new Dictionary<string, ParticleSystem>();
|
||||
|
||||
private string _playingEffect = "";
|
||||
|
||||
public bool GetLoadEnd()
|
||||
{
|
||||
return _isLoadEnd;
|
||||
}
|
||||
|
||||
public void Init(AreaSelectUI areaselectUI, Transform effectParent)
|
||||
{
|
||||
_areaSelectUI = areaselectUI;
|
||||
_effectParent = effectParent;
|
||||
_playingEffect = "";
|
||||
}
|
||||
|
||||
public void Term()
|
||||
{
|
||||
foreach (KeyValuePair<string, ParticleSystem> effect in _effectList)
|
||||
{
|
||||
ParticleSystem value = effect.Value;
|
||||
if (!(null == value))
|
||||
{
|
||||
Object.Destroy(value.gameObject);
|
||||
}
|
||||
}
|
||||
_effectList.Clear();
|
||||
_playingEffect = "";
|
||||
}
|
||||
|
||||
public void LoadEffect(List<StoryChapterData> chapterDataList)
|
||||
{
|
||||
_isLoadEnd = false;
|
||||
string path = "";
|
||||
int i = 0;
|
||||
for (int count = chapterDataList.Count; i < count; i++)
|
||||
{
|
||||
path = chapterDataList[i].ChapterEffectPath;
|
||||
if (!string.IsNullOrEmpty(path) && !_effectList.ContainsKey(path))
|
||||
{
|
||||
_effectList.Add(path, null);
|
||||
_loadedResources.Add(Toolbox.ResourcesManager.GetAssetTypePath(path, ResourcesManager.AssetLoadPathType.Effect2D));
|
||||
}
|
||||
}
|
||||
_areaSelectUI.StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(_loadedResources, delegate
|
||||
{
|
||||
List<GameObject> list = new List<GameObject>(_effectList.Count);
|
||||
int j = 0;
|
||||
for (int count2 = chapterDataList.Count; j < count2; j++)
|
||||
{
|
||||
path = chapterDataList[j].ChapterEffectPath;
|
||||
if (!string.IsNullOrEmpty(path) && !(null != _effectList[path]))
|
||||
{
|
||||
GameObject gameObject = Toolbox.ResourcesManager.LoadObject(Toolbox.ResourcesManager.GetAssetTypePath(path, ResourcesManager.AssetLoadPathType.Effect2D, isfetch: true)) as GameObject;
|
||||
if (!(null == gameObject))
|
||||
{
|
||||
_effectList[path] = Object.Instantiate(gameObject).GetComponent<ParticleSystem>();
|
||||
_effectList[path].transform.parent = _effectParent;
|
||||
_effectList[path].transform.localPosition = Vector3.zero;
|
||||
_effectList[path].transform.localScale = EFFECT_SCALE;
|
||||
_effectList[path].gameObject.SetActive(value: false);
|
||||
list.Add(_effectList[path].gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
_loadedResources.AddRange(GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(list, delegate
|
||||
{
|
||||
_isLoadEnd = true;
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
public void UnLoadEffect()
|
||||
{
|
||||
Toolbox.ResourcesManager.RemoveAssetGroup(_loadedResources);
|
||||
_loadedResources.Clear();
|
||||
}
|
||||
|
||||
public void PlayEffect(string path, Vector3 pos)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(path) && !(_playingEffect == path) && _effectList.ContainsKey(path) && !(null == _effectList[path]))
|
||||
{
|
||||
_effectList[path].gameObject.SetActive(value: true);
|
||||
_effectList[path].Play();
|
||||
_effectList[path].transform.localPosition = pos;
|
||||
_playingEffect = path;
|
||||
SetParticleSystemsSpeed(1f);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopEffect(float? simulationSpeedAfterStop)
|
||||
{
|
||||
if (_effectList.ContainsKey(_playingEffect) && !(null == _effectList[_playingEffect]))
|
||||
{
|
||||
if (simulationSpeedAfterStop.HasValue)
|
||||
{
|
||||
SetParticleSystemsSpeed(simulationSpeedAfterStop.Value);
|
||||
}
|
||||
_effectList[_playingEffect].Stop();
|
||||
_playingEffect = "";
|
||||
}
|
||||
}
|
||||
|
||||
public string GetPlayingEffect()
|
||||
{
|
||||
return _playingEffect;
|
||||
}
|
||||
|
||||
private void SetParticleSystemsSpeed(float speed)
|
||||
{
|
||||
ParticleSystem.MainModule main = _effectList[_playingEffect].main;
|
||||
main.simulationSpeed = speed;
|
||||
ParticleSystem[] componentsInChildren = _effectList[_playingEffect].GetComponentsInChildren<ParticleSystem>();
|
||||
for (int i = 0; i < componentsInChildren.Length; i++)
|
||||
{
|
||||
ParticleSystem.MainModule main2 = componentsInChildren[i].main;
|
||||
main2.simulationSpeed = speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
SVSim.BattleEngine/Engine/AreaSelectEffectControlBase.cs
Normal file
27
SVSim.BattleEngine/Engine/AreaSelectEffectControlBase.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class AreaSelectEffectControlBase : MonoBehaviour
|
||||
{
|
||||
[NonSerialized]
|
||||
public Dictionary<int, ParticleSystem> _backGroundEffects = new Dictionary<int, ParticleSystem>();
|
||||
|
||||
[NonSerialized]
|
||||
public readonly int BASE_EFFECT_INDEX = -1;
|
||||
|
||||
protected bool IsSetupEnd { get; private set; }
|
||||
|
||||
public virtual void SetupEnd()
|
||||
{
|
||||
IsSetupEnd = true;
|
||||
}
|
||||
|
||||
public virtual void OnChangeSelectChapter(AreaSelectBG areaSelectBG, StorySectionData sectionData, int? sectionClassId, StoryChapterData chapterData, bool isFirstClear)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void SetActiveBGEffect(bool effect)
|
||||
{
|
||||
}
|
||||
}
|
||||
138
SVSim.BattleEngine/Engine/AreaSelectMapIcon.cs
Normal file
138
SVSim.BattleEngine/Engine/AreaSelectMapIcon.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class AreaSelectMapIcon
|
||||
{
|
||||
public const int MAPICONLIST_CAPACITY_DEFAULT = 8;
|
||||
|
||||
private readonly string MAP_ICON_EFFECT_NAME_CIRCLE4 = "ef_circle4_add_nor_1";
|
||||
|
||||
private readonly string MAP_ICON_EFFECT_NAME_TWINKLE1 = "ef_twinkle1_add_nor_1";
|
||||
|
||||
private readonly Color32 MAP_ICON_EFFECT_COLOR_CLEARED = new Color32(byte.MaxValue, 192, 64, byte.MaxValue);
|
||||
|
||||
private readonly Color32 MAP_ICON_EFFECT_COLOR_ALREADY_READ_CIRCLE4 = new Color32(78, 95, 125, byte.MaxValue);
|
||||
|
||||
private readonly Color32 MAP_ICON_EFFECT_COLOR_ALREADY_READ_TWINKLE1 = new Color32(190, 218, 242, byte.MaxValue);
|
||||
|
||||
[SerializeField]
|
||||
private GameObject MapIconRoot;
|
||||
|
||||
[SerializeField]
|
||||
private UISprite MapIconOriginal;
|
||||
|
||||
private List<UISprite> MapIconList;
|
||||
|
||||
private GameObject MapIconEffect;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
}
|
||||
|
||||
public void Term()
|
||||
{
|
||||
MapIconEffect = null;
|
||||
GameMgr.GetIns().GetEffectMgr().Stop(EffectMgr.EffectType.CMN_MAP_MAPICON_CLEARED);
|
||||
GameMgr.GetIns().GetEffectMgr().Stop(EffectMgr.EffectType.CMN_MAP_MAPICON_NOTCLEARED);
|
||||
}
|
||||
|
||||
public void SetupMapIcon(List<StoryChapterData> chapterDataList)
|
||||
{
|
||||
if (MapIconList != null)
|
||||
{
|
||||
int i = 0;
|
||||
for (int count = MapIconList.Count; i < count; i++)
|
||||
{
|
||||
UnityEngine.Object.Destroy(MapIconList[i].gameObject);
|
||||
}
|
||||
}
|
||||
MapIconList = new List<UISprite>(8);
|
||||
UISprite uISprite = null;
|
||||
for (int j = 0; j < chapterDataList.Count; j++)
|
||||
{
|
||||
if (chapterDataList[j].IsReleased)
|
||||
{
|
||||
uISprite = UnityEngine.Object.Instantiate(MapIconOriginal);
|
||||
if (!(null == uISprite))
|
||||
{
|
||||
uISprite.transform.parent = MapIconRoot.transform;
|
||||
uISprite.transform.localPosition = new Vector3(chapterDataList[j].MapIconPos.x, chapterDataList[j].MapIconPos.y);
|
||||
uISprite.transform.localScale = Vector3.one;
|
||||
uISprite.name = "mapicon_" + j;
|
||||
uISprite.gameObject.SetActive(chapterDataList[j].IsDisplayMapIcon);
|
||||
MapIconList.Add(uISprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
MapIconOriginal.gameObject.SetActive(value: false);
|
||||
}
|
||||
|
||||
public Vector3 GetMapIconPos(int chapterIndex, bool isLocal)
|
||||
{
|
||||
if (MapIconList == null)
|
||||
{
|
||||
return Vector3.zero;
|
||||
}
|
||||
if (chapterIndex < 0 || chapterIndex >= MapIconList.Count)
|
||||
{
|
||||
return Vector3.zero;
|
||||
}
|
||||
if (!isLocal)
|
||||
{
|
||||
return MapIconList[chapterIndex].transform.position;
|
||||
}
|
||||
return MapIconList[chapterIndex].transform.localPosition;
|
||||
}
|
||||
|
||||
public void SetActiveMapIcon(int chapterIndex, bool isActive)
|
||||
{
|
||||
if (chapterIndex >= 0 && chapterIndex < MapIconList.Count)
|
||||
{
|
||||
MapIconList[chapterIndex].gameObject.SetActive(isActive);
|
||||
}
|
||||
}
|
||||
|
||||
public void StartMapIconEffect(StoryChapterData.ChapterClearStatus clearState, int chapterIndex)
|
||||
{
|
||||
Effect effect = null;
|
||||
switch (clearState)
|
||||
{
|
||||
case StoryChapterData.ChapterClearStatus.Cleared:
|
||||
effect = GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_MAP_MAPICON_CLEARED);
|
||||
effect.ChangeParticleColor(MAP_ICON_EFFECT_COLOR_CLEARED);
|
||||
break;
|
||||
case StoryChapterData.ChapterClearStatus.AlreadyRead:
|
||||
effect = GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_MAP_MAPICON_CLEARED);
|
||||
MotionUtils.ChangeParticleSystemColor(effect.transform.Find(MAP_ICON_EFFECT_NAME_CIRCLE4).gameObject, MAP_ICON_EFFECT_COLOR_ALREADY_READ_CIRCLE4);
|
||||
MotionUtils.ChangeParticleSystemColor(effect.transform.Find(MAP_ICON_EFFECT_NAME_TWINKLE1).gameObject, MAP_ICON_EFFECT_COLOR_ALREADY_READ_TWINKLE1);
|
||||
break;
|
||||
default:
|
||||
effect = GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_MAP_MAPICON_NOTCLEARED);
|
||||
break;
|
||||
}
|
||||
MapIconEffect = effect.GetGameObjIns();
|
||||
UpdateMapIconEffectPos(chapterIndex);
|
||||
}
|
||||
|
||||
public void StopMapIconEffect()
|
||||
{
|
||||
GameMgr.GetIns().GetEffectMgr().Stop(EffectMgr.EffectType.CMN_MAP_MAPICON_CLEARED);
|
||||
GameMgr.GetIns().GetEffectMgr().Stop(EffectMgr.EffectType.CMN_MAP_MAPICON_NOTCLEARED);
|
||||
}
|
||||
|
||||
public void UpdateMapIconEffectPos(int chapterIndex)
|
||||
{
|
||||
if (MapIconList != null && chapterIndex >= 0 && chapterIndex < MapIconList.Count && !(null == MapIconEffect))
|
||||
{
|
||||
Vector3 position = MapIconList[chapterIndex].transform.position;
|
||||
MapIconEffect.transform.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
public GameObject GetMapIconObject(int chapterIndex)
|
||||
{
|
||||
return MapIconList[chapterIndex].gameObject;
|
||||
}
|
||||
}
|
||||
1406
SVSim.BattleEngine/Engine/AreaSelectUI.cs
Normal file
1406
SVSim.BattleEngine/Engine/AreaSelectUI.cs
Normal file
File diff suppressed because it is too large
Load Diff
39
SVSim.BattleEngine/Engine/AreaSelectUtility.cs
Normal file
39
SVSim.BattleEngine/Engine/AreaSelectUtility.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class AreaSelectUtility
|
||||
{
|
||||
public const string BTN_IMAGE_NAME_SUFFIX_OFF = "{0}_off";
|
||||
|
||||
public const string BTN_IMAGE_NAME_SUFFIX_ON = "{0}_on";
|
||||
|
||||
public static readonly string ChapterSelectBtnPathPrefix = "btn_story_select";
|
||||
|
||||
public static readonly Color32 CLEAR_LABEL_COLOR_CLEARD_GRADIENT_TOP = new Color32(byte.MaxValue, 245, 161, byte.MaxValue);
|
||||
|
||||
public static readonly Color32 CLEAR_LABEL_COLOR_CLEARD_GRADIENT_BUTTOM = new Color32(byte.MaxValue, 209, 71, byte.MaxValue);
|
||||
|
||||
public static readonly Color32 CLEAR_LABEL_COLOR_CLEARD_EFFECT_OUTLINE8 = new Color32(94, 67, 31, byte.MaxValue);
|
||||
|
||||
public static readonly Color32 CLEAR_LABEL_COLOR_ALREADY_READ_GRADIENT_TOP = new Color32(245, 249, byte.MaxValue, byte.MaxValue);
|
||||
|
||||
public static readonly Color32 CLEAR_LABEL_COLOR_ALREADY_READ_GRADIENT_BUTTOM = new Color32(190, 218, 242, byte.MaxValue);
|
||||
|
||||
public static readonly Color32 CLEAR_LABEL_COLOR_ALREADY_READ_EFFECT_OUTLINE8 = new Color32(60, 73, 96, byte.MaxValue);
|
||||
|
||||
public static void SetClearLabelColor(UILabel clearLabel, StoryChapterData.ChapterClearStatus clearState)
|
||||
{
|
||||
switch (clearState)
|
||||
{
|
||||
case StoryChapterData.ChapterClearStatus.Cleared:
|
||||
clearLabel.gradientTop = CLEAR_LABEL_COLOR_CLEARD_GRADIENT_TOP;
|
||||
clearLabel.gradientBottom = CLEAR_LABEL_COLOR_CLEARD_GRADIENT_BUTTOM;
|
||||
clearLabel.effectColor = CLEAR_LABEL_COLOR_CLEARD_EFFECT_OUTLINE8;
|
||||
break;
|
||||
case StoryChapterData.ChapterClearStatus.AlreadyRead:
|
||||
clearLabel.gradientTop = CLEAR_LABEL_COLOR_ALREADY_READ_GRADIENT_TOP;
|
||||
clearLabel.gradientBottom = CLEAR_LABEL_COLOR_ALREADY_READ_GRADIENT_BUTTOM;
|
||||
clearLabel.effectColor = CLEAR_LABEL_COLOR_ALREADY_READ_EFFECT_OUTLINE8;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
346
SVSim.BattleEngine/Engine/ArenaColosseum.cs
Normal file
346
SVSim.BattleEngine/Engine/ArenaColosseum.cs
Normal file
@@ -0,0 +1,346 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
using Wizard.Scripts.Network.Data.TableData.Arena.TwoPick;
|
||||
using Wizard.Scripts.Network.Data.TaskData.Arena.TwoPick;
|
||||
|
||||
public class ArenaColosseum : ArenaEntryDataBase
|
||||
{
|
||||
public enum eRound
|
||||
{
|
||||
FinalNotAdvance = -1,
|
||||
Round1 = 1,
|
||||
Round2B = 2,
|
||||
Round2A = 3,
|
||||
FinalB = 4,
|
||||
FinalA = 5,
|
||||
FinalMin = 4,
|
||||
RoundMax = 5,
|
||||
Undecided = 6,
|
||||
Lose = 7
|
||||
}
|
||||
|
||||
public enum eStageNo
|
||||
{
|
||||
Stage1 = 1,
|
||||
Stage2,
|
||||
FinalStage,
|
||||
Max
|
||||
}
|
||||
|
||||
public enum eEntryStatus
|
||||
{
|
||||
TwoPickClassSelect = 1,
|
||||
TwoPickCardSelect,
|
||||
SetUpComplete
|
||||
}
|
||||
|
||||
public enum eRule
|
||||
{
|
||||
NONE = 0,
|
||||
RotationBo1 = 1,
|
||||
UnlimitedBo1 = 2,
|
||||
TwoPick = 3,
|
||||
TwoPickChaos = 4,
|
||||
Crossover = 5,
|
||||
MyRotation = 6,
|
||||
HOF = 31,
|
||||
WindFall = 33,
|
||||
Avatar = 39
|
||||
}
|
||||
|
||||
public enum eDeckIndex
|
||||
{
|
||||
Main = 0,
|
||||
First = 0,
|
||||
Second = 1,
|
||||
Third = 2
|
||||
}
|
||||
|
||||
public struct Detail
|
||||
{
|
||||
public string RoundTimeText { get; set; }
|
||||
|
||||
public string RoundTimeStartText { get; set; }
|
||||
|
||||
public string RoundTimeEndText { get; set; }
|
||||
|
||||
public string GroupName { get; set; }
|
||||
|
||||
public int MaxBattleNum { get; set; }
|
||||
|
||||
public int BreakThroughNum { get; set; }
|
||||
|
||||
public int MaxEntryNum { get; set; }
|
||||
}
|
||||
|
||||
public class TwoPick
|
||||
{
|
||||
public CandidateClass CandidateClass { get; set; }
|
||||
|
||||
public CandidateCardInfo CandidateCard { get; set; }
|
||||
|
||||
public Deck DeckData { get; set; }
|
||||
|
||||
public CandidateChaos CandidateChaos { get; set; }
|
||||
}
|
||||
|
||||
public enum eResultEffect
|
||||
{
|
||||
None,
|
||||
GroupA,
|
||||
Final,
|
||||
Clear
|
||||
}
|
||||
|
||||
private bool _isRankMatching;
|
||||
|
||||
public bool CanUseNonPossessionCard;
|
||||
|
||||
public int DeckEntryId { get; set; }
|
||||
|
||||
public bool IsColosseumPeriod { get; set; }
|
||||
|
||||
public bool IsRoundPeriod { get; set; }
|
||||
|
||||
public eEntryStatus EntryStatus { get; set; }
|
||||
|
||||
public Format DeckFormat { get; set; }
|
||||
|
||||
public eRule Rule { get; set; }
|
||||
|
||||
public bool IsNormalTwoPick { get; set; }
|
||||
|
||||
public int ChaosNum { get; set; }
|
||||
|
||||
public bool IsTwoPickRule
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Rule != eRule.TwoPick)
|
||||
{
|
||||
return Rule == eRule.TwoPickChaos;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool NeedsFirstTips { get; set; }
|
||||
|
||||
public int ColosseumId { get; set; }
|
||||
|
||||
public int ChaoseTipsId { get; set; }
|
||||
|
||||
public bool IsSpecialDeckSelectRule
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Rule != eRule.HOF)
|
||||
{
|
||||
return Rule == eRule.WindFall;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDeckMaxNumberChange => Rule == eRule.WindFall;
|
||||
|
||||
public int DeckMaxNumber
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Rule == eRule.WindFall)
|
||||
{
|
||||
return 35;
|
||||
}
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRankMatching
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isRankMatching;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_isRankMatching != value)
|
||||
{
|
||||
_isRankMatching = value;
|
||||
if (RealTimeNetworkAgent.FinishTaskBase != null)
|
||||
{
|
||||
RealTimeNetworkAgent.FinishTaskBase = new ColosseumBattleFinishTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<DeckData> DeckList { get; set; }
|
||||
|
||||
public eRound Round { get; set; }
|
||||
|
||||
public int ServerRoundId { get; set; }
|
||||
|
||||
public eStageNo StageNo { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<ReceivedReward> RewardList { get; set; }
|
||||
|
||||
public eResultEffect ResultEffect { get; set; }
|
||||
|
||||
public string ColorCodeId { get; set; }
|
||||
|
||||
public string CardPool { get; set; }
|
||||
|
||||
public int RetryRemainingNum { get; set; }
|
||||
|
||||
public int BattleMax { get; set; }
|
||||
|
||||
public int ClearWinNum { get; set; }
|
||||
|
||||
public bool IsDeckEntry { get; set; }
|
||||
|
||||
public bool IsFreeEntry
|
||||
{
|
||||
get
|
||||
{
|
||||
return Data.MyPageNotifications.data.IsColosseumFreeEntry;
|
||||
}
|
||||
set
|
||||
{
|
||||
Data.MyPageNotifications.data.IsColosseumFreeEntry = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRetry { get; set; }
|
||||
|
||||
public bool IsLastDay { get; set; }
|
||||
|
||||
public bool IsClear { get; set; }
|
||||
|
||||
public bool IsRetire { get; set; }
|
||||
|
||||
public bool IsFinish { get; set; }
|
||||
|
||||
public bool IsDeckDeleted { get; set; }
|
||||
|
||||
public bool IsFinalRoundTry { get; set; }
|
||||
|
||||
public eRound NextRound { get; set; }
|
||||
|
||||
public double RemainingUnixTime { get; set; }
|
||||
|
||||
public float RemainingSinceTime { get; set; }
|
||||
|
||||
public double RemainingServerUnixTime { get; set; }
|
||||
|
||||
public string NextRoundStartTimeText { get; set; }
|
||||
|
||||
public string NowRoundTimeText { get; set; }
|
||||
|
||||
public string ColosseumTimeText { get; set; }
|
||||
|
||||
public string AnnounceNo { get; set; }
|
||||
|
||||
public Detail[] DetailData { get; set; }
|
||||
|
||||
public eStageNo FocusStageNo { get; set; }
|
||||
|
||||
public int WinBattleNum { get; set; }
|
||||
|
||||
public List<bool> BattleResultList { get; set; }
|
||||
|
||||
public List<int> BoxGradeList { get; set; }
|
||||
|
||||
public int FinalRoundEliminateCount { get; set; }
|
||||
|
||||
public TwoPick TwoPickData { get; set; }
|
||||
|
||||
public ArenaColosseum()
|
||||
{
|
||||
base.LootBoxType = PlayerStaticData.LootBoxType.COLOSSEUM;
|
||||
DeckList = new List<DeckData>();
|
||||
RewardList = new List<ReceivedReward>();
|
||||
BattleResultList = new List<bool>();
|
||||
BoxGradeList = new List<int>();
|
||||
DetailData = new Detail[5];
|
||||
Rule = eRule.TwoPick;
|
||||
TwoPickData = new TwoPick();
|
||||
TwoPickData.CandidateClass = new CandidateClass();
|
||||
TwoPickData.CandidateCard = new CandidateCardInfo();
|
||||
TwoPickData.CandidateChaos = new CandidateChaos();
|
||||
}
|
||||
|
||||
public int GetRoundNumber(eRound inRound)
|
||||
{
|
||||
switch (inRound)
|
||||
{
|
||||
case eRound.Round1:
|
||||
return 1;
|
||||
case eRound.Round2B:
|
||||
case eRound.Round2A:
|
||||
return 2;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetGroupText(eRound inRound)
|
||||
{
|
||||
switch (inRound)
|
||||
{
|
||||
case eRound.Round2A:
|
||||
case eRound.FinalA:
|
||||
return Data.SystemText.Get("Colosseum_0020");
|
||||
case eRound.Round2B:
|
||||
case eRound.FinalB:
|
||||
return Data.SystemText.Get("Colosseum_0021");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public eStageNo GetStageNoFromRoundId(eRound inRoundId)
|
||||
{
|
||||
switch (inRoundId)
|
||||
{
|
||||
case eRound.Round1:
|
||||
return eStageNo.Stage1;
|
||||
case eRound.Round2B:
|
||||
case eRound.Round2A:
|
||||
return eStageNo.Stage2;
|
||||
case eRound.FinalB:
|
||||
case eRound.FinalA:
|
||||
return eStageNo.FinalStage;
|
||||
default:
|
||||
return eStageNo.Stage1;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsFinalRound()
|
||||
{
|
||||
if (Round == eRound.FinalA || Round == eRound.FinalB)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public DialogBase CreateDetailDialog(GameObject defaultDetailPrefab)
|
||||
{
|
||||
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
|
||||
GameObject gameObject = Object.Instantiate(defaultDetailPrefab);
|
||||
dialogBase.SetObj(gameObject);
|
||||
gameObject.GetComponent<ColosseumDetail>().Init(dialogBase);
|
||||
return dialogBase;
|
||||
}
|
||||
|
||||
public void ApiRuleParseAndSet(int apiRule)
|
||||
{
|
||||
ArenaColosseum colosseumData = Data.ArenaData.ColosseumData;
|
||||
colosseumData.Rule = (eRule)apiRule;
|
||||
colosseumData.DeckFormat = ArenaData.ApiDeckFormatParse(colosseumData.Rule);
|
||||
}
|
||||
}
|
||||
206
SVSim.BattleEngine/Engine/ArenaCompetition.cs
Normal file
206
SVSim.BattleEngine/Engine/ArenaCompetition.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LitJson;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
using Wizard.Scripts.Network.Data.TaskData.Arena;
|
||||
|
||||
public class ArenaCompetition : ArenaEntryDataBase
|
||||
{
|
||||
public enum EntryStatusType
|
||||
{
|
||||
NotEntry,
|
||||
NotChallenge,
|
||||
NotRegistDeck,
|
||||
InBattle
|
||||
}
|
||||
|
||||
public enum FreebieStatusType
|
||||
{
|
||||
InFreeBattle,
|
||||
CanPermanentEntry,
|
||||
PermanentEntryDone
|
||||
}
|
||||
|
||||
public enum EntryCostType
|
||||
{
|
||||
EntryWithFree,
|
||||
EntryWithCost
|
||||
}
|
||||
|
||||
private bool _isRankMatching;
|
||||
|
||||
public bool IsCompetitionPeriod { get; private set; }
|
||||
|
||||
public bool IsEntry { get; set; }
|
||||
|
||||
public bool IsInFreeBattleRegistDeck { get; set; }
|
||||
|
||||
public bool NeedsFirstTips { get; private set; }
|
||||
|
||||
public int CompetitionId { get; private set; }
|
||||
|
||||
public FreebieStatusType FreebieStatus { get; set; }
|
||||
|
||||
public Format DeckFormat { get; private set; }
|
||||
|
||||
public ArenaColosseum.eRule Rule { get; private set; }
|
||||
|
||||
public bool IsSpecialMode { get; private set; }
|
||||
|
||||
public string NowRoundTimeText { get; private set; }
|
||||
|
||||
public string EntryEndTimeText { get; private set; }
|
||||
|
||||
public string EndTimeText { get; private set; }
|
||||
|
||||
public double EntryRemainingUnixTime { get; set; }
|
||||
|
||||
public double RemainingUnixTime { get; set; }
|
||||
|
||||
public float RemainingSinceTime { get; set; }
|
||||
|
||||
public double RemainingServerUnixTime { get; set; }
|
||||
|
||||
public string EntryTimeText { get; private set; }
|
||||
|
||||
public List<DeckData> DeckList { get; set; }
|
||||
|
||||
public List<Wizard.Scripts.Network.Data.TaskData.Arena.Reward> EntryRewardList { get; set; }
|
||||
|
||||
public bool IsRewardReceived { get; private set; }
|
||||
|
||||
public string AnnounceId { get; private set; }
|
||||
|
||||
public string CompetitionName { get; private set; }
|
||||
|
||||
public int MaxEntryCount { get; private set; }
|
||||
|
||||
public int MaxChallengeCount { get; private set; }
|
||||
|
||||
public int MaxWinCount { get; private set; }
|
||||
|
||||
public int BestWinCount { get; private set; }
|
||||
|
||||
public int MaxLoseCount { get; private set; }
|
||||
|
||||
public int RestChallangeCount { get; private set; }
|
||||
|
||||
public int RestEntryCount { get; private set; }
|
||||
|
||||
public int CurrentWinCount { get; private set; }
|
||||
|
||||
public int FreebieChallengeCount { get; private set; }
|
||||
|
||||
public bool IsChampion { get; private set; }
|
||||
|
||||
public bool IsEntryTimeEnd { get; private set; }
|
||||
|
||||
public int MaxBattleCount { get; private set; }
|
||||
|
||||
public int IsCompletedTwoPickDeck { get; private set; }
|
||||
|
||||
public int MaxFreebieChallengeCount { get; private set; }
|
||||
|
||||
public EntryStatusType EntryStatus { get; private set; }
|
||||
|
||||
public EntryCostType CostType { get; private set; }
|
||||
|
||||
public bool IsRankMatching
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isRankMatching;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_isRankMatching != value)
|
||||
{
|
||||
_isRankMatching = value;
|
||||
if (RealTimeNetworkAgent.FinishTaskBase != null)
|
||||
{
|
||||
RealTimeNetworkAgent.FinishTaskBase = new CompetitionBattleFinishTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ArenaCompetition()
|
||||
{
|
||||
}
|
||||
|
||||
public ArenaCompetition(JsonData responseData)
|
||||
{
|
||||
JsonData jsonData = responseData["data"]["competition_info"];
|
||||
IsCompetitionPeriod = jsonData["is_competition_period"].ToBoolean();
|
||||
if (IsCompetitionPeriod)
|
||||
{
|
||||
Rule = (ArenaColosseum.eRule)jsonData["deck_format"].ToInt();
|
||||
DeckFormat = ArenaData.ApiDeckFormatParse(Rule);
|
||||
IsEntry = jsonData["is_entry"].ToBoolean();
|
||||
IsInFreeBattleRegistDeck = jsonData["is_in_battle"].ToBoolean();
|
||||
IsSpecialMode = jsonData["is_special_mode"].ToInt() == 1;
|
||||
string text = ConvertTime.ToLocal(DateTime.Parse(jsonData["entry_start_time"].ToString()));
|
||||
EntryRemainingUnixTime = ConvertTime.DateTimeToUnixTime(DateTime.Parse(jsonData["entry_end_time"].ToString()));
|
||||
string text2 = ConvertTime.ToLocal(DateTime.Parse(jsonData["entry_end_time"].ToString()));
|
||||
EntryTimeText = Data.SystemText.Get("Colosseum_0033", text, text2);
|
||||
EntryEndTimeText = text2;
|
||||
string text3 = ConvertTime.ToLocal(DateTime.Parse(jsonData["start_time"].ToString()));
|
||||
RemainingUnixTime = ConvertTime.DateTimeToUnixTime(DateTime.Parse(jsonData["end_time"].ToString()));
|
||||
string text4 = ConvertTime.ToLocal(DateTime.Parse(jsonData["end_time"].ToString()));
|
||||
NowRoundTimeText = Data.SystemText.Get("Colosseum_0033", text3, text4);
|
||||
EndTimeText = text4;
|
||||
RemainingSinceTime = Time.realtimeSinceStartup;
|
||||
RemainingServerUnixTime = responseData["data_headers"]["servertime"].ToDouble();
|
||||
NeedsFirstTips = jsonData.GetValueOrDefault("is_display_tips", 0) == 1;
|
||||
CompetitionId = jsonData.GetValueOrDefault("competition_id", 0);
|
||||
FreebieStatus = (FreebieStatusType)jsonData["freebie_status"].ToInt();
|
||||
DeckList = new List<DeckData>();
|
||||
EntryRewardList = new List<Wizard.Scripts.Network.Data.TaskData.Arena.Reward>();
|
||||
JsonData jsonData2 = jsonData["featured_entry_reward_list"];
|
||||
for (int i = 0; i < jsonData2.Count; i++)
|
||||
{
|
||||
Wizard.Scripts.Network.Data.TaskData.Arena.Reward item = new Wizard.Scripts.Network.Data.TaskData.Arena.Reward(jsonData2[i]);
|
||||
EntryRewardList.Add(item);
|
||||
}
|
||||
IsRewardReceived = jsonData["is_received_featured_entry_reward"].ToBoolean();
|
||||
if (jsonData["announce_id"] != null)
|
||||
{
|
||||
AnnounceId = jsonData["announce_id"].ToString();
|
||||
}
|
||||
MaxEntryCount = jsonData.GetValueOrDefault("max_entry_count", 0);
|
||||
MaxChallengeCount = jsonData.GetValueOrDefault("max_challenge_count", 0);
|
||||
MaxWinCount = jsonData.GetValueOrDefault("max_win_count", 0);
|
||||
MaxLoseCount = jsonData.GetValueOrDefault("max_lose_count", 0);
|
||||
MaxBattleCount = jsonData.GetValueOrDefault("max_battle_count", 0);
|
||||
MaxFreebieChallengeCount = jsonData["max_freebie_challenge_count"].ToInt();
|
||||
crystalCost = jsonData.GetValueOrDefault("crystal_cost", 0);
|
||||
rupyCost = jsonData.GetValueOrDefault("rupy_cost", 0);
|
||||
BestWinCount = jsonData["max_win_count_in_entry"].ToInt();
|
||||
RestChallangeCount = jsonData["rest_challenge_num"].ToInt();
|
||||
RestEntryCount = jsonData["rest_entry_num"].ToInt();
|
||||
CurrentWinCount = jsonData["current_win_count"].ToInt();
|
||||
FreebieChallengeCount = jsonData["freebie_challenge_count"].ToInt();
|
||||
EntryStatus = (EntryStatusType)jsonData["entry_status"].ToInt();
|
||||
CostType = (EntryCostType)jsonData["entry_type"].ToInt();
|
||||
IsChampion = jsonData.GetValueOrDefault("is_champion", 0) == 1;
|
||||
CompetitionName = jsonData.GetValueOrDefault("competition_name", string.Empty).Replace("\\n", "\n");
|
||||
double num = RemainingServerUnixTime + (double)Time.realtimeSinceStartup - (double)RemainingSinceTime;
|
||||
IsEntryTimeEnd = EntryRemainingUnixTime - num < 0.0;
|
||||
bool flag = CompetitionId <= PlayerPrefsWrapper.GetValue(PlayerPrefsWrapper.COMPETITION_JOIN_BUTTON_LATEST_ID);
|
||||
Data.MyPageNotifications.data.IsCompetitionBadge = !IsRewardReceived && EntryStatus == EntryStatusType.NotEntry && !IsEntryTimeEnd && !flag;
|
||||
base.ExpirtyInfo = new ShopExpirtyInfo(jsonData["sales_period_info"]);
|
||||
if (DeckFormat == Format.TwoPick)
|
||||
{
|
||||
IsCompletedTwoPickDeck = jsonData["is_completed_two_pick_deck"].ToInt();
|
||||
}
|
||||
}
|
||||
base.LootBoxType = PlayerStaticData.LootBoxType.COMPETITION;
|
||||
}
|
||||
|
||||
public void SetRestChallangeCountByEntry(JsonData responseData)
|
||||
{
|
||||
RestChallangeCount = responseData["rest_challenge_count"].ToInt();
|
||||
IsEntry = true;
|
||||
}
|
||||
}
|
||||
80
SVSim.BattleEngine/Engine/ArenaData.cs
Normal file
80
SVSim.BattleEngine/Engine/ArenaData.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using LitJson;
|
||||
using Wizard;
|
||||
|
||||
public class ArenaData : HeaderData
|
||||
{
|
||||
public enum eARENA_PAY
|
||||
{
|
||||
None = 0,
|
||||
Crystal = 1,
|
||||
Ticket = 3,
|
||||
Rupy = 4,
|
||||
Free = 5
|
||||
}
|
||||
|
||||
public ArenaTwoPickData TwoPickData { get; set; }
|
||||
|
||||
public SealedData SealedData { get; private set; }
|
||||
|
||||
public SealedMyPageResponseData SealedMyPageResponseData { get; private set; }
|
||||
|
||||
public ArenaColosseum ColosseumData { get; set; }
|
||||
|
||||
public ArenaCompetition CompetitionData { get; set; }
|
||||
|
||||
public ArenaData()
|
||||
{
|
||||
SealedData = new SealedData();
|
||||
ColosseumData = new ArenaColosseum();
|
||||
CompetitionData = new ArenaCompetition();
|
||||
}
|
||||
|
||||
public ArenaData(JsonData data)
|
||||
: this()
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
JsonData data2 = data[0];
|
||||
TwoPickData = new ArenaTwoPickData(data2);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearSealedData()
|
||||
{
|
||||
SealedData = new SealedData();
|
||||
}
|
||||
|
||||
public void SetSealedMyPageResponseData(JsonData rootData)
|
||||
{
|
||||
if (rootData.Keys.Contains("sealed_info"))
|
||||
{
|
||||
SealedMyPageResponseData = new SealedMyPageResponseData(rootData["sealed_info"]);
|
||||
}
|
||||
}
|
||||
|
||||
public static Format ApiDeckFormatParse(ArenaColosseum.eRule rule)
|
||||
{
|
||||
Format format = Format.Rotation;
|
||||
switch (rule)
|
||||
{
|
||||
case ArenaColosseum.eRule.RotationBo1:
|
||||
return Format.Rotation;
|
||||
case ArenaColosseum.eRule.UnlimitedBo1:
|
||||
return Format.Unlimited;
|
||||
case ArenaColosseum.eRule.TwoPick:
|
||||
case ArenaColosseum.eRule.TwoPickChaos:
|
||||
return Format.TwoPick;
|
||||
case ArenaColosseum.eRule.HOF:
|
||||
case ArenaColosseum.eRule.WindFall:
|
||||
return Format.Max;
|
||||
case ArenaColosseum.eRule.Crossover:
|
||||
return Format.Crossover;
|
||||
case ArenaColosseum.eRule.MyRotation:
|
||||
return Format.MyRotation;
|
||||
case ArenaColosseum.eRule.Avatar:
|
||||
return Format.Avatar;
|
||||
default:
|
||||
return Format.Max;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
SVSim.BattleEngine/Engine/ArenaEntryBase.cs
Normal file
158
SVSim.BattleEngine/Engine/ArenaEntryBase.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
|
||||
public abstract class ArenaEntryBase : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
protected GameObject ArenaEntryDialog;
|
||||
|
||||
[SerializeField]
|
||||
protected GameObject CompetitionEntryDialog;
|
||||
|
||||
[SerializeField]
|
||||
protected UIButton ButtonEntry;
|
||||
|
||||
[SerializeField]
|
||||
protected UIButton ButtonResume;
|
||||
|
||||
[SerializeField]
|
||||
protected GameObject HeadLineObject;
|
||||
|
||||
private UIWidget[] _headlineWidgetArray;
|
||||
|
||||
private Color[] _headlineWidgetDefaultColorArray;
|
||||
|
||||
private Coroutine _initCoroutine;
|
||||
|
||||
protected bool _isFreeEntry;
|
||||
|
||||
protected bool _isCompetition;
|
||||
|
||||
protected string[] _labelsText;
|
||||
|
||||
protected string _entryDialogTitleText;
|
||||
|
||||
protected Action _initFunc;
|
||||
|
||||
protected Action _resumeFunc;
|
||||
|
||||
protected Func<bool> _isJoinFunc;
|
||||
|
||||
protected Action _freeEntryFunc;
|
||||
|
||||
protected Action _freeBattleFunc;
|
||||
|
||||
protected Func<bool> _isFreeBattleCompetition;
|
||||
|
||||
protected Action _entryFunc;
|
||||
|
||||
private const float GLAY_SCALE = 0.33f;
|
||||
|
||||
protected abstract void EntryDialogCreate(GameObject inDialog);
|
||||
|
||||
protected virtual void EntryBaseInit(GameObject costRootObject)
|
||||
{
|
||||
_headlineWidgetArray = costRootObject.transform.GetComponentsInChildren<UIWidget>();
|
||||
_headlineWidgetDefaultColorArray = new Color[_headlineWidgetArray.Length];
|
||||
for (int i = 0; i < _headlineWidgetArray.Length; i++)
|
||||
{
|
||||
_headlineWidgetDefaultColorArray[i] = _headlineWidgetArray[i].color;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
UpdateMenu();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_initCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_initCoroutine);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateMenu()
|
||||
{
|
||||
_initCoroutine = UIManager.GetInstance().StartCoroutine(_InitCoroutine());
|
||||
}
|
||||
|
||||
protected virtual IEnumerator _InitCoroutine()
|
||||
{
|
||||
while (!MyPageMenu.IsMyPageRequestEnd)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
if (_initFunc != null)
|
||||
{
|
||||
_initFunc();
|
||||
}
|
||||
ButtonEntry.onClick.Clear();
|
||||
ButtonEntry.onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_DECIDE);
|
||||
if (_entryFunc != null)
|
||||
{
|
||||
_entryFunc();
|
||||
}
|
||||
else if (_isFreeEntry)
|
||||
{
|
||||
_freeEntryFunc();
|
||||
}
|
||||
else
|
||||
{
|
||||
DialogBase.Size size = ((Data.ArenaData.CompetitionData.CostType != ArenaCompetition.EntryCostType.EntryWithCost) ? DialogBase.Size.M : DialogBase.Size.XL);
|
||||
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
|
||||
dialogBase.SetSize(size);
|
||||
dialogBase.SetTitleLabel(_entryDialogTitleText);
|
||||
GameObject gameObject = UnityEngine.Object.Instantiate(_isCompetition ? CompetitionEntryDialog : ArenaEntryDialog);
|
||||
EntryDialogCreate(gameObject);
|
||||
if (_isCompetition && Data.ArenaData.CompetitionData.CostType == ArenaCompetition.EntryCostType.EntryWithCost)
|
||||
{
|
||||
PlayerPrefsWrapper.SetValue(PlayerPrefsWrapper.COMPETITION_JOIN_BUTTON_LATEST_ID, Data.ArenaData.CompetitionData.CompetitionId);
|
||||
Data.MyPageNotifications.data.IsCompetitionBadge = false;
|
||||
UIManager.GetInstance()._Footer.UpdateArenaBadgeIcon();
|
||||
UpdateCompetitionEntryBadge();
|
||||
}
|
||||
gameObject.GetComponent<ArenaEntryDialogBase>().ParentDialog = dialogBase;
|
||||
dialogBase.SetObj(gameObject.gameObject);
|
||||
DialogBase.ButtonLayout buttonLayout = DialogBase.ButtonLayout.CloseBtn;
|
||||
dialogBase.SetButtonLayout(buttonLayout);
|
||||
}
|
||||
}));
|
||||
ButtonResume.onClick.Clear();
|
||||
ButtonResume.onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
_resumeFunc();
|
||||
}));
|
||||
UpdateEntryResumeButton();
|
||||
}
|
||||
|
||||
protected virtual void UpdateCompetitionEntryBadge()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void UpdateEntryResumeButton()
|
||||
{
|
||||
bool flag = _isJoinFunc();
|
||||
ButtonEntry.gameObject.SetActive(!flag);
|
||||
ButtonResume.gameObject.SetActive(flag);
|
||||
if (flag)
|
||||
{
|
||||
for (int i = 0; i < _headlineWidgetArray.Length; i++)
|
||||
{
|
||||
_headlineWidgetArray[i].color = new Color(_headlineWidgetDefaultColorArray[i].r * 0.33f, _headlineWidgetDefaultColorArray[i].g * 0.33f, _headlineWidgetDefaultColorArray[i].b * 0.33f, 255f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int j = 0; j < _headlineWidgetArray.Length; j++)
|
||||
{
|
||||
_headlineWidgetArray[j].color = _headlineWidgetDefaultColorArray[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
SVSim.BattleEngine/Engine/ArenaEntryDataBase.cs
Normal file
16
SVSim.BattleEngine/Engine/ArenaEntryDataBase.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Wizard;
|
||||
|
||||
public abstract class ArenaEntryDataBase
|
||||
{
|
||||
public bool isJoin;
|
||||
|
||||
public int crystalCost;
|
||||
|
||||
public int rupyCost;
|
||||
|
||||
public int ticketCost;
|
||||
|
||||
public ShopExpirtyInfo ExpirtyInfo { get; set; }
|
||||
|
||||
public PlayerStaticData.LootBoxType LootBoxType { get; set; }
|
||||
}
|
||||
175
SVSim.BattleEngine/Engine/ArenaEntryDialogBase.cs
Normal file
175
SVSim.BattleEngine/Engine/ArenaEntryDialogBase.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
|
||||
public abstract class ArenaEntryDialogBase : MonoBehaviour
|
||||
{
|
||||
protected Se.TYPE _entryButtonSe = Se.TYPE.SYS_BTN_DECIDE_TRANS;
|
||||
|
||||
private const int CHECK_DIALOG_DEPTH = 10;
|
||||
|
||||
private ArenaEntryDataBase _entryData;
|
||||
|
||||
private ArenaEntryDialogData _dialogData;
|
||||
|
||||
protected ArenaData.eARENA_PAY _payType;
|
||||
|
||||
public bool IsCompetition;
|
||||
|
||||
protected string _mainTextId;
|
||||
|
||||
protected string _ticketSpriteName;
|
||||
|
||||
protected string _arenaNameTextId;
|
||||
|
||||
public DialogBase ParentDialog { get; set; }
|
||||
|
||||
protected abstract void Init();
|
||||
|
||||
protected abstract int GetTicketNum();
|
||||
|
||||
protected abstract ArenaEntryDataBase GetEntryData();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Init();
|
||||
_entryData = GetEntryData();
|
||||
_dialogData = GetComponent<ArenaEntryDialogData>();
|
||||
SystemText systemText = Data.SystemText;
|
||||
if (_dialogData._mainText != null)
|
||||
{
|
||||
_dialogData._mainText.text = systemText.Get(_mainTextId);
|
||||
}
|
||||
if (_dialogData._ticketHaveTitle != null)
|
||||
{
|
||||
_dialogData._ticketHaveTitle.text = systemText.Get("Arena_0037");
|
||||
_dialogData._ticketHaveNum.text = GetTicketNum().ToString();
|
||||
_dialogData._ticketHaveUnit.text = systemText.Get("Common_0117");
|
||||
_dialogData._ticketButtonTitle.text = systemText.Get("Arena_0004");
|
||||
_dialogData._ticketButtonSubTitle.text = systemText.Get("Arena_0038");
|
||||
_dialogData._ticketButtonUseNum.text = _entryData.ticketCost.ToString();
|
||||
_dialogData._ticketSprite.spriteName = _ticketSpriteName;
|
||||
_dialogData._ticketButton.onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
if (ParentDialog.GetNowScene() == DialogBase.DialogScene.WAIT)
|
||||
{
|
||||
OnClickTicketEntryButton();
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (_dialogData._rupyHaveTitle != null)
|
||||
{
|
||||
_dialogData._rupyHaveTitle.text = systemText.Get("Shop_0065");
|
||||
_dialogData._rupyHaveNum.text = PlayerStaticData.UserRupyCount.ToString();
|
||||
_dialogData._rupyHaveUnit.text = systemText.Get("Common_0120");
|
||||
_dialogData._rupyButtonTitle.text = (IsCompetition ? systemText.Get("Competition_0067") : systemText.Get("Arena_0036"));
|
||||
_dialogData._rupyButtonSubTitle.text = systemText.Get("Shop_0062");
|
||||
_dialogData._rupyButtonUseNum.text = _entryData.rupyCost.ToString();
|
||||
_dialogData._rupyButton.onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
if (ParentDialog.GetNowScene() == DialogBase.DialogScene.WAIT)
|
||||
{
|
||||
OnClickRupyEntryButton();
|
||||
}
|
||||
}));
|
||||
}
|
||||
_dialogData._crystalHaveTitle.text = systemText.Get("Shop_0064");
|
||||
_dialogData._crystalHaveNum.text = PlayerStaticData.UserCrystalCount.ToString();
|
||||
_dialogData._crystalHaveUnit.text = systemText.Get("Common_0116");
|
||||
_dialogData._crystalButtonTitle.text = (IsCompetition ? systemText.Get("Competition_0046") : systemText.Get("Arena_0023"));
|
||||
_dialogData._crystalButtonSubTitle.text = systemText.Get("Shop_0061");
|
||||
_dialogData._crystalButtonUseNum.text = _entryData.crystalCost.ToString();
|
||||
if (_entryData.ticketCost > GetTicketNum())
|
||||
{
|
||||
SetButtonDisable(_dialogData._ticketButton, _dialogData._ticketButtonTitle);
|
||||
}
|
||||
if (_entryData.rupyCost > PlayerStaticData.UserRupyCount)
|
||||
{
|
||||
SetButtonDisable(_dialogData._rupyButton, _dialogData._rupyButtonTitle);
|
||||
}
|
||||
_dialogData._crystalButton.onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
if (ParentDialog.GetNowScene() == DialogBase.DialogScene.WAIT)
|
||||
{
|
||||
OnClickCrystalEntryButton();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void SetButtonDisable(UIButton in_Button, UILabel in_Label)
|
||||
{
|
||||
in_Button.GetComponent<UIButton>().isEnabled = false;
|
||||
in_Label.color = LabelDefine.TEXT_COLOR_BUTTON_DISABLE;
|
||||
in_Button.GetComponent<TweenColor>().duration = 0f;
|
||||
}
|
||||
|
||||
private void OnClickTicketEntryButton()
|
||||
{
|
||||
int ticketNum = GetTicketNum();
|
||||
SystemText systemText = Data.SystemText;
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_DECIDE);
|
||||
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
|
||||
dialogBase.SetTitleLabel(systemText.Get("Arena_0005"));
|
||||
dialogBase.SetPanelDepth(10);
|
||||
ArenaBuyDialog component = Object.Instantiate(_dialogData.BuyDialogObject).GetComponent<ArenaBuyDialog>();
|
||||
dialogBase.SetObj(component.gameObject);
|
||||
component.SetTicketConfirmDialog(_entryData.ticketCost, ticketNum, _arenaNameTextId, _ticketSpriteName);
|
||||
dialogBase.onPushButton1 = Entry;
|
||||
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.BlueBtn_CancelBtn);
|
||||
dialogBase.SetButtonText(systemText.Get("Dia_Arena_003_Button"));
|
||||
dialogBase.ClickSe_Btn1 = _entryButtonSe;
|
||||
_payType = ArenaData.eARENA_PAY.Ticket;
|
||||
}
|
||||
|
||||
private void OnClickCrystalEntryButton()
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_DECIDE);
|
||||
if (PlayerStaticData.IsLootBoxRegulation(_entryData.LootBoxType))
|
||||
{
|
||||
LootBoxDialogUtility.CreateLootBoxRegulationDialog(_entryData.LootBoxType);
|
||||
return;
|
||||
}
|
||||
if (_entryData.crystalCost > PlayerStaticData.UserCrystalCount)
|
||||
{
|
||||
ShopCommonUtility.CreateCrystalShortagePopup();
|
||||
return;
|
||||
}
|
||||
int userCrystalCount = PlayerStaticData.UserCrystalCount;
|
||||
SystemText systemText = Data.SystemText;
|
||||
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
|
||||
dialogBase.SetTitleLabel(systemText.Get("Arena_0005"));
|
||||
dialogBase.SetPanelDepth(10);
|
||||
ArenaBuyDialog component = Object.Instantiate(_dialogData.BuyDialogObject).GetComponent<ArenaBuyDialog>();
|
||||
dialogBase.SetObj(component.gameObject);
|
||||
component.SetClystalConfirmDialog(_entryData.crystalCost, userCrystalCount, _arenaNameTextId, _entryData.ExpirtyInfo);
|
||||
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.BlueBtn_CancelBtn);
|
||||
string text_btn = (IsCompetition ? systemText.Get("Competition_0036") : systemText.Get("Dia_Arena_004_Button"));
|
||||
dialogBase.SetButtonText(text_btn);
|
||||
dialogBase.ClickSe_Btn1 = _entryButtonSe;
|
||||
dialogBase.onPushButton1 = Entry;
|
||||
_payType = ArenaData.eARENA_PAY.Crystal;
|
||||
}
|
||||
|
||||
private void OnClickRupyEntryButton()
|
||||
{
|
||||
int userRupyCount = PlayerStaticData.UserRupyCount;
|
||||
SystemText systemText = Data.SystemText;
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_DECIDE);
|
||||
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
|
||||
dialogBase.SetTitleLabel(systemText.Get("Arena_0005"));
|
||||
dialogBase.SetPanelDepth(10);
|
||||
ArenaBuyDialog component = Object.Instantiate(_dialogData.BuyDialogObject).GetComponent<ArenaBuyDialog>();
|
||||
dialogBase.SetObj(component.gameObject);
|
||||
component.SetRupyConfirmDialog(_entryData.rupyCost, userRupyCount, _arenaNameTextId);
|
||||
dialogBase.onPushButton1 = Entry;
|
||||
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.BlueBtn_CancelBtn);
|
||||
string text_btn = (IsCompetition ? systemText.Get("Competition_0036") : systemText.Get("Dia_Arena_005_Button"));
|
||||
dialogBase.SetButtonText(text_btn);
|
||||
dialogBase.ClickSe_Btn1 = _entryButtonSe;
|
||||
_payType = ArenaData.eARENA_PAY.Rupy;
|
||||
}
|
||||
|
||||
protected virtual void Entry()
|
||||
{
|
||||
ParentDialog.CloseWithoutSelect();
|
||||
}
|
||||
}
|
||||
52
SVSim.BattleEngine/Engine/ArenaEntryDialogData.cs
Normal file
52
SVSim.BattleEngine/Engine/ArenaEntryDialogData.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class ArenaEntryDialogData : MonoBehaviour
|
||||
{
|
||||
public GameObject BuyDialogObject;
|
||||
|
||||
public UILabel _mainText;
|
||||
|
||||
public UISprite _ticketSprite;
|
||||
|
||||
public UIButton _ticketButton;
|
||||
|
||||
public UIButton _rupyButton;
|
||||
|
||||
public UIButton _crystalButton;
|
||||
|
||||
public UILabel _ticketHaveTitle;
|
||||
|
||||
public UILabel _ticketHaveNum;
|
||||
|
||||
public UILabel _ticketHaveUnit;
|
||||
|
||||
public UILabel _ticketButtonTitle;
|
||||
|
||||
public UILabel _ticketButtonSubTitle;
|
||||
|
||||
public UILabel _ticketButtonUseNum;
|
||||
|
||||
public UILabel _rupyHaveTitle;
|
||||
|
||||
public UILabel _rupyHaveNum;
|
||||
|
||||
public UILabel _rupyHaveUnit;
|
||||
|
||||
public UILabel _rupyButtonTitle;
|
||||
|
||||
public UILabel _rupyButtonSubTitle;
|
||||
|
||||
public UILabel _rupyButtonUseNum;
|
||||
|
||||
public UILabel _crystalHaveTitle;
|
||||
|
||||
public UILabel _crystalHaveNum;
|
||||
|
||||
public UILabel _crystalHaveUnit;
|
||||
|
||||
public UILabel _crystalButtonTitle;
|
||||
|
||||
public UILabel _crystalButtonSubTitle;
|
||||
|
||||
public UILabel _crystalButtonUseNum;
|
||||
}
|
||||
80
SVSim.BattleEngine/Engine/ArenaField.cs
Normal file
80
SVSim.BattleEngine/Engine/ArenaField.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class ArenaField : BackGroundBase
|
||||
{
|
||||
public override int FieldId => 9;
|
||||
|
||||
public ArenaField(string bgmId = "NONE")
|
||||
: base(bgmId)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void BattleFieldBuild()
|
||||
{
|
||||
BattleCoroutine.GetInstance().StartCoroutine(BackGroundBase.ObjectChecker(0.5f, _str3DFieldPath, delegate
|
||||
{
|
||||
base.Field = GameObject.Find(_str3DFieldPath);
|
||||
base.Field.transform.parent = GameMgr.GetIns().m_GameManagerObj.transform;
|
||||
GimicAudioList = base.Field.GetComponent<AudioList>().GimicAudioList;
|
||||
_fieldModel = base.Field.transform.Find("md_bf_arna_root").gameObject;
|
||||
_fieldParticles = _fieldModel.transform.Find("Particles09").gameObject;
|
||||
_fieldObjDictionary.Add(_fieldParticles.name, _fieldParticles);
|
||||
_fieldParticleSystemDictionary.Add("opening", _fieldParticles.transform.Find("opening").GetComponent<ParticleSystem>());
|
||||
List<string> list = new List<string>(_fieldObjDictionary.Keys);
|
||||
List<GameObject> list2 = new List<GameObject>();
|
||||
for (int i = 0; i < _fieldObjDictionary.Count; i++)
|
||||
{
|
||||
list2.Add(_fieldObjDictionary[list[i]]);
|
||||
}
|
||||
GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(list2, delegate
|
||||
{
|
||||
base.SetShaderGlobalColorBG = base.Field.transform.Find("SetMaterialColorBGManager").GetComponent<SetShaderGlobalColorBG>();
|
||||
base.IsLoadDone = true;
|
||||
}, isBattle: true, isField: true);
|
||||
}));
|
||||
}
|
||||
|
||||
public override void StartFieldSetEffect(Vector3 pos)
|
||||
{
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_FIELD_SET_9, pos);
|
||||
}
|
||||
|
||||
public override void StartFieldTapEffect(int areaId, Vector3 pos)
|
||||
{
|
||||
base.StartFieldTapEffect(areaId, pos);
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_FIELD_TAP_9_1, pos);
|
||||
}
|
||||
|
||||
protected override IEnumerator RunFieldOpening()
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySeByStr($"se_field_{_str3DFieldNo}_appear_1", "se_field_" + _str3DFieldNo, 0f, 0L);
|
||||
_fieldParticleSystemDictionary["opening"].Play();
|
||||
_battleCamera.Camera.transform.localPosition = new Vector3(2700f, -880f, 300f);
|
||||
_battleCamera.Camera.transform.localRotation = Quaternion.Euler(new Vector3(-19f, -90f, 90f));
|
||||
Vector3[] bezierCubic = MotionUtils.GetBezierCubic(new Vector3(2700f, -880f, 300f), new Vector3(1700f, -550f, -220f), new Vector3(700f, -200f, 20f), new Vector3(-240f, -190f, -70f), 10);
|
||||
iTween.MoveTo(_battleCamera.Camera.gameObject, iTween.Hash("path", bezierCubic, "movetopath", false, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutQuad));
|
||||
iTween.RotateTo(_battleCamera.Camera.gameObject, iTween.Hash("rotation", new Vector3(-37f, -117f, 107f), "time", 1f, "delay", 1f, "islocal", true, "easetype", iTween.EaseType.easeInOutQuad));
|
||||
yield return new WaitForSeconds(2f);
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_CAMERA_ZOOM_OUT);
|
||||
iTween.MoveTo(_battleCamera.Camera.gameObject, iTween.Hash("position", _battleCamera.BattleCameraPos, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutExpo));
|
||||
iTween.RotateTo(_battleCamera.Camera.gameObject, iTween.Hash("rotation", _battleCamera.BattleCameraRot, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutExpo));
|
||||
yield return new WaitForSeconds(0f);
|
||||
}
|
||||
|
||||
protected override IEnumerator RunFieldGimic(GameObject obj)
|
||||
{
|
||||
string tag = obj.tag;
|
||||
if (tag != null && tag == "FieldGimic1")
|
||||
{
|
||||
_ = _gimicCntDictionary[obj.tag];
|
||||
}
|
||||
yield return new WaitForSeconds(0f);
|
||||
}
|
||||
|
||||
protected override IEnumerator RunFieldShake()
|
||||
{
|
||||
yield return new WaitForSeconds(0f);
|
||||
}
|
||||
}
|
||||
101
SVSim.BattleEngine/Engine/ArenaNextSceneSelector.cs
Normal file
101
SVSim.BattleEngine/Engine/ArenaNextSceneSelector.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Cute;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
|
||||
public class ArenaNextSceneSelector : INextSceneSelector
|
||||
{
|
||||
private BattleResultUIController m_battleResultNewControl;
|
||||
|
||||
private bool _movingToMyPage;
|
||||
|
||||
public ArenaNextSceneSelector(BattleResultUIController battleResultControl)
|
||||
{
|
||||
m_battleResultNewControl = battleResultControl;
|
||||
_movingToMyPage = false;
|
||||
}
|
||||
|
||||
public void Setup(bool isWin, GameObject gameObject)
|
||||
{
|
||||
if (m_battleResultNewControl.ResultMsgReportBtnFlag)
|
||||
{
|
||||
m_battleResultNewControl.ReportBtnObj.labels[0].text = Data.SystemText.Get("Con_Management_001_Button");
|
||||
m_battleResultNewControl.ReportBtnObj.gameObject.SetActive(value: true);
|
||||
m_battleResultNewControl.ReportBtnObj.buttons[0].onClick.Clear();
|
||||
m_battleResultNewControl.ReportBtnObj.buttons[0].onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
ConsistencyReportButtonAction.CreateReportConfirmWindow();
|
||||
}));
|
||||
}
|
||||
m_battleResultNewControl.MissionBtnObj.labels[0].text = Data.SystemText.Get("Battle_0200");
|
||||
m_battleResultNewControl.MissionBtnObj.buttons[0].onClick.Clear();
|
||||
m_battleResultNewControl.MissionBtnObj.buttons[0].onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
|
||||
UIManager.GetInstance().createInSceneCenterLoading();
|
||||
MissionInfoTask missionInfoTask = GameMgr.GetIns().GetMissionInfoTask();
|
||||
missionInfoTask.SetParameter();
|
||||
m_battleResultNewControl.StartCoroutine(Toolbox.NetworkManager.Connect(missionInfoTask, delegate
|
||||
{
|
||||
m_battleResultNewControl.CreateMissionList();
|
||||
}, BaseTask.OnRequestFailed, BaseTask.OnFailedErrorCode));
|
||||
}));
|
||||
m_battleResultNewControl.HomeBtnObj.labels[0].text = Data.SystemText.Get("Battle_0202");
|
||||
m_battleResultNewControl.HomeBtnObj.buttons[0].onClick.Clear();
|
||||
m_battleResultNewControl.HomeBtnObj.buttons[0].onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
MoveToMyPage();
|
||||
}));
|
||||
if (GameMgr.GetIns().GetDataMgr().IsColosseumBattleType())
|
||||
{
|
||||
m_battleResultNewControl.RetryBtnObj.labels[0].text = Data.SystemText.Get("Battle_0489");
|
||||
}
|
||||
else if (GameMgr.GetIns().GetDataMgr().IsCompetitionBattleType())
|
||||
{
|
||||
m_battleResultNewControl.RetryBtnObj.labels[0].text = Data.SystemText.Get("Competition_0021");
|
||||
}
|
||||
else if (GameMgr.GetIns().GetDataMgr().m_BattleType == DataMgr.BattleType.Sealed)
|
||||
{
|
||||
m_battleResultNewControl.RetryBtnObj.labels[0].text = Data.SystemText.Get("Sealed_BattleResult_0001");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_battleResultNewControl.RetryBtnObj.labels[0].text = Data.SystemText.Get("Battle_0203");
|
||||
}
|
||||
m_battleResultNewControl.RetryBtnObj.buttons[0].onClick.Clear();
|
||||
m_battleResultNewControl.RetryBtnObj.buttons[0].onClick.Add(new EventDelegate(delegate
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
|
||||
if (GameMgr.GetIns().GetDataMgr().m_BattleType == DataMgr.BattleType.TwoPick)
|
||||
{
|
||||
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.TwoPick);
|
||||
}
|
||||
else if (GameMgr.GetIns().GetDataMgr().m_BattleType == DataMgr.BattleType.Sealed)
|
||||
{
|
||||
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.Sealed);
|
||||
}
|
||||
else if (GameMgr.GetIns().GetDataMgr().IsCompetitionBattleType())
|
||||
{
|
||||
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.CompetitionLobby);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.Colosseum);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
iTween.MoveTo(m_battleResultNewControl.ButtonGrid.gameObject, iTween.Hash("position", m_battleResultNewControl.DefaultPosDict["ButtonGrid"], "time", 0.5f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
|
||||
}
|
||||
|
||||
private void MoveToMyPage()
|
||||
{
|
||||
if (!_movingToMyPage)
|
||||
{
|
||||
_movingToMyPage = true;
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_CANCEL_TRANS);
|
||||
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.MyPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
204
SVSim.BattleEngine/Engine/ArenaResultAnimationAgent.cs
Normal file
204
SVSim.BattleEngine/Engine/ArenaResultAnimationAgent.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
|
||||
public class ArenaResultAnimationAgent : ResultAnimationAgent
|
||||
{
|
||||
public override IEnumerator RunUI(BattleResultUIController battleResultControl, INextSceneSelector nextSceneSelector, bool isWin)
|
||||
{
|
||||
m_BattleCamera.m_CutInCamera.gameObject.SetActive(value: false);
|
||||
if (battleResultControl.IsDraw)
|
||||
{
|
||||
battleResultControl.TitleWin.gameObject.SetActive(value: false);
|
||||
battleResultControl.TitleLose.gameObject.SetActive(value: false);
|
||||
battleResultControl.TitleDraw.gameObject.SetActive(value: true);
|
||||
battleResultControl.TitleDraw.transform.localScale = Vector3.one * 10f;
|
||||
battleResultControl.TitleDraw.alpha = 0f;
|
||||
battleResultControl.Bg.color = new Color32(0, 48, 16, 0);
|
||||
battleResultControl.ResultTitle.spriteName = "result_top_lose";
|
||||
}
|
||||
else if (isWin)
|
||||
{
|
||||
battleResultControl.TitleWin.gameObject.SetActive(value: true);
|
||||
battleResultControl.TitleLose.gameObject.SetActive(value: false);
|
||||
battleResultControl.TitleDraw.gameObject.SetActive(value: false);
|
||||
battleResultControl.TitleWin.transform.localScale = Vector3.one * 10f;
|
||||
battleResultControl.TitleWin.alpha = 0f;
|
||||
battleResultControl.Bg.color = new Color32(32, 24, 0, 0);
|
||||
battleResultControl.ResultTitle.spriteName = "result_top_win";
|
||||
}
|
||||
else
|
||||
{
|
||||
battleResultControl.TitleWin.gameObject.SetActive(value: false);
|
||||
battleResultControl.TitleLose.gameObject.SetActive(value: true);
|
||||
battleResultControl.TitleDraw.gameObject.SetActive(value: false);
|
||||
battleResultControl.TitleLose.transform.localScale = Vector3.one * 10f;
|
||||
battleResultControl.TitleLose.alpha = 0f;
|
||||
battleResultControl.Bg.color = new Color32(0, 24, 48, 0);
|
||||
battleResultControl.ResultTitle.spriteName = "result_top_lose";
|
||||
}
|
||||
battleResultControl.MainPanel.alpha = 1f;
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
if (battleResultControl.IsDraw)
|
||||
{
|
||||
TweenAlpha.Begin(battleResultControl.TitleDraw.gameObject, 0.2f, 1f);
|
||||
iTween.ScaleTo(battleResultControl.TitleDraw.gameObject, iTween.Hash("scale", Vector3.one, "time", 0.2f, "islocal", true, "easetype", iTween.EaseType.easeInQuad));
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_YOULOSE);
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_JINGLE_LOSE);
|
||||
}
|
||||
else if (isWin)
|
||||
{
|
||||
TweenAlpha.Begin(battleResultControl.TitleWin.gameObject, 0.2f, 1f);
|
||||
iTween.ScaleTo(battleResultControl.TitleWin.gameObject, iTween.Hash("scale", Vector3.one, "time", 0.2f, "islocal", true, "easetype", iTween.EaseType.easeInQuad));
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_YOUWIN);
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_JINGLE_WIN);
|
||||
}
|
||||
else
|
||||
{
|
||||
TweenAlpha.Begin(battleResultControl.TitleLose.gameObject, 0.2f, 1f);
|
||||
iTween.ScaleTo(battleResultControl.TitleLose.gameObject, iTween.Hash("scale", Vector3.one, "time", 0.2f, "islocal", true, "easetype", iTween.EaseType.easeInQuad));
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_YOULOSE);
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_JINGLE_LOSE);
|
||||
}
|
||||
TweenAlpha.Begin(battleResultControl.Bg.gameObject, 0.5f, 0.75f);
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
TweenAlpha.Begin(battleResultControl.ArcaneIn.gameObject, 0.5f, 1f);
|
||||
TweenAlpha.Begin(battleResultControl.ArcaneOut.gameObject, 0.5f, 1f);
|
||||
iTween.ScaleTo(battleResultControl.ArcaneIn.gameObject, iTween.Hash("scale", Vector3.one, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
|
||||
iTween.ScaleTo(battleResultControl.ArcaneOut.gameObject, iTween.Hash("scale", Vector3.one, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
|
||||
if (battleResultControl.IsDraw)
|
||||
{
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_TITLE_3, Vector3.zero);
|
||||
battleResultControl.TitleDraw.transform.localScale = Vector3.one;
|
||||
iTween.ScaleTo(battleResultControl.TitleDraw.gameObject, iTween.Hash("scale", Vector3.one * 1.1f, "time", 2f, "islocal", true, "easetype", iTween.EaseType.linear));
|
||||
}
|
||||
else if (isWin)
|
||||
{
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_TITLE_1, Vector3.zero);
|
||||
battleResultControl.TitleWin.transform.localScale = Vector3.one;
|
||||
iTween.ScaleTo(battleResultControl.TitleWin.gameObject, iTween.Hash("scale", Vector3.one * 1.1f, "time", 2f, "islocal", true, "easetype", iTween.EaseType.linear));
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_TITLE_2, Vector3.zero);
|
||||
battleResultControl.TitleLose.transform.localScale = Vector3.one;
|
||||
iTween.ScaleTo(battleResultControl.TitleLose.gameObject, iTween.Hash("scale", Vector3.one * 1.1f, "time", 2f, "islocal", true, "easetype", iTween.EaseType.linear));
|
||||
}
|
||||
HideEmotionMessage();
|
||||
if (battleResultControl.ResultMsgWindowFlag)
|
||||
{
|
||||
StartCoroutine(battleResultControl.ShowSpecialResultInfo());
|
||||
}
|
||||
yield return new WaitForSeconds(2f);
|
||||
RankWinnerReward winnerReward = GameMgr.GetIns()._rankWinnerReward;
|
||||
if (winnerReward == null)
|
||||
{
|
||||
int value = PlayerPrefsWrapper.GetValue(PlayerPrefsWrapper.BATTLE_WINNER_REWARD_GRADE);
|
||||
string value2 = PlayerPrefsWrapper.GetValue(PlayerPrefsWrapper.BATTLE_WINNER_REWARD_STRING);
|
||||
if (value != 0 && value2 != "")
|
||||
{
|
||||
winnerReward = UIManager.GetInstance().createRankWinnerReward();
|
||||
GameMgr.GetIns()._rankWinnerReward = winnerReward;
|
||||
winnerReward.SetInfomation(value, value2);
|
||||
winnerReward.gameObject.SetActive(value: false);
|
||||
}
|
||||
}
|
||||
if (winnerReward != null && isWin)
|
||||
{
|
||||
float seconds = 3f;
|
||||
StartCoroutine(winnerReward.ResultWinnerReward());
|
||||
yield return new WaitForSeconds(seconds);
|
||||
StartCoroutine(winnerReward.HideRewardObject());
|
||||
}
|
||||
if (!battleResultControl.IsDraw && ShowRewardDialog(battleResultControl))
|
||||
{
|
||||
while (battleResultControl.IsRewardWait)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
if (Data.ArenaBattleFinish.data != null)
|
||||
{
|
||||
TreasureBoxCpResultInfo treasureBoxCpResultInfo = Data.ArenaBattleFinish.data.TreasureBoxCpResultInfo;
|
||||
if (treasureBoxCpResultInfo.IsPlayGradeUpAnimation())
|
||||
{
|
||||
yield return TreasureBoxCpOpenBoxAnimation(battleResultControl, treasureBoxCpResultInfo.AfterGrade);
|
||||
}
|
||||
if (treasureBoxCpResultInfo.IsBoxOpened())
|
||||
{
|
||||
yield return CreateTreasureBoxCpRewardDialog(treasureBoxCpResultInfo);
|
||||
}
|
||||
}
|
||||
if (battleResultControl.IsDraw)
|
||||
{
|
||||
TweenAlpha.Begin(battleResultControl.TitleDraw.gameObject, 0.2f, 0f);
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_BACK_3, battleResultControl.AnchorBottom.transform.position, battleResultControl.AnchorBottom.gameObject);
|
||||
}
|
||||
else if (isWin)
|
||||
{
|
||||
TweenAlpha.Begin(battleResultControl.TitleWin.gameObject, 0.2f, 0f);
|
||||
iTween.ScaleTo(battleResultControl.TitleWin.gameObject, iTween.Hash("scale", Vector3.one * 3f, "time", 0.2f, "islocal", true, "easetype", iTween.EaseType.easeInQuad));
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_BACK_1, battleResultControl.AnchorBottom.transform.position, battleResultControl.AnchorBottom.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
TweenAlpha.Begin(battleResultControl.TitleLose.gameObject, 0.2f, 0f);
|
||||
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_BACK_2, battleResultControl.AnchorBottom.transform.position, battleResultControl.AnchorBottom.gameObject);
|
||||
}
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
if (isWin)
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlayBGM(Bgm.BGM_TYPE.SYS_WIN_LOOP);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlayBGM(Bgm.BGM_TYPE.SYS_LOSE_LOOP);
|
||||
}
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_WINDOW_APPER);
|
||||
iTween.MoveTo(battleResultControl.ClassCharObj.gameObject, iTween.Hash("position", battleResultControl.DefaultPosDict["ClassCharObj"], "time", 0.5f, "delay", 0.1f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
|
||||
iTween.MoveTo(battleResultControl.ResultTitle.gameObject, iTween.Hash("position", battleResultControl.DefaultPosDict["ResultTitle"], "time", 0.5f, "delay", 0f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
|
||||
iTween.MoveTo(battleResultControl.ClassInfo.gameObject, iTween.Hash("position", battleResultControl.DefaultPosDict["ClassInfo"], "time", 0.5f, "delay", 0.3f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
|
||||
yield return new WaitForSeconds(1f);
|
||||
if (isWin)
|
||||
{
|
||||
PlayWinVoice();
|
||||
}
|
||||
if (battleResultControl.AddClassExp > 0)
|
||||
{
|
||||
battleResultControl.SettingAddClassExpTextAnimation();
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_GAUGEUP);
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
}
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
}
|
||||
bool _isFinishBattlePass = false;
|
||||
battleResultControl.SetBattlePassGauge(delegate
|
||||
{
|
||||
_isFinishBattlePass = true;
|
||||
});
|
||||
while (!_isFinishBattlePass)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
if (Data.RedEtherCampaignResultData != null)
|
||||
{
|
||||
bool isFinishRedEther = false;
|
||||
RedEtherCampaignPanel.Create(battleResultControl.gameObject, Data.RedEtherCampaignResultData, battleResultControl, delegate
|
||||
{
|
||||
isFinishRedEther = true;
|
||||
});
|
||||
while (!isFinishRedEther)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
yield return ShowRewardDialog(Data.RedEtherCampaignResultData.RewardList);
|
||||
}
|
||||
battleResultControl.GreySpriteBGVisible = false;
|
||||
nextSceneSelector.Show();
|
||||
battleResultControl.PrepareAchievementLog();
|
||||
battleResultControl.FinishResult();
|
||||
}
|
||||
}
|
||||
22
SVSim.BattleEngine/Engine/ArenaResultAnimationHandler.cs
Normal file
22
SVSim.BattleEngine/Engine/ArenaResultAnimationHandler.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class ArenaResultAnimationHandler : IResultAnimationHandler
|
||||
{
|
||||
private readonly GameObject m_resultAnimationAgentObj;
|
||||
|
||||
private readonly ArenaResultAnimationAgent m_resultAnimationAgentIns;
|
||||
|
||||
public ResultAnimationAgent m_resultAnimationAgent => m_resultAnimationAgentIns;
|
||||
|
||||
public ArenaResultAnimationHandler(BattleCamera battleCamera)
|
||||
{
|
||||
m_resultAnimationAgentObj = new GameObject();
|
||||
m_resultAnimationAgentIns = m_resultAnimationAgentObj.AddComponent<ArenaResultAnimationAgent>();
|
||||
m_resultAnimationAgentIns.GetComponent<ArenaResultAnimationAgent>().SetBattleCamera(battleCamera);
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
Object.Destroy(m_resultAnimationAgentObj);
|
||||
}
|
||||
}
|
||||
63
SVSim.BattleEngine/Engine/ArenaResultReporter.cs
Normal file
63
SVSim.BattleEngine/Engine/ArenaResultReporter.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
using LitJson;
|
||||
using Wizard;
|
||||
using Wizard.Lottery;
|
||||
|
||||
public class ArenaResultReporter : IBattleResultReporter
|
||||
{
|
||||
public bool IsEnd => Data.ArenaBattleFinish.data != null;
|
||||
|
||||
public int ClassExp => GetClassExp();
|
||||
|
||||
public List<UserAchievement> UserAchievement => GetUserAchievementList();
|
||||
|
||||
public List<UserMission> UserMission => GetUserMissionList();
|
||||
|
||||
public List<ReceivedReward> MissionRewards => Data.ArenaBattleFinish.data._missionRewards;
|
||||
|
||||
public List<ReceivedReward> VictoryRewards => Data.ArenaBattleFinish.data._victoryRewards;
|
||||
|
||||
public LotteryApplyData LotteryData => LotteryApplyData.EmptyData();
|
||||
|
||||
public bool IsDataExist
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Data.ArenaBattleFinish.data != null)
|
||||
{
|
||||
return Data.ArenaBattleFinish.data.IsProcessed;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public MyPageHomeDialogData HomeDialogData => null;
|
||||
|
||||
public void Report(bool isWin)
|
||||
{
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
}
|
||||
|
||||
public JsonData GetFinishResponseData()
|
||||
{
|
||||
return Data.ArenaBattleFinish.data._responseData;
|
||||
}
|
||||
|
||||
public List<UserAchievement> GetUserAchievementList()
|
||||
{
|
||||
return Data.ArenaBattleFinish.data.achieved_achievement_list;
|
||||
}
|
||||
|
||||
public List<UserMission> GetUserMissionList()
|
||||
{
|
||||
return Data.ArenaBattleFinish.data.achieved_mission_list;
|
||||
}
|
||||
|
||||
public int GetClassExp()
|
||||
{
|
||||
return Data.ArenaBattleFinish.data.get_class_chara_experience;
|
||||
}
|
||||
}
|
||||
24
SVSim.BattleEngine/Engine/ArenaTwoPickData.cs
Normal file
24
SVSim.BattleEngine/Engine/ArenaTwoPickData.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using LitJson;
|
||||
using Wizard;
|
||||
|
||||
public class ArenaTwoPickData : ArenaEntryDataBase
|
||||
{
|
||||
public ChallengeData ChallengeData { get; private set; }
|
||||
|
||||
public ArenaTwoPickData(JsonData data)
|
||||
{
|
||||
isJoin = data["is_join"].ToBoolean();
|
||||
crystalCost = data["cost"].ToInt();
|
||||
rupyCost = data["rupy_cost"].ToInt();
|
||||
ticketCost = data["ticket_cost"].ToInt();
|
||||
base.LootBoxType = PlayerStaticData.LootBoxType.TWOPICK;
|
||||
if (data.Keys.Contains("sales_period_info"))
|
||||
{
|
||||
base.ExpirtyInfo = new ShopExpirtyInfo(data["sales_period_info"]);
|
||||
}
|
||||
if (data.Keys.Contains("format_info"))
|
||||
{
|
||||
ChallengeData = new ChallengeData(data["format_info"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
117
SVSim.BattleEngine/Engine/ArrowControl.cs
Normal file
117
SVSim.BattleEngine/Engine/ArrowControl.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class ArrowControl : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
private GameObject ArrowHead;
|
||||
|
||||
[SerializeField]
|
||||
private GameObject ArrowEfc;
|
||||
|
||||
[SerializeField]
|
||||
private int DivideCnt = 10;
|
||||
|
||||
[SerializeField]
|
||||
private bool isEvo;
|
||||
|
||||
private IList<GameObject> ArrowEfcList;
|
||||
|
||||
private GameObject FromObj;
|
||||
|
||||
private GameObject ToObj;
|
||||
|
||||
private bool isOn;
|
||||
|
||||
private bool _isTargettingEnemy;
|
||||
|
||||
private float ChangeTime;
|
||||
|
||||
private IList<int> ArrowTarList;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
ArrowEfcList = new List<GameObject>();
|
||||
ArrowEfcList.Add(ArrowEfc);
|
||||
for (int i = 1; i < DivideCnt; i++)
|
||||
{
|
||||
GameObject gameObject = Object.Instantiate(ArrowEfc);
|
||||
if (!(null == gameObject))
|
||||
{
|
||||
gameObject.transform.parent = base.transform;
|
||||
ArrowEfcList.Add(gameObject);
|
||||
}
|
||||
}
|
||||
ArrowTarList = new List<int>();
|
||||
for (int j = 0; j < DivideCnt; j++)
|
||||
{
|
||||
ArrowTarList.Add(j);
|
||||
}
|
||||
HideArrow();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (isOn)
|
||||
{
|
||||
SetArrowLine();
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowArrow(GameObject fromObj, GameObject toObj, bool isTargettingEnemy)
|
||||
{
|
||||
FromObj = fromObj;
|
||||
ToObj = toObj;
|
||||
_isTargettingEnemy = isTargettingEnemy;
|
||||
isOn = true;
|
||||
base.gameObject.SetActive(value: true);
|
||||
}
|
||||
|
||||
public void HideArrow()
|
||||
{
|
||||
isOn = false;
|
||||
for (int i = 0; i < DivideCnt; i++)
|
||||
{
|
||||
ArrowEfcList[i].SetActive(value: false);
|
||||
}
|
||||
base.gameObject.SetActive(value: false);
|
||||
}
|
||||
|
||||
private void SetArrowLine()
|
||||
{
|
||||
if (isEvo)
|
||||
{
|
||||
ChangeTime -= Time.deltaTime * 5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
ChangeTime -= Time.deltaTime;
|
||||
}
|
||||
if (ChangeTime <= 0f)
|
||||
{
|
||||
ChangeTime = 1f;
|
||||
ArrowTarList.Add(ArrowTarList[0]);
|
||||
ArrowTarList.RemoveAt(0);
|
||||
}
|
||||
ArrowHead.transform.position = ToObj.transform.position;
|
||||
Vector3 position = FromObj.transform.position;
|
||||
Vector3 position2 = ToObj.transform.position;
|
||||
Vector3 p = (_isTargettingEnemy ? position : position2) + Vector3.back * Vector3.Distance(position, position2) + Vector3.down * Vector3.Distance(position, position2) * -0.5f;
|
||||
Vector3[] array = new Vector3[DivideCnt];
|
||||
array = MotionUtils.GetBezierQuad(position, p, position2, DivideCnt);
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
{
|
||||
float num = 1f - ChangeTime;
|
||||
if (ArrowTarList[i] != 0)
|
||||
{
|
||||
ArrowEfcList[i].SetActive(value: true);
|
||||
ArrowEfcList[i].transform.position = (array[ArrowTarList[i]] - array[ArrowTarList[i] - 1]) * num + array[ArrowTarList[i] - 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
ArrowEfcList[i].SetActive(value: false);
|
||||
ArrowEfcList[i].transform.position = array[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
SVSim.BattleEngine/Engine/AspectCamera.cs
Normal file
76
SVSim.BattleEngine/Engine/AspectCamera.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using UnityEngine;
|
||||
|
||||
[ExecuteInEditMode]
|
||||
public class AspectCamera : MonoBehaviour
|
||||
{
|
||||
public Vector2 aspect = new Vector2(4f, 3f);
|
||||
|
||||
public Color32 backgroundColor = Color.black;
|
||||
|
||||
private float aspectRate;
|
||||
|
||||
private Camera _camera;
|
||||
|
||||
private static Camera _backgroundCamera;
|
||||
|
||||
private int sizeVal = 1;
|
||||
|
||||
public const float LOWER_LIMIT_ASPECT_RATIO = 0.5625f;
|
||||
|
||||
public const float UPPER_LIMIT_ASPECT_RATIO = 0.4618f;
|
||||
|
||||
public const float LOWER_LIMIT_ASPECT_RATIO_RECIPROCAL = 1.7777778f;
|
||||
|
||||
private const float SAFE_AREA_RATE = 0.892f;
|
||||
|
||||
private const float SAFE_AREA_NONE_RATE = 1f;
|
||||
|
||||
public static float SafeAreaRate;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
aspectRate = aspect.x / aspect.y;
|
||||
_camera = GetComponent<Camera>();
|
||||
SafeAreaRate = 1f;
|
||||
float num = (float)Screen.height / (float)Screen.width;
|
||||
if (num < 0.5625f)
|
||||
{
|
||||
num = Mathf.Max(num, 0.4618f);
|
||||
float t = (0.5625f - num) / 0.10069999f;
|
||||
SafeAreaRate = Mathf.Lerp(1f, 0.892f, t);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateScreenRate()
|
||||
{
|
||||
float num = aspect.y / aspect.x;
|
||||
float num2 = (float)Screen.height / (float)Screen.width;
|
||||
if (num2 < 0.5625f)
|
||||
{
|
||||
num2 = 0.5625f;
|
||||
}
|
||||
if (num > num2)
|
||||
{
|
||||
float num3 = num2 / num;
|
||||
_camera.rect = new Rect(0f, 0f, 1f, 1f);
|
||||
_camera.orthographicSize = (float)sizeVal * num3;
|
||||
}
|
||||
else
|
||||
{
|
||||
float num4 = num / num2;
|
||||
_camera.rect = new Rect(0f, 0f, 1f, 1f);
|
||||
_camera.orthographicSize = (float)sizeVal / num4;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsChangeAspect()
|
||||
{
|
||||
return _camera.aspect == aspectRate;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
UpdateScreenRate();
|
||||
_camera.ResetAspect();
|
||||
}
|
||||
}
|
||||
44
SVSim.BattleEngine/Engine/AspectCameraPerspective.cs
Normal file
44
SVSim.BattleEngine/Engine/AspectCameraPerspective.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class AspectCameraPerspective : MonoBehaviour
|
||||
{
|
||||
private Camera m_camera;
|
||||
|
||||
private bool m_isSetFOV;
|
||||
|
||||
public void UpdateFov()
|
||||
{
|
||||
m_isSetFOV = false;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
m_camera = GetComponent<Camera>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!m_isSetFOV && GameMgr.GetIns() != null && m_camera != null)
|
||||
{
|
||||
float num = 0f;
|
||||
float num2 = 0f;
|
||||
if (Screen.width > Screen.height)
|
||||
{
|
||||
num = Screen.width;
|
||||
num2 = Screen.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
num = Screen.height;
|
||||
num2 = Screen.width;
|
||||
}
|
||||
float num3 = num / num2;
|
||||
if (num3 > 1.7777778f)
|
||||
{
|
||||
num3 = 1.7777778f;
|
||||
}
|
||||
m_camera.fieldOfView = Mathf.Atan2(1f, num3) * 57.29578f * 2f;
|
||||
m_isSetFOV = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
SVSim.BattleEngine/Engine/AssetBundleEditorTag.cs
Normal file
54
SVSim.BattleEngine/Engine/AssetBundleEditorTag.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
public class AssetBundleEditorTag
|
||||
{
|
||||
public enum BUNDLE_CATEGORY
|
||||
{
|
||||
BG,
|
||||
CARD,
|
||||
EFFECT,
|
||||
MASTER,
|
||||
STORY,
|
||||
UI,
|
||||
UIDOWNLOAD,
|
||||
TUTORIAL,
|
||||
PACKBOX,
|
||||
UILANG,
|
||||
STORYLANG,
|
||||
FONT,
|
||||
SLEEVE,
|
||||
MAX
|
||||
}
|
||||
|
||||
public enum CardStatType
|
||||
{
|
||||
CARD_STAT_NORMAL,
|
||||
CARD_STAT_FOIL,
|
||||
CARD_STAT_PROMOTION
|
||||
}
|
||||
|
||||
public struct categoryProps
|
||||
{
|
||||
public string name;
|
||||
|
||||
public categoryProps(string in_name)
|
||||
{
|
||||
name = in_name;
|
||||
}
|
||||
}
|
||||
|
||||
public static categoryProps[] categoryNameList = new categoryProps[13]
|
||||
{
|
||||
new categoryProps("bg"),
|
||||
new categoryProps("card"),
|
||||
new categoryProps("effect"),
|
||||
new categoryProps("master"),
|
||||
new categoryProps("story"),
|
||||
new categoryProps("ui"),
|
||||
new categoryProps("uidownload"),
|
||||
new categoryProps("tutorial"),
|
||||
new categoryProps("packbox"),
|
||||
new categoryProps("uilang"),
|
||||
new categoryProps("storylang"),
|
||||
new categoryProps("font"),
|
||||
new categoryProps("sleeve")
|
||||
};
|
||||
}
|
||||
74
SVSim.BattleEngine/Engine/AttachedSkillInformation.cs
Normal file
74
SVSim.BattleEngine/Engine/AttachedSkillInformation.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Generic;
|
||||
using Wizard.Battle;
|
||||
|
||||
public class AttachedSkillInformation
|
||||
{
|
||||
public SkillCollectionBase AttachedSkills { get; protected set; }
|
||||
|
||||
public List<string> OwnerCardNameList { get; protected set; }
|
||||
|
||||
public List<int> OwnerCardIdList { get; protected set; }
|
||||
|
||||
public List<long> DuplicateBanNum { get; protected set; }
|
||||
|
||||
public List<SkillBase> CreatorSkillList { get; protected set; }
|
||||
|
||||
public List<int> CreatorSkillIndexList { get; protected set; }
|
||||
|
||||
public AttachedSkillInformation(BattleCardBase card)
|
||||
{
|
||||
AttachedSkills = new SkillCollectionBase(card);
|
||||
OwnerCardNameList = new List<string>();
|
||||
OwnerCardIdList = new List<int>();
|
||||
DuplicateBanNum = new List<long>();
|
||||
CreatorSkillList = new List<SkillBase>();
|
||||
CreatorSkillIndexList = new List<int>();
|
||||
}
|
||||
|
||||
public AttachedSkillInformation(BattleCardBase card, SkillCollectionBase skills, List<string> nameList, List<int> idList, List<long> duplicateBanNum, List<SkillBase> createrList, List<int> creatorSkillIndexList)
|
||||
{
|
||||
AttachedSkills = skills.Clone(card);
|
||||
OwnerCardNameList = new List<string>(nameList);
|
||||
OwnerCardIdList = new List<int>(idList);
|
||||
DuplicateBanNum = new List<long>(duplicateBanNum);
|
||||
CreatorSkillList = new List<SkillBase>(createrList);
|
||||
CreatorSkillIndexList = new List<int>(creatorSkillIndexList);
|
||||
}
|
||||
|
||||
public void Add(SkillBase skill, string ownerCardName, int ownerCardID, long duplicateBanNum, SkillBase creatorSkill, int index)
|
||||
{
|
||||
AttachedSkills.Add(skill);
|
||||
OwnerCardNameList.Add(ownerCardName);
|
||||
OwnerCardIdList.Add(ownerCardID);
|
||||
DuplicateBanNum.Add(duplicateBanNum);
|
||||
CreatorSkillList.Add(creatorSkill);
|
||||
CreatorSkillIndexList.Add(index);
|
||||
}
|
||||
|
||||
public void Remove(SkillBase skill, BattleCardBase owner, long duplicateBanNum, SkillBase creatorSkill, int index)
|
||||
{
|
||||
string name = owner.GetName();
|
||||
int cardId = owner.CardId;
|
||||
Remove(skill, name, cardId, duplicateBanNum, creatorSkill, index);
|
||||
}
|
||||
|
||||
public void Remove(SkillBase skill, string ownerCardName, int ownerCardID, long duplicateBanNum, SkillBase creatorSkill, int index)
|
||||
{
|
||||
AttachedSkills.Remove(skill);
|
||||
OwnerCardNameList.Remove(ownerCardName);
|
||||
OwnerCardIdList.Remove(ownerCardID);
|
||||
DuplicateBanNum.Remove(duplicateBanNum);
|
||||
CreatorSkillList.Remove(creatorSkill);
|
||||
CreatorSkillIndexList.Remove(index);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
AttachedSkills.Clear();
|
||||
OwnerCardNameList.Clear();
|
||||
OwnerCardIdList.Clear();
|
||||
DuplicateBanNum.Clear();
|
||||
CreatorSkillList.Clear();
|
||||
CreatorSkillIndexList.Clear();
|
||||
}
|
||||
}
|
||||
15
SVSim.BattleEngine/Engine/AttachingAbilityInfo.cs
Normal file
15
SVSim.BattleEngine/Engine/AttachingAbilityInfo.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using Wizard.Battle;
|
||||
|
||||
public class AttachingAbilityInfo
|
||||
{
|
||||
public SkillBase Skill { get; private set; }
|
||||
|
||||
public List<IReadOnlyBattleCardInfo> TargetCards { get; private set; }
|
||||
|
||||
public AttachingAbilityInfo(SkillBase skill, List<IReadOnlyBattleCardInfo> targetCards)
|
||||
{
|
||||
Skill = skill;
|
||||
TargetCards = targetCards;
|
||||
}
|
||||
}
|
||||
528
SVSim.BattleEngine/Engine/AttackSelectControl.cs
Normal file
528
SVSim.BattleEngine/Engine/AttackSelectControl.cs
Normal file
@@ -0,0 +1,528 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
using Wizard.Battle.Touch;
|
||||
using Wizard.Battle.View;
|
||||
using Wizard.Battle.View.Vfx;
|
||||
|
||||
public class AttackSelectControl
|
||||
{
|
||||
public class AttackPair
|
||||
{
|
||||
public class AttackPairCard
|
||||
{
|
||||
public IBattleCardView _battleCardView;
|
||||
|
||||
public bool _isReady;
|
||||
|
||||
public bool _hasStartedMoving;
|
||||
|
||||
public AttackPairCard(IBattleCardView battleCardBase)
|
||||
{
|
||||
_battleCardView = battleCardBase;
|
||||
}
|
||||
|
||||
public AttackPairCard(AttackPairCard attackPairCard)
|
||||
{
|
||||
_battleCardView = attackPairCard._battleCardView;
|
||||
_isReady = attackPairCard._isReady;
|
||||
_hasStartedMoving = attackPairCard._hasStartedMoving;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_battleCardView = null;
|
||||
_isReady = false;
|
||||
_hasStartedMoving = false;
|
||||
}
|
||||
}
|
||||
|
||||
public AttackPairCard _attackInitiator;
|
||||
|
||||
public AttackPairCard _attackTarget;
|
||||
|
||||
public bool IsAttackPairReady
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_attackInitiator._isReady)
|
||||
{
|
||||
return _attackTarget._isReady;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public AttackPair(IBattleCardView attackInitiator, IBattleCardView attackTarget)
|
||||
{
|
||||
_attackInitiator = new AttackPairCard(attackInitiator);
|
||||
_attackTarget = new AttackPairCard(attackTarget);
|
||||
}
|
||||
|
||||
public AttackPair(AttackPair attackPair)
|
||||
{
|
||||
_attackInitiator = new AttackPairCard(attackPair._attackInitiator);
|
||||
_attackTarget = new AttackPairCard(attackPair._attackTarget);
|
||||
}
|
||||
|
||||
public bool Compare(IBattleCardView attackInitiatorView, IBattleCardView attackTargetView)
|
||||
{
|
||||
if (_attackInitiator._battleCardView == attackInitiatorView)
|
||||
{
|
||||
return _attackTarget._battleCardView == attackTargetView;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_attackInitiator.Clear();
|
||||
_attackTarget.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public class WaitUntilAttackPairIsReadyVfx : VfxBase
|
||||
{
|
||||
private AttackPair _attackPair;
|
||||
|
||||
public WaitUntilAttackPairIsReadyVfx(AttackPair attackPair)
|
||||
{
|
||||
_attackPair = attackPair;
|
||||
}
|
||||
|
||||
public override void Play()
|
||||
{
|
||||
BattleCoroutine.GetInstance().StartCoroutine(Wait());
|
||||
}
|
||||
|
||||
private IEnumerator Wait()
|
||||
{
|
||||
while (!_attackPair.IsAttackPairReady)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
IsEnd = true;
|
||||
}
|
||||
}
|
||||
|
||||
private BattleCardBase currentAttackInitiatorBattleCard;
|
||||
|
||||
private bool areAttackPairsBeingUpdated;
|
||||
|
||||
private readonly AttackPair currentAttackPair = new AttackPair(null, null);
|
||||
|
||||
private readonly List<AttackPair> successfulAttackPairs = new List<AttackPair>();
|
||||
|
||||
public const float Z_FLOAT_AMOUNT = -100f;
|
||||
|
||||
private const float EPSILON = 0.1f;
|
||||
|
||||
private const float SMOOTHING_AMOUNT = 0.01f;
|
||||
|
||||
private const float DECAY_MULTIPLIER = 10f;
|
||||
|
||||
private const float IDLING_POSITION = 0.025390625f;
|
||||
|
||||
private IBattleCardView currentAttackInitiator
|
||||
{
|
||||
get
|
||||
{
|
||||
return currentAttackPair._attackInitiator._battleCardView;
|
||||
}
|
||||
set
|
||||
{
|
||||
currentAttackPair._attackInitiator._battleCardView = value;
|
||||
}
|
||||
}
|
||||
|
||||
private IBattleCardView currentAttackTarget
|
||||
{
|
||||
get
|
||||
{
|
||||
return currentAttackPair._attackTarget._battleCardView;
|
||||
}
|
||||
set
|
||||
{
|
||||
currentAttackPair._attackTarget._battleCardView = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
float t = MotionUtils.CalculateFrameRateIndependantDampingConstant(0.01f, 10f);
|
||||
if (currentAttackInitiator != null && !currentAttackInitiator._attackTargetSelectInfo.IsCardInvolvedInAttack)
|
||||
{
|
||||
MoveCardUpwards(currentAttackPair._attackInitiator, t);
|
||||
}
|
||||
if (currentAttackTarget != null && !currentAttackTarget._attackTargetSelectInfo.IsCardInvolvedInAttack)
|
||||
{
|
||||
MoveCardUpwards(currentAttackPair._attackTarget, t);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterAttackInitiator(BattleCardBase attackInitiatorCard, BattlePlayerBase opponentBattlePlayer)
|
||||
{
|
||||
currentAttackInitiatorBattleCard = attackInitiatorCard;
|
||||
currentAttackInitiator = attackInitiatorCard.BattleCardView;
|
||||
ToggleAttackableCardFrameEffects(isEnabled: true, opponentBattlePlayer);
|
||||
attackInitiatorCard.BattleCardView._attackTargetSelectInfo._isBeingSelectedInAttack = true;
|
||||
if (!attackInitiatorCard.BattleCardView._attackTargetSelectInfo.IsCardInvolvedInAttack)
|
||||
{
|
||||
ResetCardOrientationAndStopMovement(attackInitiatorCard.BattleCardView);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterAttackTarget(IBattleCardView attackTargetCard)
|
||||
{
|
||||
if (currentAttackTarget == attackTargetCard)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (attackTargetCard != null)
|
||||
{
|
||||
attackTargetCard._attackTargetSelectInfo._isBeingSelectedInAttack = true;
|
||||
if (!attackTargetCard._attackTargetSelectInfo.IsCardInvolvedInAttack)
|
||||
{
|
||||
ResetCardOrientationAndStopMovement(attackTargetCard);
|
||||
}
|
||||
}
|
||||
currentAttackPair._attackTarget._isReady = !IsCardTranslatable(attackTargetCard);
|
||||
currentAttackPair._attackTarget._hasStartedMoving = !IsCardTranslatable(attackTargetCard);
|
||||
ResetCardPosition(currentAttackTarget);
|
||||
if (currentAttackTarget != null)
|
||||
{
|
||||
currentAttackTarget._attackTargetSelectInfo._isBeingSelectedInAttack = false;
|
||||
}
|
||||
currentAttackTarget = attackTargetCard;
|
||||
}
|
||||
|
||||
public virtual void RegisterAttackPair(AttackPair attackPair)
|
||||
{
|
||||
IBattleCardView battleCardView = attackPair._attackInitiator._battleCardView;
|
||||
IBattleCardView battleCardView2 = attackPair._attackTarget._battleCardView;
|
||||
if (attackPair == null || battleCardView == null || battleCardView._attackTargetSelectInfo._attackPairsCardIsInvolvedIn == null)
|
||||
{
|
||||
ResetCardPosition(currentAttackInitiator);
|
||||
ResetCardPosition(currentAttackTarget);
|
||||
return;
|
||||
}
|
||||
successfulAttackPairs.Add(attackPair);
|
||||
battleCardView._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Enqueue(attackPair);
|
||||
battleCardView2._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Enqueue(attackPair);
|
||||
if (!areAttackPairsBeingUpdated)
|
||||
{
|
||||
BattleCoroutine.GetInstance().StartCoroutine(UpdateAttackPairs());
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelAttackSelect(bool wasAttackSuccessful, BattlePlayerBase opponentBattlePlayer)
|
||||
{
|
||||
if (wasAttackSuccessful)
|
||||
{
|
||||
AttackPair attackPair = new AttackPair(currentAttackPair);
|
||||
RegisterAttackPair(attackPair);
|
||||
}
|
||||
else
|
||||
{
|
||||
ResetCardPosition(currentAttackInitiator);
|
||||
ResetCardPosition(currentAttackTarget);
|
||||
}
|
||||
if (currentAttackInitiatorBattleCard != null)
|
||||
{
|
||||
ToggleAttackableCardFrameEffects(isEnabled: false, opponentBattlePlayer);
|
||||
}
|
||||
if (currentAttackInitiator != null)
|
||||
{
|
||||
currentAttackInitiator._attackTargetSelectInfo._isBeingSelectedInAttack = false;
|
||||
}
|
||||
if (currentAttackTarget != null)
|
||||
{
|
||||
currentAttackTarget._attackTargetSelectInfo._isBeingSelectedInAttack = false;
|
||||
}
|
||||
currentAttackInitiatorBattleCard = null;
|
||||
currentAttackPair.Clear();
|
||||
}
|
||||
|
||||
public void ResetCardOrientationAndStopMovement(IBattleCardView targetCard)
|
||||
{
|
||||
if (!targetCard._attackTargetSelectInfo.IsUneffectedByAttackTargetting)
|
||||
{
|
||||
iTween.Stop(targetCard.CardWrapObject);
|
||||
targetCard.CardWrapObject.transform.rotation = Quaternion.identity;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual VfxBase ResetCardAfterAttackOnReplay()
|
||||
{
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
for (int i = 0; i < successfulAttackPairs.Count(); i++)
|
||||
{
|
||||
IBattleCardView battleCardView = successfulAttackPairs[i]._attackInitiator._battleCardView;
|
||||
if (battleCardView._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Count > 0)
|
||||
{
|
||||
battleCardView._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Dequeue();
|
||||
}
|
||||
ResetCardPosition(battleCardView);
|
||||
IBattleCardView battleCardView2 = successfulAttackPairs[i]._attackTarget._battleCardView;
|
||||
if (battleCardView2._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Count > 0)
|
||||
{
|
||||
battleCardView2._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Dequeue();
|
||||
}
|
||||
ResetCardPosition(battleCardView2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public virtual VfxBase ResetCardAfterAttack(IBattleCardView cardToReset)
|
||||
{
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
if (cardToReset._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Count > 0)
|
||||
{
|
||||
cardToReset._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Dequeue();
|
||||
}
|
||||
if (cardToReset._attackTargetSelectInfo.IsCardInvolvedInAttack)
|
||||
{
|
||||
cardToReset._attackTargetSelectInfo.CurrentAttackPairCardIsInvolvedIn._attackTarget._isReady = true;
|
||||
}
|
||||
ResetCardPosition(cardToReset);
|
||||
});
|
||||
}
|
||||
|
||||
private void ResetCardPosition(IBattleCardView targetCard)
|
||||
{
|
||||
if (!BattleManagerBase.GetIns().IsRecovery && IsCardTranslatable(targetCard) && !targetCard._attackTargetSelectInfo.IsCardInvolvedInAttack && !targetCard._attackTargetSelectInfo.IsUneffectedByAttackTargetting)
|
||||
{
|
||||
ImmediateVfxMgr.GetInstance().Register(SequentialVfxPlayer.Create(InstantVfx.Create(delegate
|
||||
{
|
||||
iTween.Stop(targetCard.CardWrapObject);
|
||||
}), new DelaySetupVfx(() => (targetCard._attackTargetSelectInfo._isBeingSelectedInAttack || targetCard._attackTargetSelectInfo.IsCardInvolvedInAttack) ? ((VfxBase)NullVfx.GetInstance()) : ((VfxBase)new FallToGroundVfx(targetCard.CardWrapObject)))));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void StartCardIdling(IBattleCardView battleCardView)
|
||||
{
|
||||
iTween.Stop(battleCardView.CardWrapObject);
|
||||
iTween.MoveAdd(battleCardView.CardWrapObject, iTween.Hash("z", 0.025390625f, "time", Random.Range(0.5f, 0.6f), "looptype", iTween.LoopType.pingPong, "easetype", iTween.EaseType.easeInOutQuad));
|
||||
}
|
||||
|
||||
public virtual VfxBase RemoveAttackPairVfx(IBattleCardView attackInitiator, IBattleCardView attackTarget)
|
||||
{
|
||||
AttackPair attackPairToRemove = null;
|
||||
for (int i = 0; i < successfulAttackPairs.Count; i++)
|
||||
{
|
||||
if (successfulAttackPairs[i].Compare(attackInitiator, attackTarget))
|
||||
{
|
||||
attackPairToRemove = successfulAttackPairs[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (attackPairToRemove != null)
|
||||
{
|
||||
VfxBase vfxBase = CreateWaitUntilAttackPairIsReadyVfx(attackPairToRemove);
|
||||
VfxBase vfxBase2 = InstantVfx.Create(delegate
|
||||
{
|
||||
successfulAttackPairs.Remove(attackPairToRemove);
|
||||
});
|
||||
return SequentialVfxPlayer.Create(vfxBase, vfxBase2);
|
||||
}
|
||||
return NullVfx.GetInstance();
|
||||
}
|
||||
|
||||
private void ToggleAttackableCardFrameEffects(bool isEnabled, BattlePlayerBase opponentBattlePlayer)
|
||||
{
|
||||
List<BattleCardBase> classAndInPlayCardList = opponentBattlePlayer.ClassAndInPlayCardList;
|
||||
for (int i = 0; i < classAndInPlayCardList.Count; i++)
|
||||
{
|
||||
if (CanCardAttackTarget(currentAttackInitiatorBattleCard, classAndInPlayCardList[i], opponentBattlePlayer.InPlayCards) && classAndInPlayCardList[i].AreCanBeAttackedConditionsFulfilled)
|
||||
{
|
||||
classAndInPlayCardList[i].BattleCardView._inPlayFrameEffect.ToggleTargetSelectEffect(isEnabled);
|
||||
}
|
||||
}
|
||||
currentAttackInitiator._inPlayFrameEffect.ToggleTargetSelectEffect(isEnabled, isAttackTargetSelectInitiator: true);
|
||||
}
|
||||
|
||||
private VfxBase CreateWaitUntilAttackPairIsReadyVfx(AttackPair attackPair)
|
||||
{
|
||||
return new WaitUntilAttackPairIsReadyVfx(attackPair);
|
||||
}
|
||||
|
||||
private IEnumerator UpdateAttackPairs()
|
||||
{
|
||||
areAttackPairsBeingUpdated = true;
|
||||
while (successfulAttackPairs.Count > 0)
|
||||
{
|
||||
float t = MotionUtils.CalculateFrameRateIndependantDampingConstant(0.01f, 10f);
|
||||
for (int i = 0; i < successfulAttackPairs.Count; i++)
|
||||
{
|
||||
AttackPair attackPair = successfulAttackPairs[i];
|
||||
if (!attackPair.IsAttackPairReady)
|
||||
{
|
||||
AttackPair.AttackPairCard attackInitiator = attackPair._attackInitiator;
|
||||
AttackPair.AttackPairCard attackTarget = attackPair._attackTarget;
|
||||
if (attackInitiator._battleCardView._attackTargetSelectInfo.CurrentAttackPairCardIsInvolvedIn == attackPair)
|
||||
{
|
||||
MoveCardUpwards(attackInitiator, t);
|
||||
}
|
||||
if (attackTarget._battleCardView._attackTargetSelectInfo.CurrentAttackPairCardIsInvolvedIn == attackPair)
|
||||
{
|
||||
MoveCardUpwards(attackTarget, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
areAttackPairsBeingUpdated = false;
|
||||
}
|
||||
|
||||
private void MoveCardUpwards(AttackPair.AttackPairCard attackPairCard, float t)
|
||||
{
|
||||
if (BattleManagerBase.GetIns().IsRecovery)
|
||||
{
|
||||
attackPairCard._isReady = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (attackPairCard == null || attackPairCard._battleCardView == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
IBattleCardView battleCardView = attackPairCard._battleCardView;
|
||||
if (IsCardTranslatable(battleCardView) && !battleCardView._attackTargetSelectInfo.IsUneffectedByAttackTargetting && !attackPairCard._isReady)
|
||||
{
|
||||
if (!attackPairCard._hasStartedMoving)
|
||||
{
|
||||
attackPairCard._hasStartedMoving = true;
|
||||
ResetCardOrientationAndStopMovement(battleCardView);
|
||||
}
|
||||
Transform transform = battleCardView.CardWrapObject.transform;
|
||||
if (!IsCardFullyTranslated(battleCardView))
|
||||
{
|
||||
Vector3 b = CalculateFinalFloatingPosition(battleCardView);
|
||||
transform.localPosition = Vector3.Lerp(transform.transform.localPosition, b, t);
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.localPosition = CalculateFinalFloatingPosition(battleCardView);
|
||||
attackPairCard._isReady = true;
|
||||
StartCardIdling(battleCardView);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 CalculateFinalFloatingPosition(IBattleCardView battleCardView)
|
||||
{
|
||||
Vector3 localPosition = battleCardView.CardWrapObject.transform.transform.localPosition;
|
||||
localPosition.z = -100f;
|
||||
return localPosition;
|
||||
}
|
||||
|
||||
public bool IsCardTranslatable(IBattleCardView cardToTranslate)
|
||||
{
|
||||
if (cardToTranslate != null)
|
||||
{
|
||||
return !cardToTranslate.CardInfo.IsClass;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsCardFullyTranslated(IBattleCardView cardBeingTranslated)
|
||||
{
|
||||
return Mathf.Abs(cardBeingTranslated.CardWrapObject.transform.localPosition.z - -100f) < 0.1f;
|
||||
}
|
||||
|
||||
public static bool CanCardAttackTarget(BattleCardBase Attacker, BattleCardBase Target, IEnumerable<BattleCardBase> TargetInPlayCards)
|
||||
{
|
||||
bool flag = false;
|
||||
bool isClass = Target.IsClass;
|
||||
if (TargetInPlayCards.Any((BattleCardBase c) => c.SkillApplyInformation.IsGuard && !c.CantBeFocusedAttack(Attacker)))
|
||||
{
|
||||
flag = true;
|
||||
}
|
||||
if (Attacker.SkillApplyInformation.IsIgnoreGuard)
|
||||
{
|
||||
flag = false;
|
||||
}
|
||||
if (Attacker.AttackableCount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if ((!Attacker.SkillApplyInformation.IsQuick || !Attacker.SkillApplyInformation.IsRush) && !Attacker.Attackable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (isClass)
|
||||
{
|
||||
if (!Attacker.SkillApplyInformation.IsQuick)
|
||||
{
|
||||
if (Attacker.IsFirstTurn)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!Attacker.Attackable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (Attacker.IsCantAttackClass)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Attacker.SkillApplyInformation.IsForceAttackUnit && Attacker.OpponentBattlePlayer.InPlayCards.Any((BattleCardBase c) => !c.CantBeFocusedAttack(Attacker) && c.IsUnit && !AttackTargetSelectTouchProcessor.CheckAttackToUnitNotHasGuardError(Attacker, c)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!Target.IsInplay)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Target.IsField || Target.CantBeFocusedAttack(Attacker))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (flag && (isClass || !Target.SkillApplyInformation.IsGuard))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (isClass && Attacker.IsCantAttackClass)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Target.IsUnit && Attacker.SkillApplyInformation.IsSkillCantAtkUnit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Target.IsUnit && Attacker.SkillApplyInformation.IsSkillCantAtkUnitBaseCardId && Attacker.SkillApplyInformation.CantAtkUnitBaseCardIdList.Contains(Target.BaseParameter.BaseCardId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!isClass && Attacker.SkillApplyInformation.IsSkillCantAtkUnitNotHasGuard && !Target.SkillApplyInformation.IsGuard)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsAttackPossible(BattleCardBase attacker, BattleCardBase target, IEnumerable<BattleCardBase> opponentInPlayCards)
|
||||
{
|
||||
if (attacker.Attackable)
|
||||
{
|
||||
return CanCardAttackTarget(attacker, target, opponentInPlayCards);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsAttackPossible(AIVirtualCard attacker, AIVirtualCard target, BattlePlayerBase opponent)
|
||||
{
|
||||
if (attacker.BaseCard.Attackable)
|
||||
{
|
||||
return CanCardAttackTarget(attacker.BaseCard, target.BaseCard, opponent.InPlayCards);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
7
SVSim.BattleEngine/Engine/AudioList.cs
Normal file
7
SVSim.BattleEngine/Engine/AudioList.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class AudioList : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
public string[] GimicAudioList;
|
||||
}
|
||||
154
SVSim.BattleEngine/Engine/BMFont.cs
Normal file
154
SVSim.BattleEngine/Engine/BMFont.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class BMFont
|
||||
{
|
||||
[HideInInspector]
|
||||
[SerializeField]
|
||||
private int mSize = 16;
|
||||
|
||||
[HideInInspector]
|
||||
[SerializeField]
|
||||
private int mBase;
|
||||
|
||||
[HideInInspector]
|
||||
[SerializeField]
|
||||
private int mWidth;
|
||||
|
||||
[HideInInspector]
|
||||
[SerializeField]
|
||||
private int mHeight;
|
||||
|
||||
[HideInInspector]
|
||||
[SerializeField]
|
||||
private string mSpriteName;
|
||||
|
||||
[HideInInspector]
|
||||
[SerializeField]
|
||||
private List<BMGlyph> mSaved = new List<BMGlyph>();
|
||||
|
||||
private Dictionary<int, BMGlyph> mDict = new Dictionary<int, BMGlyph>();
|
||||
|
||||
public bool isValid => mSaved.Count > 0;
|
||||
|
||||
public int charSize
|
||||
{
|
||||
get
|
||||
{
|
||||
return mSize;
|
||||
}
|
||||
set
|
||||
{
|
||||
mSize = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int baseOffset
|
||||
{
|
||||
get
|
||||
{
|
||||
return mBase;
|
||||
}
|
||||
set
|
||||
{
|
||||
mBase = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int texWidth
|
||||
{
|
||||
get
|
||||
{
|
||||
return mWidth;
|
||||
}
|
||||
set
|
||||
{
|
||||
mWidth = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int texHeight
|
||||
{
|
||||
get
|
||||
{
|
||||
return mHeight;
|
||||
}
|
||||
set
|
||||
{
|
||||
mHeight = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int glyphCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!isValid)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return mSaved.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public string spriteName
|
||||
{
|
||||
get
|
||||
{
|
||||
return mSpriteName;
|
||||
}
|
||||
set
|
||||
{
|
||||
mSpriteName = value;
|
||||
}
|
||||
}
|
||||
|
||||
public List<BMGlyph> glyphs => mSaved;
|
||||
|
||||
public BMGlyph GetGlyph(int index, bool createIfMissing)
|
||||
{
|
||||
BMGlyph value = null;
|
||||
if (mDict.Count == 0)
|
||||
{
|
||||
int i = 0;
|
||||
for (int count = mSaved.Count; i < count; i++)
|
||||
{
|
||||
BMGlyph bMGlyph = mSaved[i];
|
||||
mDict.Add(bMGlyph.index, bMGlyph);
|
||||
}
|
||||
}
|
||||
if (!mDict.TryGetValue(index, out value) && createIfMissing)
|
||||
{
|
||||
value = new BMGlyph();
|
||||
value.index = index;
|
||||
mSaved.Add(value);
|
||||
mDict.Add(index, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public BMGlyph GetGlyph(int index)
|
||||
{
|
||||
return GetGlyph(index, createIfMissing: false);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
mDict.Clear();
|
||||
mSaved.Clear();
|
||||
}
|
||||
|
||||
public void Trim(int xMin, int yMin, int xMax, int yMax)
|
||||
{
|
||||
if (isValid)
|
||||
{
|
||||
int i = 0;
|
||||
for (int count = mSaved.Count; i < count; i++)
|
||||
{
|
||||
mSaved[i]?.Trim(xMin, yMin, xMax, yMax);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
SVSim.BattleEngine/Engine/BMGlyph.cs
Normal file
88
SVSim.BattleEngine/Engine/BMGlyph.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
[Serializable]
|
||||
public class BMGlyph
|
||||
{
|
||||
public int index;
|
||||
|
||||
public int x;
|
||||
|
||||
public int y;
|
||||
|
||||
public int width;
|
||||
|
||||
public int height;
|
||||
|
||||
public int offsetX;
|
||||
|
||||
public int offsetY;
|
||||
|
||||
public int advance;
|
||||
|
||||
public int channel;
|
||||
|
||||
public List<int> kerning;
|
||||
|
||||
public int GetKerning(int previousChar)
|
||||
{
|
||||
if (kerning != null && previousChar != 0)
|
||||
{
|
||||
int i = 0;
|
||||
for (int count = kerning.Count; i < count; i += 2)
|
||||
{
|
||||
if (kerning[i] == previousChar)
|
||||
{
|
||||
return kerning[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void SetKerning(int previousChar, int amount)
|
||||
{
|
||||
if (kerning == null)
|
||||
{
|
||||
kerning = new List<int>();
|
||||
}
|
||||
for (int i = 0; i < kerning.Count; i += 2)
|
||||
{
|
||||
if (kerning[i] == previousChar)
|
||||
{
|
||||
kerning[i + 1] = amount;
|
||||
return;
|
||||
}
|
||||
}
|
||||
kerning.Add(previousChar);
|
||||
kerning.Add(amount);
|
||||
}
|
||||
|
||||
public void Trim(int xMin, int yMin, int xMax, int yMax)
|
||||
{
|
||||
int num = x + width;
|
||||
int num2 = y + height;
|
||||
if (x < xMin)
|
||||
{
|
||||
int num3 = xMin - x;
|
||||
x += num3;
|
||||
width -= num3;
|
||||
offsetX += num3;
|
||||
}
|
||||
if (y < yMin)
|
||||
{
|
||||
int num4 = yMin - y;
|
||||
y += num4;
|
||||
height -= num4;
|
||||
offsetY += num4;
|
||||
}
|
||||
if (num > xMax)
|
||||
{
|
||||
width -= num - xMax;
|
||||
}
|
||||
if (num2 > yMax)
|
||||
{
|
||||
height -= num2 - yMax;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
SVSim.BattleEngine/Engine/BMSymbol.cs
Normal file
93
SVSim.BattleEngine/Engine/BMSymbol.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class BMSymbol
|
||||
{
|
||||
public string sequence;
|
||||
|
||||
public string spriteName;
|
||||
|
||||
private UISpriteData mSprite;
|
||||
|
||||
private bool mIsValid;
|
||||
|
||||
private int mLength;
|
||||
|
||||
private int mOffsetX;
|
||||
|
||||
private int mOffsetY;
|
||||
|
||||
private int mWidth;
|
||||
|
||||
private int mHeight;
|
||||
|
||||
private int mAdvance;
|
||||
|
||||
private Rect mUV;
|
||||
|
||||
public int length
|
||||
{
|
||||
get
|
||||
{
|
||||
if (mLength == 0)
|
||||
{
|
||||
mLength = sequence.Length;
|
||||
}
|
||||
return mLength;
|
||||
}
|
||||
}
|
||||
|
||||
public int offsetX => mOffsetX;
|
||||
|
||||
public int offsetY => mOffsetY;
|
||||
|
||||
public int width => mWidth;
|
||||
|
||||
public int height => mHeight;
|
||||
|
||||
public int advance => mAdvance;
|
||||
|
||||
public Rect uvRect => mUV;
|
||||
|
||||
public void MarkAsChanged()
|
||||
{
|
||||
mIsValid = false;
|
||||
}
|
||||
|
||||
public bool Validate(UIAtlas atlas)
|
||||
{
|
||||
if (atlas == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!mIsValid)
|
||||
{
|
||||
if (string.IsNullOrEmpty(spriteName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
mSprite = ((atlas != null) ? atlas.GetSprite(spriteName) : null);
|
||||
if (mSprite != null)
|
||||
{
|
||||
Texture texture = atlas.texture;
|
||||
if (texture == null)
|
||||
{
|
||||
mSprite = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
mUV = new Rect(mSprite.x, mSprite.y, mSprite.width, mSprite.height);
|
||||
mUV = NGUIMath.ConvertToTexCoords(mUV, texture.width, texture.height);
|
||||
mOffsetX = mSprite.paddingLeft;
|
||||
mOffsetY = mSprite.paddingTop;
|
||||
mWidth = mSprite.width;
|
||||
mHeight = mSprite.height;
|
||||
mAdvance = mSprite.width + (mSprite.paddingLeft + mSprite.paddingRight);
|
||||
mIsValid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mSprite != null;
|
||||
}
|
||||
}
|
||||
272
SVSim.BattleEngine/Engine/BackGroundBase.cs
Normal file
272
SVSim.BattleEngine/Engine/BackGroundBase.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Cute;
|
||||
using UnityEngine;
|
||||
using Wizard.Battle.View.Vfx;
|
||||
|
||||
public class BackGroundBase
|
||||
{
|
||||
protected string _bgmId;
|
||||
|
||||
protected BattleCamera _battleCamera;
|
||||
|
||||
protected GameObject _fieldModel;
|
||||
|
||||
protected GameObject _fieldParticles;
|
||||
|
||||
protected IDictionary<string, Animation> m_FieldAnimationDictionary;
|
||||
|
||||
protected IDictionary<string, GameObject> _fieldObjDictionary;
|
||||
|
||||
protected IDictionary<string, Animator> m_FieldAnimatorDictionary;
|
||||
|
||||
protected IDictionary<string, ParticleSystem> _fieldParticleSystemDictionary;
|
||||
|
||||
protected IDictionary<string, int> _gimicCntDictionary;
|
||||
|
||||
public string[] GimicAudioList;
|
||||
|
||||
protected string _str3DFieldNo;
|
||||
|
||||
protected string _str3DFieldPath;
|
||||
|
||||
protected BattleManagerBase m_BtlMgrIns;
|
||||
|
||||
protected string m_FieldAssetPath;
|
||||
|
||||
protected List<string> m_SoundAssetPathList;
|
||||
|
||||
protected float m_RandomActionTime;
|
||||
|
||||
protected bool IsFieldRandom;
|
||||
|
||||
private Coroutine battleLoadCoroutine;
|
||||
|
||||
public virtual int FieldId => 1;
|
||||
|
||||
public virtual int FieldEffectId => FieldId;
|
||||
|
||||
public GameObject Field { get; protected set; }
|
||||
|
||||
public GameObject m_Battle3DContainer { get; protected set; }
|
||||
|
||||
public GameObject m_BattleCutInContainer { get; protected set; }
|
||||
|
||||
public SetShaderGlobalColorBG SetShaderGlobalColorBG { get; protected set; }
|
||||
|
||||
public bool IsLoadDone { get; protected set; }
|
||||
|
||||
public BackGroundBase(string bgmId = "NONE")
|
||||
{
|
||||
_battleCamera = null;
|
||||
m_Battle3DContainer = null;
|
||||
m_BattleCutInContainer = null;
|
||||
m_BtlMgrIns = BattleManagerBase.GetIns();
|
||||
IsLoadDone = false;
|
||||
_str3DFieldNo = "";
|
||||
_str3DFieldPath = "";
|
||||
m_FieldAssetPath = "";
|
||||
Field = null;
|
||||
_fieldModel = null;
|
||||
_fieldParticles = null;
|
||||
_bgmId = bgmId;
|
||||
m_RandomActionTime = 0f;
|
||||
IsFieldRandom = false;
|
||||
m_SoundAssetPathList = new List<string>();
|
||||
_fieldObjDictionary = new Dictionary<string, GameObject>();
|
||||
m_FieldAnimationDictionary = new Dictionary<string, Animation>();
|
||||
m_FieldAnimatorDictionary = new Dictionary<string, Animator>();
|
||||
_fieldParticleSystemDictionary = new Dictionary<string, ParticleSystem>();
|
||||
_gimicCntDictionary = new Dictionary<string, int>();
|
||||
SetShaderGlobalColorBG = null;
|
||||
Physics.gravity = new Vector3(0f, 0f, 9.8f);
|
||||
_str3DFieldNo = GetFieldIdString(FieldId);
|
||||
_gimicCntDictionary.Add("FieldGimic1", 0);
|
||||
_gimicCntDictionary.Add("FieldGimic2", 0);
|
||||
_gimicCntDictionary.Add("FieldGimic3", 0);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(Field);
|
||||
Field = null;
|
||||
_fieldModel = null;
|
||||
_fieldParticles = null;
|
||||
_fieldObjDictionary.Clear();
|
||||
m_FieldAnimationDictionary.Clear();
|
||||
m_FieldAnimatorDictionary.Clear();
|
||||
_fieldParticleSystemDictionary.Clear();
|
||||
m_SoundAssetPathList.Clear();
|
||||
_gimicCntDictionary.Clear();
|
||||
SetShaderGlobalColorBG = null;
|
||||
BattleCoroutine.GetInstance().StopCoroutine(battleLoadCoroutine);
|
||||
}
|
||||
|
||||
public void CreateField(BattleCamera battleCamera, GameObject battle3DContainer, GameObject cutInContainer)
|
||||
{
|
||||
_battleCamera = battleCamera;
|
||||
m_Battle3DContainer = battle3DContainer;
|
||||
m_BattleCutInContainer = cutInContainer;
|
||||
Camera componentInChildren = m_Battle3DContainer.GetComponentInChildren<Camera>();
|
||||
Camera component = componentInChildren.transform.Find("Camera 3DGround").GetComponent<Camera>();
|
||||
_battleCamera.SetUp(componentInChildren, m_BattleCutInContainer.transform.Find("Camera").GetComponent<UICamera>(), component);
|
||||
LoadField();
|
||||
}
|
||||
|
||||
protected void LoadField()
|
||||
{
|
||||
IsLoadDone = false;
|
||||
m_BtlMgrIns = BattleManagerBase.GetIns();
|
||||
_str3DFieldNo = GetFieldIdString(FieldEffectId);
|
||||
_str3DFieldPath = "3DField" + GetFieldIdString(FieldId);
|
||||
m_SoundAssetPathList.Add($"s/se_field_{_str3DFieldNo}.acb");
|
||||
m_SoundAssetPathList.Add(string.Format("b/bgm_field_{0}.acb", (_bgmId != "NONE") ? GetFieldIdString(_bgmId) : _str3DFieldNo));
|
||||
m_SoundAssetPathList.Add(string.Format("b/bgm_field_{0}.awb", (_bgmId != "NONE") ? GetFieldIdString(_bgmId) : _str3DFieldNo));
|
||||
m_FieldAssetPath = Toolbox.ResourcesManager.GetAssetTypePath(_str3DFieldPath, ResourcesManager.AssetLoadPathType.Field3D);
|
||||
List<string> additionalAssetList = CollectAdditionalAssets();
|
||||
GameMgr.GetIns().GetEffectMgr().InitCommonEffect(string.Format("Json/FIeld" + _str3DFieldNo + "EffectData", _str3DFieldNo), isBattle: true);
|
||||
battleLoadCoroutine = BattleCoroutine.GetInstance().StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(m_SoundAssetPathList, delegate
|
||||
{
|
||||
BattleCoroutine.GetInstance().StartCoroutine(Toolbox.ResourcesManager.LoadAssetAsync(m_FieldAssetPath, delegate
|
||||
{
|
||||
Toolbox.ResourcesManager.BattleListAssetPathList.AddRange(m_SoundAssetPathList);
|
||||
Toolbox.ResourcesManager.BattleListAssetPathList.Add(m_FieldAssetPath);
|
||||
(UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(Toolbox.ResourcesManager.GetAssetTypePath(_str3DFieldPath, ResourcesManager.AssetLoadPathType.Field3D, isfetch: true))) as GameObject).name = _str3DFieldPath;
|
||||
if (additionalAssetList.IsNotNullOrEmpty())
|
||||
{
|
||||
BattleCoroutine.GetInstance().StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(additionalAssetList, delegate
|
||||
{
|
||||
Toolbox.ResourcesManager.BattleListAssetPathList.AddRange(additionalAssetList);
|
||||
BattleFieldBuild();
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
BattleFieldBuild();
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
private string GetFieldIdString(int fieldId)
|
||||
{
|
||||
return fieldId.ToString((fieldId < 100) ? "00" : "0000");
|
||||
}
|
||||
|
||||
private string GetFieldIdString(string fileldId)
|
||||
{
|
||||
if (int.TryParse(fileldId, out var result))
|
||||
{
|
||||
return result.ToString((result < 100) ? "00" : "0000");
|
||||
}
|
||||
return fileldId;
|
||||
}
|
||||
|
||||
protected virtual void BattleFieldBuild()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual List<string> CollectAdditionalAssets()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual void StartFieldSetEffect(Vector3 pos)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void StartFieldTapEffect(int areaId, Vector3 pos)
|
||||
{
|
||||
BattleManagerBase.GetIns().BattlePlayer.PlayerBattleView.IsTouchable();
|
||||
}
|
||||
|
||||
public void StartFieldOpening()
|
||||
{
|
||||
PlayBgm();
|
||||
OpeningVfx.OpenningLogStep = "StartFieldOpening";
|
||||
IsFieldRandom = true;
|
||||
BattleCoroutine.GetInstance().StartCoroutine(RunFieldOpening());
|
||||
}
|
||||
|
||||
public void PlayBgm()
|
||||
{
|
||||
GameMgr.GetIns().GetSoundMgr().PlayBGM(string.Format("bgm_field_{0}", (_bgmId != "NONE") ? GetFieldIdString(_bgmId) : _str3DFieldNo), 0f, 0L);
|
||||
}
|
||||
|
||||
protected virtual IEnumerator RunFieldOpening()
|
||||
{
|
||||
yield return new WaitForSeconds(0f);
|
||||
}
|
||||
|
||||
public static IEnumerator ObjectChecker(float fWaitSecs, string strObjectFind, Action callback)
|
||||
{
|
||||
while (GameObject.Find(strObjectFind) == null || !GameMgr.GetIns().GetEffectMgr().IsFieldEffectReady || !GameMgr.GetIns().GetEffectMgr().IsBattleUIEffectReady)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
public void StartFieldGimic(GameObject obj)
|
||||
{
|
||||
if (!GameMgr.GetIns().IsReplayBattle && BattleManagerBase.GetIns().BattlePlayer.PlayerBattleView.IsTouchable())
|
||||
{
|
||||
BattleCoroutine.GetInstance().StartCoroutine(RunFieldGimic(obj));
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual IEnumerator RunFieldGimic(GameObject obj)
|
||||
{
|
||||
yield return new WaitForSeconds(0f);
|
||||
}
|
||||
|
||||
public void StartFieldShake()
|
||||
{
|
||||
BattleCoroutine.GetInstance().StartCoroutine(RunFieldShake());
|
||||
}
|
||||
|
||||
protected virtual IEnumerator RunFieldShake()
|
||||
{
|
||||
yield return new WaitForSeconds(0f);
|
||||
}
|
||||
|
||||
public virtual void UpdateFieldRandom()
|
||||
{
|
||||
}
|
||||
|
||||
public void AddParticleToFieldObjDictionary(string targetPath)
|
||||
{
|
||||
string[] array = targetPath.Split('/');
|
||||
List<Transform> list = new List<Transform>();
|
||||
list.Add(_fieldParticles.transform);
|
||||
List<Transform> list2 = new List<Transform>();
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
{
|
||||
list2 = new List<Transform>();
|
||||
for (int j = 0; j < list.Count; j++)
|
||||
{
|
||||
list2.AddRange(FindAllChildByName(list[j], array[i]));
|
||||
}
|
||||
list = new List<Transform>(list2);
|
||||
}
|
||||
for (int k = 0; k < list2.Count; k++)
|
||||
{
|
||||
_fieldObjDictionary.Add(targetPath + "_" + k, list2[k].gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Transform> FindAllChildByName(Transform parent, string name)
|
||||
{
|
||||
List<Transform> list = new List<Transform>();
|
||||
for (int i = 0; i < parent.childCount; i++)
|
||||
{
|
||||
Transform child = parent.GetChild(i);
|
||||
if (child.name == name)
|
||||
{
|
||||
list.Add(child);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
23
SVSim.BattleEngine/Engine/BaseCardIDComp.cs
Normal file
23
SVSim.BattleEngine/Engine/BaseCardIDComp.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using Wizard.Battle;
|
||||
|
||||
internal class BaseCardIDComp : EqualityComparer<IReadOnlyBattleCardInfo>
|
||||
{
|
||||
public override bool Equals(IReadOnlyBattleCardInfo x, IReadOnlyBattleCardInfo y)
|
||||
{
|
||||
if (x == y)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (x.BaseParameter.BaseCardId == y.BaseParameter.BaseCardId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode(IReadOnlyBattleCardInfo obj)
|
||||
{
|
||||
return obj.BaseParameter.BaseCardId;
|
||||
}
|
||||
}
|
||||
79
SVSim.BattleEngine/Engine/BattleCamera.cs
Normal file
79
SVSim.BattleEngine/Engine/BattleCamera.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using UnityEngine;
|
||||
using Wizard.Battle.View.Vfx;
|
||||
|
||||
public class BattleCamera
|
||||
{
|
||||
public UICamera m_CutInCamera;
|
||||
|
||||
public Camera Camera;
|
||||
|
||||
public Camera _backgroundCamera;
|
||||
|
||||
public Vector3 BattleCameraPos { get; private set; }
|
||||
|
||||
public Vector3 BattleCameraRot { get; private set; }
|
||||
|
||||
public BattleCamera()
|
||||
{
|
||||
Camera = null;
|
||||
}
|
||||
|
||||
public void SetUp(Camera camera, UICamera cutInCamera, Camera backgroundCamera)
|
||||
{
|
||||
Camera = camera;
|
||||
m_CutInCamera = cutInCamera;
|
||||
_backgroundCamera = backgroundCamera;
|
||||
BattleCameraPos = Camera.transform.localPosition;
|
||||
BattleCameraRot = Camera.transform.eulerAngles;
|
||||
}
|
||||
|
||||
public VfxBase ShakeCamera(Vector3 amount, float time, float delay)
|
||||
{
|
||||
ParallelVfxPlayer parallelVfxPlayer = ParallelVfxPlayer.Create();
|
||||
parallelVfxPlayer.Register(InstantVfx.Create(delegate
|
||||
{
|
||||
iTween.ShakePosition(Camera.gameObject, iTween.Hash("amount", amount, "time", time, "delay", delay));
|
||||
}));
|
||||
return parallelVfxPlayer;
|
||||
}
|
||||
|
||||
public static VfxBase ShakeCameraGameObject(GameObject obj, Vector3 amount, float time, float delay)
|
||||
{
|
||||
ParallelVfxPlayer parallelVfxPlayer = ParallelVfxPlayer.Create();
|
||||
parallelVfxPlayer.Register(InstantVfx.Create(delegate
|
||||
{
|
||||
iTween.ShakePosition(obj, iTween.Hash("amount", amount, "time", time, "delay", delay));
|
||||
}));
|
||||
return parallelVfxPlayer;
|
||||
}
|
||||
|
||||
public VfxBase ShakeComplete()
|
||||
{
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
Camera.transform.localPosition = BattleCameraPos;
|
||||
Camera.transform.eulerAngles = BattleCameraRot;
|
||||
iTween.Stop(Camera.gameObject);
|
||||
});
|
||||
}
|
||||
|
||||
public static VfxBase ShakeCompleteGameObject(GameObject obj, Vector3 position, Vector3 euler)
|
||||
{
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
obj.transform.localPosition = position;
|
||||
obj.transform.eulerAngles = euler;
|
||||
iTween.Stop(obj);
|
||||
});
|
||||
}
|
||||
|
||||
public Camera Get3DCamera()
|
||||
{
|
||||
return Camera;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Camera = null;
|
||||
}
|
||||
}
|
||||
3876
SVSim.BattleEngine/Engine/BattleCardBase.cs
Normal file
3876
SVSim.BattleEngine/Engine/BattleCardBase.cs
Normal file
File diff suppressed because it is too large
Load Diff
29
SVSim.BattleEngine/Engine/BattleCardBaseExtensions.cs
Normal file
29
SVSim.BattleEngine/Engine/BattleCardBaseExtensions.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Wizard.Battle;
|
||||
using Wizard.Battle.View;
|
||||
|
||||
public static class BattleCardBaseExtensions
|
||||
{
|
||||
public static List<IBattleCardView> ConvertToViewList(this IList<BattleCardBase> battleCardBaseList)
|
||||
{
|
||||
return battleCardBaseList?.Select((BattleCardBase c) => c.BattleCardView).ToList();
|
||||
}
|
||||
|
||||
public static BattleCardBase FindFromCardId(this IList<BattleCardBase> battleCardBaseList, IBattleCardUniqueID cardId)
|
||||
{
|
||||
if (battleCardBaseList == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < battleCardBaseList.Count; i++)
|
||||
{
|
||||
BattleCardBase battleCardBase = battleCardBaseList[i];
|
||||
if (battleCardBase.EquelsID(cardId))
|
||||
{
|
||||
return battleCardBase;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
487
SVSim.BattleEngine/Engine/BattleCardIconAnimations.cs
Normal file
487
SVSim.BattleEngine/Engine/BattleCardIconAnimations.cs
Normal file
@@ -0,0 +1,487 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cute;
|
||||
using UnityEngine;
|
||||
using Wizard.Battle.View.Vfx;
|
||||
|
||||
public class BattleCardIconAnimations : MonoBehaviour
|
||||
{
|
||||
public class SkillIcon
|
||||
{
|
||||
public string _key;
|
||||
|
||||
public string _iconSpriteName;
|
||||
|
||||
public int LabelNumber;
|
||||
|
||||
public SkillIcon(string key, string iconSpriteName, int labelNumber)
|
||||
{
|
||||
_key = key;
|
||||
_iconSpriteName = iconSpriteName;
|
||||
LabelNumber = labelNumber;
|
||||
}
|
||||
}
|
||||
|
||||
private List<SkillIcon> skillIconList = new List<SkillIcon>();
|
||||
|
||||
private List<SkillIcon> skillIconListWithoutDuplicates = new List<SkillIcon>();
|
||||
|
||||
private CardTemplate cardTemplate;
|
||||
|
||||
private BattleCardBase _card;
|
||||
|
||||
private SkillCollectionBase collection;
|
||||
|
||||
private bool skillIconAlphaFlg;
|
||||
|
||||
private int skillCount;
|
||||
|
||||
private int _inductionLabelNumber = -1;
|
||||
|
||||
private const float ALPHA_BLEND_RATE = 0.6f;
|
||||
|
||||
private const string INDUCTION_ICON_SPRITE_NAME = "battle_notice_status_04";
|
||||
|
||||
private const string WHITE_RITUAL_STACK_SPRITE_NAME = "battle_notice_status_11";
|
||||
|
||||
public VfxBase Initialize(BattleCardBase card, SkillCollectionBase collection, bool isStackWhiteRitual = false)
|
||||
{
|
||||
_card = card;
|
||||
this.collection = collection;
|
||||
ISkillApplyInformation skillApplyInformation = card.SkillApplyInformation;
|
||||
bool isEarthRiteField = (IsEarthRiteField() ? true : false);
|
||||
bool hasInductionSkill = HasInductionSkill();
|
||||
bool hasInductionNumberSkill = HasInductionNumberSkill();
|
||||
bool hasKiller = (skillApplyInformation.IsKiller ? true : false);
|
||||
bool hasDrain = (skillApplyInformation.IsDrain ? true : false);
|
||||
bool hasWhenDestroySkill = (HasWhenDestroySkill() ? true : false);
|
||||
bool hasGetonSkill = HasGetonSkill();
|
||||
bool isGetOnAfter = _card.GetOnCards.Any();
|
||||
bool hasWhiteRirualStackSkill = HasStackWhiteRitualSkill();
|
||||
int whiteRitualCount = _card.SkillApplyInformation.WhiteRitualCount;
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
InitializeIcon(isEarthRiteField && !hasWhiteRirualStackSkill, isEarthRiteField && hasWhiteRirualStackSkill, whiteRitualCount, hasInductionSkill, hasInductionNumberSkill, hasKiller, hasDrain, hasWhenDestroySkill, hasGetonSkill, isGetOnAfter, isReplay: false, isStackWhiteRitual);
|
||||
});
|
||||
}
|
||||
|
||||
public VfxBase InitializeOnlyStack(BattleCardBase card, SkillCollectionBase collection)
|
||||
{
|
||||
_card = card;
|
||||
this.collection = collection;
|
||||
bool isEarthRiteField = IsEarthRiteField();
|
||||
bool hasWhiteRirualStackSkill = HasStackWhiteRitualSkill();
|
||||
int whiteRitualCount = _card.SkillApplyInformation.WhiteRitualCount;
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
InitializeIcon(isEarthRiteField && !hasWhiteRirualStackSkill, isEarthRiteField && hasWhiteRirualStackSkill, whiteRitualCount, hasInductionSkill: false, hasInductionNumberSkill: false, hasKiller: false, hasDrain: false, hasWhenDestroySkill: false, hasGetonSkill: false);
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeIcon(bool hasWhiteRirualSkill, bool hasWhiteRirualStackSkill, int whiteRitualCount, bool hasInductionSkill, bool hasInductionNumberSkill, bool hasKiller, bool hasDrain, bool hasWhenDestroySkill, bool hasGetonSkill, bool isGetOnAfter = false, bool isReplay = false, bool isStackWhiteRitual = false)
|
||||
{
|
||||
if (!(_card.BattleCardView.GameObject == null))
|
||||
{
|
||||
ClearAllSkillIcons();
|
||||
cardTemplate = _card.BattleCardView.GameObject.GetComponent<CardTemplate>();
|
||||
cardTemplate.SkillIconTemp.gameObject.transform.localPosition = new Vector3(0f, -30f, -0.1f);
|
||||
cardTemplate.SkillIconTemp.gameObject.transform.localScale = new Vector3(0.2f, 0.2f, 1f);
|
||||
cardTemplate.SkillIconTemp.atlas = UIManager.GetInstance().GetAtlasList().FirstOrDefault((UIAtlas s) => s.name == "Battle");
|
||||
AddToIconList("white_ritual", "battle_notice_status_08", hasWhiteRirualSkill);
|
||||
AddToIconList("stack_white_ritual", "battle_notice_status_11", hasWhiteRirualStackSkill, whiteRitualCount);
|
||||
AddToIconList("induction", "battle_notice_status_04", hasInductionSkill);
|
||||
AddToIconList("induction_number", "battle_notice_status_04", hasInductionNumberSkill, GetInductionLabelNumber());
|
||||
AddToIconList("killer", "battle_notice_status_01", hasKiller);
|
||||
AddToIconList("drain", "battle_notice_status_07", hasDrain);
|
||||
AddToIconList("destroy", "battle_notice_status_06", hasWhenDestroySkill);
|
||||
if (isReplay)
|
||||
{
|
||||
AddToIconList("geton", "battle_notice_status_09", hasGetonSkill);
|
||||
AddToIconList("geton_after", "battle_notice_status_10", isGetOnAfter);
|
||||
}
|
||||
else if (isGetOnAfter)
|
||||
{
|
||||
AddToIconList("geton_after", "battle_notice_status_10", hasGetonSkill);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddToIconList("geton", "battle_notice_status_09", hasGetonSkill);
|
||||
}
|
||||
PopulateSkillIconListWithoutDuplicates();
|
||||
string spriteName = (skillIconListWithoutDuplicates.Any() ? skillIconListWithoutDuplicates[0]._iconSpriteName : string.Empty);
|
||||
cardTemplate.SkillIconTemp.spriteName = spriteName;
|
||||
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIconListWithoutDuplicates.Any() ? skillIconListWithoutDuplicates[0].LabelNumber : (-1));
|
||||
UpdateSkillIconLabelColor();
|
||||
skillCount = 0;
|
||||
cardTemplate.SkillIconTemp.gameObject.SetActive(value: true);
|
||||
if (isStackWhiteRitual)
|
||||
{
|
||||
cardTemplate.SkillIconTemp.alpha = 1.5f;
|
||||
skillIconAlphaFlg = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public VfxBase UpdateLabelNumber()
|
||||
{
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
SkillIcon skillIcon = skillIconListWithoutDuplicates.FirstOrDefault((SkillIcon i) => i._key == "induction_number");
|
||||
if (skillIcon != null)
|
||||
{
|
||||
skillIcon.LabelNumber = GetInductionLabelNumber();
|
||||
if (cardTemplate.SkillIconTemp.spriteName == "battle_notice_status_04")
|
||||
{
|
||||
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIcon.LabelNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public VfxBase UpdateWhiteRitualCountLabel()
|
||||
{
|
||||
if (!HasStackWhiteRitualSkill() || !IsEarthRiteField())
|
||||
{
|
||||
return NullVfx.GetInstance();
|
||||
}
|
||||
int whiteRitualCount = _card.SkillApplyInformation.WhiteRitualCount;
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
SkillIcon skillIcon = skillIconListWithoutDuplicates.FirstOrDefault((SkillIcon i) => i._key == "stack_white_ritual");
|
||||
if (skillIcon != null)
|
||||
{
|
||||
skillIcon.LabelNumber = whiteRitualCount;
|
||||
if (cardTemplate.SkillIconTemp.spriteName == "battle_notice_status_11")
|
||||
{
|
||||
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIcon.LabelNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void AddToIconList(string key, string spriteName, bool addCondition, int labelNumber = -1)
|
||||
{
|
||||
if (addCondition)
|
||||
{
|
||||
AddSkillIcon(key, spriteName, labelNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateSkillIconListWithoutDuplicates()
|
||||
{
|
||||
AddToIconListWithoutDuplicates("white_ritual");
|
||||
AddToIconListWithoutDuplicates("stack_white_ritual");
|
||||
AddToIconListWithoutDuplicates("induction");
|
||||
AddToIconListWithoutDuplicates("induction_number");
|
||||
AddToIconListWithoutDuplicates("destroy");
|
||||
AddToIconListWithoutDuplicates("killer");
|
||||
AddToIconListWithoutDuplicates("drain");
|
||||
AddToIconListWithoutDuplicates("geton");
|
||||
}
|
||||
|
||||
private void AddToIconListWithoutDuplicates(string key)
|
||||
{
|
||||
if (skillIconList.Any((SkillIcon c) => c._key == key) && !skillIconListWithoutDuplicates.Any((SkillIcon c) => c._key == key))
|
||||
{
|
||||
SkillIcon skillIcon = skillIconList.SingleOrDefault((SkillIcon c) => c._key == key && c._iconSpriteName != null);
|
||||
skillIconListWithoutDuplicates.Add(new SkillIcon(key, skillIcon._iconSpriteName, skillIcon.LabelNumber));
|
||||
}
|
||||
}
|
||||
|
||||
public VfxBase ShowIcon()
|
||||
{
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
cardTemplate.SkillIconTemp.gameObject.SetActive(value: true);
|
||||
});
|
||||
}
|
||||
|
||||
public VfxBase HideIcon()
|
||||
{
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
cardTemplate.SkillIconTemp.gameObject.SetActive(value: false);
|
||||
});
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (cardTemplate != null)
|
||||
{
|
||||
if (cardTemplate.SkillIconTemp.gameObject.activeSelf)
|
||||
{
|
||||
SkillIconAlphaBlend();
|
||||
}
|
||||
else
|
||||
{
|
||||
cardTemplate.SkillIconTemp.alpha = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddSkillIcon(string key, string fileName, int labelNumber = -1)
|
||||
{
|
||||
string iconSpriteName = ((!skillIconList.Any((SkillIcon v) => v._key == key)) ? fileName : null);
|
||||
skillIconList.Add(new SkillIcon(key, iconSpriteName, labelNumber));
|
||||
skillIconListWithoutDuplicates = skillIconList.Where((SkillIcon v) => v._iconSpriteName != null).ToList();
|
||||
}
|
||||
|
||||
public void DeleteSkillIcon(string key)
|
||||
{
|
||||
if (skillIconList.Any((SkillIcon v) => v._key == key))
|
||||
{
|
||||
skillIconList.Remove(skillIconList.Where((SkillIcon v) => v._key == key).Last());
|
||||
}
|
||||
skillIconListWithoutDuplicates = skillIconList.Where((SkillIcon v) => v._iconSpriteName != null).ToList();
|
||||
if (skillIconListWithoutDuplicates.Count == 0)
|
||||
{
|
||||
cardTemplate.SkillIconTemp.spriteName = string.Empty;
|
||||
cardTemplate.SkillIconLabelTemp.text = string.Empty;
|
||||
}
|
||||
ChangeTexture();
|
||||
}
|
||||
|
||||
public void DeleteUnneededSkillIcons()
|
||||
{
|
||||
RemoveSkillIconFromList("white_ritual", () => !IsEarthRiteField());
|
||||
RemoveSkillIconFromList("induction", () => !HasInductionSkill());
|
||||
RemoveSkillIconFromList("induction_number", () => !HasInductionNumberSkill());
|
||||
RemoveSkillIconFromList("destroy", () => !HasWhenDestroySkill());
|
||||
}
|
||||
|
||||
private void RemoveSkillIconFromList(string key, Func<bool> deleteCondition)
|
||||
{
|
||||
if (deleteCondition())
|
||||
{
|
||||
DeleteSkillIcon(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void ChangeTexture()
|
||||
{
|
||||
if (skillIconListWithoutDuplicates.Count() - 1 > skillCount)
|
||||
{
|
||||
skillCount++;
|
||||
cardTemplate.SkillIconTemp.spriteName = skillIconListWithoutDuplicates[skillCount]._iconSpriteName;
|
||||
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIconListWithoutDuplicates[skillCount].LabelNumber);
|
||||
}
|
||||
else if (skillIconListWithoutDuplicates.Count() != 0)
|
||||
{
|
||||
skillCount = 0;
|
||||
cardTemplate.SkillIconTemp.spriteName = skillIconListWithoutDuplicates[skillCount]._iconSpriteName;
|
||||
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIconListWithoutDuplicates[skillCount].LabelNumber);
|
||||
}
|
||||
UpdateSkillIconLabelColor();
|
||||
}
|
||||
|
||||
private void UpdateSkillIconLabelColor()
|
||||
{
|
||||
if (cardTemplate.SkillIconTemp.spriteName == "battle_notice_status_11")
|
||||
{
|
||||
cardTemplate.SkillIconLabelTemp.color = Color.white;
|
||||
cardTemplate.SkillIconLabelTemp.effectColor = Color.black;
|
||||
}
|
||||
else if (cardTemplate.SkillIconTemp.spriteName == "battle_notice_status_04")
|
||||
{
|
||||
cardTemplate.SkillIconLabelTemp.color = Color.black;
|
||||
cardTemplate.SkillIconLabelTemp.effectColor = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
private void ChangeSkillIconLabel(UILabel label, int labelNumber)
|
||||
{
|
||||
if (labelNumber == -1)
|
||||
{
|
||||
label.text = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
label.text = labelNumber.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearAllSkillIcons()
|
||||
{
|
||||
skillIconList.Clear();
|
||||
skillIconListWithoutDuplicates.Clear();
|
||||
}
|
||||
|
||||
private void SkillIconAlphaBlend()
|
||||
{
|
||||
bool flag = cardTemplate.SkillIconLabelTemp.text.IsNotNullOrEmpty();
|
||||
if (skillIconListWithoutDuplicates.Count > 1)
|
||||
{
|
||||
if (skillIconAlphaFlg)
|
||||
{
|
||||
cardTemplate.SkillIconTemp.alpha += (flag ? (0.6f * Time.deltaTime * 2f) : (0.6f * Time.deltaTime));
|
||||
}
|
||||
else
|
||||
{
|
||||
cardTemplate.SkillIconTemp.alpha -= (flag ? (0.6f * Time.deltaTime * 2f) : (0.6f * Time.deltaTime));
|
||||
}
|
||||
}
|
||||
else if (cardTemplate.SkillIconTemp.spriteName == string.Empty)
|
||||
{
|
||||
cardTemplate.SkillIconTemp.alpha = 1f;
|
||||
if (skillIconListWithoutDuplicates.Count > 0)
|
||||
{
|
||||
if (cardTemplate.SkillIconTemp.spriteName != skillIconListWithoutDuplicates[0]._iconSpriteName)
|
||||
{
|
||||
cardTemplate.SkillIconTemp.spriteName = skillIconListWithoutDuplicates[0]._iconSpriteName;
|
||||
}
|
||||
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIconListWithoutDuplicates[0].LabelNumber);
|
||||
UpdateSkillIconLabelColor();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cardTemplate.SkillIconTemp.alpha = 1f;
|
||||
}
|
||||
if (skillIconAlphaFlg && cardTemplate.SkillIconTemp.alpha >= (flag ? 2f : 1f))
|
||||
{
|
||||
skillIconAlphaFlg = false;
|
||||
}
|
||||
else if (!skillIconAlphaFlg && cardTemplate.SkillIconTemp.alpha <= 0f)
|
||||
{
|
||||
ChangeTexture();
|
||||
skillIconAlphaFlg = true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasWhenDestroySkill()
|
||||
{
|
||||
return collection._skillTimingInfo.IsWhenDestroy;
|
||||
}
|
||||
|
||||
public bool HasInductionSkill()
|
||||
{
|
||||
for (int i = 0; i < collection.Count(); i++)
|
||||
{
|
||||
SkillBase skillBase = collection.ElementAt(i);
|
||||
if (skillBase.IsInductionSkill && skillBase.SkillPrm.buildInfo._icon == "induction")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasStackWhiteRitualSkill()
|
||||
{
|
||||
return collection.Any((SkillBase x) => x is Skill_stack_white_ritual);
|
||||
}
|
||||
|
||||
public bool HasGetonSkill()
|
||||
{
|
||||
return collection.Any((SkillBase x) => x is Skill_geton);
|
||||
}
|
||||
|
||||
public bool HasInductionNumberSkill()
|
||||
{
|
||||
for (int i = 0; i < collection.Count(); i++)
|
||||
{
|
||||
SkillBase skillBase = collection.ElementAt(i);
|
||||
if (skillBase.IsInductionSkill && skillBase.SkillPrm.buildInfo._icon != "induction" && skillBase.SkillPrm.buildInfo._icon.Contains("induction"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int GetInductionLabelNumber()
|
||||
{
|
||||
if (_inductionLabelNumber != -1)
|
||||
{
|
||||
return _inductionLabelNumber;
|
||||
}
|
||||
SkillBase skillBase = collection.FirstOrDefault((SkillBase s) => s.IsInductionSkill && s.SkillPrm.buildInfo._icon != "induction" && s.SkillPrm.buildInfo._icon.Contains("induction"));
|
||||
if (skillBase == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
SkillOptionValue skillOptionValue = new SkillOptionValue(skillBase.SkillPrm.buildInfo._icon);
|
||||
skillOptionValue.SetupFilterVariable(BattleManagerBase.GetIns().GetBattlePlayerInfoPair(_card.IsPlayer), _card, isPrePlay: false, null);
|
||||
return skillOptionValue.GetInt(SkillFilterCreator.ContentKeyword.induction);
|
||||
}
|
||||
|
||||
private bool IsEarthRiteField()
|
||||
{
|
||||
if (_card.IsField || _card.IsChantField)
|
||||
{
|
||||
return _card.IsTribe(CardBasePrm.TribeType.WHITE_RITUAL);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public VfxBase UpdateSkillIconInReplay(List<NetworkBattleReceiver.InplaySkillEffect> inplaySkillEffectList, int inductionNumber, bool isInitialize, bool isStackWhiteRitual = false)
|
||||
{
|
||||
if (!isInitialize && _card.HasStackWhiteRitualAndOtherIconSkill() && skillIconListWithoutDuplicates.Count < 2)
|
||||
{
|
||||
return NullVfx.GetInstance();
|
||||
}
|
||||
_inductionLabelNumber = inductionNumber;
|
||||
bool hasWhiteRitualSkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.WhiteRitual);
|
||||
bool hasWhiteRirualStackSkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.StackWhiteRitual);
|
||||
bool hasInductionSkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Induction);
|
||||
bool hasInductionNumberSkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.InductionNumber);
|
||||
bool hasKiller = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Killer);
|
||||
bool hasDrain = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Drain);
|
||||
bool hasWhenDestroySkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Destroy);
|
||||
bool hasGeton = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Geton);
|
||||
bool hasGetonAfter = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.GetonAfter);
|
||||
int whiteRitualCount = _card.SkillApplyInformation.WhiteRitualCount;
|
||||
return InstantVfx.Create(delegate
|
||||
{
|
||||
if (skillIconList.Count == 0 || isInitialize)
|
||||
{
|
||||
InitializeIcon(hasWhiteRitualSkill, hasWhiteRirualStackSkill, whiteRitualCount, hasInductionSkill, hasInductionNumberSkill, hasKiller, hasDrain, hasWhenDestroySkill, hasGeton, hasGetonAfter, isReplay: true, isStackWhiteRitual);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateSkillIcon("white_ritual", "battle_notice_status_08", hasWhiteRitualSkill);
|
||||
UpdateSkillIcon("stack_white_ritual", "battle_notice_status_11", hasWhiteRirualStackSkill, whiteRitualCount);
|
||||
UpdateSkillIcon("induction", "battle_notice_status_04", hasInductionSkill);
|
||||
UpdateSkillIcon("induction_number", "battle_notice_status_04", hasInductionNumberSkill, GetInductionLabelNumber());
|
||||
UpdateSkillIcon("killer", "battle_notice_status_01", hasKiller);
|
||||
UpdateSkillIcon("drain", "battle_notice_status_07", hasDrain);
|
||||
UpdateSkillIcon("destroy", "battle_notice_status_06", hasWhenDestroySkill);
|
||||
UpdateSkillIcon("geton", "battle_notice_status_09", hasGeton);
|
||||
UpdateSkillIcon("geton_after", "battle_notice_status_10", hasGetonAfter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateSkillIcon(string key, string spriteName, bool hasIcon, int labelNumber = -1)
|
||||
{
|
||||
if (hasIcon && !skillIconList.Any((SkillIcon v) => v._key == key))
|
||||
{
|
||||
AddToIconList(key, spriteName, hasIcon, labelNumber);
|
||||
}
|
||||
else if (!hasIcon && skillIconList.Any((SkillIcon v) => v._key == key))
|
||||
{
|
||||
DeleteSkillIcon(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteSkillIcons()
|
||||
{
|
||||
if (!(cardTemplate == null))
|
||||
{
|
||||
DeleteSkillIcon("white_ritual");
|
||||
DeleteSkillIcon("stack_white_ritual");
|
||||
DeleteSkillIcon("induction");
|
||||
DeleteSkillIcon("induction_number");
|
||||
DeleteSkillIcon("destroy");
|
||||
DeleteSkillIcon("killer");
|
||||
DeleteSkillIcon("drain");
|
||||
DeleteSkillIcon("geton");
|
||||
}
|
||||
}
|
||||
|
||||
public int GetIconListCount()
|
||||
{
|
||||
return skillIconListWithoutDuplicates.Count;
|
||||
}
|
||||
}
|
||||
124
SVSim.BattleEngine/Engine/BattleControl.cs
Normal file
124
SVSim.BattleEngine/Engine/BattleControl.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Cute;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
using Wizard.Battle.UI;
|
||||
using Wizard.Battle.View.Vfx;
|
||||
|
||||
public class BattleControl : MonoBehaviour
|
||||
{
|
||||
private BattleManagerBase m_BtlMgrIns;
|
||||
|
||||
private int FirstAttack;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
m_BtlMgrIns = BattleManagerBase.GetIns();
|
||||
GameMgr.GetIns().GetInputMgr().SetLayerMask(512);
|
||||
LocalLog.AccumulateLastTraceLog("StartBattleCoroutine ");
|
||||
StartCoroutine(WaitLoadOpponentObjectToBattleStart(m_BtlMgrIns.LoadOpponentObjects()));
|
||||
}
|
||||
|
||||
private IEnumerator WaitLoadOpponentObjectToBattleStart(VfxBase vfx)
|
||||
{
|
||||
if (GameMgr.GetIns().IsNetworkBattle)
|
||||
{
|
||||
FirstAttack = ToolboxGame.RealTimeNetworkAgent.GetIsFirstPlayer();
|
||||
}
|
||||
while (!vfx.IsEnd)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
LocalLog.AccumulateLastTraceLog("DecideFirstUser End ");
|
||||
m_BtlMgrIns.StartOpening(FirstAttack);
|
||||
}
|
||||
|
||||
public void BattleEnd(UIManager.ViewScene MoveTo, Action callback = null, Action<UIManager.ChangeViewSceneParam> paramCustomize = null, object sceneParam = null)
|
||||
{
|
||||
ToolboxGame.UIManager.gameObject.SetActive(value: true);
|
||||
UIManager.ChangeViewSceneParam changeViewSceneParam = new UIManager.ChangeViewSceneParam();
|
||||
changeViewSceneParam.OnBeforeChange = delegate
|
||||
{
|
||||
BattleManagerBase.GetIns().DisposeBattleGameObj();
|
||||
};
|
||||
changeViewSceneParam.OnChange = delegate
|
||||
{
|
||||
GameMgr.GetIns().GetEffectMgr().DestroyBattleEffectContainer();
|
||||
GameMgr.GetIns().GetDataMgr().ResetEnemyData();
|
||||
GameMgr.GetIns().DestroyBattleManagements();
|
||||
GameMgr.GetIns().GetGameObjMgr().GetUIContainer()
|
||||
.SetActive(value: false);
|
||||
if (callback != null)
|
||||
{
|
||||
callback();
|
||||
}
|
||||
};
|
||||
paramCustomize.Call(changeViewSceneParam);
|
||||
StartCoroutine(UnloadAllResources(MoveTo, changeViewSceneParam, null, sceneParam));
|
||||
}
|
||||
|
||||
private IEnumerator UnloadAllResources(UIManager.ViewScene MoveTo = UIManager.ViewScene.None, UIManager.ChangeViewSceneParam param = null, Action callback = null, object sceneParam = null)
|
||||
{
|
||||
BattleLogManager.GetInstance().Clear();
|
||||
GameMgr.GetIns().GetEffectMgr().ClearLastCacheEffect();
|
||||
StopAllTweens();
|
||||
yield return Resources.UnloadUnusedAssets();
|
||||
GC.Collect();
|
||||
callback?.Invoke();
|
||||
if (MoveTo != UIManager.ViewScene.None)
|
||||
{
|
||||
UIManager.GetInstance().ChangeViewScene(MoveTo, param, sceneParam);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator BattleEnd(Action callback = null)
|
||||
{
|
||||
BattleRelease();
|
||||
yield return Resources.UnloadUnusedAssets();
|
||||
GC.Collect();
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
public void BattleRelease()
|
||||
{
|
||||
ToolboxGame.UIManager.gameObject.SetActive(value: true);
|
||||
GameMgr.GetIns().GetEffectMgr().DestroyBattleEffectContainer();
|
||||
GameMgr.GetIns().GetDataMgr().ResetEnemyData();
|
||||
if (BattleManagerBase.GetIns() != null)
|
||||
{
|
||||
BattleManagerBase.GetIns().DisposeBattleGameObj();
|
||||
}
|
||||
GameMgr.GetIns().DestroyBattleManagements();
|
||||
GameMgr.GetIns().GetGameObjMgr().GetUIContainer()
|
||||
.SetActive(value: false);
|
||||
BattleLogManager.GetInstance().Clear();
|
||||
GameMgr.GetIns().GetEffectMgr().ClearLastCacheEffect();
|
||||
StopAllTweens();
|
||||
}
|
||||
|
||||
private void StopAllTweens()
|
||||
{
|
||||
HashSet<GameObject> hashSet = new HashSet<GameObject>();
|
||||
for (int i = 0; i < iTween.tweens.Count; i++)
|
||||
{
|
||||
if (iTween.tweens[i] != null)
|
||||
{
|
||||
GameObject gameObject = (GameObject)iTween.tweens[i]["target"];
|
||||
if (gameObject != null)
|
||||
{
|
||||
hashSet.Add(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (GameObject item in hashSet)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
iTween.Stop(item);
|
||||
}
|
||||
}
|
||||
iTween.tweens.Clear();
|
||||
}
|
||||
}
|
||||
52
SVSim.BattleEngine/Engine/BattleCoroutine.cs
Normal file
52
SVSim.BattleEngine/Engine/BattleCoroutine.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class BattleCoroutine
|
||||
{
|
||||
private static BattleCoroutine m_instance;
|
||||
|
||||
private static MonoBehaviour _coroutineObject;
|
||||
|
||||
public static BattleCoroutine GetInstance()
|
||||
{
|
||||
if (m_instance == null)
|
||||
{
|
||||
m_instance = new BattleCoroutine();
|
||||
}
|
||||
if (_coroutineObject == null)
|
||||
{
|
||||
GameObject gameObject = Object.Instantiate(Resources.Load("Prefab/Game/_BattleCoroutine")) as GameObject;
|
||||
if (null != gameObject)
|
||||
{
|
||||
_coroutineObject = gameObject.GetComponent<MonoBehaviour>();
|
||||
}
|
||||
}
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
public Coroutine StartCoroutine(IEnumerator enumerator)
|
||||
{
|
||||
return _coroutineObject.StartCoroutine(enumerator);
|
||||
}
|
||||
|
||||
public void StopAllCoroutines()
|
||||
{
|
||||
_coroutineObject.StopAllCoroutines();
|
||||
}
|
||||
|
||||
public void StopCoroutine(IEnumerator enumerator)
|
||||
{
|
||||
if (enumerator != null)
|
||||
{
|
||||
_coroutineObject.StopCoroutine(enumerator);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopCoroutine(Coroutine enumerator)
|
||||
{
|
||||
if (enumerator != null)
|
||||
{
|
||||
_coroutineObject.StopCoroutine(enumerator);
|
||||
}
|
||||
}
|
||||
}
|
||||
246
SVSim.BattleEngine/Engine/BattleEnemy.cs
Normal file
246
SVSim.BattleEngine/Engine/BattleEnemy.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cute;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
using Wizard.Battle.Player.Emotion;
|
||||
using Wizard.Battle.View;
|
||||
using Wizard.Battle.View.Vfx;
|
||||
|
||||
public class BattleEnemy : BattlePlayerBase
|
||||
{
|
||||
private readonly Vector3 OFFSET_THINK_ICON_FROM_CLASSVIEW = new Vector3(0.62f, 0.15f, 0f);
|
||||
|
||||
private IEmotion _emotion;
|
||||
|
||||
private readonly Vector3 FIELD_CENTER_POSITION = new Vector3(0f, 0.25f, 0f);
|
||||
|
||||
public override bool IsGameFirst => !base.BattleMgr.IsFirst;
|
||||
|
||||
public override bool IsPlayer => false;
|
||||
|
||||
public override IBattlePlayerView BattleView => BattleEnemyView;
|
||||
|
||||
public override IEmotion Emotion => _emotion;
|
||||
|
||||
public virtual IBattlePlayerView BattleEnemyView { get; protected set; }
|
||||
|
||||
public bool EnableEnemyAI { get; set; }
|
||||
|
||||
public override int Turn
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!base.BattleMgr.IsFirst)
|
||||
{
|
||||
return base.BattleMgr.FirstTurn;
|
||||
}
|
||||
return base.BattleMgr.SecondTurn;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (base.BattleMgr.IsFirst)
|
||||
{
|
||||
base.BattleMgr.SecondTurn = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.BattleMgr.FirstTurn = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<List<int>> OnMulliganEndForReplay;
|
||||
|
||||
public BattleEnemy(BattleManagerBase battleMgr, BattleCamera battleCamera, BackGroundBase backGround, IInnerOptionsBuilder innerOptionsBuilder)
|
||||
: base(battleMgr, battleCamera, backGround, innerOptionsBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
BattleEnemyView = new BattleEnemyView(this);
|
||||
}
|
||||
|
||||
protected override void CreateSelfBattleCard()
|
||||
{
|
||||
EnemyClassBattleCard item = new EnemyClassBattleCard(new ClassBattleCardBase.ClassBuildInfo(_isPlayer: false, 20, this, base.BattleMgr.BattlePlayer, base.BattleMgr, base.BattleMgr.BattleResourceMgr));
|
||||
base.ClassAndInPlayCardList.Add(item);
|
||||
}
|
||||
|
||||
public override void Setup(BattlePlayerBase opponentBattlePlayer)
|
||||
{
|
||||
_emotion = _innerOptionsBuilder.CreateEnemyEmotion((IClassBattleCardView)base.Class.BattleCardView);
|
||||
base.Setup(opponentBattlePlayer);
|
||||
}
|
||||
|
||||
public override void SetupClone(BattlePlayerBase sourceBattlePlayer, BattlePlayerBase virtualOpponentBattlePlayer, CloneActualFlags cloneFlags)
|
||||
{
|
||||
sourceBattlePlayer.CopyToVirtualBase(this, virtualOpponentBattlePlayer, cloneFlags);
|
||||
}
|
||||
|
||||
public override VfxBase StartTurnControl(string log = "")
|
||||
{
|
||||
if (GameMgr.GetIns().IsAdminWatch)
|
||||
{
|
||||
UpdateHandCardsPlayability();
|
||||
}
|
||||
Turn++;
|
||||
SequentialVfxPlayer sequentialVfxPlayer = TurnEvolveControl(BattleView.EpIcon);
|
||||
VfxBase vfx = TurnStart();
|
||||
sequentialVfxPlayer.Register(vfx);
|
||||
VfxBase vfx2 = BattleManagerBase.GetIns().JudgeBattleResult();
|
||||
sequentialVfxPlayer.Register(vfx2);
|
||||
sequentialVfxPlayer.Register(CreateThinkingVfx(base.BattleMgr));
|
||||
return sequentialVfxPlayer;
|
||||
}
|
||||
|
||||
public VfxBase CreateThinkingVfx(BattleManagerBase battleMgr)
|
||||
{
|
||||
if (GameMgr.GetIns().IsAdminWatch)
|
||||
{
|
||||
return NullVfx.GetInstance();
|
||||
}
|
||||
return new DelaySetupVfx(() => new ThinkIconShowVfx(delegate
|
||||
{
|
||||
Vector3 position = base.BattleCamera.Get3DCamera().WorldToScreenPoint(base.Class.BattleCardView.Transform.position + OFFSET_THINK_ICON_FROM_CLASSVIEW);
|
||||
return UIManager.GetInstance().getCamera().ScreenToWorldPoint(position);
|
||||
}, battleMgr.BattleResourceMgr));
|
||||
}
|
||||
|
||||
public override VfxBase UsePp(int pp, bool isNewReplayMoveTurn = false)
|
||||
{
|
||||
base.UsePp(pp);
|
||||
int usedPp = base.Pp;
|
||||
int maxPp = base.PpTotal;
|
||||
Vector3 labelPosition = default(Vector3);
|
||||
SequentialVfxPlayer sequentialVfxPlayer = SequentialVfxPlayer.Create();
|
||||
sequentialVfxPlayer.Register(InstantVfx.Create(delegate
|
||||
{
|
||||
Vector3 position = base.BattleCamera.Get3DCamera().WorldToScreenPoint(StatusPanelControl.GetPPPanel().transform.Find("PPIcon/PPLabel").transform.position);
|
||||
labelPosition = UIManager.GetInstance().getCamera().ScreenToWorldPoint(position);
|
||||
}));
|
||||
sequentialVfxPlayer.Register(new DelaySetupVfx(() => m_vfxCreator.CreateUsePp(usedPp, maxPp, labelPosition, isNewReplayMoveTurn)));
|
||||
return sequentialVfxPlayer;
|
||||
}
|
||||
|
||||
protected override VfxBase TurnStartDrawCard(SkillProcessor skillProcessor)
|
||||
{
|
||||
NullVfx.GetInstance();
|
||||
int drawCount = ((IsGameFirst || Turn != 1) ? 1 : 2);
|
||||
VfxWith<IEnumerable<BattleCardBase>> vfxWith = RandomCardDraw(drawCount, skillProcessor);
|
||||
VfxBase vfxBase = CardDrawVfx(vfxWith.Value);
|
||||
SequentialVfxPlayer result = SequentialVfxPlayer.Create(vfxWith.Vfx, vfxBase);
|
||||
if (!base.Class.IsDead && EnableEnemyAI)
|
||||
{
|
||||
base.BattleMgr.EnemyAI.ExecuteEnemyAI(useWait: true);
|
||||
}
|
||||
_ = base.Class.IsDead;
|
||||
return result;
|
||||
}
|
||||
|
||||
public override VfxBase CardDrawVfx(IEnumerable<BattleCardBase> DrawList, bool skipShuffle = false, bool isOpenDrawSkill = false)
|
||||
{
|
||||
SequentialVfxPlayer sequentialVfxPlayer = SequentialVfxPlayer.Create();
|
||||
if (GameMgr.GetIns().IsAdminWatch)
|
||||
{
|
||||
foreach (BattleCardBase card in DrawList)
|
||||
{
|
||||
if (card.BaseCost != card.Cost)
|
||||
{
|
||||
List<int> costList = card.BattleCardView.GetUseCostList(card.Cost);
|
||||
sequentialVfxPlayer.Register(InstantVfx.Create(delegate
|
||||
{
|
||||
card.BattleCardView.UpdateCost(costList);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
sequentialVfxPlayer.Register(new OpponentDrawCardVfx(DrawList, isOpenDrawSkill));
|
||||
sequentialVfxPlayer.Register(new OpponentDrawCardToHandVfx(DrawList.ToList(), 0.4f, isOpenDrawSkill, skipShuffle));
|
||||
return sequentialVfxPlayer;
|
||||
}
|
||||
|
||||
public override VfxBase TurnEnd()
|
||||
{
|
||||
ParallelVfxPlayer result = ParallelVfxPlayer.Create(base.TurnEnd(), new ThinkIconHideVfx(base.BattleMgr.BattleResourceMgr));
|
||||
if (GameMgr.GetIns().IsAdminWatch)
|
||||
{
|
||||
foreach (BattleCardBase handCard in base.HandCardList)
|
||||
{
|
||||
handCard.BattleCardView.HideCanPlayEffect();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override void SetActive()
|
||||
{
|
||||
if (GameMgr.GetIns().IsAdminWatch)
|
||||
{
|
||||
UpdateHandCardsPlayability();
|
||||
}
|
||||
if (!IsGameFirst || Turn != 1)
|
||||
{
|
||||
base.IsChoiceBraveEffectTiming = true;
|
||||
BattleEnemyView.UpdateChoiceBraveButtonPulsateEffectAndSprite();
|
||||
}
|
||||
}
|
||||
|
||||
public override BattlePlayerBase CreateVirtualPlayer()
|
||||
{
|
||||
return new VirtualBattleEnemy(base.BattleMgr, base.BattleCamera, base.BackGround);
|
||||
}
|
||||
|
||||
public override void UpdateHandCardsPlayability(bool areArrowsForcedOff = false)
|
||||
{
|
||||
foreach (BattleCardBase handCard in _opponentBattlePlayer.HandCardList)
|
||||
{
|
||||
handCard.BattleCardView.areArrowsForcedOff = areArrowsForcedOff;
|
||||
handCard.BattleCardView.UpdateMovability();
|
||||
}
|
||||
if (!GameMgr.GetIns().IsAdmin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (BattleCardBase handCard2 in base.HandCardList)
|
||||
{
|
||||
handCard2.BattleCardView.areArrowsForcedOff = areArrowsForcedOff;
|
||||
handCard2.BattleCardView.UpdateMovability();
|
||||
}
|
||||
if (base.IsSelfTurn)
|
||||
{
|
||||
BattleView.UpdateChoiceBraveButtonPulsateEffectAndSprite();
|
||||
}
|
||||
}
|
||||
|
||||
public override VfxBase MoveToHand(List<BattleCardBase> cardsToMoveToHand)
|
||||
{
|
||||
return SequentialVfxPlayer.Create(new OpponentDrawCardToHandVfx(cardsToMoveToHand.ToList(), 0.3f), InstantVfx.Create(delegate
|
||||
{
|
||||
UpdateHandCardsPlayability();
|
||||
}));
|
||||
}
|
||||
|
||||
public override EffectBattle GetSkillEffect(string skillEffectPath)
|
||||
{
|
||||
return GameMgr.GetIns().GetEffectMgr().GetEnemyEffectBattle(skillEffectPath);
|
||||
}
|
||||
|
||||
public override Vector3 GetFieldCenterPosition()
|
||||
{
|
||||
return FIELD_CENTER_POSITION;
|
||||
}
|
||||
|
||||
public override VfxBase TurnStartDraw(SkillProcessor skillProcessor)
|
||||
{
|
||||
return base.TurnStartDraw(skillProcessor);
|
||||
}
|
||||
|
||||
public void CallRecordingMulliganEnd(List<int> cardIndexList)
|
||||
{
|
||||
this.OnMulliganEndForReplay.Call(cardIndexList);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user