mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-30 05:48:53 -04:00
Compare commits
1009
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
071f05337c | ||
|
|
e9d274fbfd | ||
|
|
5709527bf2 | ||
|
|
e6d2430405 | ||
|
|
6c425a43c6 | ||
|
|
a505bad554 | ||
|
|
e27e5a1d92 | ||
|
|
5e0357101d | ||
|
|
5420f7419f | ||
|
|
43ed2d3c7d | ||
|
|
93500f59ed | ||
|
|
08912fd53f | ||
|
|
92c8fc0afc | ||
|
|
fae6bdb782 | ||
|
|
bba0100542 | ||
|
|
f9b66de742 | ||
|
|
3d0f1c826a | ||
|
|
2a59add285 | ||
|
|
0b83128aa0 | ||
|
|
e2b04ff770 | ||
|
|
4dabe50d43 | ||
|
|
3dd0795b88 | ||
|
|
70f1a8207f | ||
|
|
3ac18cce6e | ||
|
|
aa0a0672d0 | ||
|
|
ec2958400b | ||
|
|
fcb6bea78b | ||
|
|
5a76a39b77 | ||
|
|
b5a4acfb7a | ||
|
|
1248230e67 | ||
|
|
096db6cdcd | ||
|
|
72e416b9c7 | ||
|
|
9e8e80965b | ||
|
|
2325b436b1 | ||
|
|
57421a2884 | ||
|
|
4fdd3eba8e | ||
|
|
e6efe800c0 | ||
|
|
95aa99e18c | ||
|
|
270484e745 | ||
|
|
719addf633 | ||
|
|
31e386871f | ||
|
|
bfd7e596d0 | ||
|
|
643545d54c | ||
|
|
fcb017f980 | ||
|
|
bc9f28811d | ||
|
|
ff9eb60cd3 | ||
|
|
dd52da7c42 | ||
|
|
ff45def0e8 | ||
|
|
de0b53e529 | ||
|
|
f078584e56 | ||
|
|
a11382b5a1 | ||
|
|
56ecbfe3c0 | ||
|
|
62f03c536d | ||
|
|
2fa03b6013 | ||
|
|
59af4053a1 | ||
|
|
1aba8c52b6 | ||
|
|
6a386937c8 | ||
|
|
00132f6428 | ||
|
|
7fe222422b | ||
|
|
812f389231 | ||
|
|
d1b8094539 | ||
|
|
b8d79af911 | ||
|
|
24acddf750 | ||
|
|
1d696c58be | ||
|
|
ef4fbecad6 | ||
|
|
96075ed34a | ||
|
|
155b4b2182 | ||
|
|
690ed845b2 | ||
|
|
a7198fd324 | ||
|
|
bc69ce91f3 | ||
|
|
1de1d8cdc3 | ||
|
|
1f8171dd94 | ||
|
|
087bf75500 | ||
|
|
bac435ba7b | ||
|
|
74a6106eb6 | ||
|
|
5ec993ad3c | ||
|
|
73a03d90da | ||
|
|
e6fda6137e | ||
|
|
afae39acc8 | ||
|
|
37264aff0f | ||
|
|
2b60e6de77 | ||
|
|
3c2bf0e2b3 | ||
|
|
ac695b1092 | ||
|
|
4f29e7457c | ||
|
|
04a6ab31f4 | ||
|
|
a2cdc847dd | ||
|
|
0bca678f56 | ||
|
|
067c3607aa | ||
|
|
823c1f6444 | ||
|
|
bfc00635cd | ||
|
|
b2e5b85ac5 | ||
|
|
2c27476954 | ||
|
|
aab78d84fc | ||
|
|
cb632c2a65 | ||
|
|
8d219f1338 | ||
|
|
1e6a4ed984 | ||
|
|
64240c31fc | ||
|
|
d2222c3858 | ||
|
|
c7f31c3e14 | ||
|
|
740e846b2d | ||
|
|
246d00aa3f | ||
|
|
06f9d60595 | ||
|
|
b3ed47fbca | ||
|
|
3a6ad70b76 | ||
|
|
8120270bf8 | ||
|
|
d54439ce14 | ||
|
|
32ee4bb72a | ||
|
|
e7d2a46cc0 | ||
|
|
3ee94ec076 | ||
|
|
589293abc4 | ||
|
|
279865cc7c | ||
|
|
b597b149c3 | ||
|
|
b7f592f2a8 | ||
|
|
70e17c22e7 | ||
|
|
51b18ca0f6 | ||
|
|
a83f243a3b | ||
|
|
f5b088c708 | ||
|
|
dc454cbb68 | ||
|
|
3b99e38da2 | ||
|
|
483af57964 | ||
|
|
a941bdddf3 | ||
|
|
246ba4a93b | ||
|
|
3347c084ed | ||
|
|
8175cee454 | ||
|
|
37d54053a5 | ||
|
|
b8846bd74c | ||
|
|
1716e135c4 | ||
|
|
0ac804cfcb | ||
|
|
aecc132814 | ||
|
|
8b47876dff | ||
|
|
ffc108953a | ||
|
|
0a106174fe | ||
|
|
90210bee3c | ||
|
|
8603cd4ad0 | ||
|
|
7b2c6827c2 | ||
|
|
902b4a6d21 | ||
|
|
a847cb5926 | ||
|
|
050c6f5adc | ||
|
|
fb95fb3cf7 | ||
|
|
d507367411 | ||
|
|
faa19cebcb | ||
|
|
aced374367 | ||
|
|
d424a6f1d3 | ||
|
|
71bb1c32de | ||
|
|
f726fcedb9 | ||
|
|
77344ccc2f | ||
|
|
90bc16be90 | ||
|
|
d1b743a01f | ||
|
|
aed651c0a0 | ||
|
|
4c477e1675 | ||
|
|
d1cdc6789c | ||
|
|
70fe1682b0 | ||
|
|
9058bde326 | ||
|
|
bf425efd40 | ||
|
|
cd3127b983 | ||
|
|
68f5eff048 | ||
|
|
cebff87656 | ||
|
|
d24fb13877 | ||
|
|
c3d2918a2b | ||
|
|
1d82df464a | ||
|
|
0432525c06 | ||
|
|
a60f27e282 | ||
|
|
5f0182cb7e | ||
|
|
4a3ae9407b | ||
|
|
766c2838d2 | ||
|
|
4cb95589b4 | ||
|
|
a8d3c2dd52 | ||
|
|
cb5d7b6ad7 | ||
|
|
c7dcc64595 | ||
|
|
6573e08da7 | ||
|
|
cd695ade4a | ||
|
|
55cb413cfc | ||
|
|
4f1bf7fcbb | ||
|
|
154ee80ead | ||
|
|
1c9f9bb198 | ||
|
|
6836343f11 | ||
|
|
aa0d1cd943 | ||
|
|
798f1da8b3 | ||
|
|
d98efb52b6 | ||
|
|
f450d9d2d6 | ||
|
|
2f27a03f08 | ||
|
|
e33025b1fa | ||
|
|
ade2481f42 | ||
|
|
0752ab3cac | ||
|
|
576e2301ce | ||
|
|
a568d45d75 | ||
|
|
bc19a7e4c1 | ||
|
|
616e438f17 | ||
|
|
9bb11c46d5 | ||
|
|
5f470bb703 | ||
|
|
276f13cea5 | ||
|
|
ac069687f1 | ||
|
|
f248c22eed | ||
|
|
65a5687f92 | ||
|
|
ea50fa534b | ||
|
|
4bd3c4af58 | ||
|
|
283d80f7ed | ||
|
|
0238ca80f0 | ||
|
|
d24c5fe874 | ||
|
|
e65e21f2ff | ||
|
|
e1d417718f | ||
|
|
d4308b0a4f | ||
|
|
2a1d1e4474 | ||
|
|
bc0d530484 | ||
|
|
56650418d2 | ||
|
|
0543b84d34 | ||
|
|
1f50687a59 | ||
|
|
ed04faf0d8 | ||
|
|
1ed020f3e0 | ||
|
|
7e385d8c94 | ||
|
|
ee509b5438 | ||
|
|
ac9394131c | ||
|
|
d058a306a7 | ||
|
|
b9be0c3fcb | ||
|
|
b59b46568a | ||
|
|
408cbc8c49 | ||
|
|
1537816132 | ||
|
|
1705639950 | ||
|
|
ce32541dd6 | ||
|
|
3ffea4f4dd | ||
|
|
fe35a63769 | ||
|
|
f942d7c34a | ||
|
|
0f1ada4624 | ||
|
|
b4ee4629bf | ||
|
|
d0b64e2d82 | ||
|
|
a8ce3c2175 | ||
|
|
80ae127379 | ||
|
|
bd3a9fb3af | ||
|
|
d0eaccb81b | ||
|
|
1ed2042283 | ||
|
|
0b531d967e | ||
|
|
b0d1aec9ab | ||
|
|
f4d47098b5 | ||
|
|
1989b8aa92 | ||
|
|
948ca61ef3 | ||
|
|
e60aca30ba | ||
|
|
fb5055c39e | ||
|
|
dfb5650121 | ||
|
|
8a0cbf28fe | ||
|
|
a6b9060d57 | ||
|
|
8e57995e44 | ||
|
|
1d8257808c | ||
|
|
e290fe9c75 | ||
|
|
06a50cfeeb | ||
|
|
00888c8ad5 | ||
|
|
e3582ccfb4 | ||
|
|
4c6822b1c6 | ||
|
|
2e6a4c7341 | ||
|
|
8edb5e1013 | ||
|
|
6fb139cbb9 | ||
|
|
c81c95142b | ||
|
|
afde822860 | ||
|
|
474c386535 | ||
|
|
3963e83717 | ||
|
|
7893e365da | ||
|
|
71b9ae8dee | ||
|
|
b87b47d62a | ||
|
|
2bb85718d7 | ||
|
|
ee187d7b89 | ||
|
|
b6a7051b2c | ||
|
|
3532cac278 | ||
|
|
1447ab25b4 | ||
|
|
9a06ca78c6 | ||
|
|
f6ba70634b | ||
|
|
148fd04a93 | ||
|
|
17f60623ee | ||
|
|
c5ece27c11 | ||
|
|
296ccd1a76 | ||
|
|
82a4f71f24 | ||
|
|
2cb1cf2f83 | ||
|
|
f13a4e0e24 | ||
|
|
17e1bbd6a1 | ||
|
|
c36ff0a0fe | ||
|
|
08b58e2f78 | ||
|
|
62d2b8db2d | ||
|
|
3869f7ed93 | ||
|
|
8dbb350230 | ||
|
|
c9c44872d6 | ||
|
|
cae53efedc | ||
|
|
52f9ffb1f6 | ||
|
|
5fac24cee8 | ||
|
|
1954dfec80 | ||
|
|
2eea2b64c2 | ||
|
|
a127a93be8 | ||
|
|
9b32f85817 | ||
|
|
ec139d2677 | ||
|
|
4327be0cf9 | ||
|
|
c25c832b31 | ||
|
|
eabaabc535 | ||
|
|
9acf97f5a0 | ||
|
|
24fc212c76 | ||
|
|
04f88bdd63 | ||
|
|
a4eb58aa57 | ||
|
|
030195e3ff | ||
|
|
d21a52ae68 | ||
|
|
3c1f1dbacb | ||
|
|
6704d4e5ee | ||
|
|
787c4851ff | ||
|
|
bf6e7fafd3 | ||
|
|
ea6df764c0 | ||
|
|
f242de272d | ||
|
|
5232e2e9bc | ||
|
|
ca35898243 | ||
|
|
869457d235 | ||
|
|
6736c8160b | ||
|
|
243c50550d | ||
|
|
fe7d3ef23a | ||
|
|
57ad77fc39 | ||
|
|
d70e7bd1b3 | ||
|
|
254cc931ea | ||
|
|
01fe5cffe8 | ||
|
|
3e18995a37 | ||
|
|
ff161c9c9b | ||
|
|
6b7b4f326d | ||
|
|
f50f7dec29 | ||
|
|
a5def20c58 | ||
|
|
b23204f9c9 | ||
|
|
4972de0aaa | ||
|
|
58a1f3d7b4 | ||
|
|
f516edb858 | ||
|
|
161aa952fa | ||
|
|
3b26a13ad6 | ||
|
|
014189217a | ||
|
|
978debb40c | ||
|
|
5be707811e | ||
|
|
ac44a98f8a | ||
|
|
03ecc2ae8d | ||
|
|
1518379107 | ||
|
|
a58c8c59e1 | ||
|
|
37959b5048 | ||
|
|
cbcc9fc6f1 | ||
|
|
d236aaa7da | ||
|
|
a2cd5a671c | ||
|
|
1d6a45bf4d | ||
|
|
5bfe703ed7 | ||
|
|
fc8485388e | ||
|
|
a5593b3131 | ||
|
|
9541bf3267 | ||
|
|
1037fe0a0e | ||
|
|
a6eea3198a | ||
|
|
b28e2313a8 | ||
|
|
0f0b4e6c86 | ||
|
|
e033467054 | ||
|
|
0f092bf6bf | ||
|
|
66ba762360 | ||
|
|
c7c11838a0 | ||
|
|
301d80fabe | ||
|
|
95d238ada0 | ||
|
|
c88087de29 | ||
|
|
fade5502d9 | ||
|
|
86bb802140 | ||
|
|
6b644b384d | ||
|
|
c370af8914 | ||
|
|
1cd6d2bfc7 | ||
|
|
fc720916fb | ||
|
|
ce55035408 | ||
|
|
d4665ddaeb | ||
|
|
4375c6f667 | ||
|
|
1edeae2a60 | ||
|
|
409f1056b9 | ||
|
|
d8c470db3d | ||
|
|
c642c4b84b | ||
|
|
0b80f7e547 | ||
|
|
d543f45607 | ||
|
|
e5699c1cdc | ||
|
|
f4a7c6e873 | ||
|
|
2b4fdfd219 | ||
|
|
3bef1dad4a | ||
|
|
da50ee59cf | ||
|
|
1a3b655d3f | ||
|
|
1f5f278970 | ||
|
|
0b4eb5239a | ||
|
|
118e8f6205 | ||
|
|
cf126c5906 | ||
|
|
645cf61911 | ||
|
|
6e8985d97e | ||
|
|
19c4c0d50d | ||
|
|
b23110950e | ||
|
|
60c213c44b | ||
|
|
87d5ede3dd | ||
|
|
203e5d89cd | ||
|
|
ce611cc61c | ||
|
|
3df8dea71b | ||
|
|
3cca50b7cc | ||
|
|
1ff5622694 | ||
|
|
f6ad7ff865 | ||
|
|
d69627a9c8 | ||
|
|
411f388bd3 | ||
|
|
5b8d35604a | ||
|
|
619d002bc4 | ||
|
|
7049656f64 | ||
|
|
e6e7c2982d | ||
|
|
aafdb7688a | ||
|
|
73b53f9702 | ||
|
|
33317a57c7 | ||
|
|
72d8f26cdb | ||
|
|
aea1afe145 | ||
|
|
0fec45b7ea | ||
|
|
af98b823aa | ||
|
|
07d9da2ca9 | ||
|
|
c2c86b2ef1 | ||
|
|
54e8763470 | ||
|
|
db34687df0 | ||
|
|
7f008d85ff | ||
|
|
06ebf51078 | ||
|
|
39c5bcd73c | ||
|
|
8f5f158b60 | ||
|
|
3bc1e35125 | ||
|
|
460bedddae | ||
|
|
8b64c315f3 | ||
|
|
7c824a2b72 | ||
|
|
f197ac5077 | ||
|
|
e0be669482 | ||
|
|
82686b0511 | ||
|
|
f4eca30163 | ||
|
|
2814183012 | ||
|
|
bbc3c2a8d6 | ||
|
|
7eeb7feeae | ||
|
|
b2f850c1c8 | ||
|
|
08ea21fff4 | ||
|
|
9dac604a93 | ||
|
|
23db346070 | ||
|
|
d3b7d739bb | ||
|
|
28c0f8ab7d | ||
|
|
fcbe0db568 | ||
|
|
83cefe476e | ||
|
|
690ccbd260 | ||
|
|
63846d53fb | ||
|
|
86cb865742 | ||
|
|
1a0c9b6a82 | ||
|
|
985eb103fe | ||
|
|
aec76a78da | ||
|
|
eb6c134f4b | ||
|
|
22733e7117 | ||
|
|
17f3edf2be | ||
|
|
9c5bfda20f | ||
|
|
aa0b8d88e8 | ||
|
|
6d545fc1ca | ||
|
|
9a8ad27122 | ||
|
|
9f1dbc9ff0 | ||
|
|
99942748b0 | ||
|
|
64c34230bf | ||
|
|
36881b70be | ||
|
|
3cf9425247 | ||
|
|
9b052478f6 | ||
|
|
339f8339f0 | ||
|
|
db9a802c3f | ||
|
|
b5578e72bd | ||
|
|
e5666e7a82 | ||
|
|
3aea0909a4 | ||
|
|
5adef3336e | ||
|
|
1a96b418bc | ||
|
|
e373e86b60 | ||
|
|
6f3035184c | ||
|
|
7ae8b1957c | ||
|
|
a588213323 | ||
|
|
56b2ba08ad | ||
|
|
824b9718ee | ||
|
|
8540a73885 | ||
|
|
7e6ca7be5e | ||
|
|
ef43f6c087 | ||
|
|
c8a70caf79 | ||
|
|
25ce784a60 | ||
|
|
5358cccb62 | ||
|
|
ba11758e1f | ||
|
|
f0412d6b1d | ||
|
|
c43b3bbb4d | ||
|
|
d826aeeb07 | ||
|
|
f90c6967ce | ||
|
|
f5f87ce1a3 | ||
|
|
f28b269734 | ||
|
|
d545efd843 | ||
|
|
b9cf18adbd | ||
|
|
30650e1f9f | ||
|
|
eaa558a7c8 | ||
|
|
0dc725e513 | ||
|
|
f8682c2123 | ||
|
|
f62e31a1eb | ||
|
|
ea623b08a6 | ||
|
|
a575c6bc3d | ||
|
|
9c102a7850 | ||
|
|
22a1eaab19 | ||
|
|
0118ace5fc | ||
|
|
d4a93abbf7 | ||
|
|
ce3d5a413c | ||
|
|
d0ee3f3e9d | ||
|
|
698fa9dff9 | ||
|
|
d23fd6e23c | ||
|
|
46ae53f565 | ||
|
|
7d4e5892ba | ||
|
|
f9de415020 | ||
|
|
59395a254d | ||
|
|
5d7c6c8d6f | ||
|
|
4d123b6e4b | ||
|
|
54dc2e7f6f | ||
|
|
1d60ba093f | ||
|
|
0fef74dce7 | ||
|
|
6529293d21 | ||
|
|
a1fce624ea | ||
|
|
b08647bb32 | ||
|
|
de9e89f040 | ||
|
|
84a29ebc2a | ||
|
|
37b13ae959 | ||
|
|
1cacbe8306 | ||
|
|
12d224d15a | ||
|
|
a57eee3afc | ||
|
|
b7773f7471 | ||
|
|
57595c999c | ||
|
|
83615b7eb3 | ||
|
|
c53c544a8b | ||
|
|
c7c272c79c | ||
|
|
c759161367 | ||
|
|
19b7b4804f | ||
|
|
3a005cc813 | ||
|
|
a8e16efa48 | ||
|
|
c3ddfce0a7 | ||
|
|
9500f7a1f3 | ||
|
|
83a8d92e3f | ||
|
|
e105c5d4d7 | ||
|
|
3d816f57e2 | ||
|
|
1e6dcfba9b | ||
|
|
e59d755d32 | ||
|
|
f927e47bfe | ||
|
|
48a37b2018 | ||
|
|
c155b5575d | ||
|
|
9c6b8b231a | ||
|
|
92b404dac4 | ||
|
|
8a1c536d68 | ||
|
|
04719d22a5 | ||
|
|
dacc312756 | ||
|
|
ee4a3d46ba | ||
|
|
80b11f1c3d | ||
|
|
ddcf4eea7b | ||
|
|
6a0681e4c6 | ||
|
|
8ebb378da7 | ||
|
|
e92a878074 | ||
|
|
0b97b41c08 | ||
|
|
20fe732cfc | ||
|
|
0575aa29d9 | ||
|
|
29b560d244 | ||
|
|
dcc98b4a54 | ||
|
|
d3ce455391 | ||
|
|
85c6b633d8 | ||
|
|
956b599060 | ||
|
|
f88552f99b | ||
|
|
e2b559f3eb | ||
|
|
39816d33b6 | ||
|
|
0cf2a23fd4 | ||
|
|
c4d8eae47e | ||
|
|
2a5a292386 | ||
|
|
f6b2da6ed6 | ||
|
|
e8a09f5c48 | ||
|
|
e82193cdfd | ||
|
|
87e7cdebf6 | ||
|
|
9ca84d852f | ||
|
|
8194f93d13 | ||
|
|
c158ec33fa | ||
|
|
1e27c7494e | ||
|
|
54a1f8d932 | ||
|
|
275779b20e | ||
|
|
635f29e1ac | ||
|
|
db2001b60a | ||
|
|
b5098a2ad0 | ||
|
|
2102f8ff88 | ||
|
|
d2af0c9332 | ||
|
|
8c939ee276 | ||
|
|
645b8993c8 | ||
|
|
d96799cac7 | ||
|
|
b822b626c4 | ||
|
|
4d9b182fb3 | ||
|
|
88d155eaf4 | ||
|
|
af96469b7a | ||
|
|
65f1e7a3be | ||
|
|
379a566d71 | ||
|
|
f30ff739ed | ||
|
|
16cd2de02f | ||
|
|
22a44bd7e9 | ||
|
|
d37bd4ef03 | ||
|
|
a3d9a10f2e | ||
|
|
7801e3feee | ||
|
|
4a54cb4674 | ||
|
|
a8949cf1f9 | ||
|
|
24fe1c125c | ||
|
|
61db6334f9 | ||
|
|
023160f87a | ||
|
|
c45e34648c | ||
|
|
17fbf7c6f0 | ||
|
|
ec86b7d4bf | ||
|
|
7561eb116f | ||
|
|
6467b97a23 | ||
|
|
6d5909a1e3 | ||
|
|
2bb0d43ca8 | ||
|
|
1fb3f0ebdc | ||
|
|
d56586ecac | ||
|
|
fb2154e2c3 | ||
|
|
3b14d19fdb | ||
|
|
6840c2fdef | ||
|
|
8fc2031806 | ||
|
|
9df8be33e9 | ||
|
|
4795e2e1bc | ||
|
|
c563627b05 | ||
|
|
73ebf7a789 | ||
|
|
a1b3d3d2ca | ||
|
|
8baacbcf90 | ||
|
|
f2e357b749 | ||
|
|
ff2afd546e | ||
|
|
b95900b29a | ||
|
|
b8b7756a45 | ||
|
|
3d6146f551 | ||
|
|
03585fafb0 | ||
|
|
31d81db243 | ||
|
|
4c36db8685 | ||
|
|
e3d0252d75 | ||
|
|
dbe8e57341 | ||
|
|
6f37fd69cd | ||
|
|
b191f4e9fb | ||
|
|
a5a7cc20ba | ||
|
|
02b6c46082 | ||
|
|
881a30cca4 | ||
|
|
d01011f4d0 | ||
|
|
309ed379c7 | ||
|
|
edbf667f45 | ||
|
|
b6bc766adf | ||
|
|
1ecf556cf2 | ||
|
|
6d32073604 | ||
|
|
7cedfb9047 | ||
|
|
a7ab2e37c2 | ||
|
|
4723f91500 | ||
|
|
aa0f5d8a05 | ||
|
|
6202c14215 | ||
|
|
e23aa6b066 | ||
|
|
b0790341c7 | ||
|
|
4f0fa6af4a | ||
|
|
4e24e09924 | ||
|
|
48676067b7 | ||
|
|
6432db9f7d | ||
|
|
2cb28fb384 | ||
|
|
3b59148151 | ||
|
|
d6198631ba | ||
|
|
47daea6195 | ||
|
|
aca66a4bb0 | ||
|
|
e9fe1774c0 | ||
|
|
bb8b8a77c9 | ||
|
|
c692b6b5c1 | ||
|
|
68c6d5d9b2 | ||
|
|
221547aabf | ||
|
|
f6b3591d80 | ||
|
|
f9878d6137 | ||
|
|
94da3b8d21 | ||
|
|
3a2ff9de75 | ||
|
|
b249896316 | ||
|
|
1cb6979b90 | ||
|
|
e983d2c1db | ||
|
|
9ee0cf941b | ||
|
|
8436c93317 | ||
|
|
7f0329acb7 | ||
|
|
83489b7230 | ||
|
|
081a25f04b | ||
|
|
38bdfae0db | ||
|
|
bd3a9e30f1 | ||
|
|
7ce968f467 | ||
|
|
9ac8cc56f0 | ||
|
|
98159e1e0e | ||
|
|
025b5aae74 | ||
|
|
2fddb21585 | ||
|
|
6b75976236 | ||
|
|
fdf419ec8f | ||
|
|
d2ceb2d58a | ||
|
|
6d8311a66c | ||
|
|
18a3d1c22c | ||
|
|
b382a1b2db | ||
|
|
dd17fa138e | ||
|
|
09dd623ea7 | ||
|
|
8c01bb831f | ||
|
|
2cb4372496 | ||
|
|
7597b2d98e | ||
|
|
5ac2729a77 | ||
|
|
bec1fca7a5 | ||
|
|
32d145f3cc | ||
|
|
a5f1a14e21 | ||
|
|
57784b8620 | ||
|
|
a29b3342a1 | ||
|
|
64d6560caf | ||
|
|
2e314e48fb | ||
|
|
e8d1e3766b | ||
|
|
012682268e | ||
|
|
38428e4ea9 | ||
|
|
567132bc0c | ||
|
|
e7209e4000 | ||
|
|
273b520c0b | ||
|
|
0f02313a14 | ||
|
|
3249e4bbb1 | ||
|
|
36aca65003 | ||
|
|
c6635213a2 | ||
|
|
bf6fab3936 | ||
|
|
b76158d1d7 | ||
|
|
916ec97fce | ||
|
|
36b38d36cb | ||
|
|
f1163977e1 | ||
|
|
a7dd6d52c8 | ||
|
|
6898804e83 | ||
|
|
2331603b27 | ||
|
|
c8e9cbbe59 | ||
|
|
d374635648 | ||
|
|
803d3be325 | ||
|
|
50bacbe25e | ||
|
|
94ef9830b7 | ||
|
|
87502f36c4 | ||
|
|
bed511d403 | ||
|
|
4170b00e31 | ||
|
|
3cc90daaf0 | ||
|
|
b0ecd24f42 | ||
|
|
ade5603331 | ||
|
|
34bcefb09f | ||
|
|
dccc9dfbcb | ||
|
|
9a1d6461d9 | ||
|
|
2dc76ffe3e | ||
|
|
87225d8981 | ||
|
|
4bf4101fd1 | ||
|
|
58c6ea0269 | ||
|
|
034bd2eec6 | ||
|
|
a24be04d68 | ||
|
|
0d878d57b7 | ||
|
|
08880e2294 | ||
|
|
1c45654930 | ||
|
|
dae3acb5a0 | ||
|
|
7bb6f8a12e | ||
|
|
b25f3ca3f6 | ||
|
|
610b762188 | ||
|
|
c4c954c898 | ||
|
|
10b70a0e3f | ||
|
|
af437f9d3e | ||
|
|
ca5a96e979 | ||
|
|
a2936ebcff | ||
|
|
c3d7537b3e | ||
|
|
81e5d830c6 | ||
|
|
4b09714ed5 | ||
|
|
fa4a4c7e51 | ||
|
|
493b8bbecf | ||
|
|
9b519daff1 | ||
|
|
a5ba3e7084 | ||
|
|
6d2d146543 | ||
|
|
22d93cb92c | ||
|
|
31c55a9b6f | ||
|
|
5281dbd81c | ||
|
|
8b863070fe | ||
|
|
da1064772d | ||
|
|
8ca1957a60 | ||
|
|
cbab2b7e5c | ||
|
|
c47bd604d5 | ||
|
|
63776ca7a0 | ||
|
|
c66c7170f6 | ||
|
|
e9e4158666 | ||
|
|
b15917898f | ||
|
|
ef3179bfa2 | ||
|
|
f0afb8b3d9 | ||
|
|
17e58b0ce5 | ||
|
|
fb0bfb2d19 | ||
|
|
0d87de157a | ||
|
|
4189da0cc7 | ||
|
|
662ccdc268 | ||
|
|
6779995680 | ||
|
|
c4b6cc7c00 | ||
|
|
07ef404518 | ||
|
|
4c8a8351ac | ||
|
|
17f5ed03d0 | ||
|
|
a3d3c987bc | ||
|
|
6db0567f03 | ||
|
|
3e89499588 | ||
|
|
7982034964 | ||
|
|
8f462aab40 | ||
|
|
ac3628d907 | ||
|
|
4a1e0020f7 | ||
|
|
3c1486e45b | ||
|
|
4563770835 | ||
|
|
c49eed3aaf | ||
|
|
cc80f69c47 | ||
|
|
128366f9c2 | ||
|
|
1d158ae12c | ||
|
|
ddd63ac841 | ||
|
|
118a682451 | ||
|
|
d202efc0a9 | ||
|
|
81a1314493 | ||
|
|
d20673d9bf | ||
|
|
4634dc7539 | ||
|
|
c2db648d25 | ||
|
|
76f1ace6b7 | ||
|
|
a316329359 | ||
|
|
4aa1b804bc | ||
|
|
233d27413d | ||
|
|
05621ccb51 | ||
|
|
ea59981b58 | ||
|
|
960460ec08 | ||
|
|
8d587c5287 | ||
|
|
e0b79c9bab | ||
|
|
6206330b76 | ||
|
|
8e7a2a0356 | ||
|
|
7657cf4f73 | ||
|
|
7b137fbe14 | ||
|
|
46d278ca55 | ||
|
|
4d472d0b36 | ||
|
|
339038956d | ||
|
|
fd442211d3 | ||
|
|
144f857a3b | ||
|
|
80131e57f8 | ||
|
|
30d89a2704 | ||
|
|
5e147545b7 | ||
|
|
e962079cda | ||
|
|
6f934cfbd4 | ||
|
|
47ecef81a6 | ||
|
|
16762ae933 | ||
|
|
ba47f6a833 | ||
|
|
522ffefd0a | ||
|
|
e17ca360df | ||
|
|
a559737106 | ||
|
|
e18d5837af | ||
|
|
65ae67d9ac | ||
|
|
05c77a6156 | ||
|
|
8ae6792785 | ||
|
|
5f67fc167a | ||
|
|
8dd62269ed | ||
|
|
0eee6e8634 | ||
|
|
a16a7c31fe | ||
|
|
a4254df0ca | ||
|
|
21df52daf7 | ||
|
|
5e81e26c69 | ||
|
|
116a93d8cc | ||
|
|
acebc35d3f | ||
|
|
d103629fd2 | ||
|
|
6e2a0cd3b9 | ||
|
|
77490783e4 | ||
|
|
ed28c70c5a | ||
|
|
3f71584a6e | ||
|
|
4f09e56510 | ||
|
|
b534183a50 | ||
|
|
56ea6c7161 | ||
|
|
37f6010381 | ||
|
|
798dd64f84 | ||
|
|
c1acf8494c | ||
|
|
d4601326a7 | ||
|
|
a037af6948 | ||
|
|
976c94cb6b | ||
|
|
422154e4c7 | ||
|
|
517ac94b8d | ||
|
|
b5ec2b28fd | ||
|
|
f45a4b784b | ||
|
|
91dda046d8 | ||
|
|
71fabbd245 | ||
|
|
fa0f29b085 | ||
|
|
49577d2518 | ||
|
|
12de6750de | ||
|
|
4e1b9785a0 | ||
|
|
2f2de32493 | ||
|
|
a38b68ce23 | ||
|
|
7d605f8bf9 | ||
|
|
75134f41e3 | ||
|
|
2bad8ec673 | ||
|
|
1524e9c476 | ||
|
|
c8ad0cd7e7 | ||
|
|
33a4e31b60 | ||
|
|
250724ef1f | ||
|
|
3e1837543d | ||
|
|
1526c2f037 | ||
|
|
408cf8c5ca | ||
|
|
ceb70dbf2d | ||
|
|
6d46992ad7 | ||
|
|
84242194ba | ||
|
|
529e4bef46 | ||
|
|
aa1658bb5d | ||
|
|
068df2b138 | ||
|
|
3499758203 | ||
|
|
7d97df3407 | ||
|
|
1add7ea4cb | ||
|
|
e497cd445a | ||
|
|
22b0e0612a | ||
|
|
e52c63d565 | ||
|
|
fe7f72cb3d | ||
|
|
8187905987 | ||
|
|
11b3043e6c | ||
|
|
8d871ac949 | ||
|
|
8f7a523404 | ||
|
|
bb98992635 | ||
|
|
d3984843e2 | ||
|
|
7a14c51df2 | ||
|
|
a7b023203f | ||
|
|
e382e89e06 | ||
|
|
faf64a6092 | ||
|
|
2b64174cfe | ||
|
|
918282893c | ||
|
|
dbbca02619 | ||
|
|
c50d5841d8 | ||
|
|
5489cfb4b0 | ||
|
|
e421de547f | ||
|
|
aa8d0a9d48 | ||
|
|
075dcdb9e9 | ||
|
|
2ebe8d6434 | ||
|
|
d7c77f5f3f | ||
|
|
7a8dbbb4ce | ||
|
|
b8e1abc80b | ||
|
|
12df9d29f5 | ||
|
|
2df0542631 | ||
|
|
e22b79a47c | ||
|
|
572682a23a | ||
|
|
6241865f95 | ||
|
|
67dbda3113 | ||
|
|
c50d3183e6 | ||
|
|
9eae394ff5 | ||
|
|
76b640984c | ||
|
|
72d142a781 | ||
|
|
af1df22e4d | ||
|
|
66787bfea4 | ||
|
|
c113e68488 | ||
|
|
8284b04426 | ||
|
|
0c4bbef855 | ||
|
|
6fb34b5ee3 | ||
|
|
f345b19d57 | ||
|
|
f1bdaad97d | ||
|
|
f725e5fcba | ||
|
|
2237e1eeca | ||
|
|
bb54f8414e | ||
|
|
99c7d39705 | ||
|
|
bdd27078b5 | ||
|
|
7c3fdbc921 | ||
|
|
9596f08957 | ||
|
|
d9433960cd | ||
|
|
89b6fd56b6 | ||
|
|
10c441e3cd | ||
|
|
461b22efa8 | ||
|
|
57a19909aa | ||
|
|
dc1473919e | ||
|
|
ee20986ee0 | ||
|
|
436aa5ea75 | ||
|
|
e49f761417 | ||
|
|
95e3ab213a | ||
|
|
0c33de61b9 | ||
|
|
91a3fb5031 | ||
|
|
3a46b2c218 | ||
|
|
2f631ddc64 | ||
|
|
2a2ef6ff7d | ||
|
|
d3d94a3614 | ||
|
|
f34cc3072b | ||
|
|
68bff6a40b | ||
|
|
d1a99608a5 | ||
|
|
2a6381d610 | ||
|
|
93dba4f3b8 | ||
|
|
a7e7c2e814 | ||
|
|
6bf62b3f9d | ||
|
|
00be470281 | ||
|
|
57afbe79c2 | ||
|
|
d5f336ce73 | ||
|
|
b79a0b1b6f | ||
|
|
57721d1696 | ||
|
|
b0711528d6 | ||
|
|
7e968fcff9 | ||
|
|
e399130fda | ||
|
|
0083e54f6a | ||
|
|
9512252b72 | ||
|
|
73028d4c2a | ||
|
|
2f5910c436 | ||
|
|
d1197cbc0b | ||
|
|
2ccf699a86 | ||
|
|
d767420814 | ||
|
|
77100300b1 | ||
|
|
5c80fc03e2 | ||
|
|
aa39bb84fe | ||
|
|
a767b3d0fe | ||
|
|
3dbcbc34d0 | ||
|
|
0bdadb3794 | ||
|
|
05b5782cd5 | ||
|
|
0c220a8990 | ||
|
|
da787bf9bf | ||
|
|
57a839d000 | ||
|
|
e77e49c8b1 | ||
|
|
ce6d142bd4 | ||
|
|
f2db63164c | ||
|
|
604b4d39ea | ||
|
|
5d3741f2e5 | ||
|
|
5e21ac94dc | ||
|
|
1d055253d2 | ||
|
|
d56c5520f9 | ||
|
|
89bc893f06 | ||
|
|
acc8d066e5 | ||
|
|
6608268117 | ||
|
|
bdecab25a6 | ||
|
|
81ed97a68b | ||
|
|
1e051deb9e | ||
|
|
38c54e255a | ||
|
|
e7ad38d1df | ||
|
|
67c0abea8f | ||
|
|
85c1543df1 | ||
|
|
f38a1f83e2 | ||
|
|
28785a01f6 | ||
|
|
01c4260763 | ||
|
|
150b3eb178 | ||
|
|
5ab52d37f2 | ||
|
|
ce191fd4c7 | ||
|
|
9a7e349101 | ||
|
|
07a7d2a51a | ||
|
|
d2b192cb07 | ||
|
|
d8e3e93a87 | ||
|
|
af12df38ed | ||
|
|
aee89f1c92 | ||
|
|
7fb8268fae | ||
|
|
8b49eb1526 | ||
|
|
c6cdcf83a9 | ||
|
|
4bf82083b4 | ||
|
|
6579fd8ebe | ||
|
|
4d62ef771a | ||
|
|
7784a3a20c |
No files matched your search
@@ -0,0 +1,20 @@
|
||||
# Changelog
|
||||
## [1.8.0 (MONTH Xth, 2026)](#1-8-0)
|
||||
|
||||
### Additions
|
||||
- Added a new `cobblemon:structure`, which is a feature without any restrictions allowing the Builders to place certain structures in much more specific manner.
|
||||
|
||||
### Changes
|
||||
|
||||
### Fixes
|
||||
- Fixed rare Pokémon being far too likely to spawn multiple times in a single spawn cycle. The dev told me that the exact bug is too complicated to explain.
|
||||
|
||||
### Developer
|
||||
- Tweaked the spawning selector interface to take a bucket function rather than a bucket directly.
|
||||
|
||||
### Molang & Datapacks
|
||||
|
||||
### Particles
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
@@ -0,0 +1,16 @@
|
||||
### Additions
|
||||
|
||||
### Changes
|
||||
- Wild Pokémon now reliably have at least one offensive move, if they can have offensive moves at that level.
|
||||
|
||||
### Fixes
|
||||
|
||||
### Developer
|
||||
|
||||
### Molang & Datapacks
|
||||
- Added `moveset_builders` datapack folder for different styles of movesets.
|
||||
- Added `party_pools` datapack folder for defining groups of Pokémon for the purposes of complex NPC party definitions.
|
||||
- Added `party_compositions` datapack folder for arranging parties on NPCs that rely on party pools.
|
||||
- Added `composed_pool` NPC party provider type which combines a party pool with a party composition to create more complex NPC parties.
|
||||
|
||||
### Particles
|
||||
@@ -2,7 +2,6 @@
|
||||
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
|
||||
|
||||
### Additions
|
||||
- Added config options to award experience to fainted Pokémon for opponents they helped defeat (`awardExperienceToFaintedPokemon`) and to still award experience for defeated enemies even after battle loss/forfeit (`awardExperienceOnBattleLoss`)
|
||||
- Party Overlay Exp Gained Animation will now display the amount of EXP the Pokémon gained
|
||||
- Added a toast to notify players of how they can scroll through their party. Did you know you can hold R and scroll instead of using arrow keys??!
|
||||
- Added new layer property that makes a layer scroll in a direction. "scrolling": {"speedU": 0.1, "speedV": 0.1}
|
||||
@@ -38,21 +37,15 @@
|
||||
- [Various UFO Ride Improvements](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2156)
|
||||
- Fixed NeoForge brewing stand unexpectedly crashing users when shift clicking items within it's menu
|
||||
- Fixed NPCs switching pokemon very often during battles
|
||||
- Fixed Campfire pot causing crashes under certain circumstances
|
||||
- Fixed Type Gems not being consumed upon use
|
||||
- Fixed Ancient Pokeball's to their respective modifiers
|
||||
- Fixed a Server warning when recalling Pokemon
|
||||
|
||||
### Developer
|
||||
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
|
||||
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters
|
||||
- Added `BattleFleeAttemptEvent` fired from `PokemonBattle.checkFleeAttempt`, allowing developers to intercept and control flee attempts
|
||||
|
||||
### Molang & Datapacks
|
||||
- q.item.is_enchanted()
|
||||
- q.item.has_enchantment(minecraft:sharpness, 3)
|
||||
- Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions
|
||||
- Added `marks`, `has_mark`, & `remove_marks` as available Molang functions for pokemon
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
@@ -55,7 +55,7 @@ dependencies {
|
||||
// modCompileOnly(libs.flywheelFabric)
|
||||
|
||||
// Showdown
|
||||
modCompileOnly(libs.graal.core)
|
||||
modCompileOnly(libs.graal)
|
||||
|
||||
// Data Storage
|
||||
modCompileOnly(libs.bundles.mongo)
|
||||
|
||||
@@ -101,7 +101,7 @@ public abstract class ServerEntityMixin {
|
||||
cobblemon$broadcast(new ClientboundUpdateDriverInputPacket(driverInput, entity.getId()));
|
||||
}
|
||||
|
||||
public void cobblemon$broadcast(NetworkPacket<?> packet) {
|
||||
private void cobblemon$broadcast(NetworkPacket<?> packet) {
|
||||
if (!(entity.level() instanceof ServerLevel level)) return;
|
||||
ChunkMap chunkMap = level.getChunkSource().chunkMap;
|
||||
Int2ObjectMap<?> entityMap = ((ChunkMapAccessor) chunkMap).getEntityMap();
|
||||
|
||||
@@ -28,8 +28,6 @@ object CobblemonActivities {
|
||||
@JvmField
|
||||
val POKEMON_HERD = register(Activity("pokemon_herd"))
|
||||
@JvmField
|
||||
val POKEMON_BOIDS = register(Activity("pokemon_boid"))
|
||||
@JvmField
|
||||
val POKEMON_POLLINATION = register(Activity("pokemon_pollination"))
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -74,6 +74,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
|
||||
@JvmField
|
||||
val NPC_EDITOR = create("npc_editor", CobblemonItem(Item.Properties().stacksTo(1)))
|
||||
@JvmField
|
||||
val POKEEDIT_WAND = create("pokeedit_wand", PokeEditWandItem())
|
||||
|
||||
@JvmField
|
||||
val pokeBalls = mutableListOf<PokeBallItem>()
|
||||
|
||||
@@ -21,7 +21,6 @@ import net.minecraft.core.UUIDUtil
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.ai.memory.MemoryModuleType
|
||||
import java.util.*
|
||||
import net.minecraft.world.entity.player.Player
|
||||
|
||||
object CobblemonMemories {
|
||||
val memories = mutableMapOf<String, MemoryModuleType<*>>()
|
||||
@@ -93,9 +92,6 @@ object CobblemonMemories {
|
||||
val DISABLE_WALK_TO_WANTED_ITEM = register<Boolean>("disable_walk_to_wanted_item")
|
||||
@JvmField
|
||||
val TIME_TRYING_TO_REACH_WANTED_ITEM = register<Int>("time_trying_to_wanted_item")
|
||||
/** Is the nearest player holding a diamond for debugging purposes? */
|
||||
@JvmField
|
||||
val DIAMOND_DEBUGGER = register<Player>("diamond_debugger")
|
||||
|
||||
@JvmStatic
|
||||
fun <U> register(id: String, codec: Codec<U>): MemoryModuleType<U> {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common
|
||||
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.moves.MoveSelector
|
||||
import com.cobblemon.mod.common.api.moves.MovesetBuilder
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.MovesetBuilderAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.StringToObjectAdapter
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
|
||||
object CobblemonMovesetBuilders : JsonDataRegistry<MovesetBuilder> {
|
||||
override val gson: Gson = GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderAdapter)
|
||||
.registerTypeAdapter(MoveSelector::class.java, StringToObjectAdapter(MoveSelector.selectors))
|
||||
.create()
|
||||
|
||||
override val typeToken = TypeToken.get(MovesetBuilder::class.java)
|
||||
override val resourcePath = "moveset_builders"
|
||||
override val id: ResourceLocation = cobblemonResource("moveset_builders")
|
||||
override val type = PackType.SERVER_DATA
|
||||
override val observable = SimpleObservable<CobblemonMovesetBuilders>()
|
||||
|
||||
val movesetBuilders = mutableMapOf<ResourceLocation, MovesetBuilder>()
|
||||
|
||||
fun getOrThrow(id: ResourceLocation): MovesetBuilder {
|
||||
return movesetBuilders[id]
|
||||
?: throw IllegalArgumentException("Unknown MovesetBuilder id: $id")
|
||||
}
|
||||
|
||||
override fun sync(player: ServerPlayer) { /* These don't sync. But what if they did? Nah probably shouldn't sync. */ }
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, MovesetBuilder>) {
|
||||
movesetBuilders.clear()
|
||||
data.forEach { (id, value) -> value.id = id }
|
||||
movesetBuilders.putAll(data)
|
||||
observable.emit(this)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
|
||||
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.data.RideSettingsSyncHandler
|
||||
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
|
||||
import com.cobblemon.mod.common.client.net.debug.S2CUpdateObsAvoidHandler
|
||||
import com.cobblemon.mod.common.client.net.dialogue.DialogueClosedHandler
|
||||
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
|
||||
import com.cobblemon.mod.common.client.net.effect.PokeSnackBlockParticlesHandler
|
||||
@@ -81,7 +80,6 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallba
|
||||
import com.cobblemon.mod.common.net.messages.client.cooking.SeasoningRegistrySyncPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.cooking.ToggleCookingPotLidPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.data.*
|
||||
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
|
||||
@@ -172,6 +170,7 @@ import com.cobblemon.mod.common.net.messages.server.pasture.UnpastureAllPokemonP
|
||||
import com.cobblemon.mod.common.net.messages.server.pasture.UnpasturePokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishScanningPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.StartScanningPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokemon.interact.InteractPokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetActiveMarkPacket
|
||||
@@ -229,6 +228,7 @@ import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHa
|
||||
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishScanningHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.StartScanningHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokeedit.PokeEditActionHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
|
||||
@@ -255,11 +255,7 @@ import com.cobblemon.mod.common.net.serverhandling.trade.OfferTradeHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.trade.PerformTradeHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.trade.UpdateTradeOfferHandler
|
||||
import com.cobblemon.mod.common.util.server
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.entity.Entity
|
||||
import com.cobblemon.mod.common.mixin.accessor.ChunkMapAccessor
|
||||
import com.cobblemon.mod.common.mixin.accessor.TrackedEntityAccessor
|
||||
|
||||
/**
|
||||
* Registers Cobblemon network packets.
|
||||
@@ -282,19 +278,6 @@ object CobblemonNetwork {
|
||||
fun sendToAllPlayers(packet: NetworkPacket<*>) = sendPacketToPlayers(server()!!.playerList.players, packet)
|
||||
@JvmStatic
|
||||
fun sendPacketToPlayers(players: Iterable<ServerPlayer>, packet: NetworkPacket<*>) = players.forEach { sendPacketToPlayer(it, packet) }
|
||||
fun sendPacketToTrackingPlayers(entity: Entity, packet: NetworkPacket<*>) {
|
||||
val level = entity.level() as? ServerLevel ?: return
|
||||
|
||||
val chunkMap = level.chunkSource.chunkMap
|
||||
val entityTracker = (chunkMap as ChunkMapAccessor).entityMap[entity.id]
|
||||
|
||||
if (entityTracker is TrackedEntityAccessor) {
|
||||
entityTracker.seenBy.forEach { connection ->
|
||||
sendPacketToPlayer(connection.player, packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val s2cPayloads = generateS2CPacketInfoList()
|
||||
val c2sPayloads = generateC2SPacketInfoList()
|
||||
@@ -502,7 +485,6 @@ object CobblemonNetwork {
|
||||
|
||||
// Debug / cheats
|
||||
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
|
||||
list.add(PacketRegisterInfo(ClientboundUpdateObsAvoidPacket.ID, ClientboundUpdateObsAvoidPacket::decode, S2CUpdateObsAvoidHandler))
|
||||
|
||||
// Riding
|
||||
list.add(PacketRegisterInfo(ClientboundUpdateRidingStatePacket.ID, ClientboundUpdateRidingStatePacket::decode, ClientboundUpdateRidingStateHandler))
|
||||
@@ -581,6 +563,9 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(StartScanningPacket.ID, StartScanningPacket::decode, StartScanningHandler))
|
||||
list.add(PacketRegisterInfo(FinishScanningPacket.ID, FinishScanningPacket::decode, FinishScanningHandler))
|
||||
|
||||
// PokeEdit actions
|
||||
list.add(PacketRegisterInfo(PokeEditActionPacket.ID, PokeEditActionPacket::decode, PokeEditActionHandler))
|
||||
|
||||
// Pasture
|
||||
list.add(PacketRegisterInfo(PasturePokemonPacket.ID, PasturePokemonPacket::decode, PasturePokemonHandler))
|
||||
list.add(PacketRegisterInfo(UnpasturePokemonPacket.ID, UnpasturePokemonPacket::decode, UnpasturePokemonHandler))
|
||||
@@ -639,4 +624,4 @@ object CobblemonNetwork {
|
||||
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
|
||||
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common
|
||||
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.npc.PartyComposition
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
|
||||
object CobblemonPartyCompositions : JsonDataRegistry<PartyComposition> {
|
||||
override val id: ResourceLocation = cobblemonResource("party_compositions")
|
||||
override val type = PackType.SERVER_DATA
|
||||
override val typeToken: TypeToken<PartyComposition> = TypeToken.get(PartyComposition::class.java)
|
||||
override val resourcePath: String = "party_compositions"
|
||||
override val observable = SimpleObservable<CobblemonPartyCompositions>()
|
||||
override val gson: Gson = GsonBuilder()
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.setPrettyPrinting()
|
||||
.create()
|
||||
|
||||
val partyCompositions = mutableMapOf<ResourceLocation, PartyComposition>()
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
// probably worth syncing which exist, yeah. the deeper details probably not. I'll get to this eventually.
|
||||
}
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, PartyComposition>) {
|
||||
data.entries.forEach { it.value.id = it.key }
|
||||
partyCompositions.clear()
|
||||
partyCompositions.putAll(data)
|
||||
observable.emit(this)
|
||||
Cobblemon.LOGGER.info("Loaded ${data.size} party compositions.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common
|
||||
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.moves.MovesetBuilder
|
||||
import com.cobblemon.mod.common.api.npc.PartyPool
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.util.ScriptableIntRange
|
||||
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.MovesetBuilderReferenceAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.PokemonPropertiesAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
|
||||
object CobblemonPartyPools : JsonDataRegistry<PartyPool> {
|
||||
override val id: ResourceLocation = cobblemonResource("party_pools")
|
||||
override val type = PackType.SERVER_DATA
|
||||
override val typeToken: TypeToken<PartyPool> = TypeToken.get(PartyPool::class.java)
|
||||
override val resourcePath: String = "party_pools"
|
||||
override val observable = SimpleObservable<CobblemonPartyPools>()
|
||||
override val gson: Gson = GsonBuilder()
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.registerTypeAdapter(PokemonProperties::class.java, PokemonPropertiesAdapter(saveLong = false))
|
||||
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
|
||||
.registerTypeAdapter(ScriptableIntRange::class.java, ScriptableIntRangeAdapter)
|
||||
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
|
||||
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
|
||||
.setPrettyPrinting()
|
||||
.create()
|
||||
|
||||
val partyPools = mutableMapOf<ResourceLocation, PartyPool>()
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
// probably worth syncing. Knowing the details might help exploit the server? lot of effort for little though.
|
||||
}
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, PartyPool>) {
|
||||
data.entries.forEach { it.value.id = it.key }
|
||||
partyPools.clear()
|
||||
partyPools.putAll(data)
|
||||
observable.emit(this)
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,6 @@ object CobblemonSensors {
|
||||
@JvmField
|
||||
val NEARBY_SWEET_BERRY_BUSH = register("nearby_sweet_berry_bush", ::SweetBerryBushSensor)
|
||||
|
||||
@JvmField
|
||||
val PLAYER_DEBUGGING = register("player_debugging", ::PlayerDebuggingSensor)
|
||||
|
||||
@JvmField
|
||||
val POKEMON_NEARBY_WANTED_ITEM = register("pokemon_nearby_wanted_item", ::PokemonItemSensor)
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.ai.config.task
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonMemories
|
||||
import com.cobblemon.mod.common.CobblemonSensors
|
||||
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
|
||||
import com.cobblemon.mod.common.api.ai.WrapperLivingEntityTask
|
||||
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.tasks.WakeUpTask
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.ai.behavior.BehaviorControl
|
||||
|
||||
class BoidsTaskConfig : SingleTaskConfig {
|
||||
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
|
||||
override fun createTask(
|
||||
entity: LivingEntity,
|
||||
behaviourConfigurationContext: BehaviourConfigurationContext
|
||||
): BehaviorControl<in LivingEntity>? {
|
||||
if (entity !is PokemonEntity) {
|
||||
return null
|
||||
}
|
||||
behaviourConfigurationContext.addMemories(CobblemonMemories.POKEMON_DROWSY, CobblemonMemories.POKEMON_SLEEPING)
|
||||
behaviourConfigurationContext.addSensors(CobblemonSensors.POKEMON_DROWSY)
|
||||
return WrapperLivingEntityTask(WakeUpTask.create(), PokemonEntity::class.java)
|
||||
}
|
||||
}
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.ai.config.task
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonMemories
|
||||
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
|
||||
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.BoidsMoveControl
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.ai.behavior.BehaviorControl
|
||||
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
|
||||
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
|
||||
|
||||
class SwitchFromBoidTaskConfig : SingleTaskConfig {
|
||||
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
|
||||
override fun createTask(
|
||||
entity: LivingEntity,
|
||||
behaviourConfigurationContext: BehaviourConfigurationContext
|
||||
): BehaviorControl<in LivingEntity>? {
|
||||
behaviourConfigurationContext.addMemories(CobblemonMemories.DIAMOND_DEBUGGER)
|
||||
return BehaviorBuilder.create {
|
||||
it.group(
|
||||
it.absent(CobblemonMemories.DIAMOND_DEBUGGER)
|
||||
).apply(it) { _ ->
|
||||
Trigger { world, entity, time ->
|
||||
if(entity !is PokemonEntity) return@Trigger false
|
||||
if (entity.moveControl !is PokemonMoveControl) {
|
||||
entity.moveControl = PokemonMoveControl(entity)
|
||||
}
|
||||
entity.brain.updateActivityFromSchedule(world.dayTime, world.gameTime)
|
||||
return@Trigger true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.ai.config.task
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonActivities
|
||||
import com.cobblemon.mod.common.CobblemonMemories
|
||||
import com.cobblemon.mod.common.CobblemonSensors
|
||||
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
|
||||
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.BoidsMoveControl
|
||||
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.ai.behavior.BehaviorControl
|
||||
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
|
||||
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
|
||||
import net.minecraft.world.entity.player.Player
|
||||
|
||||
class SwitchToBoidTaskConfig : SingleTaskConfig {
|
||||
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
|
||||
override fun createTask(
|
||||
entity: LivingEntity,
|
||||
behaviourConfigurationContext: BehaviourConfigurationContext
|
||||
): BehaviorControl<in LivingEntity>? {
|
||||
behaviourConfigurationContext.addMemories(CobblemonMemories.DIAMOND_DEBUGGER)
|
||||
behaviourConfigurationContext.addSensors(CobblemonSensors.PLAYER_DEBUGGING)
|
||||
return BehaviorBuilder.create {
|
||||
it.group(
|
||||
it.present(CobblemonMemories.DIAMOND_DEBUGGER)
|
||||
).apply(it) { _ ->
|
||||
Trigger { world, entity, time ->
|
||||
if(entity !is PokemonEntity) return@Trigger false
|
||||
val player = entity.brain.getMemory(CobblemonMemories.DIAMOND_DEBUGGER).orElse(null)
|
||||
if (entity.moveControl !is BoidsMoveControl && player is LivingEntity) {
|
||||
entity.moveControl = BoidsMoveControl(entity, player)
|
||||
} else {
|
||||
return@Trigger false
|
||||
}
|
||||
entity.brain.setActiveActivityIfPossible(CobblemonActivities.POKEMON_BOIDS)
|
||||
// SpawnSnowstormEntityParticlePacket(
|
||||
// cobblemonResource("evo_particles"),
|
||||
// entity.id,
|
||||
// listOf("shiny_particles", "middle")).sendToPlayersAround(
|
||||
// entity.x,
|
||||
// entity.y,
|
||||
// entity.z,
|
||||
// 64.0,
|
||||
// entity.level().dimension()
|
||||
// )
|
||||
return@Trigger true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,8 +124,6 @@ interface TaskConfig {
|
||||
cobblemonResource("target_entity") to TargetEntityTaskConfig::class.java,
|
||||
cobblemonResource("memory_aspect") to MemoryAspectTaskConfig::class.java,
|
||||
cobblemonResource("activity_change") to ActivityChangeTaskConfig::class.java,
|
||||
cobblemonResource("switch_from_boid") to SwitchFromBoidTaskConfig::class.java,
|
||||
cobblemonResource("switch_to_boid") to SwitchToBoidTaskConfig::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,21 +22,18 @@ import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.FleeableBattleActor
|
||||
import com.cobblemon.mod.common.api.events.CobblemonEvents
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.BattleEvSource
|
||||
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.api.text.red
|
||||
import com.cobblemon.mod.common.api.text.white
|
||||
import com.cobblemon.mod.common.api.text.yellow
|
||||
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
|
||||
import com.cobblemon.mod.common.battles.BattleCaptureAction
|
||||
import com.cobblemon.mod.common.battles.BattleFormat
|
||||
import com.cobblemon.mod.common.battles.BattleRegistry
|
||||
import com.cobblemon.mod.common.battles.BattleSide
|
||||
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
|
||||
import com.cobblemon.mod.common.battles.ForfeitActionResponse
|
||||
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
|
||||
import com.cobblemon.mod.common.battles.dispatch.BattleDispatch
|
||||
@@ -51,9 +48,7 @@ import com.cobblemon.mod.common.entity.npc.NPCBattleActor
|
||||
import com.cobblemon.mod.common.entity.npc.NPCEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
|
||||
import com.cobblemon.mod.common.pokemon.evolution.progress.DefeatEvolutionProgress
|
||||
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
|
||||
import com.cobblemon.mod.common.pokemon.requirements.DefeatRequirement
|
||||
@@ -142,9 +137,6 @@ open class PokemonBattle(
|
||||
val captureActions = mutableListOf<BattleCaptureAction>()
|
||||
|
||||
val majorBattleActions = hashMapOf<UUID, BattleMessage>()
|
||||
|
||||
/** Monotonically increasing counter to track faint ordering within this battle. */
|
||||
var faintCounter = 0
|
||||
val minorBattleActions = hashMapOf<UUID, BattleMessage>()
|
||||
val contextManager = ContextManager()
|
||||
|
||||
@@ -252,23 +244,12 @@ open class PokemonBattle(
|
||||
|
||||
fun end() {
|
||||
ended = true
|
||||
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
|
||||
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
|
||||
this.actors.forEach { actor ->
|
||||
val faintedPokemons = actor.pokemonList.filter { it.health <= 0 }
|
||||
actor.getSide().getOppositeSide().actors.forEach { opponent ->
|
||||
val opponentPokemons = if (awardToFainted) opponent.pokemonList else opponent.pokemonList.filter { it.health > 0 }
|
||||
val opponentNonFaintedPokemons = opponent.pokemonList.filter { it.health > 0 }
|
||||
faintedPokemons.forEach { faintedPokemon ->
|
||||
for (opponentPokemon in opponentPokemons) {
|
||||
// Faint-order check: if awarding XP to fainted pokemon, only award for enemies that fainted before this pokemon did
|
||||
if (awardToFainted) {
|
||||
val enemyFaintedAt = faintedPokemon.faintedAt ?: continue
|
||||
val opponentFaintedAt = opponentPokemon.faintedAt
|
||||
if (opponentFaintedAt != null && opponentFaintedAt <= enemyFaintedAt) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for (opponentPokemon in opponentNonFaintedPokemons) {
|
||||
val facedFainted = opponentPokemon.facedOpponents.contains(faintedPokemon)
|
||||
val pokemon = opponentPokemon.effectedPokemon
|
||||
if (facedFainted) {
|
||||
@@ -289,8 +270,7 @@ open class PokemonBattle(
|
||||
else -> continue
|
||||
}
|
||||
val experience = Cobblemon.experienceCalculator.calculate(opponentPokemon, faintedPokemon, multiplier)
|
||||
val enemyTeamWiped = actor.pokemonList.all { it.health <= 0 }
|
||||
if (experience > 0 && (enemyTeamWiped || awardOnLoss)) {
|
||||
if (experience > 0 && actor.pokemonList.all { it.health <= 0 }) {
|
||||
opponent.awardExperience(opponentPokemon, experience)
|
||||
}
|
||||
Cobblemon.evYieldCalculator.calculate(opponentPokemon, faintedPokemon).forEach { (stat, amount) ->
|
||||
@@ -525,7 +505,6 @@ open class PokemonBattle(
|
||||
|
||||
fun checkForInputDispatch() {
|
||||
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
|
||||
if (checkFleeAttempt()) return
|
||||
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
|
||||
if (readyToInput && captureActions.isEmpty()) {
|
||||
actors.filter { it.responses.isNotEmpty() }.forEach { it.writeShowdownResponse() }
|
||||
@@ -533,20 +512,6 @@ open class PokemonBattle(
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkFleeAttempt(): Boolean {
|
||||
val runner = actors.find { it.responses.any { it is FleeAttemptActionResponse } } as? PlayerBattleActor ?: return false
|
||||
CobblemonEvents.BATTLE_FLEE_ATTEMPT.post(BattleFleeAttemptEvent(this, runner))
|
||||
runner.responses.clear()
|
||||
|
||||
runner.mustChoose = true
|
||||
runner.request?.let { runner.sendUpdate(BattleQueueRequestPacket(it)) }
|
||||
runner.sendUpdate(BattleMakeChoicePacket())
|
||||
|
||||
runner.entity?.sendSystemMessage(battleLang("run_prompt").white())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/** Forces Showdown to end the battle when a [BattleActor] chooses to forfeit. */
|
||||
private fun checkForfeit(): Boolean {
|
||||
val forfeit = actors.find { it.responses.any { it is ForfeitActionResponse } }
|
||||
|
||||
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.events
|
||||
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleFaintedEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleStartedEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
|
||||
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
|
||||
@@ -111,9 +110,6 @@ object CobblemonEvents {
|
||||
@JvmField
|
||||
val BATTLE_STARTED_POST = EventObservable<BattleStartedEvent.Post>()
|
||||
|
||||
@JvmField
|
||||
val BATTLE_FLEE_ATTEMPT = EventObservable<BattleFleeAttemptEvent>()
|
||||
|
||||
@JvmField
|
||||
val BATTLE_FLED = EventObservable<BattleFledEvent>()
|
||||
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.events.battles
|
||||
|
||||
import com.bedrockk.molang.runtime.value.MoValue
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
|
||||
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
|
||||
import com.cobblemon.mod.common.util.asArrayValue
|
||||
|
||||
/**
|
||||
* Event fired when a [PokemonBattle] is fled by a [PlayerBattleActor].
|
||||
*
|
||||
* @author saultsc
|
||||
* @since March 31th 2026
|
||||
*/
|
||||
|
||||
data class BattleFleeAttemptEvent(
|
||||
override val battle: PokemonBattle,
|
||||
val player: PlayerBattleActor
|
||||
) : BattleEvent {
|
||||
val context = mutableMapOf<String, MoValue>(
|
||||
"battle" to battle.struct,
|
||||
"players" to battle.actors.filter { it.type == ActorType.PLAYER }.asArrayValue { it.struct },
|
||||
"npcs" to battle.actors.filter { it.type == ActorType.NPC }.asArrayValue { it.struct },
|
||||
"wild_pokemon" to battle.actors.filter { it.type == ActorType.WILD }.asArrayValue { it.struct }
|
||||
)
|
||||
}
|
||||
@@ -1842,30 +1842,6 @@ object MoLangFunctions {
|
||||
map.put("remove_cosmetic_item") { _ ->
|
||||
pokemon.removeCosmeticItem()
|
||||
}
|
||||
map.put("marks") { _ -> pokemon.marks.asArrayValue { StringValue(it.identifier.toString()) } }
|
||||
map.put("has_mark") { params ->
|
||||
var hasMark = false
|
||||
val identifier = params.getString(0).asIdentifierDefaultingNamespace()
|
||||
val mark = Marks.getByIdentifier(identifier)
|
||||
if (mark != null) {
|
||||
hasMark = pokemon.marks.contains(mark)
|
||||
}
|
||||
|
||||
DoubleValue(hasMark)
|
||||
}
|
||||
map.put("remove_marks") { params ->
|
||||
var removedMark = false
|
||||
for (param in params.params) {
|
||||
val identifier = param.asString().asIdentifierDefaultingNamespace()
|
||||
val mark = Marks.getByIdentifier(identifier)
|
||||
if (mark != null) {
|
||||
pokemon.exchangeMark(mark, false)
|
||||
removedMark = true
|
||||
}
|
||||
}
|
||||
|
||||
DoubleValue(removedMark)
|
||||
}
|
||||
map.put("add_marks") { params ->
|
||||
var appliedMark = false
|
||||
for (param in params.params) {
|
||||
@@ -1965,8 +1941,7 @@ object MoLangFunctions {
|
||||
return@put DoubleValue(if (pokemon.addRideBoost(stat, value)) 1.0 else 0.0)
|
||||
}
|
||||
map.put("initialize_moveset") { params ->
|
||||
val preferLatest = params.getBooleanOrNull(0) ?: true
|
||||
pokemon.initializeMoveset(preferLatest)
|
||||
pokemon.initializeMovesetFromDefault()
|
||||
return@put DoubleValue.ONE
|
||||
}
|
||||
map.put("validate_moveset") { params ->
|
||||
@@ -2059,13 +2034,9 @@ object MoLangFunctions {
|
||||
}
|
||||
}
|
||||
map.put("is_sprinting") {
|
||||
if (pokemonEntity.hasControllingPassenger()) { // If its being ridden then the riding state sets the animation
|
||||
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
|
||||
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
|
||||
return@ifRidingAvailableSupply moValue.functions.get("sprinting")?.apply(MoParams.EMPTY) as? MoValue ?: DoubleValue.ZERO
|
||||
}
|
||||
} else {
|
||||
pokemonEntity.isPokemonSprinting
|
||||
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
|
||||
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
|
||||
return@ifRidingAvailableSupply moValue.functions.get("sprinting")?.apply(MoParams.EMPTY) as? MoValue ?: DoubleValue.ZERO
|
||||
}
|
||||
}
|
||||
map.put("is_drifting") {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.moves
|
||||
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
/**
|
||||
* This is all you need tbh. A moveset builder that picks moves from up to 4 slots of move selectors.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since December 5th, 2025
|
||||
*/
|
||||
class DefaultMovesetBuilder : MovesetBuilder {
|
||||
companion object {
|
||||
val TYPE = cobblemonResource("default")
|
||||
}
|
||||
|
||||
override val type: ResourceLocation = TYPE
|
||||
override var id: ResourceLocation = cobblemonResource("temp") // replaced on load.
|
||||
|
||||
val slot1: List<MoveSelector> = listOf()
|
||||
val slot2: List<MoveSelector> = listOf()
|
||||
val slot3: List<MoveSelector> = listOf()
|
||||
val slot4: List<MoveSelector> = listOf()
|
||||
|
||||
override fun build(form: FormData, level: Int): MoveSet {
|
||||
val chosenMoves = mutableSetOf<MoveTemplate>()
|
||||
tryAddMoveFromSlot(form, level, slot1, chosenMoves)
|
||||
tryAddMoveFromSlot(form, level, slot2, chosenMoves)
|
||||
tryAddMoveFromSlot(form, level, slot3, chosenMoves)
|
||||
tryAddMoveFromSlot(form, level, slot4, chosenMoves)
|
||||
if (chosenMoves.isEmpty()) {
|
||||
chosenMoves.add(Moves.getExceptional())
|
||||
}
|
||||
val moveset = MoveSet()
|
||||
chosenMoves.map(MoveTemplate::create).forEach(moveset::add)
|
||||
return moveset
|
||||
}
|
||||
|
||||
fun tryAddMoveFromSlot(form: FormData, level: Int, selectors: List<MoveSelector>, chosenMoves: MutableSet<MoveTemplate>) {
|
||||
for (selector in selectors) {
|
||||
val move = selector(form, form.moves, level, chosenMoves)
|
||||
if (move != null) {
|
||||
chosenMoves.add(move)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.moves
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
|
||||
import com.cobblemon.mod.common.api.pokemon.moves.Learnset
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.util.weightedSelection
|
||||
|
||||
/**
|
||||
* A function that selects a move from a given [Learnset] for a particular [FormData] at a certain level,
|
||||
* taking into account any moves that have already been chosen for the moveset.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since December 5th, 2025
|
||||
*/
|
||||
fun interface MoveSelector {
|
||||
operator fun invoke(form: FormData, learnset: Learnset, level: Int, chosenMoves: Set<MoveTemplate>): MoveTemplate?
|
||||
|
||||
companion object {
|
||||
val NONE = MoveSelector { _, _, _, _ -> null }
|
||||
val LAST_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
|
||||
levels.forEach {
|
||||
val moves = (learnset.levelUpMoves[it] ?: return@forEach) - chosenMoves
|
||||
if (moves.isNotEmpty()) {
|
||||
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
}
|
||||
return@MoveSelector null
|
||||
}
|
||||
val LAST_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
|
||||
levels.forEach {
|
||||
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory != DamageCategories.STATUS } ?: return@forEach) - chosenMoves
|
||||
if (moves.isNotEmpty()) {
|
||||
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
}
|
||||
return@MoveSelector null
|
||||
}
|
||||
val LAST_SUITABLE_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
|
||||
val suitableCategories = if (form.baseStats[Stats.ATTACK]!! > form.baseStats[Stats.SPECIAL_ATTACK]!!) {
|
||||
setOf(DamageCategories.PHYSICAL)
|
||||
} else if (form.baseStats[Stats.SPECIAL_ATTACK]!! > form.baseStats[Stats.ATTACK]!!) {
|
||||
setOf(DamageCategories.SPECIAL)
|
||||
} else {
|
||||
setOf(DamageCategories.PHYSICAL, DamageCategories.SPECIAL)
|
||||
}
|
||||
levels.forEach {
|
||||
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory in suitableCategories } ?: return@forEach) - chosenMoves
|
||||
if (moves.isNotEmpty()) {
|
||||
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
}
|
||||
return@MoveSelector null
|
||||
}
|
||||
val LAST_STATUS = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
|
||||
levels.forEach {
|
||||
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory == DamageCategories.STATUS } ?: return@forEach) - chosenMoves
|
||||
if (moves.isNotEmpty()) {
|
||||
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
}
|
||||
return@MoveSelector null
|
||||
}
|
||||
val STAB = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
|
||||
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory != DamageCategories.STATUS }
|
||||
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val STAB_PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
|
||||
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.PHYSICAL }
|
||||
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val STAB_SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
|
||||
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.SPECIAL }
|
||||
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
|
||||
val physicalMoves = moves.filter { it.damageCategory == DamageCategories.PHYSICAL }
|
||||
return@MoveSelector physicalMoves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
|
||||
val specialMoves = moves.filter { it.damageCategory == DamageCategories.SPECIAL }
|
||||
return@MoveSelector specialMoves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
|
||||
val offensiveMoves = moves.filter { it.damageCategory != DamageCategories.STATUS }
|
||||
return@MoveSelector offensiveMoves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val RANDOM_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
|
||||
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val STATUS = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
|
||||
val statusMoves = moves.filter { it.damageCategory == DamageCategories.STATUS }
|
||||
return@MoveSelector statusMoves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val TM = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
|
||||
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val STAB_TM = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
|
||||
val stabMoves = moves.filter { it.elementalType in form.types }
|
||||
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
val EGG = MoveSelector { form, learnset, level, chosenMoves ->
|
||||
val moves = learnset.eggMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
|
||||
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
|
||||
}
|
||||
|
||||
val selectors = mutableMapOf(
|
||||
"none" to NONE,
|
||||
"last_levelup" to LAST_LEVELUP,
|
||||
"last_offensive" to LAST_OFFENSIVE,
|
||||
"last_suitable_offensive" to LAST_SUITABLE_OFFENSIVE,
|
||||
"last_status" to LAST_STATUS,
|
||||
"levelup" to RANDOM_LEVELUP,
|
||||
"stab" to STAB,
|
||||
"stab_physical" to STAB_PHYSICAL,
|
||||
"stab_special" to STAB_SPECIAL,
|
||||
"physical" to PHYSICAL,
|
||||
"special" to SPECIAL,
|
||||
"offensive" to OFFENSIVE,
|
||||
"status" to STATUS,
|
||||
"tm" to TM,
|
||||
"stab_tm" to STAB_TM,
|
||||
"egg" to EGG
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@ import com.bedrockk.molang.runtime.struct.MoStruct
|
||||
import com.bedrockk.molang.runtime.struct.QueryStruct
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.bedrockk.molang.runtime.value.StringValue
|
||||
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
|
||||
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
|
||||
import com.cobblemon.mod.common.api.moves.categories.DamageCategory
|
||||
import com.cobblemon.mod.common.api.types.ElementalType
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.battles.MoveTarget
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.util.codec.CodecUtils
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
@@ -53,7 +53,8 @@ open class MoveTemplate(
|
||||
val pp: Int,
|
||||
val priority: Int,
|
||||
val critRatio: Double,
|
||||
val effectChances: Array<Double>
|
||||
val effectChances: Array<Double>,
|
||||
val weight: Float
|
||||
) {
|
||||
val struct: MoStruct by lazy {
|
||||
QueryStruct(hashMapOf())
|
||||
@@ -69,9 +70,9 @@ open class MoveTemplate(
|
||||
.addFunction("max_pp") { DoubleValue(maxPp) }
|
||||
.addFunction("priority") { DoubleValue(priority) }
|
||||
.addFunction("crit_ratio") { DoubleValue(critRatio) }
|
||||
.addFunction("weight") { DoubleValue(weight.toDouble()) }
|
||||
}
|
||||
|
||||
|
||||
val displayName: MutableComponent
|
||||
get() = lang("move.$name")
|
||||
val description: MutableComponent
|
||||
@@ -89,7 +90,8 @@ open class MoveTemplate(
|
||||
pp = 5,
|
||||
priority = 0,
|
||||
critRatio = 0.0,
|
||||
effectChances = emptyArray()
|
||||
effectChances = emptyArray(),
|
||||
weight = 0F,
|
||||
)
|
||||
|
||||
companion object {
|
||||
@@ -160,4 +162,12 @@ open class MoveTemplate(
|
||||
}
|
||||
return this.elementalType
|
||||
}
|
||||
|
||||
fun getSelectionWeight(form: FormData): Float {
|
||||
return if (this in form.signatureMoves) {
|
||||
weight * MovesetBuilder.signatureMoveWeightMultiplier
|
||||
} else {
|
||||
weight
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,15 @@ package com.cobblemon.mod.common.api.moves
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.data.DataRegistry
|
||||
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
|
||||
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.battles.MoveTarget
|
||||
import com.cobblemon.mod.common.battles.runner.ShowdownService
|
||||
import com.cobblemon.mod.common.net.messages.client.data.MovesRegistrySyncPacket
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.fromJson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
@@ -46,6 +46,14 @@ object Moves : DataRegistry {
|
||||
this.idMapping.clear()
|
||||
this.moveScripts.clear()
|
||||
|
||||
val weights = mutableMapOf<String, Number>()
|
||||
manager.getResourceStack(cobblemonResource("move_weights.json")).forEach {
|
||||
it.open().use {
|
||||
val map = GsonBuilder().create().fromJson<Map<String, Number>>(it.reader())
|
||||
weights.putAll(map)
|
||||
}
|
||||
}
|
||||
|
||||
ShowdownService.service.resetRegistryData("move")
|
||||
manager.listResources("moves") { it.path.endsWith(".js") }.forEach { (identifier, resource) ->
|
||||
resource.open().use { stream ->
|
||||
@@ -75,6 +83,7 @@ object Moves : DataRegistry {
|
||||
val priority = jsMove.get("priority").asInt
|
||||
val critRatio = jsMove.get("critRatio")?.asDouble ?: 1.0
|
||||
val effectChances = arrayListOf<Double>()
|
||||
val weight = weights[id]?.toFloat() ?: 50F
|
||||
val secondariesMember = jsMove.get("secondaries")
|
||||
val secondaryMember = jsMove.get("secondary")
|
||||
if (secondariesMember != null && secondariesMember is JsonArray) {
|
||||
@@ -92,7 +101,20 @@ object Moves : DataRegistry {
|
||||
effectChances += secondaryMember.get("chance").asDouble
|
||||
}
|
||||
}
|
||||
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
|
||||
val move = MoveTemplate(
|
||||
name = id,
|
||||
num = num,
|
||||
elementalType = elementalType,
|
||||
damageCategory = damageCategory,
|
||||
power = power,
|
||||
target = target,
|
||||
accuracy = accuracy,
|
||||
pp = pp,
|
||||
priority = priority,
|
||||
critRatio = critRatio,
|
||||
effectChances = effectChances.toTypedArray(),
|
||||
weight = weight
|
||||
)
|
||||
this.register(move)
|
||||
} catch (e: Exception) {
|
||||
Cobblemon.LOGGER.error("Caught exception trying to resolve the move '{}'", id, e)
|
||||
@@ -113,7 +135,7 @@ object Moves : DataRegistry {
|
||||
@JvmStatic
|
||||
fun getByNameOrDummy(name: String) = allMoves[name.lowercase()] ?: MoveTemplate.dummy(name.lowercase())
|
||||
@JvmStatic
|
||||
fun getExceptional() = getByName("tackle") ?: allMoves.values.random()
|
||||
fun getExceptional() = getByName("tackle") ?: allMoves.values.first()
|
||||
@JvmStatic
|
||||
fun count() = allMoves.size
|
||||
@JvmStatic
|
||||
@@ -129,5 +151,4 @@ object Moves : DataRegistry {
|
||||
this.allMoves[move.name] = move
|
||||
this.idMapping[move.num] = move
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.moves
|
||||
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
/**
|
||||
* Builders a moveset for a particular [FormData] and level. Probably only really need [DefaultMovesetBuilder] though.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since December 5th, 2025
|
||||
*/
|
||||
interface MovesetBuilder {
|
||||
/** The ID of this builder. It's autofilled from the registry loader. */
|
||||
var id: ResourceLocation
|
||||
/** The subtype designation for this moveset builder. Generally unused once read from JSON. */
|
||||
val type: ResourceLocation
|
||||
|
||||
/** Builds a brand new moveset for the [form] and [level]. */
|
||||
fun build(form: FormData, level: Int): MoveSet
|
||||
|
||||
companion object {
|
||||
/** Weight multiplier for moves that are in the form's signatureMoves list. Makes them way more likely to be chosen. */
|
||||
var signatureMoveWeightMultiplier = 5F
|
||||
/** Subtypes of MovesetBuilder. I personally doubt you'll need any other subtypes, but hey. */
|
||||
val movesetBuilderTypes = mutableMapOf<ResourceLocation, Class<out MovesetBuilder>>(
|
||||
cobblemonResource("default") to DefaultMovesetBuilder::class.java
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,14 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.npc
|
||||
|
||||
import com.cobblemon.mod.common.api.npc.partyproviders.ComposedPoolPartyProvider
|
||||
import com.cobblemon.mod.common.api.npc.partyproviders.PoolPartyProvider
|
||||
import com.cobblemon.mod.common.api.npc.partyproviders.ScriptPartyProvider
|
||||
import com.cobblemon.mod.common.api.npc.partyproviders.SimplePartyProvider
|
||||
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
|
||||
import com.cobblemon.mod.common.entity.npc.NPCEntity
|
||||
import com.google.gson.JsonElement
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
/**
|
||||
@@ -25,14 +27,15 @@ import net.minecraft.server.level.ServerPlayer
|
||||
*/
|
||||
interface NPCPartyProvider {
|
||||
companion object {
|
||||
val types = mutableMapOf<String, (String) -> NPCPartyProvider>(
|
||||
SimplePartyProvider.TYPE to { SimplePartyProvider() },
|
||||
PoolPartyProvider.TYPE to { PoolPartyProvider() },
|
||||
ScriptPartyProvider.TYPE to { ScriptPartyProvider() }
|
||||
val types = mutableMapOf(
|
||||
SimplePartyProvider.TYPE to ::SimplePartyProvider,
|
||||
PoolPartyProvider.TYPE to ::PoolPartyProvider,
|
||||
ScriptPartyProvider.TYPE to ::ScriptPartyProvider,
|
||||
ComposedPoolPartyProvider.TYPE to ::ComposedPoolPartyProvider
|
||||
)
|
||||
}
|
||||
|
||||
val type: String
|
||||
val type: ResourceLocation
|
||||
val isStatic: Boolean
|
||||
fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer> = emptyList()): NPCPartyStore
|
||||
// Why did I opt for manual JSON loading??? I must have had a reason but I can't remember. Maybe for S2C? Use a codec doofus
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.npc
|
||||
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* A kind of definition for how to compose a party of Pokémon given a [PartyPool].
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since December 7th, 2025
|
||||
*/
|
||||
class PartyComposition {
|
||||
var id = cobblemonResource("dummy_composition")
|
||||
// val displayName = "cobblemon.party_composition.$id" // dunno if i will use this yet
|
||||
val scrambleOrder: Boolean = false
|
||||
val slot1 = listOf<String>()
|
||||
val slot2 = listOf<String>()
|
||||
val slot3 = listOf<String>()
|
||||
val slot4 = listOf<String>()
|
||||
val slot5 = listOf<String>()
|
||||
val slot6 = listOf<String>()
|
||||
|
||||
fun compose(pool: PartyPool, level: Int, aspects: Set<String>, desiredPokemonCount: Int, runtime: MoLangRuntime, random: Random = Random.Default): List<Pokemon> {
|
||||
val chosenEntries = mutableListOf<PartyPool.PoolEntry>()
|
||||
val availableEntries = pool.entries.filter { level in it.npcLevels.resolve(runtime) && (it.npcAspects.isEmpty() || it.npcAspects.all(aspects::contains)) }
|
||||
|
||||
for (labelSet in listOf(slot1, slot2, slot3, slot4, slot5, slot6)) {
|
||||
pool.tryChoosingEntry(labelSet, runtime, random, availableEntries, chosenEntries)
|
||||
if (chosenEntries.size >= desiredPokemonCount) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
val finalEntries = if (scrambleOrder) {
|
||||
chosenEntries.shuffled(random)
|
||||
} else {
|
||||
chosenEntries
|
||||
}
|
||||
|
||||
val pokemon = finalEntries.map { entry ->
|
||||
val levelVariation = entry.levelVariation.resolve(runtime)
|
||||
val lvlVariation = if (levelVariation.first == levelVariation.last) {
|
||||
levelVariation.first
|
||||
} else {
|
||||
levelVariation.random(random)
|
||||
}
|
||||
|
||||
val finalLevel = level + lvlVariation
|
||||
val properties = entry.pokemon.copy()
|
||||
properties.level = properties.level ?: finalLevel
|
||||
val pokemon = properties.create()
|
||||
entry.movesetBuilders.randomOrNull()?.let(pokemon::initializeMovesetFrom)
|
||||
pokemon
|
||||
}
|
||||
|
||||
return pokemon
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.npc
|
||||
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.moves.MovesetBuilder
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.util.ScriptableIntRange
|
||||
import com.cobblemon.mod.common.util.asExpressionLike
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.toProperties
|
||||
import com.cobblemon.mod.common.util.weightedSelection
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* A pool of Pokémon to choose from, typically for generating a party for an NPC. The entries
|
||||
* of the pool work on a weight and label system which is used as part of a [PartyComposition].
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since December 7th, 2025
|
||||
*/
|
||||
class PartyPool {
|
||||
var id = cobblemonResource("dummy")
|
||||
val displayName = "cobblemon.party_pool.$id"
|
||||
val entries = mutableListOf<PoolEntry>()
|
||||
|
||||
class PoolEntry {
|
||||
val pokemon: PokemonProperties = "pikachu".toProperties()
|
||||
val labels: List<String> = emptyList()
|
||||
val npcAspects: List<String> = emptyList()
|
||||
val required: List<String> = emptyList()
|
||||
val excluded: List<String> = emptyList()
|
||||
val maxSelectableTimes: ExpressionLike = "6".asExpressionLike()
|
||||
val levelVariation: ScriptableIntRange = ScriptableIntRange(0, 0)
|
||||
val npcLevels: ScriptableIntRange = ScriptableIntRange(1, 100)
|
||||
val movesetBuilders: List<MovesetBuilder> = emptyList()
|
||||
val weight: ExpressionLike = "50".asExpressionLike()
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through each of the labels in order, trying to find a valid choice. Once it finds one, it skips the later
|
||||
* labels and adds the value to [chosenEntries] and returns. If none of the labels could find a valid entry, nothing is added.
|
||||
*/
|
||||
fun tryChoosingEntry(
|
||||
labels: List<String>,
|
||||
runtime: MoLangRuntime,
|
||||
random: Random,
|
||||
availableEntries: List<PoolEntry>,
|
||||
chosenEntries: MutableList<PoolEntry>,
|
||||
) {
|
||||
for (label in labels) {
|
||||
val entries = availableEntries
|
||||
.filter { label in it.labels || label.lowercase() == "any" }
|
||||
.filter { entry ->
|
||||
// Check required labels
|
||||
entry.required.all { it in labels } &&
|
||||
// Check excluded labels
|
||||
entry.excluded.none { it in labels } &&
|
||||
// Check max selectable times
|
||||
chosenEntries.count { it == entry } < entry.maxSelectableTimes.resolveFloat(runtime)
|
||||
}
|
||||
|
||||
if (entries.isNotEmpty()) {
|
||||
val selectedEntry = entries.weightedSelection(random) { it.weight.resolveFloat(runtime) } ?: return
|
||||
chosenEntries.add(selectedEntry)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.npc.partyproviders
|
||||
|
||||
import com.bedrockk.molang.Expression
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.cobblemon.mod.common.CobblemonPartyCompositions
|
||||
import com.cobblemon.mod.common.CobblemonPartyPools
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
|
||||
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
|
||||
import com.cobblemon.mod.common.api.npc.PartyComposition
|
||||
import com.cobblemon.mod.common.api.npc.PartyPool
|
||||
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
|
||||
import com.cobblemon.mod.common.entity.npc.NPCEntity
|
||||
import com.cobblemon.mod.common.util.asArrayValue
|
||||
import com.cobblemon.mod.common.util.asExpression
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.resolveInt
|
||||
import com.cobblemon.mod.common.util.withQueryValue
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import kotlin.random.Random
|
||||
import kotlin.ranges.random
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
/**
|
||||
* An [NPCPartyProvider] which uses a [PartyComposition] and [PartyPool] to generate a party.
|
||||
*
|
||||
* There are controls for minimum and maximum Pokémon count, as well as whether to use a fixed random seed for the NPC.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since December 8th, 2025
|
||||
*/
|
||||
class ComposedPoolPartyProvider : NPCPartyProvider {
|
||||
companion object {
|
||||
val TYPE = cobblemonResource("composed_pool")
|
||||
}
|
||||
|
||||
override val type = TYPE
|
||||
override var isStatic: Boolean = true
|
||||
var useFixedRandom: Boolean = false
|
||||
var minPokemon: Expression = "1".asExpression()
|
||||
var maxPokemon: Expression = "6".asExpression()
|
||||
lateinit var pool: PartyPool
|
||||
lateinit var composition: PartyComposition
|
||||
|
||||
override fun loadFromJSON(json: JsonElement) {
|
||||
json as JsonObject
|
||||
isStatic = json.getAsJsonPrimitive("isStatic")?.asBoolean ?: true
|
||||
useFixedRandom = json.getAsJsonPrimitive("useFixedRandom")?.asBoolean ?: false
|
||||
pool = CobblemonPartyPools.partyPools[json.get("pool").asString.asIdentifierDefaultingNamespace()]
|
||||
?: throw IllegalArgumentException("Unknown PartyPool id: ${json.get("pool").asString}")
|
||||
composition = CobblemonPartyCompositions.partyCompositions[json.get("composition").asString.asIdentifierDefaultingNamespace()]
|
||||
?: throw IllegalArgumentException("Unknown PartyComposition id: ${json.get("composition").asString}")
|
||||
minPokemon = json.getAsJsonPrimitive("minPokemon").asString?.asExpression() ?: "1".asExpression()
|
||||
maxPokemon = json.getAsJsonPrimitive("maxPokemon").asString?.asExpression() ?: "6".asExpression()
|
||||
}
|
||||
|
||||
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
|
||||
val runtime = MoLangRuntime().setup().withQueryValue("npc", npc.struct)
|
||||
val random = if (useFixedRandom) Random(npc.uuid.hashCode()) else Random.Default
|
||||
runtime.withQueryValue("level", DoubleValue(level))
|
||||
runtime.withQueryValue("players", players.asArrayValue { it.asMoLangValue() })
|
||||
if (players.size == 1) {
|
||||
// This is for the convenience, most cases will be pvn one player
|
||||
runtime.withQueryValue("player", players.first().asMoLangValue())
|
||||
}
|
||||
val minPokemon = runtime.resolveInt(this.minPokemon)
|
||||
val maxPokemon = runtime.resolveInt(this.maxPokemon)
|
||||
val desiredPokemonCount = (minPokemon..maxPokemon).random(random)
|
||||
|
||||
val pokemonList = composition.compose(
|
||||
pool = pool,
|
||||
level = level,
|
||||
aspects = npc.aspects,
|
||||
desiredPokemonCount = desiredPokemonCount,
|
||||
runtime = runtime,
|
||||
random = random
|
||||
)
|
||||
|
||||
val partyStore = NPCPartyStore(npc)
|
||||
pokemonList.forEach(partyStore::add)
|
||||
return partyStore
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -48,7 +48,7 @@ import kotlin.random.Random
|
||||
*/
|
||||
class PoolPartyProvider : NPCPartyProvider {
|
||||
companion object {
|
||||
const val TYPE = "pool"
|
||||
val TYPE = cobblemonResource("pool")
|
||||
}
|
||||
|
||||
@Transient
|
||||
@@ -150,10 +150,10 @@ class PoolPartyProvider : NPCPartyProvider {
|
||||
val instance = selected.pokemon.copy().also { it.level = it.level ?: dictatedLevel ?: randomLevel }.create()
|
||||
party.add(instance)
|
||||
}
|
||||
if (party.none()) {
|
||||
LOGGER.error("${npc} has no Pokemon on Party")
|
||||
}
|
||||
|
||||
if (party.none()) {
|
||||
LOGGER.error("$npc had no Pokémon produced by their party provider")
|
||||
}
|
||||
}
|
||||
|
||||
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
class ScriptPartyProvider : NPCPartyProvider {
|
||||
companion object {
|
||||
const val TYPE = "script"
|
||||
val TYPE = cobblemonResource("script")
|
||||
}
|
||||
|
||||
override val type = TYPE
|
||||
|
||||
+2
-1
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
|
||||
import com.cobblemon.mod.common.entity.npc.NPCEntity
|
||||
import com.cobblemon.mod.common.util.asArrayValue
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.getBooleanOrNull
|
||||
import com.cobblemon.mod.common.util.toProperties
|
||||
import com.google.gson.JsonElement
|
||||
@@ -29,7 +30,7 @@ import net.minecraft.server.level.ServerPlayer
|
||||
*/
|
||||
class SimplePartyProvider : NPCPartyProvider {
|
||||
companion object {
|
||||
const val TYPE = "simple"
|
||||
val TYPE = cobblemonResource("simple")
|
||||
}
|
||||
|
||||
@Transient
|
||||
|
||||
@@ -289,11 +289,11 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
|
||||
createDefault("ancient_great_ball", MultiplierModifier(1.5F), ancient = true)
|
||||
createDefault("ancient_ultra_ball", MultiplierModifier(2F), ancient = true)
|
||||
createDefault("ancient_heavy_ball", throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_leaden_ball", MultiplierModifier(1.5F), throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_gigaton_ball", MultiplierModifier(2F), throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_leaden_ball", throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_gigaton_ball", throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_feather_ball", throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_wing_ball", MultiplierModifier(1.5F), throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_jet_ball", MultiplierModifier(2F), throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_wing_ball", throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_jet_ball", throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_origin_ball", GuaranteedModifier(), ancient = true)
|
||||
// Luxury ball effect, low priority as it must be triggered before soothe bell as of gen 4
|
||||
CobblemonEvents.FRIENDSHIP_UPDATED.subscribe(priority = Priority.LOW) { event ->
|
||||
|
||||
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.pokemon
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.Cobblemon.config
|
||||
import com.cobblemon.mod.common.CobblemonMovesetBuilders
|
||||
import com.cobblemon.mod.common.api.abilities.Abilities
|
||||
import com.cobblemon.mod.common.api.abilities.Ability
|
||||
import com.cobblemon.mod.common.api.events.CobblemonEvents
|
||||
@@ -148,6 +149,13 @@ open class PokemonProperties {
|
||||
props.originalTrainerType = OriginalTrainerType.entries.toList().parsePropertyOfCollection(keyPairs, listOf("originaltrainertype", "ottype"), labelsOptional = true) { it.name.lowercase() }
|
||||
props.originalTrainer = parsePlayerProperty(keyPairs, listOf("originaltrainer", "ot"))
|
||||
props.moves = parseString(keyPairs, listOf("moves"))?.split(",")
|
||||
props.movesetBuilders = parseString(keyPairs, listOf("moveset_builders", "movesetbuilders", "moveset_builder", "movesetbuilder"))?.split(",")?.mapNotNull {
|
||||
try {
|
||||
it.asIdentifierDefaultingNamespace()
|
||||
} catch (_: ResourceLocationException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
props.heldItem = parseString(keyPairs, listOf("helditem", "held_item"))
|
||||
props.minPerfectIVs = parseIntProperty(keyPairs, listOf("min_perfect_ivs"))?.coerceIn(0, Stats.PERMANENT.size)
|
||||
props.scaleModifier = parseFloatProperty(keyPairs, listOf("scale_modifier"))
|
||||
@@ -356,6 +364,7 @@ open class PokemonProperties {
|
||||
var originalTrainerType: OriginalTrainerType? = null
|
||||
var originalTrainer: String? = null // Original Trainer by Username or UUID
|
||||
var moves: List<String>? = null
|
||||
var movesetBuilders: List<ResourceLocation>? = null
|
||||
var heldItem: String? = null
|
||||
var cosmeticItem: String? = null
|
||||
var scaleModifier: Float? = null
|
||||
@@ -462,7 +471,7 @@ open class PokemonProperties {
|
||||
}
|
||||
moves?.let { moves ->
|
||||
if (pokemon.moveSet.getMoves().isEmpty()) {
|
||||
pokemon.initializeMoveset(preferLatest = true)
|
||||
pokemon.initializeMovesetFromDefault()
|
||||
}
|
||||
val moveTemplates = moves.mapNotNull(Moves::getByName).shuffled()
|
||||
val replaceableIndices = (0..3).filterNot { pokemon.moveSet[it]?.template?.let(moveTemplates::contains) == true }.shuffled()
|
||||
@@ -475,6 +484,13 @@ open class PokemonProperties {
|
||||
pokemon.moveSet.update()
|
||||
}
|
||||
}
|
||||
movesetBuilders?.let { movesetBuilders ->
|
||||
val builders = movesetBuilders.mapNotNull { CobblemonMovesetBuilders.movesetBuilders[it] }
|
||||
if (builders.isNotEmpty()) {
|
||||
val movesetBuilder = builders.random()
|
||||
pokemon.initializeMovesetFrom(movesetBuilder)
|
||||
}
|
||||
}
|
||||
heldItem?.let { itemKey ->
|
||||
val server = server() ?: return@let
|
||||
val parser = ItemParser(server.registryAccess())
|
||||
@@ -678,6 +694,7 @@ open class PokemonProperties {
|
||||
originalTrainerType?.let { nbt.putInt(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.ordinal) }
|
||||
originalTrainer?.let { nbt.putString(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
|
||||
moves?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
|
||||
movesetBuilders?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
|
||||
heldItem?.let {nbt.putString(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
|
||||
scaleModifier?.let { nbt.putFloat(DataKeys.POKEMON_SCALE_MODIFIER, it) }
|
||||
val custom = ListTag()
|
||||
@@ -712,6 +729,7 @@ open class PokemonProperties {
|
||||
originalTrainerType = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) OriginalTrainerType.valueOf(tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) else null
|
||||
originalTrainer = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER)) tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER) else null
|
||||
moves = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVES)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVES).split(",") else null
|
||||
movesetBuilders = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS).split(",").map { it.asIdentifierDefaultingNamespace() } else null
|
||||
heldItem = if (tag.contains(DataKeys.POKEMON_PROPERTIES_HELDITEM)) tag.getString(DataKeys.POKEMON_PROPERTIES_HELDITEM) else null
|
||||
scaleModifier = if (tag.contains(DataKeys.POKEMON_SCALE_MODIFIER)) tag.getFloat(DataKeys.POKEMON_SCALE_MODIFIER) else null
|
||||
val custom = tag.getList(DataKeys.POKEMON_PROPERTIES_CUSTOM, Tag.TAG_STRING.toInt())
|
||||
@@ -747,6 +765,7 @@ open class PokemonProperties {
|
||||
originalTrainerType?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.name) }
|
||||
originalTrainer?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
|
||||
moves?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
|
||||
movesetBuilders?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
|
||||
heldItem?.let {json.addProperty(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
|
||||
scaleModifier?.let { json.addProperty(DataKeys.POKEMON_SCALE_MODIFIER, it) }
|
||||
val custom = JsonArray()
|
||||
@@ -781,6 +800,7 @@ open class PokemonProperties {
|
||||
originalTrainerType = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)?.asString?.let { OriginalTrainerType.valueOf(it) }
|
||||
originalTrainer = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER)?.asString
|
||||
moves = json.get(DataKeys.POKEMON_PROPERTIES_MOVES)?.asString?.split(",")
|
||||
movesetBuilders = json.get(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)?.asString?.split(",")?.map { it.asIdentifierDefaultingNamespace() }
|
||||
heldItem = json.get(DataKeys.POKEMON_PROPERTIES_HELDITEM)?.asString
|
||||
scaleModifier = json.get(DataKeys.POKEMON_SCALE_MODIFIER)?.asFloat
|
||||
val custom = json.get(DataKeys.POKEMON_PROPERTIES_CUSTOM)?.asJsonArray
|
||||
@@ -820,6 +840,7 @@ open class PokemonProperties {
|
||||
originalTrainer?.let { pieces.add("originaltrainer=$it") }
|
||||
customProperties.forEach { pieces.add(it.asString()) }
|
||||
moves?.let { pieces.add("moves=${it.joinToString(separator = ",")}") }
|
||||
movesetBuilders?.let { pieces.add("movesetbuilders=${it.joinToString(separator = ",") { source -> source.toString() }}") }
|
||||
heldItem?.let {pieces.add("helditem=$it")}
|
||||
scaleModifier?.let { pieces.add("scale_modifier=$it") }
|
||||
return pieces.joinToString(separator)
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.api.drop.ItemDropMethod
|
||||
import com.cobblemon.mod.common.api.entity.EntityDimensionsAdapter
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.moves.MovesetBuilder
|
||||
import com.cobblemon.mod.common.api.moves.adapters.MoveTemplateAdapter
|
||||
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies.getByIdentifier
|
||||
@@ -131,6 +132,7 @@ object PokemonSpecies : JsonDataRegistry<Species> {
|
||||
.registerTypeAdapter(RidingBehaviourSettings::class.java, RidingBehaviourSettingsAdapter)
|
||||
.registerTypeAdapter(RideSoundSettingsList::class.java, RideSoundSettingsListAdapter)
|
||||
.registerTypeAdapter(ObtainableItemCondition::class.java, ObtainableItemConditionAdapter)
|
||||
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
|
||||
.disableHtmlEscaping()
|
||||
.enableComplexMapKeySerialization()
|
||||
.create()
|
||||
@@ -314,11 +316,11 @@ object PokemonSpecies : JsonDataRegistry<Species> {
|
||||
val result = mutableMapOf<String, String>()
|
||||
this.species.forEach {species ->
|
||||
val baseSpecies = ShowdownSpecies(species, null)
|
||||
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name.lowercase(), "")] = this.gson.toJson(baseSpecies)
|
||||
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name, "")] = this.gson.toJson(baseSpecies)
|
||||
species.forms.forEach { form ->
|
||||
if (form != species.standardForm) {
|
||||
val formSpecies = ShowdownSpecies(species, form)
|
||||
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name.lowercase(), "")] = this.gson.toJson(formSpecies)
|
||||
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name, "")] = this.gson.toJson(formSpecies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -54,6 +54,8 @@ class RidingAnimationData(val ride: PokemonEntity)
|
||||
fun update() {
|
||||
val activeRide = ride.hasControllingPassenger()
|
||||
|
||||
if (!activeRide) return
|
||||
|
||||
var currRot = Vec3.ZERO
|
||||
var angDelta = Vec3.ZERO
|
||||
|
||||
@@ -151,16 +153,14 @@ class RidingAnimationData(val ride: PokemonEntity)
|
||||
/******************************************************
|
||||
* Update springs
|
||||
*****************************************************/
|
||||
if (activeRide) {
|
||||
val driver = ride.controllingPassenger
|
||||
val vertInput = when {
|
||||
driver != null && driver.jumping-> 1.0
|
||||
driver != null && driver.isShiftKeyDown -> -1.0
|
||||
else -> 0.0
|
||||
}
|
||||
val input = if(driver != null) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
|
||||
driverInputSpring.update(input, stiffness * 2, damping)
|
||||
val driver = ride.controllingPassenger
|
||||
val vertInput = when {
|
||||
driver != null && driver.jumping-> 1.0
|
||||
driver != null && driver.isShiftKeyDown -> -1.0
|
||||
else -> 0.0
|
||||
}
|
||||
val input = if(driver != null) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
|
||||
driverInputSpring.update(input, stiffness * 2, damping)
|
||||
rotDeltaSpring.update(angDelta, stiffness, damping)
|
||||
velocitySpring.update(currentVelocity, stiffness, damping)
|
||||
rotSpring.update(currRot, stiffness, damping)
|
||||
|
||||
+71
-51
@@ -44,7 +44,7 @@ import kotlin.random.Random
|
||||
* @since July 10th, 2022
|
||||
*/
|
||||
open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelectionData> {
|
||||
class SelectingSpawnInformation {
|
||||
class SelectingSpawnInformation(val spawnDetail: SpawnDetail) {
|
||||
val spawnablePositions = mutableMapOf<SpawnablePosition, Float>()
|
||||
var highestWeight = 0F
|
||||
|
||||
@@ -58,61 +58,81 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
|
||||
fun chooseSpawnablePosition() = spawnablePositions.entries.weightedSelection { it.value }!!.key
|
||||
}
|
||||
|
||||
class SpawnablePositionSelectionData(
|
||||
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
|
||||
var percentSum: Float
|
||||
): SpawnSelectionData {
|
||||
val size: Int
|
||||
get() = spawnToSpawnablePosition.size
|
||||
class SpawnablePositionBucketSelectionData(
|
||||
val selectingSpawnInformation: MutableList<SelectingSpawnInformation>
|
||||
) {
|
||||
var percentSum: Float = 0F
|
||||
val seenSpawns = mutableListOf<SpawnDetail>()
|
||||
|
||||
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
|
||||
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
|
||||
// the percentage will get amplified for every spawnable position the thing was possible, completely
|
||||
// ruining the point of this pre-selection percentage.
|
||||
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
|
||||
seenSpawns.add(spawnDetail)
|
||||
percentSum += spawnDetail.percentage
|
||||
}
|
||||
val spawnInformation = selectingSpawnInformation.find { it.spawnDetail == spawnDetail }
|
||||
?: SelectingSpawnInformation(spawnDetail).also { selectingSpawnInformation.add(it) }
|
||||
spawnInformation.add(spawnablePosition, spawnablePosition.getWeight(spawnDetail))
|
||||
}
|
||||
|
||||
fun remove(spawnInformation: SelectingSpawnInformation) {
|
||||
selectingSpawnInformation -= spawnInformation
|
||||
if (spawnInformation.spawnDetail.percentage > 0) {
|
||||
percentSum -= spawnInformation.spawnDetail.percentage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SpawnablePositionSelectionData(
|
||||
val spawner: Spawner,
|
||||
val spawnablePositions: List<SpawnablePosition>,
|
||||
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData>,
|
||||
): SpawnSelectionData {
|
||||
override val spawnActions = mutableListOf<SpawnAction<*>>()
|
||||
override val context = mutableMapOf<String, Any>()
|
||||
|
||||
fun getDataForBucket(bucket: SpawnBucket): SpawnablePositionBucketSelectionData {
|
||||
val existing = selectingSpawnInformation[bucket]
|
||||
if (existing != null) {
|
||||
return existing
|
||||
}
|
||||
|
||||
val bucketSpawnInformation = SpawnablePositionBucketSelectionData(mutableListOf())
|
||||
spawnablePositions.forEach { spawnablePosition ->
|
||||
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
|
||||
bucketSpawnInformation.add(it, spawnablePosition)
|
||||
}
|
||||
}
|
||||
|
||||
selectingSpawnInformation[bucket] = bucketSpawnInformation
|
||||
return bucketSpawnInformation
|
||||
}
|
||||
|
||||
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
|
||||
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
|
||||
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
|
||||
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
|
||||
selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation.filter { shouldRemove(it.spawnDetail) } }
|
||||
.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
|
||||
}
|
||||
|
||||
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
|
||||
val toRemove = spawnToSpawnablePosition.entries.filter { (spawnDetail, positionData) ->
|
||||
positionData.spawnablePositions.removeIf { shouldRemove(spawnDetail, it.key) }
|
||||
val toRemove = selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation }.filter { positionData ->
|
||||
positionData.spawnablePositions.removeIf { shouldRemove(positionData.spawnDetail, it.key) }
|
||||
positionData.highestWeight = positionData.spawnablePositions.maxOfOrNull { it.value } ?: 0F
|
||||
positionData.spawnablePositions.isEmpty()
|
||||
}
|
||||
|
||||
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
|
||||
|
||||
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
|
||||
toRemove.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSelectionData(
|
||||
spawner: Spawner,
|
||||
bucket: SpawnBucket,
|
||||
spawnablePositions: List<SpawnablePosition>
|
||||
): SpawnablePositionSelectionData {
|
||||
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation> = mutableMapOf()
|
||||
var percentSum = 0F
|
||||
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData> = mutableMapOf()
|
||||
|
||||
spawnablePositions.forEach { spawnablePosition ->
|
||||
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
|
||||
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
|
||||
// the percentage will get amplified for every spawnable position the thing was possible, completely
|
||||
// ruining the point of this pre-selection percentage.
|
||||
if (it.percentage > 0 && !spawnToSpawnablePosition.containsKey(it)) {
|
||||
percentSum += it.percentage
|
||||
}
|
||||
|
||||
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(
|
||||
it,
|
||||
::SelectingSpawnInformation
|
||||
)
|
||||
selectingSpawnInformation.add(spawnablePosition, spawnablePosition.getWeight(it))
|
||||
}
|
||||
}
|
||||
|
||||
return SpawnablePositionSelectionData(spawnToSpawnablePosition, percentSum)
|
||||
return SpawnablePositionSelectionData(spawner, spawnablePositions, selectingSpawnInformation)
|
||||
}
|
||||
|
||||
override fun selectSpawnAction(
|
||||
@@ -120,8 +140,8 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
|
||||
bucket: SpawnBucket,
|
||||
selectionData: SpawnablePositionSelectionData,
|
||||
): SpawnAction<*>? {
|
||||
val spawnToSpawnablePosition = selectionData.spawnToSpawnablePosition
|
||||
var percentSum = selectionData.percentSum
|
||||
val selectingSpawnInformation = selectionData.getDataForBucket(bucket) ?: return null
|
||||
var percentSum = selectingSpawnInformation.percentSum
|
||||
|
||||
// First pass is doing percentage checks.
|
||||
if (percentSum > 0) {
|
||||
@@ -129,7 +149,7 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
|
||||
if (percentSum > 100) {
|
||||
Cobblemon.LOGGER.warn(
|
||||
"""
|
||||
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
|
||||
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
|
||||
This means you don't understand how this option works.
|
||||
""".trimIndent()
|
||||
)
|
||||
@@ -140,13 +160,13 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
|
||||
* It's [0, 1) and I want (0, 1]
|
||||
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
|
||||
*/
|
||||
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
|
||||
val selectedPercentage = 100 - Random.nextFloat() * 100
|
||||
percentSum = 0F
|
||||
for ((spawnDetail, info) in spawnToSpawnablePosition) {
|
||||
if (spawnDetail.percentage > 0) {
|
||||
percentSum += spawnDetail.percentage
|
||||
for (info in selectingSpawnInformation.selectingSpawnInformation) {
|
||||
if (info.spawnDetail.percentage > 0) {
|
||||
percentSum += info.spawnDetail.percentage
|
||||
if (percentSum >= selectedPercentage) {
|
||||
return spawnDetail.choose(
|
||||
return info.spawnDetail.choose(
|
||||
spawnablePosition = info.chooseSpawnablePosition(),
|
||||
bucket = bucket,
|
||||
selectionData = selectionData
|
||||
@@ -156,9 +176,9 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
|
||||
}
|
||||
}
|
||||
|
||||
val selectedSpawn = spawnToSpawnablePosition.entries.toList().weightedSelection { it.value.highestWeight }!!
|
||||
return selectedSpawn.key.choose(
|
||||
spawnablePosition = selectedSpawn.value.chooseSpawnablePosition(),
|
||||
val selectedSpawn = selectingSpawnInformation.selectingSpawnInformation.toList().weightedSelection { it.highestWeight }!!
|
||||
return selectedSpawn.spawnDetail.choose(
|
||||
spawnablePosition = selectedSpawn.chooseSpawnablePosition(),
|
||||
bucket = bucket,
|
||||
selectionData = selectionData
|
||||
)
|
||||
@@ -169,16 +189,16 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
|
||||
bucket: SpawnBucket,
|
||||
spawnablePositions: List<SpawnablePosition>
|
||||
): Map<SpawnDetail, Float> {
|
||||
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
|
||||
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
|
||||
|
||||
if (selectionData.size == 0) {
|
||||
if (selectionData.selectingSpawnInformation.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val totalWeights = mutableMapOf<SpawnDetail, Float>()
|
||||
|
||||
for ((spawnDetail, info) in selectionData.spawnToSpawnablePosition.entries) {
|
||||
totalWeights[spawnDetail] = info.highestWeight
|
||||
for (info in selectionData.selectingSpawnInformation) {
|
||||
totalWeights[info.spawnDetail] = info.highestWeight
|
||||
}
|
||||
|
||||
return totalWeights
|
||||
|
||||
+67
-55
@@ -18,7 +18,6 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
|
||||
import com.cobblemon.mod.common.util.removeIf
|
||||
import com.cobblemon.mod.common.util.weightedSelection
|
||||
import kotlin.random.Random
|
||||
import kotlin.reflect.full.createInstance
|
||||
|
||||
/**
|
||||
* A spawning selector that compiles a distinct list of all spawn details that
|
||||
@@ -33,9 +32,9 @@ import kotlin.reflect.full.createInstance
|
||||
* of an entire spawnable position type to favour whichever are the more populous.
|
||||
*
|
||||
* The weight for a spawn when doing primary selection is whichever spawnable position-influenced
|
||||
* weight is highest (as weight multipliers exist per spawnable position) and then the
|
||||
* selection of which spawnable position to spawn the primary selected spawn uses the spawnable
|
||||
* position-adjusted weight.
|
||||
* weight is highest (as weight multipliers exist per spawnable position). After the spawn has been
|
||||
* chosen, the selection of which spawnable position to spawn it at is selected via another weighted
|
||||
* selection using spawnable position-adjusted weights.
|
||||
*
|
||||
* At a glance:
|
||||
* - Spawn detail selection is flat across spawnable position quantity
|
||||
@@ -45,7 +44,6 @@ import kotlin.reflect.full.createInstance
|
||||
* @since July 10th, 2022
|
||||
*/
|
||||
open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnablePositionWeightedSelector.SeparatedSelectionData> {
|
||||
open fun getWeight(spawnablePositionType: SpawnablePositionType<*>) = spawnablePositionType.getWeight()
|
||||
|
||||
class SelectingSpawnInformation {
|
||||
val spawnablePositionWeights = mutableMapOf<SpawnablePosition, Float>()
|
||||
@@ -62,13 +60,25 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
|
||||
fun chooseSpawnablePosition() = spawnablePositionWeights.keys.toList().weightedSelection { spawnablePositionWeights[it]!! }!!
|
||||
}
|
||||
|
||||
class SpawnablePositionSelectionData(
|
||||
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
|
||||
var percentSum: Float
|
||||
) {
|
||||
class SpawnablePositionSelectionData {
|
||||
val spawnToSpawnablePosition = mutableMapOf<SpawnDetail, SelectingSpawnInformation>()
|
||||
var percentSum: Float = 0F
|
||||
val seenSpawns = mutableListOf<SpawnDetail>()
|
||||
val size: Int
|
||||
get() = spawnToSpawnablePosition.size
|
||||
|
||||
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
|
||||
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
|
||||
// position type, otherwise the percentage will get amplified for every spawnable position the thing
|
||||
// was possible, completely ruining the point of this pre-selection percentage.
|
||||
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
|
||||
percentSum += spawnDetail.percentage
|
||||
seenSpawns.add(spawnDetail)
|
||||
}
|
||||
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(spawnDetail, ::SelectingSpawnInformation)
|
||||
selectingSpawnInformation.add(spawnDetail, spawnablePosition, 1F)
|
||||
}
|
||||
|
||||
fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
|
||||
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
|
||||
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
|
||||
@@ -89,67 +99,68 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
|
||||
}
|
||||
|
||||
class SeparatedSelectionData(
|
||||
val spawnablePositionTypeToSpawns: MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>
|
||||
): SpawnSelectionData {
|
||||
val spawner: Spawner,
|
||||
val spawnablePositions: List<SpawnablePosition>
|
||||
) : SpawnSelectionData {
|
||||
override val spawnActions = mutableListOf<SpawnAction<*>>()
|
||||
override val context = mutableMapOf<String, Any>()
|
||||
|
||||
val bucketData = mutableMapOf<SpawnBucket, MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>>()
|
||||
|
||||
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
|
||||
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnDetails(shouldRemove) }
|
||||
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
|
||||
bucketData.values.forEach {
|
||||
it.values.forEach { it.removeSpawnDetails(shouldRemove) }
|
||||
it.removeIf { it.value.size == 0 }
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
|
||||
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnablePositions(shouldRemove) }
|
||||
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
|
||||
bucketData.values.forEach {
|
||||
it.values.forEach { it.removeSpawnablePositions(shouldRemove) }
|
||||
it.removeIf { it.value.size == 0 }
|
||||
}
|
||||
}
|
||||
|
||||
fun getDataForBucket(bucket: SpawnBucket): MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData> {
|
||||
bucketData[bucket]?.let { return it }
|
||||
|
||||
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
|
||||
|
||||
spawnablePositions.forEach { spawnablePosition ->
|
||||
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
|
||||
|
||||
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
|
||||
if (possible.isNotEmpty()) {
|
||||
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns
|
||||
.getOrPut(spawnablePositionType, ::SpawnablePositionSelectionData)
|
||||
possible.forEach { spawnablePositionSelectionData.add(it, spawnablePosition) }
|
||||
}
|
||||
}
|
||||
|
||||
bucketData[bucket] = spawnablePositionTypeToSpawns
|
||||
return spawnablePositionTypeToSpawns
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSelectionData(
|
||||
spawner: Spawner,
|
||||
bucket: SpawnBucket,
|
||||
spawnablePositions: List<SpawnablePosition>
|
||||
): SeparatedSelectionData {
|
||||
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
|
||||
|
||||
spawnablePositions.forEach { spawnablePosition ->
|
||||
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
|
||||
|
||||
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
|
||||
if (possible.isNotEmpty()) {
|
||||
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns.getOrPut(spawnablePositionType) { SpawnablePositionSelectionData(mutableMapOf(), 0F) }
|
||||
possible.forEach {
|
||||
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
|
||||
// position type, otherwise the percentage will get amplified for every spawnable position the thing
|
||||
// was possible, completely ruining the point of this pre-selection percentage.
|
||||
if (it.percentage > 0 && !spawnablePositionSelectionData.spawnToSpawnablePosition.containsKey(it)) {
|
||||
spawnablePositionSelectionData.percentSum += it.percentage
|
||||
}
|
||||
|
||||
val selectingSpawnInformation = spawnablePositionSelectionData.spawnToSpawnablePosition.getOrPut(
|
||||
it,
|
||||
SelectingSpawnInformation::class::createInstance
|
||||
)
|
||||
selectingSpawnInformation.add(it, spawnablePosition, getWeight(spawnablePositionType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SeparatedSelectionData(spawnablePositionTypeToSpawns)
|
||||
}
|
||||
) = SeparatedSelectionData(spawner, spawnablePositions)
|
||||
|
||||
override fun selectSpawnAction(
|
||||
spawner: Spawner,
|
||||
bucket: SpawnBucket,
|
||||
selectionData: SeparatedSelectionData
|
||||
): SpawnAction<*>? {
|
||||
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
|
||||
val bucketData = selectionData.getDataForBucket(bucket)
|
||||
|
||||
if (bucketData.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Which spawnable position type should we use?
|
||||
val spawnablePositionSelectionData = selectionData.spawnablePositionTypeToSpawns.entries.toList()
|
||||
.weightedSelection { getWeight(it.key) * it.value.size }
|
||||
val spawnablePositionSelectionData = bucketData.entries.toList()
|
||||
.weightedSelection { it.key.getWeight() * it.value.size }
|
||||
?.value
|
||||
?: return null
|
||||
|
||||
@@ -161,7 +172,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
|
||||
if (percentSum > 100) {
|
||||
LOGGER.warn(
|
||||
"""
|
||||
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
|
||||
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
|
||||
This means you don't understand how this option works.
|
||||
""".trimIndent()
|
||||
)
|
||||
@@ -173,7 +184,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
|
||||
* It's [0, 1) and I want (0, 1]
|
||||
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
|
||||
*/
|
||||
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
|
||||
val selectedPercentage = 100 - Random.nextFloat() * 100
|
||||
percentSum = 0F
|
||||
for ((spawnDetail, info) in spawnToSpawnablePosition) {
|
||||
if (spawnDetail.percentage > 0) {
|
||||
@@ -224,7 +235,8 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
|
||||
val percentageWeight = (rescaledTotalWeight - totalWeight) / percentSum
|
||||
|
||||
for ((spawnDetail, info) in spawnToSpawnablePosition.entries) {
|
||||
totalWeights[spawnDetail] = info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
|
||||
totalWeights[spawnDetail] =
|
||||
info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
|
||||
}
|
||||
|
||||
return totalWeights
|
||||
@@ -235,18 +247,18 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
|
||||
bucket: SpawnBucket,
|
||||
spawnablePositions: List<SpawnablePosition>
|
||||
): Map<SpawnDetail, Float> {
|
||||
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
|
||||
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
|
||||
|
||||
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
|
||||
if (selectionData.isEmpty()) {
|
||||
return mapOf()
|
||||
}
|
||||
|
||||
val totalWeights = mutableMapOf<SpawnDetail, Float>()
|
||||
|
||||
val totalSpawnablePositionWeight = selectionData.spawnablePositionTypeToSpawns.keys.sumOf { getWeight(it).toDouble() }.toFloat()
|
||||
val totalSpawnablePositionWeight = selectionData.keys.sumOf { it.getWeight().toDouble() }.toFloat()
|
||||
|
||||
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData.spawnablePositionTypeToSpawns) {
|
||||
val spawnPositionWeightCorrection = getWeight(spawnablePositionType) / totalSpawnablePositionWeight
|
||||
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData) {
|
||||
val spawnPositionWeightCorrection = spawnablePositionType.getWeight() / totalSpawnablePositionWeight
|
||||
val spawnPositionProbabilities = getProbabilitiesFromSpawnablePositionType(spawner, spawnPositionSelectionData)
|
||||
|
||||
spawnPositionProbabilities.entries.forEach {
|
||||
|
||||
+9
-3
@@ -29,7 +29,7 @@ interface SpawningSelector<T : SpawnSelectionData> {
|
||||
val DEFAULT = FlatSpawnablePositionWeightedSelector()
|
||||
}
|
||||
|
||||
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
|
||||
fun getSelectionData(spawner: Spawner, spawnablePositions: List<SpawnablePosition>): T
|
||||
|
||||
fun selectSpawnAction(
|
||||
spawner: Spawner,
|
||||
@@ -37,12 +37,18 @@ interface SpawningSelector<T : SpawnSelectionData> {
|
||||
selectionData: T
|
||||
): SpawnAction<*>?
|
||||
|
||||
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
|
||||
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
|
||||
fun select(
|
||||
spawner: Spawner,
|
||||
bucketSelector: () -> SpawnBucket,
|
||||
spawnablePositions: List<SpawnablePosition>,
|
||||
maxSpawns: Int
|
||||
): List<SpawnAction<*>> {
|
||||
val selectionData = getSelectionData(spawner, spawnablePositions)
|
||||
|
||||
val spawnActions = selectionData.spawnActions
|
||||
|
||||
while (spawnActions.size < maxSpawns) {
|
||||
val bucket = bucketSelector()
|
||||
val spawnAction = selectSpawnAction(
|
||||
spawner = spawner,
|
||||
bucket = bucket,
|
||||
|
||||
@@ -29,13 +29,6 @@ import com.cobblemon.mod.common.util.isBoxLoaded
|
||||
import com.cobblemon.mod.common.util.squeezeWithinBounds
|
||||
import com.cobblemon.mod.common.util.toVec3f
|
||||
import com.cobblemon.mod.common.util.weightedSelection
|
||||
import kotlin.Any
|
||||
import kotlin.Boolean
|
||||
import kotlin.Float
|
||||
import kotlin.Int
|
||||
import kotlin.Pair
|
||||
import kotlin.String
|
||||
import kotlin.collections.plus
|
||||
import kotlin.math.max
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.SectionPos
|
||||
@@ -95,10 +88,9 @@ interface Spawner {
|
||||
): SpawnAction<*>? {
|
||||
influences.removeIf { it.isExpired() }
|
||||
spawnablePosition.influences.addAll(influences)
|
||||
val bucket = chooseBucket(cause, spawnablePosition.influences)
|
||||
return selector.select(
|
||||
spawner = this,
|
||||
bucket = bucket,
|
||||
bucketSelector = { chooseBucket(cause, spawnablePosition.influences) },
|
||||
spawnablePositions = listOf(spawnablePosition),
|
||||
maxSpawns = 1
|
||||
).firstOrNull()
|
||||
@@ -116,9 +108,9 @@ interface Spawner {
|
||||
|
||||
val areaBox = AABB.ofSize(
|
||||
Vec3(constrainedArea.getCenter().toVec3f()),
|
||||
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
|
||||
ENTITY_LIMIT_CHUNK_RANGE * 16.0,
|
||||
1000.0,
|
||||
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
|
||||
ENTITY_LIMIT_CHUNK_RANGE * 16.0
|
||||
)
|
||||
|
||||
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
|
||||
@@ -140,11 +132,10 @@ interface Spawner {
|
||||
val zone = generator.generate(this, constrainedArea)
|
||||
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
|
||||
val influences = influences + zone.unconditionalInfluences
|
||||
val bucket = chooseBucket(zoneInput.cause, influences)
|
||||
|
||||
return selector.select(
|
||||
spawner = this,
|
||||
bucket = bucket,
|
||||
bucketSelector = { chooseBucket(zoneInput.cause, influences) },
|
||||
spawnablePositions = spawnablePositions,
|
||||
maxSpawns = maxSpawns
|
||||
)
|
||||
|
||||
@@ -94,8 +94,7 @@ enum class ShowdownActionResponseType(val loader: (RegistryFriendlyByteBuf) -> S
|
||||
PASS({ PassActionResponse }),
|
||||
SHIFT({ ShiftActionResponse()}),
|
||||
HEAL_ITEM({ HealItemActionResponse("potion") }),
|
||||
FORFEIT({ ForfeitActionResponse() }),
|
||||
FLEE_ATTEMPT({ FleeAttemptActionResponse() }),
|
||||
FORFEIT({ ForfeitActionResponse() });
|
||||
}
|
||||
|
||||
abstract class ShowdownActionResponse(val type: ShowdownActionResponseType) {
|
||||
@@ -281,11 +280,6 @@ class ForfeitActionResponse : ShowdownActionResponse(ShowdownActionResponseType.
|
||||
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "forfeit"
|
||||
}
|
||||
|
||||
class FleeAttemptActionResponse : ShowdownActionResponse(ShowdownActionResponseType.FLEE_ATTEMPT) {
|
||||
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean) = true
|
||||
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "flee-attempt"
|
||||
}
|
||||
|
||||
class ShowdownMoveset {
|
||||
lateinit var moves: List<InBattleMove>
|
||||
var trapped = false
|
||||
|
||||
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.battles
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon.LOGGER
|
||||
import com.cobblemon.mod.common.battles.runner.ShowdownService
|
||||
import net.minecraft.CrashReport
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@@ -38,15 +37,8 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
|
||||
|
||||
override fun run() {
|
||||
LOGGER.info("Starting showdown service...")
|
||||
try {
|
||||
ShowdownService.service.openConnection()
|
||||
LOGGER.info("Showdown has been started!")
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to start showdown service!", e)
|
||||
} finally {
|
||||
this.latch.countDown()
|
||||
throw RuntimeException("Showdown service failed to initialize...")
|
||||
}
|
||||
|
||||
ShowdownService.service.openConnection()
|
||||
LOGGER.info("Showdown has been started!")
|
||||
this.latch.countDown()
|
||||
}
|
||||
}
|
||||
-1
@@ -51,7 +51,6 @@ class FaintInstruction(battle: PokemonBattle, val message: BattleMessage) : Inte
|
||||
|
||||
preamble.thenAccept {
|
||||
faintingPokemon.effectedPokemon.currentHealth = 0
|
||||
faintingPokemon.faintedAt = battle.faintCounter++
|
||||
faintingPokemon.sendUpdate()
|
||||
CobblemonEvents.BATTLE_FAINTED.post(BattleFaintedEvent(battle, pokemon, context, pnx))
|
||||
battle.getActorAndActiveSlotFromPNX(pnx).second.battlePokemon = null
|
||||
|
||||
@@ -110,9 +110,6 @@ open class BattlePokemon(
|
||||
/** A set of all the BattlePokemon that they faced during the battle (for exp purposes) */
|
||||
val facedOpponents = mutableSetOf<BattlePokemon>()
|
||||
|
||||
/** The faint sequence number within the battle, or null if this pokemon has not fainted. */
|
||||
var faintedAt: Int? = null
|
||||
|
||||
/**
|
||||
* The [HeldItemManager] backing this [BattlePokemon].
|
||||
*/
|
||||
|
||||
@@ -151,15 +151,13 @@ class CampfireBlock(settings: Properties, val isSoul: Boolean) : BaseEntityBlock
|
||||
override fun placeLiquid(level: LevelAccessor, pos: BlockPos, state: BlockState, fluidState: FluidState): Boolean {
|
||||
val blockEntity = level.getBlockEntity(pos)
|
||||
if (fluidState.type === Fluids.WATER && blockEntity is CampfireBlockEntity) {
|
||||
if (level is Level) {
|
||||
if (!level.isClientSide) {
|
||||
removePotItem(blockEntity, state, level, pos, null, true)
|
||||
level.playSoundServer(pos.center, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 1.0F, 1.0F)
|
||||
}
|
||||
|
||||
if (level.isClientSide) for (i in 0..19) MCCampfireBlock.makeParticles(level, pos, false, true)
|
||||
if (!level.isClientSide) {
|
||||
removePotItem(blockEntity, state, level as Level, pos, null, true)
|
||||
level.playSoundServer(pos.center, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 1.0F, 1.0F)
|
||||
}
|
||||
|
||||
if (level.isClientSide) for (i in 0..19) MCCampfireBlock.makeParticles(level as Level, pos, false, true)
|
||||
|
||||
level.scheduleTick(pos, fluidState.type, fluidState.type.getTickDelay(level))
|
||||
return true
|
||||
} else {
|
||||
|
||||
+1
-2
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.client.gui.battle.subscreen
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
|
||||
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
|
||||
import com.cobblemon.mod.common.client.CobblemonClient
|
||||
import com.cobblemon.mod.common.client.battle.SingleActionRequest
|
||||
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
|
||||
@@ -66,7 +65,7 @@ class BattleGeneralActionSelection(
|
||||
|
||||
addOption(rank++, battleLang("ui.run"), BattleGUI.runResource) {
|
||||
CobblemonClient.battle?.minimised = true
|
||||
battleGUI.selectAction(request, FleeAttemptActionResponse())
|
||||
Minecraft.getInstance().player?.displayClientMessage(battleLang("run_prompt"), false)
|
||||
playDownSound(Minecraft.getInstance().soundManager)
|
||||
}
|
||||
} else {
|
||||
|
||||
+2
-1
@@ -138,7 +138,8 @@ abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Floa
|
||||
pp = gimmickTemplate?.pp ?: moveTemplate.pp,
|
||||
priority = gimmickTemplate?.priority ?: moveTemplate.priority,
|
||||
critRatio = gimmickTemplate?.critRatio ?: moveTemplate.critRatio,
|
||||
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances
|
||||
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances,
|
||||
weight = gimmickTemplate?.weight ?: moveTemplate.weight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,4 +132,4 @@ abstract class InfoTextScrollWidget(val pX: Int, val pY: Int): ScrollingWidget<I
|
||||
return Component.literal(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.pokemon.EVs
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.pokemon.IVs
|
||||
import com.cobblemon.mod.common.pokemon.Species
|
||||
|
||||
data class PokeEditDraft(
|
||||
var species: Species,
|
||||
var form: FormData = species.standardForm,
|
||||
var shiny: Boolean = false,
|
||||
var gender: Gender = defaultGenderFor(form),
|
||||
var level: Int = 1,
|
||||
var originalTrainer: String = "",
|
||||
var abilityIndex: Int = 0,
|
||||
var ivs: IVs = IVs(),
|
||||
var evs: EVs = EVs(),
|
||||
var cosmeticItem: String? = null,
|
||||
var cosmeticAspects: Set<String> = emptySet(),
|
||||
var heldItem: String? = null,
|
||||
val forcedAspects: MutableSet<String> = mutableSetOf(),
|
||||
val choiceFeatures: MutableMap<String, String> = mutableMapOf(),
|
||||
val flagFeatures: MutableSet<String> = mutableSetOf(),
|
||||
val activeMoves: MutableList<MoveTemplate?> = MutableList(4) { null },
|
||||
val benchedMoves: MutableSet<MoveTemplate> = mutableSetOf()
|
||||
) {
|
||||
companion object {
|
||||
fun defaultGenderFor(form: FormData): Gender {
|
||||
return when {
|
||||
form.maleRatio !in 0F..1F -> Gender.GENDERLESS
|
||||
form.maleRatio == 0F -> Gender.FEMALE
|
||||
else -> Gender.MALE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit
|
||||
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.client.gui.ExitButton
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_HEIGHT
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_WIDTH
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_ICON_SIZE
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAnimationListWidget
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAnimationTab
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditEVIVTab
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditGeneralTab
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditLocatorListWidget
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditLocatorTab
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditMovesTab
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditImplementationBlankTab
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditWalkSpeedTab
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditSpeciesList
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.widgets.SearchWidget
|
||||
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditAction
|
||||
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.FlagSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
|
||||
import com.cobblemon.mod.common.pokemon.properties.AspectPropertyType
|
||||
import com.cobblemon.mod.common.pokemon.OriginalTrainerType
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.gui.components.AbstractWidget
|
||||
import net.minecraft.client.gui.components.EditBox
|
||||
import net.minecraft.client.gui.screens.Screen
|
||||
import net.minecraft.network.chat.Component
|
||||
|
||||
class PokeEditGUI private constructor() : Screen(Component.translatable("cobblemon.ui.pokeedit.title")) {
|
||||
companion object {
|
||||
private val screenBackground = cobblemonResource("textures/gui/pokedex/pokedex_screen.png")
|
||||
private val baseBackground = cobblemonResource("textures/gui/pokedex/pokedex_base_red.png")
|
||||
|
||||
private val tabSelectArrow = cobblemonResource("textures/gui/pokedex/select_arrow.png")
|
||||
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
|
||||
private val tabIcons = arrayOf(
|
||||
cobblemonResource("textures/gui/pokedex/tab_info.png"),
|
||||
cobblemonResource("textures/gui/pokedex/tab_stats.png"),
|
||||
cobblemonResource("textures/gui/pokedex/tab_moves.png"),
|
||||
cobblemonResource("textures/gui/pokedex/tab_species.png"),
|
||||
cobblemonResource("textures/gui/pokedex/tab_locations.png"),
|
||||
)
|
||||
|
||||
const val TAB_GENERAL = 0
|
||||
const val TAB_EV_IV = 1
|
||||
const val TAB_MOVES = 2
|
||||
const val TAB_ASPECTS = 3
|
||||
const val TAB_IMPLEMENTATION_BUTTON = 4
|
||||
|
||||
const val IMPL_TAB_LOCATORS = 0
|
||||
const val IMPL_TAB_ANIMATION = 1
|
||||
const val IMPL_TAB_SCALE = 2
|
||||
const val IMPL_TAB_WALK_SPEED = 3
|
||||
|
||||
fun open() {
|
||||
Minecraft.getInstance().setScreen(PokeEditGUI())
|
||||
}
|
||||
}
|
||||
|
||||
private val allSpecies = PokemonSpecies.species.sortedBy { it.nationalPokedexNumber }
|
||||
private var filteredSpecies = allSpecies
|
||||
|
||||
private lateinit var searchWidget: SearchWidget
|
||||
private lateinit var speciesList: PokeEditSpeciesList
|
||||
private lateinit var locatorListWidget: PokeEditLocatorListWidget
|
||||
private lateinit var animationListWidget: PokeEditAnimationListWidget
|
||||
private var highlightedLocator: String? = null
|
||||
private var animationTab: PokeEditAnimationTab? = null
|
||||
|
||||
private val tabButtons: MutableList<ScaledButton> = mutableListOf()
|
||||
private var tabIndex = TAB_GENERAL
|
||||
private var implementationMode = false
|
||||
private var implementationTabIndex = IMPL_TAB_LOCATORS
|
||||
private lateinit var tabElement: AbstractWidget
|
||||
|
||||
private lateinit var createButton: SummaryButton
|
||||
private lateinit var spawnButton: SummaryButton
|
||||
private lateinit var exitButton: ExitButton
|
||||
private lateinit var implementationButton: SummaryButton
|
||||
private lateinit var implementationExitButton: SummaryButton
|
||||
private val implementationTabButtons: MutableList<SummaryButton> = mutableListOf()
|
||||
|
||||
private val draft = PokeEditDraft(allSpecies.firstOrNull() ?: PokemonSpecies.random())
|
||||
|
||||
override fun renderBlurredBackground(delta: Float) {}
|
||||
override fun renderMenuBackground(context: GuiGraphics) {}
|
||||
override fun isPauseScreen() = false
|
||||
|
||||
override fun init() {
|
||||
super.init()
|
||||
clearWidgets()
|
||||
|
||||
val x = (width - BASE_WIDTH) / 2
|
||||
val y = (height - BASE_HEIGHT) / 2
|
||||
|
||||
if (::searchWidget.isInitialized) removeWidget(searchWidget)
|
||||
searchWidget = SearchWidget(x + 26, y + 28, 128, 11, update = ::updateFilters)
|
||||
addRenderableWidget(searchWidget)
|
||||
|
||||
if (::speciesList.isInitialized) removeWidget(speciesList)
|
||||
speciesList = PokeEditSpeciesList(x + 26, y + 39, ::setSelectedSpecies)
|
||||
|
||||
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
|
||||
locatorListWidget = PokeEditLocatorListWidget(
|
||||
pX = x + 26,
|
||||
pY = y + 190,
|
||||
listWidth = 139,
|
||||
listHeight = 151,
|
||||
onSelectLocator = { name ->
|
||||
highlightedLocator = name
|
||||
(tabElement as? PokeEditLocatorTab)?.setHighlightedLocator(name)
|
||||
}
|
||||
)
|
||||
locatorListWidget.setLocators(emptyList())
|
||||
|
||||
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
|
||||
animationListWidget = PokeEditAnimationListWidget(
|
||||
pX = x + 26,
|
||||
pY = y + 190,
|
||||
listWidth = 139,
|
||||
listHeight = 151,
|
||||
isPlaying = { name -> animationTab?.isAnimationPlaying(name) == true },
|
||||
isLooping = { name -> animationTab?.isAnimationLooping(name) == true },
|
||||
getDuration = { name -> animationTab?.getAnimationDuration(name) ?: 0F },
|
||||
getProgress = { name -> animationTab?.getAnimationProgress(name) ?: 0F },
|
||||
onPlay = { name -> animationTab?.playAnimation(name) },
|
||||
onPause = { name -> animationTab?.pauseAnimation(name) },
|
||||
onToggleLoop = { name, enabled -> animationTab?.setAnimationLooping(name, enabled) }
|
||||
)
|
||||
animationListWidget.setAnimations(emptyList())
|
||||
|
||||
setUpTabs()
|
||||
setUpActionButtons()
|
||||
setUpImplementationControls()
|
||||
setImplementationMode(implementationMode, force = true)
|
||||
updateFilters(true)
|
||||
if (implementationMode) {
|
||||
displayImplementationTab(implementationTabIndex, recreate = true)
|
||||
} else {
|
||||
displayTab(tabIndex, recreate = true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val matrices = context.pose()
|
||||
renderBackground(context, mouseX, mouseY, delta)
|
||||
|
||||
val x = (width - BASE_WIDTH) / 2
|
||||
val y = (height - BASE_HEIGHT) / 2
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = baseBackground,
|
||||
x = x,
|
||||
y = y,
|
||||
width = BASE_WIDTH,
|
||||
height = BASE_HEIGHT
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = screenBackground,
|
||||
x = x,
|
||||
y = y,
|
||||
width = BASE_WIDTH,
|
||||
height = BASE_HEIGHT
|
||||
)
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = lang("ui.pokeedit.title"),
|
||||
x = x + 192,
|
||||
y = y + 15,
|
||||
shadow = true
|
||||
)
|
||||
|
||||
super.render(context, mouseX, mouseY, delta)
|
||||
|
||||
tabButtons.forEach { it.render(context, mouseX, mouseY, delta) }
|
||||
|
||||
if (!implementationMode) {
|
||||
// Draw the tab arrow above overlay widgets so it stays visible.
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = tabSelectArrow,
|
||||
x = (x + 198 + (25 * tabIndex)) / SCALE,
|
||||
y = (y + 177) / SCALE,
|
||||
width = 12,
|
||||
height = 6,
|
||||
scale = SCALE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFilters(init: Boolean = false) {
|
||||
val search = searchWidget.value.trim()
|
||||
filteredSpecies = if (search.isEmpty()) {
|
||||
allSpecies
|
||||
} else {
|
||||
allSpecies.filter { species ->
|
||||
species.translatedName.string.contains(search, true)
|
||||
|| species.resourceIdentifier.path.contains(search, true)
|
||||
}
|
||||
}
|
||||
|
||||
speciesList.setEntries(filteredSpecies)
|
||||
|
||||
if (init || draft.species !in filteredSpecies) {
|
||||
filteredSpecies.firstOrNull()?.let { setSelectedSpecies(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSelectedSpecies(species: com.cobblemon.mod.common.pokemon.Species) {
|
||||
if (draft.species == species) return
|
||||
draft.species = species
|
||||
draft.form = species.standardForm
|
||||
draft.gender = PokeEditDraft.defaultGenderFor(draft.form)
|
||||
draft.shiny = false
|
||||
draft.abilityIndex = 0
|
||||
val aspectOptions = PokeEditAspectsTab.collectAspectOptions(
|
||||
species,
|
||||
draft.form,
|
||||
draft.gender,
|
||||
draft.shiny
|
||||
)
|
||||
draft.forcedAspects.retainAll(aspectOptions.toggles)
|
||||
draft.flagFeatures.retainAll(aspectOptions.flagKeys)
|
||||
draft.choiceFeatures.entries.removeIf { (key, choice) ->
|
||||
val group = aspectOptions.choiceGroups.firstOrNull { it.key == key } ?: return@removeIf true
|
||||
choice !in group.choices
|
||||
}
|
||||
val cosmetic = aspectOptions.cosmeticChoices.firstOrNull { it.itemId.toString() == draft.cosmeticItem }
|
||||
draft.cosmeticItem = cosmetic?.itemId?.toString()
|
||||
draft.cosmeticAspects = cosmetic?.aspects?.toSet() ?: emptySet()
|
||||
if (implementationMode) {
|
||||
displayImplementationTab(implementationTabIndex, recreate = true)
|
||||
} else {
|
||||
displayTab(tabIndex, recreate = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpTabs() {
|
||||
val x = (width - BASE_WIDTH) / 2
|
||||
val y = (height - BASE_HEIGHT) / 2
|
||||
|
||||
tabButtons.clear()
|
||||
for (i in tabIcons.indices) {
|
||||
tabButtons.add(
|
||||
ScaledButton(
|
||||
x + 197F + (i * 25F),
|
||||
y + 181.5F,
|
||||
TAB_ICON_SIZE,
|
||||
TAB_ICON_SIZE,
|
||||
resource = tabIcons[i],
|
||||
clickAction = {
|
||||
if (i == TAB_IMPLEMENTATION_BUTTON) {
|
||||
setImplementationMode(true)
|
||||
} else {
|
||||
displayTab(i)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
tabButtons.forEach(::addRenderableWidget)
|
||||
}
|
||||
|
||||
private fun setUpActionButtons() {
|
||||
val x = (width - BASE_WIDTH) / 2
|
||||
val y = (height - BASE_HEIGHT) / 2
|
||||
val actionY = y + 200F
|
||||
val exitX = x + 312F
|
||||
val spawnX = exitX - 56F
|
||||
val createX = spawnX - 56F
|
||||
val implX = x + 26F
|
||||
|
||||
if (::createButton.isInitialized) removeWidget(createButton)
|
||||
createButton = SummaryButton(
|
||||
buttonX = createX,
|
||||
buttonY = actionY,
|
||||
buttonWidth = 54,
|
||||
buttonHeight = 15,
|
||||
clickAction = { sendPokeEditAction(PokeEditAction.CREATE) },
|
||||
text = lang("ui.pokeedit.give"),
|
||||
resource = actionButtonResource
|
||||
)
|
||||
addRenderableWidget(createButton)
|
||||
|
||||
if (::spawnButton.isInitialized) removeWidget(spawnButton)
|
||||
spawnButton = SummaryButton(
|
||||
buttonX = spawnX,
|
||||
buttonY = actionY,
|
||||
buttonWidth = 54,
|
||||
buttonHeight = 15,
|
||||
clickAction = { sendPokeEditAction(PokeEditAction.SPAWN) },
|
||||
text = lang("ui.pokeedit.spawn"),
|
||||
resource = actionButtonResource
|
||||
)
|
||||
addRenderableWidget(spawnButton)
|
||||
|
||||
if (::exitButton.isInitialized) removeWidget(exitButton)
|
||||
exitButton = ExitButton(pX = exitX.toInt(), pY = y + 200) {
|
||||
Minecraft.getInstance().setScreen(null)
|
||||
}
|
||||
addRenderableWidget(exitButton)
|
||||
|
||||
if (::implementationButton.isInitialized) removeWidget(implementationButton)
|
||||
implementationButton = SummaryButton(
|
||||
buttonX = implX,
|
||||
buttonY = actionY,
|
||||
buttonWidth = 54,
|
||||
buttonHeight = 15,
|
||||
clickAction = { setImplementationMode(true) },
|
||||
text = Component.literal("Impl"),
|
||||
resource = actionButtonResource,
|
||||
renderRequirement = { !implementationMode },
|
||||
clickRequirement = { !implementationMode },
|
||||
largeText = false,
|
||||
boldText = false,
|
||||
textScale = 0.7F
|
||||
)
|
||||
addRenderableWidget(implementationButton)
|
||||
}
|
||||
|
||||
private fun setUpImplementationControls() {
|
||||
val x = (width - BASE_WIDTH) / 2
|
||||
val y = (height - BASE_HEIGHT) / 2
|
||||
val sideX = (x - 60F).coerceAtLeast(6F)
|
||||
|
||||
if (::implementationExitButton.isInitialized) removeWidget(implementationExitButton)
|
||||
implementationExitButton = SummaryButton(
|
||||
buttonX = sideX,
|
||||
buttonY = y + 28F,
|
||||
buttonWidth = 54,
|
||||
buttonHeight = 15,
|
||||
clickAction = { setImplementationMode(false) },
|
||||
text = Component.literal("Back"),
|
||||
resource = actionButtonResource,
|
||||
renderRequirement = { implementationMode },
|
||||
clickRequirement = { implementationMode },
|
||||
largeText = false,
|
||||
boldText = false,
|
||||
textScale = 0.7F
|
||||
)
|
||||
addRenderableWidget(implementationExitButton)
|
||||
|
||||
implementationTabButtons.forEach(::removeWidget)
|
||||
implementationTabButtons.clear()
|
||||
|
||||
val baseY = y + 48F
|
||||
val labels = listOf(
|
||||
Component.literal("Locators"),
|
||||
Component.literal("Animation"),
|
||||
Component.literal("Scale"),
|
||||
Component.literal("Walk Speed")
|
||||
)
|
||||
labels.forEachIndexed { index, label ->
|
||||
val button = SummaryButton(
|
||||
buttonX = sideX,
|
||||
buttonY = baseY + (index * 18F),
|
||||
buttonWidth = 54,
|
||||
buttonHeight = 15,
|
||||
clickAction = { displayImplementationTab(index) },
|
||||
text = label,
|
||||
resource = actionButtonResource,
|
||||
renderRequirement = { implementationMode },
|
||||
clickRequirement = { implementationMode },
|
||||
largeText = false,
|
||||
boldText = false,
|
||||
textScale = 0.7F
|
||||
)
|
||||
implementationTabButtons.add(button)
|
||||
addRenderableWidget(button)
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayTab(newIndex: Int, recreate: Boolean = false) {
|
||||
if (implementationMode) {
|
||||
return
|
||||
}
|
||||
if (tabIndex == newIndex && ::tabElement.isInitialized && !recreate) {
|
||||
return
|
||||
}
|
||||
|
||||
tabIndex = newIndex
|
||||
if (::tabElement.isInitialized) removeWidget(tabElement)
|
||||
|
||||
val x = (width - BASE_WIDTH) / 2
|
||||
val y = (height - BASE_HEIGHT) / 2
|
||||
val tabX = x + 180
|
||||
val tabY = y + 28
|
||||
|
||||
tabElement = when (newIndex) {
|
||||
TAB_GENERAL -> PokeEditGeneralTab(tabX, tabY, draft) { displayTab(tabIndex, recreate = true) }
|
||||
TAB_EV_IV -> PokeEditEVIVTab(tabX, tabY, draft)
|
||||
TAB_MOVES -> PokeEditMovesTab(tabX, tabY, draft)
|
||||
TAB_ASPECTS -> PokeEditAspectsTab(tabX, tabY, draft)
|
||||
else -> PokeEditGeneralTab(tabX, tabY, draft) { displayTab(tabIndex, recreate = true) }
|
||||
}
|
||||
|
||||
addRenderableWidget(tabElement)
|
||||
bringControlsToFront()
|
||||
}
|
||||
|
||||
private fun displayImplementationTab(newIndex: Int, recreate: Boolean = false) {
|
||||
if (!implementationMode) {
|
||||
return
|
||||
}
|
||||
if (implementationTabIndex == newIndex && ::tabElement.isInitialized && !recreate) {
|
||||
return
|
||||
}
|
||||
|
||||
implementationTabIndex = newIndex
|
||||
if (::tabElement.isInitialized) removeWidget(tabElement)
|
||||
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
|
||||
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
|
||||
animationTab = null
|
||||
|
||||
val x = (width - BASE_WIDTH) / 2
|
||||
val y = (height - BASE_HEIGHT) / 2
|
||||
val tabX = x + 180
|
||||
val tabY = y + 28
|
||||
|
||||
tabElement = when (newIndex) {
|
||||
IMPL_TAB_LOCATORS -> PokeEditLocatorTab(
|
||||
tabX,
|
||||
tabY,
|
||||
draft,
|
||||
highlightedLocator,
|
||||
::updateLocatorList
|
||||
)
|
||||
IMPL_TAB_ANIMATION -> PokeEditAnimationTab(tabX, tabY, draft) { animations ->
|
||||
animationListWidget.setAnimations(animations)
|
||||
}
|
||||
IMPL_TAB_SCALE -> PokeEditImplementationBlankTab(tabX, tabY, draft, Component.literal("Scale"))
|
||||
IMPL_TAB_WALK_SPEED -> PokeEditWalkSpeedTab(tabX, tabY, draft)
|
||||
else -> PokeEditLocatorTab(
|
||||
tabX,
|
||||
tabY,
|
||||
draft,
|
||||
highlightedLocator,
|
||||
::updateLocatorList
|
||||
)
|
||||
}
|
||||
|
||||
addRenderableWidget(tabElement)
|
||||
when (newIndex) {
|
||||
IMPL_TAB_LOCATORS -> addRenderableWidget(locatorListWidget)
|
||||
IMPL_TAB_ANIMATION -> {
|
||||
animationTab = tabElement as? PokeEditAnimationTab
|
||||
animationListWidget.setAnimations(animationTab?.getAnimations().orEmpty())
|
||||
addRenderableWidget(animationListWidget)
|
||||
}
|
||||
}
|
||||
bringControlsToFront()
|
||||
}
|
||||
|
||||
private fun setImplementationMode(enabled: Boolean, force: Boolean = false) {
|
||||
if (!force && implementationMode == enabled) return
|
||||
implementationMode = enabled
|
||||
|
||||
if (implementationMode) {
|
||||
if (::searchWidget.isInitialized) removeWidget(searchWidget)
|
||||
if (::speciesList.isInitialized) removeWidget(speciesList)
|
||||
implementationTabIndex = IMPL_TAB_LOCATORS
|
||||
displayImplementationTab(implementationTabIndex, recreate = true)
|
||||
} else {
|
||||
if (::searchWidget.isInitialized) addRenderableWidget(searchWidget)
|
||||
if (::speciesList.isInitialized) addRenderableWidget(speciesList)
|
||||
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
|
||||
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
|
||||
displayTab(tabIndex, recreate = true)
|
||||
}
|
||||
|
||||
tabButtons.forEach { it.isVisible = !implementationMode }
|
||||
if (!implementationMode) {
|
||||
searchWidget.isFocused = false
|
||||
}
|
||||
bringControlsToFront()
|
||||
}
|
||||
|
||||
private fun bringControlsToFront() {
|
||||
if (::createButton.isInitialized) {
|
||||
removeWidget(createButton)
|
||||
addRenderableWidget(createButton)
|
||||
}
|
||||
if (::spawnButton.isInitialized) {
|
||||
removeWidget(spawnButton)
|
||||
addRenderableWidget(spawnButton)
|
||||
}
|
||||
if (::exitButton.isInitialized) {
|
||||
removeWidget(exitButton)
|
||||
addRenderableWidget(exitButton)
|
||||
}
|
||||
if (::implementationButton.isInitialized) {
|
||||
removeWidget(implementationButton)
|
||||
addRenderableWidget(implementationButton)
|
||||
}
|
||||
if (tabButtons.isEmpty()) return
|
||||
tabButtons.forEach { removeWidget(it) }
|
||||
tabButtons.forEach(::addRenderableWidget)
|
||||
}
|
||||
|
||||
private fun sendPokeEditAction(action: PokeEditAction) {
|
||||
val properties = buildDraftProperties()
|
||||
val moveNames = draft.activeMoves.map { it?.name ?: "" }
|
||||
val cosmeticItem = draft.cosmeticItem?.trim().orEmpty()
|
||||
PokeEditActionPacket(action, properties.asString(), moveNames, cosmeticItem).sendToServer()
|
||||
Minecraft.getInstance().setScreen(null)
|
||||
}
|
||||
|
||||
private fun updateLocatorList(locators: Collection<String>) {
|
||||
if (::locatorListWidget.isInitialized) {
|
||||
locatorListWidget.setLocators(locators)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDraftProperties(): PokemonProperties {
|
||||
val props = PokemonProperties()
|
||||
props.species = draft.species.resourceIdentifier.toString()
|
||||
props.form = draft.form.formOnlyShowdownId()
|
||||
props.shiny = draft.shiny
|
||||
props.gender = draft.gender
|
||||
props.level = draft.level
|
||||
val ot = draft.originalTrainer.trim()
|
||||
if (ot.isBlank()) {
|
||||
props.originalTrainerType = OriginalTrainerType.NONE
|
||||
} else {
|
||||
props.originalTrainerType = OriginalTrainerType.NPC
|
||||
props.originalTrainer = ot
|
||||
}
|
||||
props.heldItem = draft.heldItem?.takeIf { it.isNotBlank() }
|
||||
props.ivs = draft.ivs
|
||||
props.evs = draft.evs
|
||||
val abilities = draft.form.abilities.map { it.template }
|
||||
props.ability = abilities.getOrNull(draft.abilityIndex)?.name
|
||||
val customProperties = mutableListOf<com.cobblemon.mod.common.api.properties.CustomPokemonProperty>()
|
||||
draft.forcedAspects.mapNotNullTo(customProperties) { aspect -> AspectPropertyType.fromString(aspect) }
|
||||
draft.flagFeatures.forEach { key ->
|
||||
val provider = SpeciesFeatures.getFeature(key) as? FlagSpeciesFeatureProvider ?: return@forEach
|
||||
provider.fromString("true")?.let { customProperties.add(it) }
|
||||
}
|
||||
draft.choiceFeatures.forEach { (key, choice) ->
|
||||
val provider = SpeciesFeatures.getFeature(key) as? ChoiceSpeciesFeatureProvider ?: return@forEach
|
||||
provider.fromString(choice)?.let { customProperties.add(it) }
|
||||
}
|
||||
props.customProperties = customProperties
|
||||
props.updateAspects()
|
||||
return props
|
||||
}
|
||||
|
||||
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
|
||||
if (focused !is EditBox && (minecraft?.options?.keyInventory?.matches(keyCode, scanCode) == true)) {
|
||||
onClose()
|
||||
return true
|
||||
}
|
||||
return super.keyPressed(keyCode, scanCode, modifiers)
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
return try {
|
||||
super.mouseClicked(mouseX, mouseY, button)
|
||||
} catch (_: ConcurrentModificationException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
|
||||
if (implementationMode) {
|
||||
if (implementationTabIndex == IMPL_TAB_LOCATORS && ::locatorListWidget.isInitialized) {
|
||||
if (locatorListWidget.mouseScrolled(mouseX, mouseY, amount, verticalAmount)) return true
|
||||
}
|
||||
if (implementationTabIndex == IMPL_TAB_ANIMATION && ::animationListWidget.isInitialized) {
|
||||
if (animationListWidget.mouseScrolled(mouseX, mouseY, amount, verticalAmount)) return true
|
||||
}
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, amount, verticalAmount)
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.cobblemon.mod.common.api.abilities.AbilityTemplate
|
||||
import com.cobblemon.mod.common.api.text.bold
|
||||
import com.cobblemon.mod.common.client.CobblemonResources
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.widgets.InfoTextScrollWidget
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.util.asTranslated
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.network.chat.Component
|
||||
|
||||
class PokeEditAbilityTab(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
private val draft: PokeEditDraft
|
||||
) : SoundlessWidget(pX, pY, 139, 163, lang("ui.pokeedit.abilities")) {
|
||||
companion object {
|
||||
private val arrowLeft = cobblemonResource("textures/gui/pokedex/info_arrow_left.png")
|
||||
private val arrowRight = cobblemonResource("textures/gui/pokedex/info_arrow_right.png")
|
||||
}
|
||||
|
||||
private val description = object : InfoTextScrollWidget(pX, pY + 60) {}
|
||||
|
||||
private val leftButton = ScaledButton(
|
||||
pX + 2.5F,
|
||||
pY + 30F,
|
||||
7,
|
||||
10,
|
||||
arrowLeft,
|
||||
clickAction = { switchAbility(false) }
|
||||
).also { addWidget(it) }
|
||||
|
||||
private val rightButton = ScaledButton(
|
||||
pX + 133F,
|
||||
pY + 30F,
|
||||
7,
|
||||
10,
|
||||
arrowRight,
|
||||
clickAction = { switchAbility(true) }
|
||||
).also { addWidget(it) }
|
||||
|
||||
private var abilitiesList: List<AbilityTemplate> = emptyList()
|
||||
|
||||
init {
|
||||
refreshAbilities()
|
||||
}
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
refreshAbilities()
|
||||
if (abilitiesList.isEmpty()) return
|
||||
|
||||
val ability = abilitiesList[draft.abilityIndex.coerceIn(0, abilitiesList.lastIndex)]
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
font = CobblemonResources.DEFAULT_LARGE,
|
||||
text = Component.translatable("cobblemon.ui.pokedex.info.ability", ability.displayName.asTranslated()).bold(),
|
||||
x = x + 9,
|
||||
y = y + 30,
|
||||
shadow = true
|
||||
)
|
||||
|
||||
description.setText(listOf(ability.description))
|
||||
description.render(context, mouseX, mouseY, delta)
|
||||
|
||||
leftButton.render(context, mouseX, mouseY, delta)
|
||||
rightButton.render(context, mouseX, mouseY, delta)
|
||||
}
|
||||
|
||||
private fun refreshAbilities() {
|
||||
val form = draft.form
|
||||
abilitiesList = form.abilities.map { it.template }
|
||||
if (draft.abilityIndex !in abilitiesList.indices) {
|
||||
draft.abilityIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchAbility(nextIndex: Boolean) {
|
||||
if (abilitiesList.isEmpty()) return
|
||||
val size = abilitiesList.size
|
||||
draft.abilityIndex = if (nextIndex) {
|
||||
(draft.abilityIndex + 1) % size
|
||||
} else {
|
||||
(draft.abilityIndex - 1 + size) % size
|
||||
}
|
||||
}
|
||||
}
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.cobblemon.mod.common.api.text.text
|
||||
import com.cobblemon.mod.common.client.gui.ScrollingWidget
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.util.Mth
|
||||
import java.util.Locale
|
||||
|
||||
class PokeEditAnimationListWidget(
|
||||
val pX: Int,
|
||||
val pY: Int,
|
||||
val listWidth: Int,
|
||||
val listHeight: Int,
|
||||
private val isPlaying: (String) -> Boolean,
|
||||
private val isLooping: (String) -> Boolean,
|
||||
private val getDuration: (String) -> Float,
|
||||
private val getProgress: (String) -> Float,
|
||||
private val onPlay: (String) -> Unit,
|
||||
private val onPause: (String) -> Unit,
|
||||
private val onToggleLoop: (String, Boolean) -> Unit
|
||||
) : ScrollingWidget<PokeEditAnimationListWidget.AnimationRow>(
|
||||
left = pX,
|
||||
top = pY - listHeight,
|
||||
width = listWidth,
|
||||
height = listHeight,
|
||||
slotHeight = ROW_HEIGHT
|
||||
) {
|
||||
companion object {
|
||||
private const val ROW_HEIGHT = 12
|
||||
private const val TEXT_SCALE = 0.5F
|
||||
private const val BORDER_COLOR = 0xFF1B1B1B.toInt()
|
||||
private const val FILL_COLOR = 0xFF3B3B3B.toInt()
|
||||
private const val TEXT_COLOR = 0xFFFCFCFC.toInt()
|
||||
private const val BUTTON_WIDTH = 12
|
||||
private const val BUTTON_HEIGHT = 9
|
||||
private const val BUTTON_GAP = 3
|
||||
private const val BUTTON_PAD_RIGHT = 4
|
||||
private const val BUTTON_PAD_TOP = 1
|
||||
private const val BUTTON_ACTIVE = 0xFF6BD46B.toInt()
|
||||
private const val BUTTON_INACTIVE = 0xFF5B5B5B.toInt()
|
||||
private const val BUTTON_BORDER = 0xFF1B1B1B.toInt()
|
||||
private const val LOOP_BOX_SIZE = 8
|
||||
private const val LOOP_GAP = 3
|
||||
private const val LOOP_LABEL_SCALE = 0.45F
|
||||
private const val PROGRESS_HEIGHT = 2
|
||||
private const val PROGRESS_BG = 0xFF2C2C2C.toInt()
|
||||
private const val PROGRESS_FG = 0xFF8AD5FF.toInt()
|
||||
private const val DURATION_SCALE = 0.45F
|
||||
private const val DURATION_WIDTH = 28
|
||||
}
|
||||
|
||||
private val listTop = pY - listHeight
|
||||
private val listBottom = pY
|
||||
private var draggingScrollbar = false
|
||||
|
||||
fun setAnimations(animations: Collection<String>) {
|
||||
clearEntries()
|
||||
animations.sorted().forEach { addEntry(AnimationRow(it)) }
|
||||
scrollAmount = 0.0
|
||||
}
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
context.fill(pX - 1, listTop - 1, pX + listWidth + 1, listBottom + 1, BORDER_COLOR)
|
||||
context.fill(pX, listTop, pX + listWidth, listBottom, FILL_COLOR)
|
||||
|
||||
focused = if (mouseX in pX..(pX + listWidth) && mouseY in listTop..listBottom) {
|
||||
getEntryAtPosition(mouseX.toDouble(), mouseY.toDouble())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
context.enableScissor(pX, listTop, pX + listWidth, listBottom)
|
||||
renderListItems(context, mouseX, mouseY, delta)
|
||||
context.disableScissor()
|
||||
|
||||
if (maxScroll > 0) {
|
||||
renderScrollbar(context, mouseX, mouseY, delta)
|
||||
}
|
||||
}
|
||||
|
||||
override fun renderListItems(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val rowX = rowLeft
|
||||
for (index in 0 until itemCount) {
|
||||
val rowY = getRowTop(index)
|
||||
val rowBottom = getRowBottom(index)
|
||||
if (rowBottom >= listTop && rowY <= listBottom) {
|
||||
val entry = getEntry(index)
|
||||
if (entry is AnimationRow) {
|
||||
entry.isPlaying = isPlaying(entry.name)
|
||||
entry.isLooping = isLooping(entry.name)
|
||||
entry.durationSeconds = getDuration(entry.name)
|
||||
entry.progress = getProgress(entry.name)
|
||||
}
|
||||
renderItem(context, mouseX, mouseY, delta, index, rowX, rowY, rowWidth, itemHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
|
||||
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|
||||
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
val maxScroll = (itemCount * itemHeight - listHeight).coerceAtLeast(0)
|
||||
if (maxScroll <= 0) return true
|
||||
val step = if (verticalAmount != 0.0) verticalAmount else amount
|
||||
scrollAmount = Mth.clamp(scrollAmount - step * itemHeight, 0.0, maxScroll.toDouble())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|
||||
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
|
||||
|| button != 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (isOverScrollbar(mouseX, mouseY) && maxScroll > 0) {
|
||||
draggingScrollbar = true
|
||||
return true
|
||||
}
|
||||
|
||||
val localY = mouseY.toInt() - listTop + scrollAmount.toInt()
|
||||
if (localY < 0) return false
|
||||
val index = localY / itemHeight
|
||||
if (index !in 0 until itemCount) return false
|
||||
val entry = getEntry(index)
|
||||
if (entry is AnimationRow) {
|
||||
val rowX = rowLeft
|
||||
val rowY = getRowTop(index)
|
||||
val (playX, pauseX, loopX, buttonY, durationX) = buttonPositions(rowX, rowY)
|
||||
val inPlay = mouseX.toInt() in playX..(playX + BUTTON_WIDTH)
|
||||
&& mouseY.toInt() in buttonY..(buttonY + BUTTON_HEIGHT)
|
||||
val inPause = mouseX.toInt() in pauseX..(pauseX + BUTTON_WIDTH)
|
||||
&& mouseY.toInt() in buttonY..(buttonY + BUTTON_HEIGHT)
|
||||
val inLoop = mouseX.toInt() in loopX..(loopX + LOOP_BOX_SIZE)
|
||||
&& mouseY.toInt() in buttonY..(buttonY + LOOP_BOX_SIZE)
|
||||
if (inPlay) {
|
||||
onPlay(entry.name)
|
||||
return true
|
||||
}
|
||||
if (inPause) {
|
||||
onPause(entry.name)
|
||||
return true
|
||||
}
|
||||
if (inLoop) {
|
||||
val newValue = !isLooping(entry.name)
|
||||
onToggleLoop(entry.name, newValue)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (button == 0 && draggingScrollbar) {
|
||||
draggingScrollbar = false
|
||||
return true
|
||||
}
|
||||
return super.mouseReleased(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
|
||||
if (!draggingScrollbar || maxScroll <= 0) {
|
||||
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
val barHeight = listBottom - listTop
|
||||
var thumbHeight = ((barHeight * barHeight).toFloat() / maxPosition.toFloat()).toInt()
|
||||
thumbHeight = Mth.clamp(thumbHeight, 16, barHeight - 4)
|
||||
val trackHeight = barHeight - thumbHeight
|
||||
if (trackHeight <= 0) return true
|
||||
val clampedY = Mth.clamp(mouseY.toInt() - listTop - thumbHeight / 2, 0, trackHeight)
|
||||
scrollAmount = (clampedY.toDouble() * maxScroll.toDouble()) / trackHeight.toDouble()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
|
||||
return mouseX in pX.toDouble()..(pX + listWidth).toDouble()
|
||||
&& mouseY in listTop.toDouble()..listBottom.toDouble()
|
||||
}
|
||||
|
||||
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val xLeft = this.scrollbarPosition
|
||||
val xRight = xLeft + 3
|
||||
val yStart = listTop
|
||||
val barHeight = listBottom - listTop
|
||||
|
||||
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
|
||||
yBottom = Mth.clamp(yBottom, 16, barHeight - 4)
|
||||
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
|
||||
if (yTop < yStart) yTop = yStart
|
||||
|
||||
context.fill(xLeft + 1, yStart, xRight - 1, listBottom, 0xFF7AEEF1.toInt())
|
||||
context.fill(xLeft, yTop, xRight, yTop + yBottom, 0xFF3A96B6.toInt())
|
||||
}
|
||||
|
||||
override fun getRowTop(index: Int): Int {
|
||||
return listTop - scrollAmount.toInt() + (index * itemHeight)
|
||||
}
|
||||
|
||||
override fun getRowBottom(index: Int): Int {
|
||||
return getRowTop(index) + itemHeight
|
||||
}
|
||||
|
||||
override fun getBottom(): Int {
|
||||
return listBottom
|
||||
}
|
||||
|
||||
private fun isOverScrollbar(mouseX: Double, mouseY: Double): Boolean {
|
||||
val xLeft = scrollbarPosition
|
||||
val xRight = xLeft + 3
|
||||
return mouseX >= xLeft && mouseX <= xRight && mouseY >= listTop && mouseY <= listBottom
|
||||
}
|
||||
|
||||
private fun buttonPositions(rowX: Int, rowY: Int): List<Int> {
|
||||
val loopX = rowX + listWidth - BUTTON_PAD_RIGHT - LOOP_BOX_SIZE
|
||||
val pauseX = loopX - LOOP_GAP - BUTTON_WIDTH
|
||||
val playX = pauseX - BUTTON_GAP - BUTTON_WIDTH
|
||||
val buttonY = rowY + BUTTON_PAD_TOP
|
||||
val durationX = playX - BUTTON_GAP - DURATION_WIDTH
|
||||
return listOf(playX, pauseX, loopX, buttonY, durationX)
|
||||
}
|
||||
|
||||
class AnimationRow(val name: String) : Slot<AnimationRow>() {
|
||||
var isPlaying: Boolean = false
|
||||
var isLooping: Boolean = false
|
||||
var durationSeconds: Float = 0F
|
||||
var progress: Float = 0F
|
||||
|
||||
override fun render(
|
||||
context: GuiGraphics,
|
||||
index: Int,
|
||||
y: Int,
|
||||
x: Int,
|
||||
entryWidth: Int,
|
||||
entryHeight: Int,
|
||||
mouseX: Int,
|
||||
mouseY: Int,
|
||||
hovered: Boolean,
|
||||
tickDelta: Float
|
||||
) {
|
||||
val bg = if (hovered) 0xFF2F2F2F.toInt() else null
|
||||
if (bg != null) {
|
||||
context.fill(x + 1, y, x + entryWidth - 1, y + ROW_HEIGHT, bg)
|
||||
}
|
||||
|
||||
val loopX = x + entryWidth - BUTTON_PAD_RIGHT - LOOP_BOX_SIZE
|
||||
val pauseX = loopX - LOOP_GAP - BUTTON_WIDTH
|
||||
val playX = pauseX - BUTTON_GAP - BUTTON_WIDTH
|
||||
val durationX = playX - BUTTON_GAP - DURATION_WIDTH
|
||||
val buttonY = y + BUTTON_PAD_TOP
|
||||
drawButton(context, playX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, !isPlaying)
|
||||
drawPlayIcon(context, playX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT)
|
||||
drawButton(context, pauseX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, isPlaying)
|
||||
drawPauseIcon(context, pauseX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT)
|
||||
drawLoopBox(context, loopX, buttonY, isLooping)
|
||||
drawDuration(context, durationX, buttonY, durationSeconds)
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = name.text(),
|
||||
x = x + 3,
|
||||
y = y + 2,
|
||||
scale = TEXT_SCALE,
|
||||
colour = TEXT_COLOR,
|
||||
shadow = false
|
||||
)
|
||||
|
||||
drawProgressBar(context, x + 2, y + ROW_HEIGHT - PROGRESS_HEIGHT, entryWidth - 4, progress)
|
||||
}
|
||||
|
||||
override fun getNarration(): Component {
|
||||
return Component.literal(name)
|
||||
}
|
||||
|
||||
private fun drawButton(
|
||||
context: GuiGraphics,
|
||||
x: Int,
|
||||
y: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
active: Boolean
|
||||
) {
|
||||
val fill = if (active) BUTTON_ACTIVE else BUTTON_INACTIVE
|
||||
context.fill(x - 1, y - 1, x + width + 1, y + height + 1, BUTTON_BORDER)
|
||||
context.fill(x, y, x + width, y + height, fill)
|
||||
}
|
||||
|
||||
private fun drawLoopBox(context: GuiGraphics, x: Int, y: Int, enabled: Boolean) {
|
||||
val fill = if (enabled) BUTTON_ACTIVE else BUTTON_INACTIVE
|
||||
context.fill(x - 1, y - 1, x + LOOP_BOX_SIZE + 1, y + LOOP_BOX_SIZE + 1, BUTTON_BORDER)
|
||||
context.fill(x, y, x + LOOP_BOX_SIZE, y + LOOP_BOX_SIZE, fill)
|
||||
drawLoopIcon(context, x, y, LOOP_BOX_SIZE)
|
||||
}
|
||||
|
||||
private fun drawPlayIcon(context: GuiGraphics, x: Int, y: Int, width: Int, height: Int) {
|
||||
val iconWidth = 6
|
||||
val iconHeight = 7
|
||||
val startX = x + (width - iconWidth) / 2
|
||||
val startY = y + (height - iconHeight) / 2
|
||||
for (i in 0 until iconWidth) {
|
||||
val top = startY + i / 2
|
||||
val bottom = startY + iconHeight - i / 2
|
||||
context.fill(startX + i, top, startX + i + 1, bottom, TEXT_COLOR)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawPauseIcon(context: GuiGraphics, x: Int, y: Int, width: Int, height: Int) {
|
||||
val barWidth = 2
|
||||
val barHeight = 7
|
||||
val gap = 2
|
||||
val startX = x + (width - (barWidth * 2 + gap)) / 2
|
||||
val startY = y + (height - barHeight) / 2
|
||||
context.fill(startX, startY, startX + barWidth, startY + barHeight, TEXT_COLOR)
|
||||
context.fill(startX + barWidth + gap, startY, startX + barWidth * 2 + gap, startY + barHeight, TEXT_COLOR)
|
||||
}
|
||||
|
||||
private fun drawLoopIcon(context: GuiGraphics, x: Int, y: Int, size: Int) {
|
||||
val startX = x + 1
|
||||
val startY = y + 1
|
||||
val endX = x + size - 1
|
||||
val endY = y + size - 1
|
||||
context.fill(startX, startY, endX, startY + 1, TEXT_COLOR)
|
||||
context.fill(startX, endY - 1, endX, endY, TEXT_COLOR)
|
||||
context.fill(startX, startY, startX + 1, endY, TEXT_COLOR)
|
||||
context.fill(endX - 1, startY, endX, endY, TEXT_COLOR)
|
||||
context.fill(endX - 2, startY, endX, startY + 2, TEXT_COLOR)
|
||||
}
|
||||
|
||||
private fun drawDuration(context: GuiGraphics, x: Int, y: Int, duration: Float) {
|
||||
val text = String.format(Locale.US, "%.2fs", duration.coerceAtLeast(0F))
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = text.text(),
|
||||
x = x + DURATION_WIDTH / 2,
|
||||
y = y + 2,
|
||||
scale = DURATION_SCALE,
|
||||
colour = TEXT_COLOR,
|
||||
maxCharacterWidth = DURATION_WIDTH,
|
||||
centered = true,
|
||||
shadow = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawProgressBar(context: GuiGraphics, x: Int, y: Int, width: Int, progress: Float) {
|
||||
val clamped = Mth.clamp(progress, 0F, 1F)
|
||||
context.fill(x, y, x + width, y + PROGRESS_HEIGHT, PROGRESS_BG)
|
||||
context.fill(x, y, x + (width * clamped).toInt(), y + PROGRESS_HEIGHT, PROGRESS_FG)
|
||||
}
|
||||
}
|
||||
}
|
||||
+496
@@ -0,0 +1,496 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
|
||||
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.animation.ActiveAnimation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PrimaryAnimation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.pokemon.RenderablePokemon
|
||||
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
|
||||
import com.cobblemon.mod.common.util.toHex
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import com.mojang.blaze3d.platform.Lighting
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.gui.components.AbstractWidget
|
||||
import net.minecraft.client.renderer.LightTexture
|
||||
import net.minecraft.client.renderer.RenderType
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector3f
|
||||
|
||||
class PokeEditAnimationTab(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
private val draft: PokeEditDraft,
|
||||
private val onAnimationsChanged: (List<String>) -> Unit
|
||||
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, Component.literal("Animation")) {
|
||||
companion object {
|
||||
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
|
||||
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
|
||||
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
|
||||
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
|
||||
private const val POKEBALL_OVERLAY_Y = 25
|
||||
private const val PLATFORM_Y = 69
|
||||
private const val SHADOW_X = 25
|
||||
private const val SHADOW_Y = 76
|
||||
private const val VIEW_SCALE = 1.2F
|
||||
private const val VIEW_SHIFT_X = 0F
|
||||
private const val VIEW_SHIFT_Y = 36F
|
||||
private const val MODEL_VIEW_SCALE = 2.4F
|
||||
private const val MODEL_DRAW_SCALE = 24F
|
||||
private const val MODEL_OFFSET_Y = 20.0
|
||||
}
|
||||
|
||||
private val state = FloatingState().also { state ->
|
||||
state.runtime.environment.query.addFunction("is_holding_item") {
|
||||
val held = currentHeldStack()
|
||||
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
|
||||
}
|
||||
state.runtime.environment.query.addFunction("is_wearing_hat") {
|
||||
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
|
||||
}
|
||||
state.runtime.environment.query.addFunction("is_wearing_face") {
|
||||
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
|
||||
}
|
||||
}
|
||||
private val heldItemRenderer = HeldItemRenderer()
|
||||
private var renderablePokemon: RenderablePokemon? = null
|
||||
private var rotationY = 30F
|
||||
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE, PoseType.STAND, PoseType.WALK, PoseType.SLEEP)
|
||||
private var selectedPoseIndex: Int = 0
|
||||
private var oldDragPosX = 0.0
|
||||
private var canDragRender = false
|
||||
private var isDraggingRender = false
|
||||
|
||||
private var availableAnimations: List<String> = emptyList()
|
||||
private val activeAnimationsByName = mutableMapOf<String, ActiveAnimation>()
|
||||
private val loopingAnimations = mutableMapOf<String, Boolean>()
|
||||
private val animationStartTimes = mutableMapOf<String, Float>()
|
||||
private val animationDurations = mutableMapOf<String, Float>()
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
|
||||
|
||||
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
|
||||
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
|
||||
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
|
||||
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
|
||||
val scaledBaseX = viewX / VIEW_SCALE
|
||||
val scaledBaseY = viewY / VIEW_SCALE
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = backgroundOverlay,
|
||||
x = scaledBaseX,
|
||||
y = scaledBaseY,
|
||||
width = HALF_OVERLAY_WIDTH,
|
||||
height = HALF_OVERLAY_HEIGHT,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = pokeBallOverlay,
|
||||
x = scaledBaseX + 15,
|
||||
y = scaledBaseY + POKEBALL_OVERLAY_Y,
|
||||
width = 109,
|
||||
height = 68,
|
||||
vOffset = 0,
|
||||
textureHeight = 1744,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = platformBase,
|
||||
x = scaledBaseX + 13,
|
||||
y = scaledBaseY + PLATFORM_Y,
|
||||
width = 113,
|
||||
height = 24,
|
||||
textureHeight = 30,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = platformShadow,
|
||||
x = scaledBaseX + SHADOW_X,
|
||||
y = scaledBaseY + SHADOW_Y,
|
||||
width = 90,
|
||||
height = 20,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
|
||||
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
|
||||
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
|
||||
|
||||
context.disableScissor()
|
||||
|
||||
renderPokemon(context, delta)
|
||||
refreshAnimationList()
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (isWithinViewport(mouseX, mouseY)) {
|
||||
canDragRender = true
|
||||
isDraggingRender = true
|
||||
oldDragPosX = mouseX
|
||||
return true
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
|
||||
if (canDragRender) {
|
||||
canDragRender = false
|
||||
}
|
||||
if (isDraggingRender) {
|
||||
isDraggingRender = false
|
||||
}
|
||||
return super.mouseReleased(pMouseX, pMouseY, pButton)
|
||||
}
|
||||
|
||||
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
|
||||
val handledDrag = isDraggingRender && canDragRender
|
||||
if (handledDrag) {
|
||||
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
|
||||
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
|
||||
}
|
||||
oldDragPosX = mouseX
|
||||
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
|
||||
fun getAnimations(): List<String> = availableAnimations
|
||||
|
||||
fun isAnimationPlaying(name: String): Boolean = activeAnimationsByName.containsKey(name)
|
||||
|
||||
fun isAnimationLooping(name: String): Boolean = loopingAnimations[name] == true
|
||||
|
||||
fun getAnimationDuration(name: String): Float {
|
||||
return animationDurations[name] ?: resolveDuration(name).also { animationDurations[name] = it }
|
||||
}
|
||||
|
||||
fun getAnimationProgress(name: String): Float {
|
||||
val duration = getAnimationDuration(name)
|
||||
if (duration <= 0F) return 0F
|
||||
val start = animationStartTimes[name] ?: return 0F
|
||||
val elapsed = state.animationSeconds - start
|
||||
val raw = if (isAnimationLooping(name)) {
|
||||
(elapsed % duration) / duration
|
||||
} else {
|
||||
elapsed / duration
|
||||
}
|
||||
return raw.coerceIn(0F, 1F)
|
||||
}
|
||||
|
||||
fun playAnimation(name: String) {
|
||||
if (activeAnimationsByName.containsKey(name)) return
|
||||
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
|
||||
val animation = model.getAnimation(state, name, state.runtime)
|
||||
val active = if (animation != null) {
|
||||
if (animation is PrimaryAnimation) animation.animation else animation
|
||||
} else {
|
||||
val pose = resolvePoseAnimation(model, name) ?: return
|
||||
PoseActiveAnimation(pose)
|
||||
}
|
||||
state.addActiveAnimation(active)
|
||||
activeAnimationsByName[name] = active
|
||||
animationStartTimes[name] = state.animationSeconds
|
||||
animationDurations[name] = active.duration
|
||||
}
|
||||
|
||||
fun pauseAnimation(name: String) {
|
||||
val active = activeAnimationsByName.remove(name) ?: return
|
||||
state.activeAnimations.remove(active)
|
||||
animationStartTimes.remove(name)
|
||||
}
|
||||
|
||||
fun setAnimationLooping(name: String, enabled: Boolean) {
|
||||
loopingAnimations[name] = enabled
|
||||
}
|
||||
|
||||
private fun refreshAnimationList() {
|
||||
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
|
||||
val names = mutableSetOf<String>()
|
||||
names.addAll(model.animations.keys)
|
||||
model.poses.values.forEach { pose ->
|
||||
names.addAll(pose.namedAnimations.keys)
|
||||
names.add(pose.poseName)
|
||||
}
|
||||
val sorted = names.sorted()
|
||||
if (sorted != availableAnimations) {
|
||||
availableAnimations = sorted
|
||||
loopingAnimations.keys.retainAll(sorted.toSet())
|
||||
animationDurations.keys.retainAll(sorted.toSet())
|
||||
animationStartTimes.keys.retainAll(sorted.toSet())
|
||||
onAnimationsChanged(sorted)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderPokemon(context: GuiGraphics, delta: Float) {
|
||||
buildRenderable()?.let { renderable ->
|
||||
state.currentAspects = renderable.aspects
|
||||
context.enableScissor(
|
||||
x + 1,
|
||||
y + 1,
|
||||
x + HALF_OVERLAY_WIDTH - 1,
|
||||
y + HALF_OVERLAY_HEIGHT - 1
|
||||
)
|
||||
|
||||
val matrices = context.pose()
|
||||
matrices.pushPose()
|
||||
matrices.translate(
|
||||
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
|
||||
y.toDouble() + MODEL_OFFSET_Y,
|
||||
1000.0
|
||||
)
|
||||
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
|
||||
|
||||
renderAnimationModel(context, renderable, delta)
|
||||
|
||||
if (!renderable.heldItem.isEmpty) {
|
||||
heldItemRenderer.renderOnModel(
|
||||
renderable.heldItem,
|
||||
state,
|
||||
matrices,
|
||||
context.bufferSource(),
|
||||
light = 0xF000F0,
|
||||
frontLight = true
|
||||
)
|
||||
}
|
||||
matrices.popPose()
|
||||
context.disableScissor()
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderAnimationModel(context: GuiGraphics, renderable: RenderablePokemon, delta: Float) {
|
||||
val speciesId = renderable.species.resourceIdentifier
|
||||
val model = VaryingModelRepository.getPoser(speciesId, state)
|
||||
val texture = VaryingModelRepository.getTexture(speciesId, state)
|
||||
|
||||
val renderContext = RenderContext()
|
||||
model.context = renderContext
|
||||
VaryingModelRepository.getTextureNoSubstitute(speciesId, state).let { renderContext.put(RenderContext.TEXTURE, it) }
|
||||
val baseScale = PokemonSpecies.getByIdentifier(speciesId)!!.getForm(state.currentAspects).baseScale
|
||||
renderContext.put(RenderContext.SCALE, baseScale)
|
||||
renderContext.put(RenderContext.SPECIES, speciesId)
|
||||
renderContext.put(RenderContext.ASPECTS, state.currentAspects)
|
||||
renderContext.put(RenderContext.RENDER_STATE, RenderContext.RenderState.PROFILE)
|
||||
renderContext.put(RenderContext.POSABLE_STATE, state)
|
||||
renderContext.put(RenderContext.DO_QUIRKS, false)
|
||||
|
||||
state.currentModel = model
|
||||
if (state.currentPose == null) {
|
||||
state.currentPose = model.getPose(PoseType.PROFILE)?.poseName
|
||||
}
|
||||
state.updatePartialTicks(delta)
|
||||
|
||||
RenderSystem.applyModelViewMatrix()
|
||||
val matrices = context.pose()
|
||||
val rotationVector = Vector3f(13F, rotationY, 0F)
|
||||
val rotation = Quaternionf().fromEulerXYZDegrees(rotationVector)
|
||||
|
||||
matrices.pushPose()
|
||||
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
|
||||
matrices.translate(
|
||||
model.profileTranslation.x,
|
||||
model.profileTranslation.y + 1.5 * model.profileScale,
|
||||
model.profileTranslation.z - 4.0
|
||||
)
|
||||
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
|
||||
matrices.mulPose(rotation)
|
||||
|
||||
model.setDefault()
|
||||
state.preRender()
|
||||
state.renderMarkers.clear()
|
||||
runActiveAnimations(renderContext, model)
|
||||
model.updateLocators(null, state)
|
||||
|
||||
Lighting.setupForEntityInInventory()
|
||||
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
|
||||
val buffer = bufferSource.getBuffer(RenderType.entityCutout(texture))
|
||||
val light1 = Vector3f(-1F, 1F, 1.0F)
|
||||
val light2 = Vector3f(1.3F, -1F, 1.0F)
|
||||
RenderSystem.setShaderLights(light1, light2)
|
||||
val packedLight = LightTexture.pack(11, 7)
|
||||
|
||||
val colour = toHex(1F, 1F, 1F, 1F)
|
||||
model.withLayerContext(bufferSource, state, VaryingModelRepository.getLayers(speciesId, state)) {
|
||||
model.render(renderContext, matrices, buffer, packedLight, OverlayTexture.NO_OVERLAY, colour)
|
||||
bufferSource.endBatch()
|
||||
}
|
||||
model.setDefault()
|
||||
Lighting.setupFor3DItems()
|
||||
matrices.popPose()
|
||||
}
|
||||
|
||||
private fun runActiveAnimations(context: RenderContext, model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel) {
|
||||
val toRestart = mutableListOf<String>()
|
||||
val iterator = activeAnimationsByName.entries.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val entry = iterator.next()
|
||||
val animation = entry.value
|
||||
val stillRunning = animation.run(
|
||||
context,
|
||||
model,
|
||||
state,
|
||||
0F,
|
||||
0F,
|
||||
state.animationSeconds,
|
||||
0F,
|
||||
0F,
|
||||
1F
|
||||
)
|
||||
if (!stillRunning) {
|
||||
state.activeAnimations.remove(animation)
|
||||
iterator.remove()
|
||||
animationStartTimes.remove(entry.key)
|
||||
if (loopingAnimations[entry.key] == true) {
|
||||
toRestart.add(entry.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toRestart.isNotEmpty()) {
|
||||
toRestart.forEach { name ->
|
||||
val animation = model.getAnimation(state, name, state.runtime)
|
||||
val active = if (animation != null) {
|
||||
if (animation is PrimaryAnimation) animation.animation else animation
|
||||
} else {
|
||||
val pose = resolvePoseAnimation(model, name) ?: return@forEach
|
||||
PoseActiveAnimation(pose)
|
||||
}
|
||||
state.addActiveAnimation(active)
|
||||
activeAnimationsByName[name] = active
|
||||
animationStartTimes[name] = state.animationSeconds
|
||||
animationDurations[name] = active.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolvePoseAnimation(
|
||||
model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel,
|
||||
name: String
|
||||
): Pose? {
|
||||
return model.poses[name] ?: model.poses.values.firstOrNull { it.poseName.equals(name, ignoreCase = true) }
|
||||
}
|
||||
|
||||
private fun resolveDuration(name: String): Float {
|
||||
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
|
||||
val animation = model.getAnimation(state, name, state.runtime)
|
||||
val active = if (animation != null) {
|
||||
if (animation is PrimaryAnimation) animation.animation else animation
|
||||
} else {
|
||||
return 0F
|
||||
}
|
||||
return active.duration
|
||||
}
|
||||
|
||||
private class PoseActiveAnimation(private val pose: Pose) : ActiveAnimation {
|
||||
override val isTransition: Boolean = false
|
||||
override val duration: Float = 0F
|
||||
override val enduresPrimaryAnimations: Boolean = true
|
||||
|
||||
override fun run(
|
||||
context: RenderContext,
|
||||
model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel,
|
||||
state: com.cobblemon.mod.common.client.render.models.blockbench.PosableState,
|
||||
limbSwing: Float,
|
||||
limbSwingAmount: Float,
|
||||
ageInTicks: Float,
|
||||
headYaw: Float,
|
||||
headPitch: Float,
|
||||
intensity: Float
|
||||
): Boolean {
|
||||
if (!pose.isSuitable(state)) return true
|
||||
pose.apply(context, model, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRenderable(): RenderablePokemon? {
|
||||
val species = draft.species
|
||||
val form = draft.form
|
||||
val aspects = mutableSetOf<String>()
|
||||
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
|
||||
aspects.addAll(form.aspects)
|
||||
aspects.addAll(draft.forcedAspects)
|
||||
aspects.addAll(draft.flagFeatures)
|
||||
aspects.addAll(draft.cosmeticAspects)
|
||||
draft.choiceFeatures.forEach { (key, choice) ->
|
||||
choiceAspectId(key, choice)?.let { aspects.add(it) }
|
||||
}
|
||||
when (draft.gender) {
|
||||
Gender.MALE -> aspects.add("male")
|
||||
Gender.FEMALE -> aspects.add("female")
|
||||
Gender.GENDERLESS -> aspects.add("genderless")
|
||||
}
|
||||
val heldStack = draft.heldItem
|
||||
?.let { ResourceLocation.tryParse(it) }
|
||||
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
|
||||
?.let { item -> ItemStack(item) }
|
||||
?: ItemStack.EMPTY
|
||||
|
||||
val prev = renderablePokemon
|
||||
if (prev == null || prev.species != species || prev.aspects != aspects) {
|
||||
activeAnimationsByName.clear()
|
||||
state.activeAnimations.clear()
|
||||
}
|
||||
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
|
||||
return renderablePokemon
|
||||
}
|
||||
|
||||
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
|
||||
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
|
||||
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
|
||||
poseList = poser.poses
|
||||
.map { it.value.poseTypes.minBy { it.ordinal } }
|
||||
.toSet()
|
||||
.filterNot { it in PoseType.SHOULDER_POSES }
|
||||
.sortedBy { it.ordinal }
|
||||
.toTypedArray()
|
||||
if (selectedPoseIndex !in poseList.indices) {
|
||||
selectedPoseIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentHeldStack(): ItemStack {
|
||||
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
|
||||
}
|
||||
|
||||
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
|
||||
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
|
||||
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
|
||||
&& children.none { child ->
|
||||
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
|
||||
}
|
||||
}
|
||||
+620
@@ -0,0 +1,620 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonCosmeticItems
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeIdentifierCondition
|
||||
import com.cobblemon.mod.common.api.conditional.RegistryLikeTagCondition
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.FlagSpeciesFeatureProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
|
||||
import com.cobblemon.mod.common.api.text.bold
|
||||
import com.cobblemon.mod.common.client.CobblemonResources
|
||||
import com.cobblemon.mod.common.client.gui.ScrollingWidget
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.pokemon.FormData
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.pokemon.Species
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.cobblemon.mod.common.util.substitute
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.resources.language.I18n
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.tags.TagKey
|
||||
import net.minecraft.util.Mth
|
||||
|
||||
class PokeEditAspectsTab(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
private val draft: PokeEditDraft
|
||||
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.aspects")) {
|
||||
companion object {
|
||||
private const val WIDTH = 139
|
||||
private const val HEIGHT = 163
|
||||
private const val SLOT_HEIGHT = 16
|
||||
private const val SCALE = 0.7F
|
||||
private const val TOP_MARGIN = 10
|
||||
private const val BOTTOM_MARGIN = 10
|
||||
private const val COLUMN_GAP = 6
|
||||
|
||||
data class ChoiceGroup(
|
||||
val key: String,
|
||||
val choices: List<String>,
|
||||
val aspectFormat: String
|
||||
)
|
||||
|
||||
data class CosmeticChoice(
|
||||
val itemId: ResourceLocation,
|
||||
val aspects: List<String>,
|
||||
val label: MutableComponent
|
||||
)
|
||||
|
||||
data class AspectOptions(
|
||||
val toggles: Set<String>,
|
||||
val choiceGroups: List<ChoiceGroup>,
|
||||
val flagKeys: Set<String>,
|
||||
val cosmeticChoices: List<CosmeticChoice>
|
||||
)
|
||||
|
||||
fun collectAspectOptions(
|
||||
species: Species,
|
||||
form: FormData? = null,
|
||||
gender: Gender? = null,
|
||||
shiny: Boolean? = null
|
||||
): AspectOptions {
|
||||
val toggles = mutableSetOf<String>()
|
||||
val choiceGroups = mutableListOf<ChoiceGroup>()
|
||||
val flagKeys = mutableSetOf<String>()
|
||||
|
||||
val forms = if (species.forms.isEmpty()) listOf(species.standardForm) else species.forms
|
||||
forms.forEach { form -> toggles.addAll(form.aspects) }
|
||||
|
||||
SpeciesFeatures.getFeaturesFor(species).forEach { provider ->
|
||||
when (provider) {
|
||||
is ChoiceSpeciesFeatureProvider -> {
|
||||
if (provider.isAspect && provider.keys.isNotEmpty() && provider.choices.isNotEmpty()) {
|
||||
choiceGroups.add(ChoiceGroup(provider.keys.first(), provider.choices, provider.aspectFormat))
|
||||
}
|
||||
}
|
||||
is FlagSpeciesFeatureProvider -> {
|
||||
if (provider.isAspect && provider.keys.isNotEmpty()) {
|
||||
flagKeys.add(provider.keys.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggles.removeAll(flagKeys)
|
||||
|
||||
val filteredChoiceGroups = choiceGroups
|
||||
.filterNot { it.key in setOf("cosmetic", "cosmetic_item", "cosmeticitem") }
|
||||
.sortedBy { it.key }
|
||||
|
||||
return AspectOptions(
|
||||
toggles = toggles.filter { it.isNotBlank() && it != "cosmetic" && it != "cosmetic_item" && it != "cosmeticitem" }.toSet(),
|
||||
choiceGroups = filteredChoiceGroups,
|
||||
flagKeys = flagKeys.filter { it.isNotBlank() }.toSet(),
|
||||
cosmeticChoices = collectCosmeticChoices(species, form, gender, shiny)
|
||||
)
|
||||
}
|
||||
|
||||
fun collectCosmeticChoices(
|
||||
species: Species,
|
||||
form: FormData?,
|
||||
gender: Gender?,
|
||||
shiny: Boolean?
|
||||
): List<CosmeticChoice> {
|
||||
val pokemon = Pokemon().also {
|
||||
it.isClient = true
|
||||
it.species = species
|
||||
it.form = form ?: species.standardForm
|
||||
if (gender != null) it.gender = gender
|
||||
if (shiny != null) it.shiny = shiny
|
||||
}
|
||||
|
||||
val registry = BuiltInRegistries.ITEM
|
||||
val choices = linkedMapOf<ResourceLocation, CosmeticChoice>()
|
||||
|
||||
CobblemonCosmeticItems.findValidForPokemon(pokemon).forEach { assignment ->
|
||||
assignment.cosmeticItems.forEach { cosmetic ->
|
||||
val aspects = cosmetic.aspects.filter { it.isNotBlank() }
|
||||
when (val condition = cosmetic.consumedItem) {
|
||||
is RegistryLikeIdentifierCondition<*> -> {
|
||||
val id = condition.identifier
|
||||
val label = registry.getOptional(id)
|
||||
.map { it.defaultInstance.hoverName.copy() }
|
||||
.orElseGet { Component.literal(id.toString()) }
|
||||
choices.putIfAbsent(id, CosmeticChoice(id, aspects, label))
|
||||
}
|
||||
is RegistryLikeTagCondition<*> -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val tag = condition.tag as? TagKey<net.minecraft.world.item.Item> ?: return@forEach
|
||||
val tagItems = registry.getTag(tag).orElse(null) ?: return@forEach
|
||||
tagItems.forEach { holder ->
|
||||
val item = holder.value()
|
||||
val id = registry.getKey(item) ?: return@forEach
|
||||
val label = item.defaultInstance.hoverName.copy()
|
||||
choices.putIfAbsent(id, CosmeticChoice(id, aspects, label))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return choices.values.sortedBy { it.label.string.lowercase() }
|
||||
}
|
||||
|
||||
fun choiceAspectId(key: String, choice: String): String? {
|
||||
val provider = SpeciesFeatures.getFeature(key) as? ChoiceSpeciesFeatureProvider ?: return null
|
||||
return provider.aspectFormat.substitute("choice", choice)
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class AspectItem {
|
||||
data class Group(val group: ChoiceGroup) : AspectItem()
|
||||
data class Toggle(val id: String, val source: ToggleSource) : AspectItem()
|
||||
data class Cosmetic(val choices: List<CosmeticChoice>) : AspectItem()
|
||||
}
|
||||
|
||||
private sealed class OptionItem {
|
||||
data class Choice(val group: ChoiceGroup, val choice: String) : OptionItem()
|
||||
data class ClearChoice(val group: ChoiceGroup) : OptionItem()
|
||||
data class Toggle(val aspect: AspectItem.Toggle, val enabled: Boolean) : OptionItem()
|
||||
data class CosmeticChoice(val choice: PokeEditAspectsTab.Companion.CosmeticChoice) : OptionItem()
|
||||
object ClearCosmetic : OptionItem()
|
||||
}
|
||||
|
||||
private enum class ToggleSource {
|
||||
FORCED_ASPECT,
|
||||
FLAG_FEATURE
|
||||
}
|
||||
|
||||
private val listX = x + 4
|
||||
private val listBottom = y + HEIGHT - BOTTOM_MARGIN
|
||||
private val listAreaWidth = WIDTH - 8
|
||||
private val listTop = y + TOP_MARGIN
|
||||
private val listHeight = listBottom - listTop
|
||||
private val leftWidth = (listAreaWidth - COLUMN_GAP) / 2
|
||||
private val rightWidth = listAreaWidth - leftWidth - COLUMN_GAP
|
||||
private val leftX = listX
|
||||
private val rightX = listX + leftWidth + COLUMN_GAP
|
||||
|
||||
private val aspectList = AspectList(
|
||||
leftX,
|
||||
listTop,
|
||||
leftWidth,
|
||||
listHeight
|
||||
).also { addWidget(it) }
|
||||
|
||||
private val optionList = OptionList(
|
||||
rightX,
|
||||
listTop,
|
||||
rightWidth,
|
||||
listHeight
|
||||
).also { addWidget(it) }
|
||||
|
||||
private var aspectOptions: AspectOptions = collectAspectOptions(
|
||||
draft.species,
|
||||
draft.form,
|
||||
draft.gender,
|
||||
draft.shiny
|
||||
)
|
||||
private var selectedAspect: AspectItem? = null
|
||||
private var lastSpeciesId: ResourceLocation? = null
|
||||
|
||||
init {
|
||||
rebuildAspectEntries()
|
||||
}
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val currentSpeciesId = draft.species.resourceIdentifier
|
||||
if (lastSpeciesId != currentSpeciesId) {
|
||||
refreshAspects()
|
||||
}
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
font = CobblemonResources.DEFAULT_LARGE,
|
||||
text = lang("ui.aspects").bold(),
|
||||
x = x + (WIDTH / 2.0),
|
||||
y = y + 2,
|
||||
scale = SCALE,
|
||||
centered = true
|
||||
)
|
||||
|
||||
context.fill(
|
||||
leftX,
|
||||
listTop,
|
||||
leftX + leftWidth,
|
||||
listBottom,
|
||||
0x22000000
|
||||
)
|
||||
context.fill(
|
||||
rightX,
|
||||
listTop,
|
||||
rightX + rightWidth,
|
||||
listBottom,
|
||||
0x22000000
|
||||
)
|
||||
|
||||
aspectList.render(context, mouseX, mouseY, delta)
|
||||
optionList.render(context, mouseX, mouseY, delta)
|
||||
}
|
||||
|
||||
private fun refreshAspects() {
|
||||
aspectOptions = collectAspectOptions(
|
||||
draft.species,
|
||||
draft.form,
|
||||
draft.gender,
|
||||
draft.shiny
|
||||
)
|
||||
val cosmetic = aspectOptions.cosmeticChoices.firstOrNull { it.itemId.toString() == draft.cosmeticItem }
|
||||
draft.cosmeticItem = cosmetic?.itemId?.toString()
|
||||
draft.cosmeticAspects = cosmetic?.aspects?.toSet() ?: emptySet()
|
||||
val valid = buildAspectEntries()
|
||||
if (selectedAspect !in valid) {
|
||||
selectedAspect = valid.firstOrNull()
|
||||
}
|
||||
rebuildAspectEntries()
|
||||
lastSpeciesId = draft.species.resourceIdentifier
|
||||
}
|
||||
|
||||
private fun buildAspectEntries(): List<AspectItem> {
|
||||
val entries = mutableListOf<AspectItem>()
|
||||
aspectOptions.choiceGroups.sortedBy { it.key }.forEach { entries.add(AspectItem.Group(it)) }
|
||||
aspectOptions.flagKeys.sortedBy { it }.forEach { entries.add(AspectItem.Toggle(it, ToggleSource.FLAG_FEATURE)) }
|
||||
aspectOptions.toggles.sortedBy { it }.forEach { entries.add(AspectItem.Toggle(it, ToggleSource.FORCED_ASPECT)) }
|
||||
return entries
|
||||
}
|
||||
|
||||
private fun rebuildAspectEntries() {
|
||||
val entries = buildAspectEntries()
|
||||
aspectList.setEntries(entries)
|
||||
if (selectedAspect == null) {
|
||||
selectedAspect = entries.firstOrNull()
|
||||
}
|
||||
rebuildOptionEntries()
|
||||
}
|
||||
|
||||
private fun rebuildOptionEntries() {
|
||||
optionList.setEntries(buildOptionEntries(selectedAspect))
|
||||
}
|
||||
|
||||
private fun buildOptionEntries(aspect: AspectItem?): List<OptionItem> {
|
||||
if (aspect == null) return emptyList()
|
||||
return when (aspect) {
|
||||
is AspectItem.Group -> {
|
||||
val entries = mutableListOf<OptionItem>()
|
||||
entries.add(OptionItem.ClearChoice(aspect.group))
|
||||
aspect.group.choices.forEach { choice ->
|
||||
entries.add(OptionItem.Choice(aspect.group, choice))
|
||||
}
|
||||
entries
|
||||
}
|
||||
is AspectItem.Cosmetic -> {
|
||||
val entries = mutableListOf<OptionItem>()
|
||||
entries.add(OptionItem.ClearCosmetic)
|
||||
aspect.choices.forEach { choice ->
|
||||
entries.add(OptionItem.CosmeticChoice(choice))
|
||||
}
|
||||
entries
|
||||
}
|
||||
is AspectItem.Toggle -> {
|
||||
listOf(
|
||||
OptionItem.Toggle(aspect, enabled = true),
|
||||
OptionItem.Toggle(aspect, enabled = false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun humanizeId(raw: String): String {
|
||||
return raw
|
||||
.replace('_', ' ')
|
||||
.replace('-', ' ')
|
||||
.split(" ")
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ") { part ->
|
||||
part.lowercase().replaceFirstChar { ch -> if (ch.isLowerCase()) ch.titlecase() else ch.toString() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun labelForId(id: String): MutableComponent {
|
||||
val key = "cobblemon.ui.aspect.$id"
|
||||
return if (I18n.exists(key)) {
|
||||
Component.translatable(key)
|
||||
} else {
|
||||
Component.literal(humanizeId(id))
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAspectActive(entry: AspectItem): Boolean = when (entry) {
|
||||
is AspectItem.Toggle -> when (entry.source) {
|
||||
ToggleSource.FORCED_ASPECT -> entry.id in draft.forcedAspects
|
||||
ToggleSource.FLAG_FEATURE -> entry.id in draft.flagFeatures
|
||||
}
|
||||
is AspectItem.Group -> entry.group.key in draft.choiceFeatures
|
||||
is AspectItem.Cosmetic -> draft.cosmeticItem != null
|
||||
}
|
||||
|
||||
private fun labelFor(entry: AspectItem): MutableComponent = when (entry) {
|
||||
is AspectItem.Group -> {
|
||||
val name = labelForId(entry.group.key).string
|
||||
val choice = draft.choiceFeatures[entry.group.key]?.let { labelForId(it).string }
|
||||
Component.literal(if (choice != null) "$name: $choice" else name)
|
||||
}
|
||||
is AspectItem.Toggle -> labelForId(entry.id)
|
||||
is AspectItem.Cosmetic -> {
|
||||
val base = lang("cosmetic_item").string
|
||||
val choice = draft.cosmeticItem?.let { id ->
|
||||
entry.choices.firstOrNull { it.itemId.toString() == id }?.label?.string
|
||||
}
|
||||
Component.literal(if (choice != null) "$base: $choice" else base)
|
||||
}
|
||||
}
|
||||
|
||||
private fun labelFor(option: OptionItem): MutableComponent = when (option) {
|
||||
is OptionItem.ClearChoice -> lang("ui.none")
|
||||
is OptionItem.Choice -> labelForId(option.choice)
|
||||
is OptionItem.Toggle -> if (option.enabled) lang("ui.enabled") else lang("ui.disabled")
|
||||
is OptionItem.CosmeticChoice -> option.choice.label
|
||||
OptionItem.ClearCosmetic -> lang("ui.none")
|
||||
}
|
||||
|
||||
private fun selectAspect(entry: AspectItem) {
|
||||
if (selectedAspect == entry) return
|
||||
selectedAspect = entry
|
||||
rebuildOptionEntries()
|
||||
}
|
||||
|
||||
private fun handleOption(option: OptionItem) {
|
||||
when (option) {
|
||||
is OptionItem.ClearChoice -> {
|
||||
draft.choiceFeatures.remove(option.group.key)
|
||||
}
|
||||
is OptionItem.Choice -> {
|
||||
draft.choiceFeatures[option.group.key] = option.choice
|
||||
}
|
||||
is OptionItem.Toggle -> {
|
||||
when (option.aspect.source) {
|
||||
ToggleSource.FORCED_ASPECT -> {
|
||||
if (option.enabled) {
|
||||
draft.forcedAspects.add(option.aspect.id)
|
||||
} else {
|
||||
draft.forcedAspects.remove(option.aspect.id)
|
||||
}
|
||||
}
|
||||
ToggleSource.FLAG_FEATURE -> {
|
||||
if (option.enabled) {
|
||||
draft.flagFeatures.add(option.aspect.id)
|
||||
} else {
|
||||
draft.flagFeatures.remove(option.aspect.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is OptionItem.CosmeticChoice -> {
|
||||
draft.cosmeticItem = option.choice.itemId.toString()
|
||||
draft.cosmeticAspects = option.choice.aspects.toSet()
|
||||
}
|
||||
OptionItem.ClearCosmetic -> {
|
||||
draft.cosmeticItem = null
|
||||
draft.cosmeticAspects = emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class AspectList(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
width: Int,
|
||||
height: Int
|
||||
) : ScrollingWidget<AspectList.AspectEntry>(
|
||||
width = width,
|
||||
height = height,
|
||||
left = pX,
|
||||
top = pY - height,
|
||||
slotHeight = SLOT_HEIGHT + 2,
|
||||
scrollBarWidth = 3
|
||||
) {
|
||||
private val listX = pX
|
||||
|
||||
fun setEntries(aspects: List<AspectItem>) {
|
||||
clearEntries()
|
||||
aspects.forEach { aspect -> addEntry(AspectEntry(aspect)) }
|
||||
scrollAmount = 0.0
|
||||
}
|
||||
|
||||
override fun getScrollbarPosition(): Int = listX + width - 3
|
||||
|
||||
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val xLeft = this.scrollbarPosition
|
||||
val xRight = xLeft + 3
|
||||
val barHeight = this.bottom - this.y
|
||||
|
||||
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
|
||||
yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
|
||||
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
|
||||
if (yTop < this.y) yTop = this.y
|
||||
|
||||
context.fill(xLeft, this.y + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
|
||||
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
|
||||
}
|
||||
|
||||
inner class AspectEntry(
|
||||
private val entry: AspectItem
|
||||
) : Slot<AspectEntry>() {
|
||||
private var x: Int = 0
|
||||
private var y: Int = 0
|
||||
|
||||
override fun render(
|
||||
context: GuiGraphics,
|
||||
index: Int,
|
||||
y: Int,
|
||||
x: Int,
|
||||
entryWidth: Int,
|
||||
entryHeight: Int,
|
||||
mouseX: Int,
|
||||
mouseY: Int,
|
||||
hovered: Boolean,
|
||||
tickDelta: Float
|
||||
) {
|
||||
this.x = x
|
||||
this.y = y + 1
|
||||
|
||||
if (selectedAspect == entry) {
|
||||
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
|
||||
} else if (isAspectActive(entry)) {
|
||||
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x4436B33F)
|
||||
} else if (hovered) {
|
||||
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
|
||||
}
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
font = CobblemonResources.DEFAULT_LARGE,
|
||||
text = labelFor(entry),
|
||||
x = this.x + 4,
|
||||
y = this.y + 3,
|
||||
scale = SCALE,
|
||||
shadow = true
|
||||
)
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (isMouseOver(mouseX, mouseY)) {
|
||||
selectAspect(entry)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getNarration(): Component {
|
||||
return labelFor(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OptionList(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
width: Int,
|
||||
height: Int
|
||||
) : ScrollingWidget<OptionList.OptionEntry>(
|
||||
width = width,
|
||||
height = height,
|
||||
left = pX,
|
||||
top = pY - height,
|
||||
slotHeight = SLOT_HEIGHT + 2,
|
||||
scrollBarWidth = 3
|
||||
) {
|
||||
private val listX = pX
|
||||
|
||||
fun setEntries(options: List<OptionItem>) {
|
||||
clearEntries()
|
||||
options.forEach { option -> addEntry(OptionEntry(option)) }
|
||||
scrollAmount = 0.0
|
||||
}
|
||||
|
||||
override fun getScrollbarPosition(): Int = listX + width - 3
|
||||
|
||||
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val xLeft = this.scrollbarPosition
|
||||
val xRight = xLeft + 3
|
||||
val barHeight = this.bottom - this.y
|
||||
|
||||
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
|
||||
yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
|
||||
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
|
||||
if (yTop < this.y) yTop = this.y
|
||||
|
||||
context.fill(xLeft, this.y + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
|
||||
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
|
||||
}
|
||||
|
||||
inner class OptionEntry(
|
||||
private val option: OptionItem
|
||||
) : Slot<OptionEntry>() {
|
||||
private var x: Int = 0
|
||||
private var y: Int = 0
|
||||
|
||||
private fun isSelectedOption(): Boolean = when (option) {
|
||||
is OptionItem.ClearChoice -> {
|
||||
draft.choiceFeatures[option.group.key] == null
|
||||
}
|
||||
is OptionItem.Choice -> {
|
||||
draft.choiceFeatures[option.group.key] == option.choice
|
||||
}
|
||||
is OptionItem.Toggle -> {
|
||||
val isEnabled = when (option.aspect.source) {
|
||||
ToggleSource.FORCED_ASPECT -> option.aspect.id in draft.forcedAspects
|
||||
ToggleSource.FLAG_FEATURE -> option.aspect.id in draft.flagFeatures
|
||||
}
|
||||
isEnabled == option.enabled
|
||||
}
|
||||
is OptionItem.CosmeticChoice -> {
|
||||
draft.cosmeticItem == option.choice.itemId.toString()
|
||||
}
|
||||
OptionItem.ClearCosmetic -> {
|
||||
draft.cosmeticItem == null
|
||||
}
|
||||
}
|
||||
|
||||
override fun render(
|
||||
context: GuiGraphics,
|
||||
index: Int,
|
||||
y: Int,
|
||||
x: Int,
|
||||
entryWidth: Int,
|
||||
entryHeight: Int,
|
||||
mouseX: Int,
|
||||
mouseY: Int,
|
||||
hovered: Boolean,
|
||||
tickDelta: Float
|
||||
) {
|
||||
this.x = x
|
||||
this.y = y + 1
|
||||
|
||||
if (isSelectedOption()) {
|
||||
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x4436B33F)
|
||||
} else if (hovered) {
|
||||
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
|
||||
}
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
font = CobblemonResources.DEFAULT_LARGE,
|
||||
text = labelFor(option),
|
||||
x = this.x + 4,
|
||||
y = this.y + 3,
|
||||
scale = SCALE,
|
||||
shadow = true
|
||||
)
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (isMouseOver(mouseX, mouseY)) {
|
||||
handleOption(option)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getNarration(): Component {
|
||||
return labelFor(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.api.text.bold
|
||||
import com.cobblemon.mod.common.api.text.text
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.pokemon.EVs
|
||||
import com.cobblemon.mod.common.pokemon.IVs
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import com.mojang.blaze3d.vertex.BufferUploader
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat
|
||||
import com.mojang.blaze3d.vertex.Tesselator
|
||||
import com.mojang.blaze3d.vertex.VertexFormat
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.gui.screens.Screen
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.world.phys.Vec2
|
||||
import org.joml.Vector3f
|
||||
|
||||
class PokeEditEVIVTab(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
private val draft: PokeEditDraft
|
||||
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.pokeedit.stats")) {
|
||||
companion object {
|
||||
private const val WIDTH = 139
|
||||
private const val HEIGHT = 163
|
||||
private const val CHART_WIDTH = 134
|
||||
private const val CHART_HEIGHT = 148
|
||||
private const val SCALE = 0.5F
|
||||
private const val TAB_MARKER_WIDTH = 8
|
||||
|
||||
private const val TAB_IV = 0
|
||||
private const val TAB_EV = 1
|
||||
|
||||
private val statsBaseResource = cobblemonResource("textures/gui/summary/summary_stats_chart_base.png")
|
||||
private val statsChartResource = cobblemonResource("textures/gui/summary/summary_stats_chart.png")
|
||||
private val tabMarkerResource = cobblemonResource("textures/gui/summary/summary_stats_tab_marker.png")
|
||||
|
||||
private val statLabels = listOf(
|
||||
lang("ui.stats.hp").bold() to Stats.HP,
|
||||
lang("ui.stats.atk").bold() to Stats.ATTACK,
|
||||
lang("ui.stats.def").bold() to Stats.DEFENCE,
|
||||
lang("ui.stats.speed").bold() to Stats.SPEED,
|
||||
lang("ui.stats.sp_def").bold() to Stats.SPECIAL_DEFENCE,
|
||||
lang("ui.stats.sp_atk").bold() to Stats.SPECIAL_ATTACK
|
||||
)
|
||||
|
||||
private val hexagonVerticesOffset = listOf(
|
||||
Pair(67.0, 10.5), // 12 o'clock
|
||||
Pair(122.0, 42.5), // 2 o'clock
|
||||
Pair(122.0, 93.5), // 4 o'clock
|
||||
Pair(67.0, 124.5), // 6 o'clock
|
||||
Pair(12.0, 93.5), // 8 o'clock
|
||||
Pair(12.0, 42.5) // 10 o'clock
|
||||
)
|
||||
}
|
||||
|
||||
private var tabIndex = TAB_IV
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val chartX = x + 2
|
||||
val chartY = y + 6
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = statsBaseResource,
|
||||
x = chartX,
|
||||
y = chartY,
|
||||
width = CHART_WIDTH,
|
||||
height = CHART_HEIGHT
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = statsChartResource,
|
||||
x = (chartX + 25.5) / SCALE,
|
||||
y = (chartY + 22) / SCALE,
|
||||
width = 166,
|
||||
height = 192,
|
||||
scale = SCALE
|
||||
)
|
||||
|
||||
if (tabIndex == TAB_IV) {
|
||||
drawStatPolygon(
|
||||
chartX.toFloat(),
|
||||
chartY.toFloat(),
|
||||
statLabels.map { (_, stat) -> draft.ivs.getOrDefault(stat).toFloat() / IVs.MAX_VALUE.toFloat() },
|
||||
colour = Vector3f(216F / 255F, 100F / 255F, 1F)
|
||||
)
|
||||
} else {
|
||||
drawStatPolygon(
|
||||
chartX.toFloat(),
|
||||
chartY.toFloat(),
|
||||
statLabels.map { (_, stat) -> draft.evs.getOrDefault(stat).toFloat() / EVs.MAX_STAT_VALUE.toFloat() },
|
||||
colour = Vector3f(1F, 1F, 100F / 255F)
|
||||
)
|
||||
}
|
||||
|
||||
val labelsHovered = statLabelsHovered(chartX, chartY, mouseX, mouseY)
|
||||
|
||||
val labels = statLabels.map { (label, stat) ->
|
||||
if (labelsHovered) {
|
||||
if (tabIndex == TAB_IV) {
|
||||
"${draft.ivs.getOrDefault(stat)}/${IVs.MAX_VALUE}".text()
|
||||
} else {
|
||||
"${draft.evs.getOrDefault(stat)}/${EVs.MAX_STAT_VALUE}".text()
|
||||
}
|
||||
} else {
|
||||
label
|
||||
}
|
||||
}
|
||||
|
||||
renderPolygonLabels(
|
||||
context = context,
|
||||
chartX = chartX.toDouble(),
|
||||
chartY = chartY.toDouble(),
|
||||
labels = labels,
|
||||
verticesOffset = hexagonVerticesOffset
|
||||
)
|
||||
|
||||
val values = statLabels.map { (_, stat) ->
|
||||
if (tabIndex == TAB_IV) {
|
||||
draft.ivs.getOrDefault(stat).toString().text()
|
||||
} else {
|
||||
draft.evs.getOrDefault(stat).toString().text()
|
||||
}
|
||||
}
|
||||
|
||||
renderPolygonLabels(
|
||||
context = context,
|
||||
chartX = chartX.toDouble(),
|
||||
chartY = chartY.toDouble(),
|
||||
labels = values,
|
||||
verticesOffset = hexagonVerticesOffset,
|
||||
offsetY = 5.5
|
||||
)
|
||||
|
||||
if (tabIndex == TAB_EV) {
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = lang("ui.pokeedit.evs.total", draft.evs.total(), EVs.MAX_TOTAL_VALUE),
|
||||
x = chartX + 10,
|
||||
y = chartY + 126,
|
||||
scale = SCALE,
|
||||
shadow = true
|
||||
)
|
||||
}
|
||||
|
||||
renderTabs(context, chartX, chartY)
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
val tabIndexAtPos = getTabIndexFromPos(mouseX, mouseY)
|
||||
if (tabIndexAtPos != -1 && tabIndexAtPos != tabIndex) {
|
||||
tabIndex = tabIndexAtPos
|
||||
return true
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
|
||||
val chartX = x + 2
|
||||
val chartY = y + 6
|
||||
val stat = getHoveredStat(chartX, chartY, mouseX.toInt(), mouseY.toInt()) ?: return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
|
||||
val step = when {
|
||||
Screen.hasControlDown() -> 25
|
||||
Screen.hasShiftDown() -> 10
|
||||
else -> 1
|
||||
}
|
||||
val delta = if (verticalAmount > 0) step else -step
|
||||
if (tabIndex == TAB_IV) {
|
||||
val current = draft.ivs.getOrDefault(stat)
|
||||
val next = (current + delta).coerceIn(0, IVs.MAX_VALUE)
|
||||
draft.ivs[stat] = next
|
||||
} else {
|
||||
val current = draft.evs.getOrDefault(stat)
|
||||
val totalWithout = draft.evs.total() - current
|
||||
val maxForStat = minOf(EVs.MAX_STAT_VALUE, EVs.MAX_TOTAL_VALUE - totalWithout)
|
||||
val next = (current + delta).coerceIn(0, maxForStat)
|
||||
draft.evs[stat] = next
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun renderTabs(context: GuiGraphics, chartX: Int, chartY: Int) {
|
||||
val tabWidth = 40
|
||||
val tabLeft = chartX + ((CHART_WIDTH - (tabWidth * 2)) / 2)
|
||||
val tabCenters = listOf(
|
||||
tabLeft + (tabWidth / 2),
|
||||
tabLeft + tabWidth + (tabWidth / 2)
|
||||
)
|
||||
val tabY = chartY + 137
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = lang("ui.stats.ivs").bold(),
|
||||
x = tabCenters[0],
|
||||
y = tabY,
|
||||
scale = SCALE,
|
||||
colour = if (tabIndex == TAB_IV) 0x00FFFFFF else 0x008F8F8F,
|
||||
centered = true
|
||||
)
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = lang("ui.stats.evs").bold(),
|
||||
x = tabCenters[1],
|
||||
y = tabY,
|
||||
scale = SCALE,
|
||||
colour = if (tabIndex == TAB_EV) 0x00FFFFFF else 0x008F8F8F,
|
||||
centered = true
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = tabMarkerResource,
|
||||
x = (tabCenters[tabIndex] - (TAB_MARKER_WIDTH / 2)) / SCALE,
|
||||
y = (tabY - 3) / SCALE,
|
||||
width = TAB_MARKER_WIDTH,
|
||||
height = 4,
|
||||
scale = SCALE
|
||||
)
|
||||
}
|
||||
|
||||
private fun getTabIndexFromPos(mouseX: Double, mouseY: Double): Int {
|
||||
val chartX = x + 2
|
||||
val chartY = y + 6
|
||||
val tabWidth = 40
|
||||
val tabLeft = chartX + ((CHART_WIDTH - (tabWidth * 2)) / 2)
|
||||
val tabTop = chartY + 134.0
|
||||
val tabBottom = tabTop + 9.0
|
||||
if (mouseY !in tabTop..tabBottom) return -1
|
||||
if (mouseX in tabLeft.toDouble()..(tabLeft + tabWidth).toDouble()) return TAB_IV
|
||||
if (mouseX in (tabLeft + tabWidth).toDouble()..(tabLeft + tabWidth * 2).toDouble()) return TAB_EV
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun getHoveredStat(chartX: Int, chartY: Int, mouseX: Int, mouseY: Int): Stat? {
|
||||
statLabels.forEachIndexed { index, (_, stat) ->
|
||||
val pos = hexagonVerticesOffset[index]
|
||||
if (mouseX.toDouble() in (chartX + pos.first - 12)..(chartX + pos.first + 12)
|
||||
&& mouseY.toDouble() in (chartY + pos.second - 1)..(chartY + pos.second + 10)
|
||||
) {
|
||||
return stat
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun statLabelsHovered(chartX: Int, chartY: Int, mouseX: Int, mouseY: Int): Boolean {
|
||||
return getHoveredStat(chartX, chartY, mouseX, mouseY) != null
|
||||
}
|
||||
|
||||
private fun renderPolygonLabels(
|
||||
context: GuiGraphics,
|
||||
chartX: Double,
|
||||
chartY: Double,
|
||||
labels: List<MutableComponent>,
|
||||
verticesOffset: List<Pair<Double, Double>>,
|
||||
offsetY: Double = 0.0
|
||||
) {
|
||||
if (labels.size != verticesOffset.size) return
|
||||
labels.forEachIndexed { index, label ->
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = label,
|
||||
x = chartX + verticesOffset[index].first,
|
||||
y = chartY + verticesOffset[index].second + offsetY,
|
||||
scale = SCALE,
|
||||
centered = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawTriangle(colour: Vector3f, v1: Vec2, v2: Vec2, v3: Vec2, opacity: Float = 0.6F) {
|
||||
RenderSystem.setShaderTexture(0, com.cobblemon.mod.common.client.CobblemonResources.WHITE)
|
||||
RenderSystem.setShaderColor(colour.x, colour.y, colour.z, opacity)
|
||||
val bufferBuilder = Tesselator.getInstance().begin(VertexFormat.Mode.TRIANGLES, DefaultVertexFormat.POSITION)
|
||||
bufferBuilder.addVertex(v1.x, v1.y, 10F)
|
||||
bufferBuilder.addVertex(v2.x, v2.y, 10F)
|
||||
bufferBuilder.addVertex(v3.x, v3.y, 10F)
|
||||
BufferUploader.drawWithShader(bufferBuilder.buildOrThrow())
|
||||
RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
|
||||
}
|
||||
|
||||
private fun drawStatPolygon(chartX: Float, chartY: Float, ratios: List<Float>, colour: Vector3f) {
|
||||
if (ratios.size != 6) return
|
||||
val topVertexY = chartY + 22F
|
||||
val centerX = chartX + 67F
|
||||
val centerY = topVertexY + 48F
|
||||
val radius = centerY - topVertexY
|
||||
|
||||
val startAngleDeg = -90.0
|
||||
val angleStepDeg = 360.0 / 6
|
||||
val anglesDeg = List(6) { i -> startAngleDeg + i * angleStepDeg }
|
||||
val anglesRad = anglesDeg.map { Math.toRadians(it) }
|
||||
|
||||
val maxPoints = anglesRad.map { angle ->
|
||||
Vec2(
|
||||
centerX + radius * Math.cos(angle).toFloat(),
|
||||
centerY + radius * Math.sin(angle).toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
val coercedRatios = ratios.map { it.coerceIn(5 / radius, 1F) }
|
||||
val vertices = maxPoints.mapIndexed { index, maxPoint ->
|
||||
Vec2(
|
||||
centerX + (maxPoint.x - centerX) * coercedRatios[index],
|
||||
centerY + (maxPoint.y - centerY) * coercedRatios[index]
|
||||
)
|
||||
}
|
||||
|
||||
val centerPoint = Vec2(centerX, centerY)
|
||||
RenderSystem.disableDepthTest()
|
||||
for (i in vertices.indices) {
|
||||
val nextIndex = (i + 1) % vertices.size
|
||||
drawTriangle(colour, vertices[i], centerPoint, vertices[nextIndex])
|
||||
}
|
||||
RenderSystem.enableDepthTest()
|
||||
}
|
||||
}
|
||||
+1422
File diff suppressed because it is too large.
Load diff
+354
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
|
||||
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.pokemon.RenderablePokemon
|
||||
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
|
||||
import com.mojang.blaze3d.platform.Lighting
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.gui.components.AbstractWidget
|
||||
import net.minecraft.client.renderer.LightTexture
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector3f
|
||||
|
||||
class PokeEditImplementationBlankTab(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
private val draft: PokeEditDraft,
|
||||
title: Component
|
||||
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, title) {
|
||||
companion object {
|
||||
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
|
||||
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
|
||||
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
|
||||
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
|
||||
private const val POKEBALL_OVERLAY_Y = 25
|
||||
private const val PLATFORM_Y = 69
|
||||
private const val SHADOW_X = 25
|
||||
private const val SHADOW_Y = 76
|
||||
private const val VIEW_SCALE = 1.2F
|
||||
private const val VIEW_SHIFT_X = 0F
|
||||
private const val VIEW_SHIFT_Y = 36F
|
||||
private const val MODEL_VIEW_SCALE = 2.4F
|
||||
private const val MODEL_DRAW_SCALE = 24F
|
||||
private const val MODEL_OFFSET_Y = 20.0
|
||||
private const val CUBE_BASE_OFFSET_X = 1.0F
|
||||
private const val CUBE_SIZE_OFFSET_MULTIPLIER = 0.18F
|
||||
private const val CUBE_OFFSET_Z = -0.5F
|
||||
}
|
||||
|
||||
private val state = FloatingState().also { state ->
|
||||
state.runtime.environment.query.addFunction("is_holding_item") {
|
||||
DoubleValue(hasNonWearableAttachment())
|
||||
}
|
||||
state.runtime.environment.query.addFunction("is_wearing_hat") {
|
||||
DoubleValue(hasWearableHatAttachment())
|
||||
}
|
||||
state.runtime.environment.query.addFunction("is_wearing_face") {
|
||||
DoubleValue(hasWearableFaceAttachment())
|
||||
}
|
||||
}
|
||||
private val heldItemRenderer = HeldItemRenderer()
|
||||
private var renderablePokemon: RenderablePokemon? = null
|
||||
private var attachmentStacks: List<ItemStack> = emptyList()
|
||||
private var rotationY = 30F
|
||||
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE)
|
||||
private var selectedPoseIndex: Int = 0
|
||||
private var oldDragPosX = 0.0
|
||||
private var canDragRender = false
|
||||
private var isDraggingRender = false
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
|
||||
|
||||
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
|
||||
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
|
||||
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
|
||||
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
|
||||
val scaledBaseX = viewX / VIEW_SCALE
|
||||
val scaledBaseY = viewY / VIEW_SCALE
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = backgroundOverlay,
|
||||
x = scaledBaseX,
|
||||
y = scaledBaseY,
|
||||
width = HALF_OVERLAY_WIDTH,
|
||||
height = HALF_OVERLAY_HEIGHT,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = pokeBallOverlay,
|
||||
x = scaledBaseX + 15,
|
||||
y = scaledBaseY + POKEBALL_OVERLAY_Y,
|
||||
width = 109,
|
||||
height = 68,
|
||||
vOffset = 0,
|
||||
textureHeight = 1744,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = platformBase,
|
||||
x = scaledBaseX + 13,
|
||||
y = scaledBaseY + PLATFORM_Y,
|
||||
width = 113,
|
||||
height = 24,
|
||||
textureHeight = 30,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = platformShadow,
|
||||
x = scaledBaseX + SHADOW_X,
|
||||
y = scaledBaseY + SHADOW_Y,
|
||||
width = 90,
|
||||
height = 20,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
|
||||
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
|
||||
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
|
||||
|
||||
context.disableScissor()
|
||||
|
||||
buildRenderable()?.let { renderable ->
|
||||
context.enableScissor(
|
||||
x + 1,
|
||||
y + 1,
|
||||
x + HALF_OVERLAY_WIDTH - 1,
|
||||
y + HALF_OVERLAY_HEIGHT - 1
|
||||
)
|
||||
|
||||
val matrices = context.pose()
|
||||
matrices.pushPose()
|
||||
matrices.translate(
|
||||
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
|
||||
y.toDouble() + MODEL_OFFSET_Y,
|
||||
1000.0
|
||||
)
|
||||
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
|
||||
val rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, rotationY, 0F))
|
||||
val cubeOffsetX = getCubeOffsetX()
|
||||
matrices.translate((-cubeOffsetX / 2F).toDouble(), 0.0, 0.0)
|
||||
|
||||
renderReferenceCube(renderable, matrices, rotation, cubeOffsetX)
|
||||
|
||||
drawProfilePokemon(
|
||||
renderablePokemon = renderable,
|
||||
poseType = poseList[selectedPoseIndex],
|
||||
matrixStack = matrices,
|
||||
partialTicks = delta,
|
||||
rotation = Quaternionf(rotation),
|
||||
state = state,
|
||||
scale = MODEL_DRAW_SCALE
|
||||
)
|
||||
if (attachmentStacks.isNotEmpty()) {
|
||||
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
|
||||
attachmentStacks.forEach { stack ->
|
||||
heldItemRenderer.renderOnModel(
|
||||
stack,
|
||||
state,
|
||||
matrices,
|
||||
bufferSource,
|
||||
light = 0xF000F0,
|
||||
frontLight = true
|
||||
)
|
||||
}
|
||||
bufferSource.endBatch()
|
||||
}
|
||||
matrices.popPose()
|
||||
context.disableScissor()
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (isWithinViewport(mouseX, mouseY)) {
|
||||
canDragRender = true
|
||||
isDraggingRender = true
|
||||
oldDragPosX = mouseX
|
||||
return true
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
|
||||
if (canDragRender) {
|
||||
canDragRender = false
|
||||
}
|
||||
if (isDraggingRender) {
|
||||
isDraggingRender = false
|
||||
}
|
||||
return super.mouseReleased(pMouseX, pMouseY, pButton)
|
||||
}
|
||||
|
||||
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
|
||||
val handledDrag = isDraggingRender && canDragRender
|
||||
if (handledDrag) {
|
||||
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
|
||||
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
|
||||
}
|
||||
oldDragPosX = mouseX
|
||||
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
|
||||
private fun buildRenderable(): RenderablePokemon? {
|
||||
val species = draft.species
|
||||
val form = draft.form
|
||||
val aspects = mutableSetOf<String>()
|
||||
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
|
||||
aspects.addAll(form.aspects)
|
||||
aspects.addAll(draft.forcedAspects)
|
||||
aspects.addAll(draft.flagFeatures)
|
||||
aspects.addAll(draft.cosmeticAspects)
|
||||
draft.choiceFeatures.forEach { (key, choice) ->
|
||||
choiceAspectId(key, choice)?.let { aspects.add(it) }
|
||||
}
|
||||
when (draft.gender) {
|
||||
Gender.MALE -> aspects.add("male")
|
||||
Gender.FEMALE -> aspects.add("female")
|
||||
Gender.GENDERLESS -> aspects.add("genderless")
|
||||
}
|
||||
val heldStack = parseItemStack(draft.heldItem)
|
||||
val cosmeticStack = parseItemStack(draft.cosmeticItem)
|
||||
attachmentStacks = buildAttachmentStacks(heldStack, cosmeticStack)
|
||||
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
|
||||
return renderablePokemon
|
||||
}
|
||||
|
||||
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
|
||||
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
|
||||
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
|
||||
poseList = poser.poses
|
||||
.map { it.value.poseTypes.minBy { it.ordinal } }
|
||||
.toSet()
|
||||
.filterNot { it in PoseType.SHOULDER_POSES }
|
||||
.sortedBy { it.ordinal }
|
||||
.toTypedArray()
|
||||
if (selectedPoseIndex !in poseList.indices) {
|
||||
selectedPoseIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseItemStack(rawId: String?): ItemStack {
|
||||
return rawId
|
||||
?.let { ResourceLocation.tryParse(it) }
|
||||
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
|
||||
?.let { item -> ItemStack(item) }
|
||||
?: ItemStack.EMPTY
|
||||
}
|
||||
|
||||
private fun buildAttachmentStacks(heldStack: ItemStack, cosmeticStack: ItemStack): List<ItemStack> {
|
||||
if (heldStack.isEmpty && cosmeticStack.isEmpty) return emptyList()
|
||||
val stacks = mutableListOf<ItemStack>()
|
||||
if (!heldStack.isEmpty) {
|
||||
stacks.add(heldStack)
|
||||
}
|
||||
if (!cosmeticStack.isEmpty && (heldStack.isEmpty || cosmeticStack.item != heldStack.item)) {
|
||||
stacks.add(cosmeticStack)
|
||||
}
|
||||
return stacks
|
||||
}
|
||||
|
||||
private fun hasNonWearableAttachment(): Boolean {
|
||||
return attachmentStacks.any { stack ->
|
||||
!stack.isEmpty && !stack.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !stack.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasWearableHatAttachment(): Boolean {
|
||||
return attachmentStacks.any { stack -> !stack.isEmpty && stack.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) }
|
||||
}
|
||||
|
||||
private fun hasWearableFaceAttachment(): Boolean {
|
||||
return attachmentStacks.any { stack -> !stack.isEmpty && stack.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS) }
|
||||
}
|
||||
|
||||
private fun renderReferenceCube(
|
||||
renderable: RenderablePokemon,
|
||||
matrices: com.mojang.blaze3d.vertex.PoseStack,
|
||||
rotation: Quaternionf,
|
||||
cubeOffsetX: Float
|
||||
) {
|
||||
val model = VaryingModelRepository.getPoser(renderable.species.resourceIdentifier, state)
|
||||
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
|
||||
matrices.pushPose()
|
||||
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
|
||||
matrices.translate(
|
||||
model.profileTranslation.x,
|
||||
model.profileTranslation.y + 1.5 * model.profileScale,
|
||||
model.profileTranslation.z - 4.0
|
||||
)
|
||||
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
|
||||
matrices.mulPose(rotation)
|
||||
matrices.mulPose(com.mojang.math.Axis.XP.rotationDegrees(180F))
|
||||
matrices.translate(cubeOffsetX.toDouble(), 0.0, CUBE_OFFSET_Z.toDouble())
|
||||
|
||||
RenderSystem.applyModelViewMatrix()
|
||||
RenderSystem.enableDepthTest()
|
||||
RenderSystem.disableCull()
|
||||
Lighting.setupForEntityInInventory()
|
||||
|
||||
Minecraft.getInstance().blockRenderer.renderSingleBlock(
|
||||
Blocks.RED_WOOL.defaultBlockState(),
|
||||
matrices,
|
||||
bufferSource,
|
||||
LightTexture.pack(15, 15),
|
||||
OverlayTexture.NO_OVERLAY
|
||||
)
|
||||
bufferSource.endBatch()
|
||||
Lighting.setupFor3DItems()
|
||||
RenderSystem.enableCull()
|
||||
matrices.popPose()
|
||||
}
|
||||
|
||||
private fun getCubeOffsetX(): Float {
|
||||
val hitboxWidth = draft.form.hitbox.width * draft.form.baseScale
|
||||
val hitboxHeight = draft.form.hitbox.height * draft.form.baseScale
|
||||
val hitboxSize = maxOf(hitboxWidth, hitboxHeight)
|
||||
val extraOffset = (hitboxSize - 1.0F).coerceAtLeast(0.0F) * CUBE_SIZE_OFFSET_MULTIPLIER
|
||||
return CUBE_BASE_OFFSET_X + extraOffset
|
||||
}
|
||||
|
||||
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
|
||||
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
|
||||
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
|
||||
&& children.none { child ->
|
||||
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
|
||||
}
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.cobblemon.mod.common.api.text.text
|
||||
import com.cobblemon.mod.common.client.gui.ScrollingWidget
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.util.Mth
|
||||
|
||||
class PokeEditLocatorListWidget(
|
||||
val pX: Int,
|
||||
val pY: Int,
|
||||
val listWidth: Int,
|
||||
val listHeight: Int,
|
||||
private val onSelectLocator: (String?) -> Unit
|
||||
) : ScrollingWidget<PokeEditLocatorListWidget.LocatorRow>(
|
||||
left = pX,
|
||||
top = pY - listHeight,
|
||||
width = listWidth,
|
||||
height = listHeight,
|
||||
slotHeight = ROW_HEIGHT
|
||||
) {
|
||||
companion object {
|
||||
private const val ROW_HEIGHT = 9
|
||||
private const val TEXT_SCALE = 0.5F
|
||||
private const val BORDER_COLOR = 0xFF1B1B1B.toInt()
|
||||
private const val FILL_COLOR = 0xFF3B3B3B.toInt()
|
||||
private const val TEXT_COLOR = 0xFFFCFCFC.toInt()
|
||||
}
|
||||
|
||||
private val listTop = pY - listHeight
|
||||
private val listBottom = pY
|
||||
|
||||
private var selectedLocator: String? = null
|
||||
private var draggingScrollbar = false
|
||||
|
||||
fun setLocators(locators: Collection<String>) {
|
||||
clearEntries()
|
||||
locators.sorted().forEach { addEntry(LocatorRow(it)) }
|
||||
if (selectedLocator != null && locators.none { it == selectedLocator }) {
|
||||
selectedLocator = null
|
||||
onSelectLocator(null)
|
||||
}
|
||||
scrollAmount = 0.0
|
||||
}
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
context.fill(pX - 1, listTop - 1, pX + listWidth + 1, listBottom + 1, BORDER_COLOR)
|
||||
context.fill(pX, listTop, pX + listWidth, listBottom, FILL_COLOR)
|
||||
|
||||
focused = if (mouseX in pX..(pX + listWidth) && mouseY in listTop..listBottom) {
|
||||
getEntryAtPosition(mouseX.toDouble(), mouseY.toDouble())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
context.enableScissor(pX, listTop, pX + listWidth, listBottom)
|
||||
renderListItems(context, mouseX, mouseY, delta)
|
||||
context.disableScissor()
|
||||
|
||||
if (maxScroll > 0) {
|
||||
renderScrollbar(context, mouseX, mouseY, delta)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
|
||||
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|
||||
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
val maxScroll = (itemCount * itemHeight - listHeight).coerceAtLeast(0)
|
||||
if (maxScroll <= 0) return true
|
||||
val step = if (verticalAmount != 0.0) verticalAmount else amount
|
||||
scrollAmount = Mth.clamp(scrollAmount - step * itemHeight, 0.0, maxScroll.toDouble())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|
||||
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (button != 0) {
|
||||
return false
|
||||
}
|
||||
if (isOverScrollbar(mouseX, mouseY) && maxScroll > 0) {
|
||||
draggingScrollbar = true
|
||||
return true
|
||||
}
|
||||
val localY = mouseY.toInt() - listTop + scrollAmount.toInt()
|
||||
if (localY < 0) return false
|
||||
val index = localY / itemHeight
|
||||
if (index !in 0 until itemCount) return false
|
||||
val entry = getEntry(index)
|
||||
if (entry is LocatorRow) {
|
||||
selectedLocator = entry.name
|
||||
onSelectLocator(selectedLocator)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (button == 0 && draggingScrollbar) {
|
||||
draggingScrollbar = false
|
||||
return true
|
||||
}
|
||||
return super.mouseReleased(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
|
||||
if (!draggingScrollbar || maxScroll <= 0) {
|
||||
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
val barHeight = listBottom - listTop
|
||||
var thumbHeight = ((barHeight * barHeight).toFloat() / maxPosition.toFloat()).toInt()
|
||||
thumbHeight = Mth.clamp(thumbHeight, 16, barHeight - 4)
|
||||
val trackHeight = barHeight - thumbHeight
|
||||
if (trackHeight <= 0) return true
|
||||
val clampedY = Mth.clamp(mouseY.toInt() - listTop - thumbHeight / 2, 0, trackHeight)
|
||||
scrollAmount = (clampedY.toDouble() * maxScroll.toDouble()) / trackHeight.toDouble()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
|
||||
return mouseX in pX.toDouble()..(pX + listWidth).toDouble()
|
||||
&& mouseY in listTop.toDouble()..listBottom.toDouble()
|
||||
}
|
||||
|
||||
override fun renderListItems(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val rowX = rowLeft
|
||||
for (index in 0 until itemCount) {
|
||||
val rowY = getRowTop(index)
|
||||
val rowBottom = getRowBottom(index)
|
||||
if (rowBottom >= listTop && rowY <= listBottom) {
|
||||
val entry = getEntry(index)
|
||||
if (entry is LocatorRow) {
|
||||
entry.isSelected = entry.name == selectedLocator
|
||||
}
|
||||
renderItem(context, mouseX, mouseY, delta, index, rowX, rowY, rowWidth, itemHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val xLeft = this.scrollbarPosition
|
||||
val xRight = xLeft + 3
|
||||
val yStart = listTop
|
||||
val barHeight = listBottom - listTop
|
||||
|
||||
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
|
||||
yBottom = Mth.clamp(yBottom, 16, barHeight - 4)
|
||||
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
|
||||
if (yTop < yStart) yTop = yStart
|
||||
|
||||
context.fill(xLeft + 1, yStart, xRight - 1, listBottom, 0xFF7AEEF1.toInt())
|
||||
context.fill(xLeft, yTop, xRight, yTop + yBottom, 0xFF3A96B6.toInt())
|
||||
}
|
||||
|
||||
override fun getRowTop(index: Int): Int {
|
||||
return listTop - scrollAmount.toInt() + (index * itemHeight)
|
||||
}
|
||||
|
||||
override fun getRowBottom(index: Int): Int {
|
||||
return getRowTop(index) + itemHeight
|
||||
}
|
||||
|
||||
override fun getBottom(): Int {
|
||||
return listBottom
|
||||
}
|
||||
|
||||
private fun isOverScrollbar(mouseX: Double, mouseY: Double): Boolean {
|
||||
val xLeft = scrollbarPosition
|
||||
val xRight = xLeft + 3
|
||||
return mouseX >= xLeft && mouseX <= xRight && mouseY >= listTop && mouseY <= listBottom
|
||||
}
|
||||
|
||||
class LocatorRow(val name: String) : Slot<LocatorRow>() {
|
||||
var isSelected: Boolean = false
|
||||
|
||||
override fun render(
|
||||
context: GuiGraphics,
|
||||
index: Int,
|
||||
y: Int,
|
||||
x: Int,
|
||||
entryWidth: Int,
|
||||
entryHeight: Int,
|
||||
mouseX: Int,
|
||||
mouseY: Int,
|
||||
hovered: Boolean,
|
||||
tickDelta: Float
|
||||
) {
|
||||
val bg = when {
|
||||
isSelected -> 0xFF4A4A4A.toInt()
|
||||
hovered -> 0xFF2F2F2F.toInt()
|
||||
else -> null
|
||||
}
|
||||
if (bg != null) {
|
||||
context.fill(x + 1, y, x + entryWidth - 1, y + ROW_HEIGHT, bg)
|
||||
}
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = name.text(),
|
||||
x = x + 3,
|
||||
y = y + 2,
|
||||
scale = TEXT_SCALE,
|
||||
colour = TEXT_COLOR,
|
||||
shadow = false
|
||||
)
|
||||
}
|
||||
|
||||
override fun getNarration(): Component {
|
||||
return Component.literal(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.api.text.text
|
||||
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
|
||||
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.pokemon.RenderablePokemon
|
||||
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.gui.components.AbstractWidget
|
||||
import net.minecraft.client.renderer.LevelRenderer
|
||||
import net.minecraft.client.renderer.RenderType
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.phys.AABB
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import org.joml.Matrix4f
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector4f
|
||||
import org.joml.Vector3f
|
||||
|
||||
class PokeEditLocatorTab(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
private val draft: PokeEditDraft,
|
||||
private var highlightedLocator: String?,
|
||||
private val onDumpLocators: (List<String>) -> Unit
|
||||
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, "locators".text()) {
|
||||
companion object {
|
||||
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
|
||||
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
|
||||
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
|
||||
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
|
||||
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
|
||||
|
||||
private const val LOCATOR_CUBE_SIZE = 0.1
|
||||
private const val LOCATOR_TOOLTIP_RADIUS = 8
|
||||
private const val BUTTON_WIDTH = 54
|
||||
private const val BUTTON_HEIGHT = 15
|
||||
private const val POKEBALL_OVERLAY_Y = 25
|
||||
private const val PLATFORM_Y = 69
|
||||
private const val SHADOW_X = 25
|
||||
private const val SHADOW_Y = 76
|
||||
private const val VIEW_SCALE = 1.2F
|
||||
private const val VIEW_SHIFT_X = 0F
|
||||
private const val VIEW_SHIFT_Y = 36F
|
||||
private const val MODEL_VIEW_SCALE = 2.4F
|
||||
private const val MODEL_DRAW_SCALE = 24F
|
||||
private const val MODEL_OFFSET_Y = 20.0
|
||||
private const val AXIS_LINE_LENGTH = 0.35
|
||||
private const val LEGEND_SCALE = 0.5F
|
||||
private const val LEGEND_X = 6
|
||||
private const val LEGEND_Y = 60
|
||||
}
|
||||
|
||||
private val state = FloatingState().also { state ->
|
||||
state.runtime.environment.query.addFunction("is_holding_item") {
|
||||
val held = currentHeldStack()
|
||||
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
|
||||
}
|
||||
state.runtime.environment.query.addFunction("is_wearing_hat") {
|
||||
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
|
||||
}
|
||||
state.runtime.environment.query.addFunction("is_wearing_face") {
|
||||
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
|
||||
}
|
||||
}
|
||||
private val heldItemRenderer = HeldItemRenderer()
|
||||
private var renderablePokemon: RenderablePokemon? = null
|
||||
private var rotationY = 30F
|
||||
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE)
|
||||
private var selectedPoseIndex: Int = 0
|
||||
private var showLocators = false
|
||||
private var showLocatorAxes = false
|
||||
private var oldDragPosX = 0.0
|
||||
private var canDragRender = false
|
||||
private var isDraggingRender = false
|
||||
private var cachedLocatorNames: List<String> = emptyList()
|
||||
|
||||
private val showLocatorsButton = SummaryButton(
|
||||
buttonX = pX + 6F,
|
||||
buttonY = pY + 6F,
|
||||
buttonWidth = BUTTON_WIDTH,
|
||||
buttonHeight = BUTTON_HEIGHT,
|
||||
clickAction = { showLocators = true },
|
||||
text = "Locators On".text(),
|
||||
resource = actionButtonResource,
|
||||
renderRequirement = { !showLocators },
|
||||
largeText = false,
|
||||
boldText = false,
|
||||
textScale = 0.7F
|
||||
).also { addWidget(it) }
|
||||
|
||||
private val hideLocatorsButton = SummaryButton(
|
||||
buttonX = pX + 66F,
|
||||
buttonY = pY + 6F,
|
||||
buttonWidth = BUTTON_WIDTH,
|
||||
buttonHeight = BUTTON_HEIGHT,
|
||||
clickAction = { showLocators = false },
|
||||
text = "Locators Off".text(),
|
||||
resource = actionButtonResource,
|
||||
renderRequirement = { showLocators },
|
||||
largeText = false,
|
||||
boldText = false,
|
||||
textScale = 0.7F
|
||||
).also { addWidget(it) }
|
||||
|
||||
private val showLocatorAxesButton = SummaryButton(
|
||||
buttonX = pX + 6F,
|
||||
buttonY = pY + 24F,
|
||||
buttonWidth = BUTTON_WIDTH,
|
||||
buttonHeight = BUTTON_HEIGHT,
|
||||
clickAction = { showLocatorAxes = true },
|
||||
text = "Axes On".text(),
|
||||
resource = actionButtonResource,
|
||||
renderRequirement = { !showLocatorAxes },
|
||||
largeText = false,
|
||||
boldText = false,
|
||||
textScale = 0.7F
|
||||
).also { addWidget(it) }
|
||||
|
||||
private val hideLocatorAxesButton = SummaryButton(
|
||||
buttonX = pX + 66F,
|
||||
buttonY = pY + 24F,
|
||||
buttonWidth = BUTTON_WIDTH,
|
||||
buttonHeight = BUTTON_HEIGHT,
|
||||
clickAction = { showLocatorAxes = false },
|
||||
text = "Axes Off".text(),
|
||||
resource = actionButtonResource,
|
||||
renderRequirement = { showLocatorAxes },
|
||||
largeText = false,
|
||||
boldText = false,
|
||||
textScale = 0.7F
|
||||
).also { addWidget(it) }
|
||||
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val matrices = context.pose()
|
||||
showLocatorsButton.buttonActive = showLocators
|
||||
hideLocatorsButton.buttonActive = !showLocators
|
||||
showLocatorAxesButton.buttonActive = showLocatorAxes
|
||||
hideLocatorAxesButton.buttonActive = !showLocatorAxes
|
||||
|
||||
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
|
||||
|
||||
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
|
||||
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
|
||||
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
|
||||
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
|
||||
val scaledBaseX = viewX / VIEW_SCALE
|
||||
val scaledBaseY = viewY / VIEW_SCALE
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = backgroundOverlay,
|
||||
x = scaledBaseX,
|
||||
y = scaledBaseY,
|
||||
width = HALF_OVERLAY_WIDTH,
|
||||
height = HALF_OVERLAY_HEIGHT,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = pokeBallOverlay,
|
||||
x = scaledBaseX + 15,
|
||||
y = scaledBaseY + POKEBALL_OVERLAY_Y,
|
||||
width = 109,
|
||||
height = 68,
|
||||
vOffset = 0,
|
||||
textureHeight = 1744,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = platformBase,
|
||||
x = scaledBaseX + 13,
|
||||
y = scaledBaseY + PLATFORM_Y,
|
||||
width = 113,
|
||||
height = 24,
|
||||
textureHeight = 30,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = platformShadow,
|
||||
x = scaledBaseX + SHADOW_X,
|
||||
y = scaledBaseY + SHADOW_Y,
|
||||
width = 90,
|
||||
height = 20,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
|
||||
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
|
||||
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
|
||||
|
||||
context.disableScissor()
|
||||
|
||||
buildRenderable()?.let { renderable ->
|
||||
context.enableScissor(
|
||||
x + 1,
|
||||
y + 1,
|
||||
x + HALF_OVERLAY_WIDTH - 1,
|
||||
y + HALF_OVERLAY_HEIGHT - 1
|
||||
)
|
||||
|
||||
matrices.pushPose()
|
||||
matrices.translate(
|
||||
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
|
||||
y.toDouble() + MODEL_OFFSET_Y,
|
||||
1000.0
|
||||
)
|
||||
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
|
||||
val rotationVector = Vector3f(13F, rotationY, 0F)
|
||||
|
||||
drawProfilePokemon(
|
||||
renderablePokemon = renderable,
|
||||
poseType = poseList[selectedPoseIndex],
|
||||
matrixStack = matrices,
|
||||
partialTicks = delta,
|
||||
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
|
||||
state = state,
|
||||
scale = MODEL_DRAW_SCALE
|
||||
)
|
||||
refreshLocatorList()
|
||||
if (!renderable.heldItem.isEmpty) {
|
||||
heldItemRenderer.renderOnModel(
|
||||
renderable.heldItem,
|
||||
state,
|
||||
matrices,
|
||||
context.bufferSource(),
|
||||
light = 0xF000F0,
|
||||
frontLight = true
|
||||
)
|
||||
}
|
||||
if (showLocators) {
|
||||
renderLocators(context, mouseX, mouseY, showLocatorAxes)
|
||||
}
|
||||
matrices.popPose()
|
||||
context.disableScissor()
|
||||
}
|
||||
|
||||
showLocatorsButton.render(context, mouseX, mouseY, delta)
|
||||
hideLocatorsButton.render(context, mouseX, mouseY, delta)
|
||||
showLocatorAxesButton.render(context, mouseX, mouseY, delta)
|
||||
hideLocatorAxesButton.render(context, mouseX, mouseY, delta)
|
||||
if (showLocators && showLocatorAxes) {
|
||||
renderAxisLegend(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (isWithinViewport(mouseX, mouseY)) {
|
||||
canDragRender = true
|
||||
isDraggingRender = true
|
||||
oldDragPosX = mouseX
|
||||
return true
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
|
||||
if (canDragRender) {
|
||||
canDragRender = false
|
||||
}
|
||||
if (isDraggingRender) {
|
||||
isDraggingRender = false
|
||||
}
|
||||
return super.mouseReleased(pMouseX, pMouseY, pButton)
|
||||
}
|
||||
|
||||
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
|
||||
val handledDrag = isDraggingRender && canDragRender
|
||||
if (handledDrag) {
|
||||
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
|
||||
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
|
||||
}
|
||||
oldDragPosX = mouseX
|
||||
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
|
||||
private fun renderLocators(context: GuiGraphics, mouseX: Int, mouseY: Int, showAxes: Boolean) {
|
||||
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
|
||||
val buffer = bufferSource.getBuffer(RenderType.lines())
|
||||
val baseMatrix = Matrix4f(context.pose().last().pose())
|
||||
var closestName: String? = null
|
||||
var closestDistance = LOCATOR_TOOLTIP_RADIUS.toFloat()
|
||||
for ((name, matrix) in state.locatorStates.entries) {
|
||||
val highlight = name == highlightedLocator
|
||||
val origin = matrix.getOrigin()
|
||||
context.pose().pushPose()
|
||||
context.pose().mulPose(matrix.matrix)
|
||||
LevelRenderer.renderLineBox(
|
||||
context.pose(),
|
||||
buffer,
|
||||
AABB.ofSize(Vec3.ZERO, LOCATOR_CUBE_SIZE, LOCATOR_CUBE_SIZE, LOCATOR_CUBE_SIZE),
|
||||
if (highlight) 1F else 0F,
|
||||
if (highlight) 0F else 1F,
|
||||
0F,
|
||||
1F
|
||||
)
|
||||
if (showAxes) {
|
||||
renderLocatorAxes(context, buffer)
|
||||
}
|
||||
context.pose().popPose()
|
||||
|
||||
val screenPos = projectToScreen(baseMatrix, origin) ?: continue
|
||||
val dx = screenPos.first - mouseX
|
||||
val dy = screenPos.second - mouseY
|
||||
val dist = kotlin.math.sqrt((dx * dx + dy * dy).toDouble()).toFloat()
|
||||
if (dist <= closestDistance) {
|
||||
closestDistance = dist
|
||||
closestName = name
|
||||
}
|
||||
}
|
||||
bufferSource.endBatch(RenderType.lines())
|
||||
|
||||
if (closestName != null) {
|
||||
context.renderTooltip(Minecraft.getInstance().font, Component.literal(closestName), mouseX, mouseY)
|
||||
}
|
||||
}
|
||||
|
||||
fun setHighlightedLocator(name: String?) {
|
||||
highlightedLocator = name
|
||||
}
|
||||
|
||||
private fun refreshLocatorList() {
|
||||
val names = state.locatorStates.keys.sorted()
|
||||
if (names != cachedLocatorNames) {
|
||||
cachedLocatorNames = names
|
||||
onDumpLocators(names)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderLocatorAxes(context: GuiGraphics, buffer: com.mojang.blaze3d.vertex.VertexConsumer) {
|
||||
val pose = context.pose().last().pose()
|
||||
val length = AXIS_LINE_LENGTH.toFloat()
|
||||
|
||||
addAxisLine(buffer, pose, 0f, 0f, 0f, length, 0f, 0f, 1f, 0f, 0f)
|
||||
addAxisLine(buffer, pose, 0f, 0f, 0f, 0f, length, 0f, 0f, 1f, 0f)
|
||||
addAxisLine(buffer, pose, 0f, 0f, 0f, 0f, 0f, length, 0f, 0f, 1f)
|
||||
}
|
||||
|
||||
private fun addAxisLine(
|
||||
buffer: com.mojang.blaze3d.vertex.VertexConsumer,
|
||||
pose: Matrix4f,
|
||||
x1: Float,
|
||||
y1: Float,
|
||||
z1: Float,
|
||||
x2: Float,
|
||||
y2: Float,
|
||||
z2: Float,
|
||||
r: Float,
|
||||
g: Float,
|
||||
b: Float
|
||||
) {
|
||||
buffer.addVertex(pose, x1, y1, z1).setColor(r, g, b, 1f).setNormal(0f, 1f, 0f)
|
||||
buffer.addVertex(pose, x2, y2, z2).setColor(r, g, b, 1f).setNormal(0f, 1f, 0f)
|
||||
}
|
||||
|
||||
private fun renderAxisLegend(context: GuiGraphics) {
|
||||
val legendX = x + LEGEND_X
|
||||
val legendY = y + LEGEND_Y
|
||||
context.fill(legendX - 2, legendY - 2, legendX + 46, legendY + 16, 0xAA1B1B1B.toInt())
|
||||
drawLegendLine(context, legendX, legendY, 0xFFFF4D4D.toInt(), "X")
|
||||
drawLegendLine(context, legendX, legendY + 5, 0xFF4DFF4D.toInt(), "Y")
|
||||
drawLegendLine(context, legendX, legendY + 10, 0xFF4D4DFF.toInt(), "Z")
|
||||
}
|
||||
|
||||
private fun drawLegendLine(context: GuiGraphics, x: Int, y: Int, color: Int, label: String) {
|
||||
context.fill(x, y + 2, x + 6, y + 3, color)
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = Component.literal(label),
|
||||
x = x + 8,
|
||||
y = y,
|
||||
scale = LEGEND_SCALE,
|
||||
colour = 0xFFFFFFFF.toInt(),
|
||||
shadow = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildRenderable(): RenderablePokemon? {
|
||||
val species = draft.species
|
||||
val form = draft.form
|
||||
val aspects = mutableSetOf<String>()
|
||||
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
|
||||
aspects.addAll(form.aspects)
|
||||
aspects.addAll(draft.forcedAspects)
|
||||
aspects.addAll(draft.flagFeatures)
|
||||
aspects.addAll(draft.cosmeticAspects)
|
||||
draft.choiceFeatures.forEach { (key, choice) ->
|
||||
choiceAspectId(key, choice)?.let { aspects.add(it) }
|
||||
}
|
||||
when (draft.gender) {
|
||||
Gender.MALE -> aspects.add("male")
|
||||
Gender.FEMALE -> aspects.add("female")
|
||||
Gender.GENDERLESS -> aspects.add("genderless")
|
||||
}
|
||||
val heldStack = draft.heldItem
|
||||
?.let { ResourceLocation.tryParse(it) }
|
||||
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
|
||||
?.let { item -> ItemStack(item) }
|
||||
?: ItemStack.EMPTY
|
||||
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
|
||||
return renderablePokemon
|
||||
}
|
||||
|
||||
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
|
||||
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
|
||||
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
|
||||
poseList = poser.poses
|
||||
.map { it.value.poseTypes.minBy { it.ordinal } }
|
||||
.toSet()
|
||||
.filterNot { it in PoseType.SHOULDER_POSES }
|
||||
.sortedBy { it.ordinal }
|
||||
.toTypedArray()
|
||||
if (selectedPoseIndex !in poseList.indices) {
|
||||
selectedPoseIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentHeldStack(): ItemStack {
|
||||
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
|
||||
}
|
||||
|
||||
private fun projectToScreen(baseMatrix: Matrix4f, position: Vec3): Pair<Float, Float>? {
|
||||
val projection = RenderSystem.getProjectionMatrix()
|
||||
val vec = Vector4f(position.x.toFloat(), position.y.toFloat(), position.z.toFloat(), 1f)
|
||||
vec.mul(baseMatrix)
|
||||
vec.mul(projection)
|
||||
if (vec.w == 0f) return null
|
||||
vec.div(vec.w)
|
||||
val window = Minecraft.getInstance().window
|
||||
val screenX = (vec.x * 0.5f + 0.5f) * window.guiScaledWidth.toFloat()
|
||||
val screenY = (1f - (vec.y * 0.5f + 0.5f)) * window.guiScaledHeight.toFloat()
|
||||
return Pair(screenX, screenY)
|
||||
}
|
||||
|
||||
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
|
||||
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
|
||||
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
|
||||
&& children.none { child ->
|
||||
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
|
||||
}
|
||||
}
|
||||
+659
@@ -0,0 +1,659 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.text.bold
|
||||
import com.cobblemon.mod.common.client.CobblemonResources
|
||||
import com.cobblemon.mod.common.client.gui.MoveCategoryIcon
|
||||
import com.cobblemon.mod.common.client.gui.TypeIcon
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.common.SummaryScrollList
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.MoveDescriptionScrollList
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.ReorderMoveButton
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.cobblemon.mod.common.util.math.toRGB
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.network.chat.MutableComponent
|
||||
import net.minecraft.util.Mth
|
||||
|
||||
class PokeEditMovesTab(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
private val draft: PokeEditDraft
|
||||
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.moves")) {
|
||||
companion object {
|
||||
private const val WIDTH = 139
|
||||
private const val HEIGHT = 163
|
||||
private const val BASE_WIDTH = 134
|
||||
private const val BASE_HEIGHT = 148
|
||||
private const val BASE_SCALE = 0.80F
|
||||
private const val SLOT_SPACING = 3
|
||||
private const val SCALE = 0.5F
|
||||
|
||||
private val decimalFormat = DecimalFormat("#.##").also {
|
||||
it.roundingMode = RoundingMode.CEILING
|
||||
}
|
||||
|
||||
private val movesBaseResource = cobblemonResource("textures/gui/summary/summary_moves_base.png")
|
||||
private val moveResource = cobblemonResource("textures/gui/summary/summary_move.png")
|
||||
private val moveOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay.png")
|
||||
private val moveSelectedOverlayResource = cobblemonResource("textures/gui/summary/summary_move_selected_overlay.png")
|
||||
private val moveSwapButtonResource = cobblemonResource("textures/gui/summary/summary_move_swap.png")
|
||||
private val moveAddButtonResource = cobblemonResource("textures/gui/summary/summary_move_add.png")
|
||||
|
||||
private val movesPowerIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_power.png")
|
||||
private val movesAccuracyIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_accuracy.png")
|
||||
private val movesEffectIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_effect.png")
|
||||
|
||||
private val moveListResource = cobblemonResource("textures/gui/summary/summary_move_condensed.png")
|
||||
private val moveListOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay_condensed.png")
|
||||
|
||||
private const val MOVE_WIDTH = 108
|
||||
private const val MOVE_HEIGHT = 22
|
||||
private const val MOVE_LIST_SLOT_HEIGHT = 18
|
||||
private const val MOVE_LIST_SLOT_SPACING = 3
|
||||
private const val MOVE_LIST_X_OFFSET = 0
|
||||
private const val MOVE_LIST_Y_OFFSET = 0
|
||||
}
|
||||
|
||||
private val baseScaledWidth = (BASE_WIDTH * BASE_SCALE).toInt()
|
||||
private val baseScaledHeight = (BASE_HEIGHT * BASE_SCALE).toInt()
|
||||
private val moveWidthScaled = (MOVE_WIDTH * BASE_SCALE).toInt()
|
||||
private val moveHeightScaled = (MOVE_HEIGHT * BASE_SCALE).toInt()
|
||||
private val slotSpacingScaled = (SLOT_SPACING * BASE_SCALE).toInt().coerceAtLeast(1)
|
||||
private val baseX = x + (WIDTH - baseScaledWidth) / 2 - 10
|
||||
private val baseY = y + 7
|
||||
private val slotX = baseX + (13 * BASE_SCALE).toInt()
|
||||
private val slotY = baseY + (6 * BASE_SCALE).toInt()
|
||||
private val descriptionListX = (baseX + (69 * BASE_SCALE).toInt())
|
||||
.coerceAtMost(baseX + baseScaledWidth - 60)
|
||||
private val descriptionListY = baseY + (113 * BASE_SCALE).toInt()
|
||||
|
||||
private val slots = (0 until 4).map { index ->
|
||||
MoveSlot(
|
||||
slotX,
|
||||
slotY + (moveHeightScaled + slotSpacingScaled) * index,
|
||||
index
|
||||
).also { addWidget(it) }
|
||||
}
|
||||
|
||||
private val descriptionList = MoveDescriptionScrollList(
|
||||
descriptionListX,
|
||||
descriptionListY,
|
||||
5
|
||||
)
|
||||
|
||||
private val movePicker = MovePickerList(
|
||||
baseX + baseScaledWidth + MOVE_LIST_X_OFFSET,
|
||||
baseY + (11 * BASE_SCALE).toInt() + MOVE_LIST_Y_OFFSET
|
||||
)
|
||||
|
||||
private var selectedSlotIndex = -1
|
||||
private var pickerSlotIndex = -1
|
||||
private var movePickerOpen = false
|
||||
private var lastDescriptionKey: String? = null
|
||||
|
||||
init {
|
||||
refreshMoves()
|
||||
}
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val matrices = context.pose()
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = movesBaseResource,
|
||||
x = baseX,
|
||||
y = baseY,
|
||||
width = baseScaledWidth,
|
||||
height = baseScaledHeight
|
||||
)
|
||||
|
||||
slots.forEach { it.render(context, mouseX, mouseY, delta) }
|
||||
|
||||
renderMoveInfo(context)
|
||||
renderDescription(context, mouseX, mouseY, delta)
|
||||
|
||||
if (movePickerOpen) {
|
||||
movePicker.renderWidget(context, mouseX, mouseY, delta)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (movePickerOpen) {
|
||||
val handled = movePicker.mouseClicked(mouseX, mouseY, button)
|
||||
if (handled) return true
|
||||
if (!movePicker.isMouseOver(mouseX, mouseY)) {
|
||||
closeMovePicker()
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptionList.isHovered) {
|
||||
descriptionList.mouseClicked(mouseX, mouseY, button)
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
|
||||
if (movePickerOpen) {
|
||||
if (movePicker.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) return true
|
||||
}
|
||||
if (descriptionList.isHovered) {
|
||||
descriptionList.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
|
||||
}
|
||||
|
||||
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
|
||||
if (movePickerOpen) {
|
||||
if (movePicker.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true
|
||||
}
|
||||
if (descriptionList.isHovered) {
|
||||
descriptionList.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
|
||||
private fun refreshMoves() {
|
||||
val moves = Moves.all().sortedBy { it.displayName.string }
|
||||
movePicker.setEntries(moves)
|
||||
}
|
||||
|
||||
private fun selectedMove(): MoveTemplate? {
|
||||
return if (selectedSlotIndex in draft.activeMoves.indices) draft.activeMoves[selectedSlotIndex] else null
|
||||
}
|
||||
|
||||
private fun renderMoveInfo(context: GuiGraphics) {
|
||||
val move = selectedMove()
|
||||
val mcFont = Minecraft.getInstance().font
|
||||
|
||||
// Power
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = movesPowerIconResource,
|
||||
x = (baseX + (7 * BASE_SCALE)) / SCALE,
|
||||
y = (baseY + (114.5 * BASE_SCALE)) / SCALE,
|
||||
width = 10,
|
||||
height = 10,
|
||||
scale = SCALE
|
||||
)
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = lang("ui.power"),
|
||||
x = baseX + (14 * BASE_SCALE),
|
||||
y = baseY + (115 * BASE_SCALE),
|
||||
scale = SCALE * BASE_SCALE,
|
||||
shadow = true
|
||||
)
|
||||
val powerText = if (move != null && move.power.toInt() > 0) move.power.toInt().toString() else "-"
|
||||
val powerComponent = Component.literal(powerText)
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = powerComponent,
|
||||
x = (baseX + (62.5 * BASE_SCALE)) - (mcFont.width(powerComponent) * SCALE * BASE_SCALE),
|
||||
y = baseY + (115 * BASE_SCALE),
|
||||
scale = SCALE * BASE_SCALE,
|
||||
shadow = true
|
||||
)
|
||||
|
||||
// Accuracy
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = movesAccuracyIconResource,
|
||||
x = (baseX + (7 * BASE_SCALE)) / SCALE,
|
||||
y = (baseY + (125.5 * BASE_SCALE)) / SCALE,
|
||||
width = 10,
|
||||
height = 10,
|
||||
scale = SCALE
|
||||
)
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = lang("ui.accuracy"),
|
||||
x = baseX + (14 * BASE_SCALE),
|
||||
y = baseY + (126 * BASE_SCALE),
|
||||
scale = SCALE * BASE_SCALE,
|
||||
shadow = true
|
||||
)
|
||||
val accuracyText = if (move != null) formatPercent(move.accuracy) else "-"
|
||||
val accuracyComponent = Component.literal(accuracyText)
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = accuracyComponent,
|
||||
x = (baseX + (62.5 * BASE_SCALE)) - (mcFont.width(accuracyComponent) * SCALE * BASE_SCALE),
|
||||
y = baseY + (126 * BASE_SCALE),
|
||||
scale = SCALE * BASE_SCALE,
|
||||
shadow = true
|
||||
)
|
||||
|
||||
// Category
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = movesEffectIconResource,
|
||||
x = (baseX + (7 * BASE_SCALE)) / SCALE,
|
||||
y = (baseY + (136.5 * BASE_SCALE)) / SCALE,
|
||||
width = 10,
|
||||
height = 10,
|
||||
scale = SCALE
|
||||
)
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = lang("ui.category"),
|
||||
x = baseX + (14 * BASE_SCALE),
|
||||
y = baseY + (137 * BASE_SCALE),
|
||||
scale = SCALE * BASE_SCALE,
|
||||
shadow = true
|
||||
)
|
||||
if (move != null) {
|
||||
MoveCategoryIcon(
|
||||
x = baseX + (51 * BASE_SCALE),
|
||||
y = baseY + (137 * BASE_SCALE),
|
||||
category = move.damageCategory
|
||||
).render(context)
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = move.damageCategory.displayName.copy(),
|
||||
x = baseX + (61 * BASE_SCALE),
|
||||
y = baseY + (137 * BASE_SCALE),
|
||||
scale = SCALE * BASE_SCALE,
|
||||
shadow = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderDescription(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val move = selectedMove()
|
||||
val nextKey = move?.name ?: ""
|
||||
if (nextKey != lastDescriptionKey) {
|
||||
descriptionList.setMoveDescription(move?.description ?: Component.literal(""))
|
||||
lastDescriptionKey = nextKey
|
||||
}
|
||||
if (move != null) {
|
||||
descriptionList.renderWidget(context, mouseX, mouseY, delta)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPercent(input: Double): String {
|
||||
if (input <= 0) return "-"
|
||||
return "${decimalFormat.format(input)}%"
|
||||
}
|
||||
|
||||
private fun toggleMovePicker(slotIndex: Int) {
|
||||
selectedSlotIndex = slotIndex
|
||||
if (movePickerOpen && pickerSlotIndex == slotIndex) {
|
||||
closeMovePicker()
|
||||
return
|
||||
}
|
||||
movePickerOpen = true
|
||||
pickerSlotIndex = slotIndex
|
||||
}
|
||||
|
||||
private fun closeMovePicker() {
|
||||
movePickerOpen = false
|
||||
pickerSlotIndex = -1
|
||||
}
|
||||
|
||||
private fun assignMove(slotIndex: Int, move: MoveTemplate) {
|
||||
draft.activeMoves.indices.forEach { index ->
|
||||
if (index != slotIndex && draft.activeMoves[index] == move) {
|
||||
draft.activeMoves[index] = null
|
||||
}
|
||||
}
|
||||
draft.activeMoves[slotIndex] = move
|
||||
}
|
||||
|
||||
private fun reorderMove(slotIndex: Int, up: Boolean) {
|
||||
val size = draft.activeMoves.size
|
||||
if (size == 0) return
|
||||
val target = if (up) {
|
||||
if (slotIndex > 0) slotIndex - 1 else size - 1
|
||||
} else {
|
||||
if (slotIndex < size - 1) slotIndex + 1 else 0
|
||||
}
|
||||
val temp = draft.activeMoves[target]
|
||||
draft.activeMoves[target] = draft.activeMoves[slotIndex]
|
||||
draft.activeMoves[slotIndex] = temp
|
||||
|
||||
if (selectedSlotIndex == slotIndex) selectedSlotIndex = target
|
||||
else if (selectedSlotIndex == target) selectedSlotIndex = slotIndex
|
||||
|
||||
if (pickerSlotIndex == slotIndex) pickerSlotIndex = target
|
||||
else if (pickerSlotIndex == target) pickerSlotIndex = slotIndex
|
||||
}
|
||||
|
||||
private inner class MoveSlot(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
private val slotIndex: Int
|
||||
) : SoundlessWidget(pX, pY, moveWidthScaled, moveHeightScaled, Component.literal("move-slot-$slotIndex")) {
|
||||
|
||||
private val moveUpButton = ReorderMoveButton(x, y, true) {
|
||||
reorderMove(slotIndex, true)
|
||||
}.apply { addWidget(this) }
|
||||
|
||||
private val moveDownButton = ReorderMoveButton(x, y, false) {
|
||||
reorderMove(slotIndex, false)
|
||||
}.apply { addWidget(this) }
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val move = draft.activeMoves[slotIndex]
|
||||
val matrices = context.pose()
|
||||
isHovered = mouseX >= x && mouseY >= y && mouseX < x + width && mouseY < y + height
|
||||
|
||||
if (move != null) {
|
||||
val rgb = move.elementalType.hue.toRGB()
|
||||
|
||||
if (selectedSlotIndex == slotIndex) {
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = moveSelectedOverlayResource,
|
||||
x = x - 1,
|
||||
y = y - 1,
|
||||
width = moveWidthScaled + 2,
|
||||
height = moveHeightScaled + 2
|
||||
)
|
||||
}
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = moveResource,
|
||||
x = x,
|
||||
y = y,
|
||||
width = moveWidthScaled,
|
||||
height = moveHeightScaled,
|
||||
vOffset = if (isHovered) moveHeightScaled else 0,
|
||||
textureHeight = moveHeightScaled * 2,
|
||||
red = rgb.first,
|
||||
green = rgb.second,
|
||||
blue = rgb.third
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = moveOverlayResource,
|
||||
x = x,
|
||||
y = y,
|
||||
width = moveWidthScaled,
|
||||
height = moveHeightScaled
|
||||
)
|
||||
|
||||
val ppText = Component.literal("${move.pp}/${move.maxPp}").bold()
|
||||
drawScaledText(
|
||||
context = context,
|
||||
font = CobblemonResources.DEFAULT_LARGE,
|
||||
text = ppText,
|
||||
x = x + (93 * BASE_SCALE),
|
||||
y = y + (13 * BASE_SCALE),
|
||||
centered = true,
|
||||
scale = BASE_SCALE
|
||||
)
|
||||
|
||||
TypeIcon(
|
||||
x = x + (2 * BASE_SCALE),
|
||||
y = y + (2 * BASE_SCALE),
|
||||
type = move.elementalType
|
||||
).render(context)
|
||||
|
||||
MoveCategoryIcon(
|
||||
x = x + (66 * BASE_SCALE),
|
||||
y = y + (13.5 * BASE_SCALE),
|
||||
category = move.damageCategory
|
||||
).render(context)
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
font = CobblemonResources.DEFAULT_LARGE,
|
||||
text = move.displayName.bold(),
|
||||
x = x + (28 * BASE_SCALE),
|
||||
y = y + (2 * BASE_SCALE),
|
||||
shadow = true,
|
||||
scale = BASE_SCALE
|
||||
)
|
||||
|
||||
moveUpButton.render(context, mouseX, mouseY, delta)
|
||||
moveDownButton.render(context, mouseX, mouseY, delta)
|
||||
}
|
||||
|
||||
renderSwapButton(context, mouseX, mouseY, move)
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (isSwapHovered(mouseX, mouseY)) {
|
||||
toggleMovePicker(slotIndex)
|
||||
return true
|
||||
}
|
||||
if (isHovered) {
|
||||
selectedSlotIndex = slotIndex
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
private fun renderSwapButton(context: GuiGraphics, mouseX: Int, mouseY: Int, move: MoveTemplate?) {
|
||||
val selected = movePickerOpen && pickerSlotIndex == slotIndex
|
||||
val swapOffsetX = 114.5F * BASE_SCALE
|
||||
val swapOffsetY = 6.5F * BASE_SCALE
|
||||
val swapWidth = 12F * BASE_SCALE
|
||||
val swapHeight = 18F * BASE_SCALE
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = if (move == null) moveAddButtonResource else moveSwapButtonResource,
|
||||
x = (x + swapOffsetX) / SCALE,
|
||||
y = (y + swapOffsetY) / SCALE,
|
||||
width = swapWidth,
|
||||
height = swapHeight,
|
||||
vOffset = if (isSwapHovered(mouseX.toDouble(), mouseY.toDouble()) || selected) swapHeight else 0F,
|
||||
textureHeight = swapHeight * 2,
|
||||
scale = SCALE
|
||||
)
|
||||
}
|
||||
|
||||
private fun isSwapHovered(mouseX: Double, mouseY: Double): Boolean {
|
||||
val swapOffsetX = 114.5F * BASE_SCALE
|
||||
val swapOffsetY = 6.5F * BASE_SCALE
|
||||
val swapWidth = 12F * BASE_SCALE
|
||||
val swapHeight = 18F * BASE_SCALE
|
||||
val swapX = x + swapOffsetX
|
||||
val swapY = y + swapOffsetY
|
||||
return mouseX.toFloat() in (swapX..(swapX + (swapWidth * SCALE)))
|
||||
&& mouseY.toFloat() in (swapY..(swapY + (swapHeight * SCALE) - 0.5F))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MovePickerList(
|
||||
listX: Int,
|
||||
listY: Int
|
||||
) : SummaryScrollList<MovePickerList.MoveEntry>(
|
||||
listX,
|
||||
listY,
|
||||
lang("ui.moves.switch"),
|
||||
MOVE_LIST_SLOT_HEIGHT + MOVE_LIST_SLOT_SPACING
|
||||
) {
|
||||
private var entries: List<MoveTemplate> = emptyList()
|
||||
|
||||
fun setEntries(moves: List<MoveTemplate>) {
|
||||
clearEntries()
|
||||
entries = moves
|
||||
moves.forEach { move -> addEntry(MoveEntry(move)) }
|
||||
scrollAmount = 0.0
|
||||
}
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) {
|
||||
if (!movePickerOpen) return
|
||||
super.renderWidget(context, mouseX, mouseY, partialTicks)
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (!movePickerOpen) return false
|
||||
return super.mouseClicked(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
|
||||
if (!movePickerOpen) return false
|
||||
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
|
||||
}
|
||||
|
||||
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
|
||||
if (!movePickerOpen) return false
|
||||
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
|
||||
inner class MoveEntry(private val move: MoveTemplate) : Entry<MoveEntry>() {
|
||||
|
||||
override fun getNarration(): MutableComponent = move.displayName.copy()
|
||||
|
||||
override fun render(
|
||||
context: GuiGraphics,
|
||||
index: Int,
|
||||
rowTop: Int,
|
||||
rowLeft: Int,
|
||||
rowWidth: Int,
|
||||
rowHeight: Int,
|
||||
mouseX: Int,
|
||||
mouseY: Int,
|
||||
isHovered: Boolean,
|
||||
partialTicks: Float
|
||||
) {
|
||||
val matrices = context.pose()
|
||||
val tweakedRowTop = rowTop - (MOVE_LIST_SLOT_SPACING / 2) + 1
|
||||
val rgb = move.elementalType.hue.toRGB()
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = moveListResource,
|
||||
x = rowLeft,
|
||||
y = tweakedRowTop,
|
||||
height = MOVE_LIST_SLOT_HEIGHT,
|
||||
width = rowWidth,
|
||||
vOffset = if (isHovered) MOVE_LIST_SLOT_HEIGHT else 0,
|
||||
textureHeight = MOVE_LIST_SLOT_HEIGHT * 2,
|
||||
red = rgb.first,
|
||||
green = rgb.second,
|
||||
blue = rgb.third
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = moveListOverlayResource,
|
||||
x = rowLeft,
|
||||
y = tweakedRowTop,
|
||||
height = MOVE_LIST_SLOT_HEIGHT,
|
||||
width = rowWidth
|
||||
)
|
||||
|
||||
TypeIcon(
|
||||
x = rowLeft - 9,
|
||||
y = tweakedRowTop,
|
||||
type = move.elementalType
|
||||
).render(context)
|
||||
|
||||
MoveCategoryIcon(
|
||||
x = rowLeft + 77,
|
||||
y = tweakedRowTop + 1.5,
|
||||
category = move.damageCategory
|
||||
).render(context)
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = move.displayName,
|
||||
x = rowLeft + 14,
|
||||
y = tweakedRowTop + 3.5,
|
||||
scale = SCALE,
|
||||
shadow = true
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = movesPowerIconResource,
|
||||
x = (rowLeft + 10) / SCALE,
|
||||
y = (tweakedRowTop + 11) / SCALE,
|
||||
width = 10,
|
||||
height = 10,
|
||||
scale = SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = movesAccuracyIconResource,
|
||||
x = (rowLeft + 30) / SCALE,
|
||||
y = (tweakedRowTop + 11) / SCALE,
|
||||
width = 10,
|
||||
height = 10,
|
||||
scale = SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = movesEffectIconResource,
|
||||
x = (rowLeft + 53.5) / SCALE,
|
||||
y = (tweakedRowTop + 11) / SCALE,
|
||||
width = 10,
|
||||
height = 10,
|
||||
scale = SCALE
|
||||
)
|
||||
|
||||
val movePower = if (move.power.toInt() > 0) move.power.toInt().toString() else "-"
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = Component.literal(movePower),
|
||||
x = rowLeft + 16.5,
|
||||
y = tweakedRowTop + 12,
|
||||
scale = SCALE,
|
||||
shadow = true
|
||||
)
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = Component.literal(formatPercent(move.accuracy)),
|
||||
x = rowLeft + 37,
|
||||
y = tweakedRowTop + 12,
|
||||
scale = SCALE,
|
||||
shadow = true
|
||||
)
|
||||
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = Component.literal(formatPercent(move.effectChances.firstOrNull() ?: 0.0)),
|
||||
x = rowLeft + 60.5,
|
||||
y = tweakedRowTop + 12,
|
||||
scale = SCALE,
|
||||
shadow = true
|
||||
)
|
||||
|
||||
val pp = move.maxPp
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = lang("ui.moves.pp", pp),
|
||||
x = rowLeft + 76,
|
||||
y = tweakedRowTop + 12,
|
||||
scale = SCALE,
|
||||
shadow = true
|
||||
)
|
||||
}
|
||||
|
||||
override fun mouseClicked(d: Double, e: Double, i: Int): Boolean {
|
||||
if (isMouseOver(d, e) && pickerSlotIndex >= 0) {
|
||||
assignMove(pickerSlotIndex, move)
|
||||
selectedSlotIndex = pickerSlotIndex
|
||||
closeMovePicker()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.text.text
|
||||
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
|
||||
import com.cobblemon.mod.common.client.gui.ScrollingWidget
|
||||
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_BASE_HEIGHT
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SIZE
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SPACING
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.pokemon.RenderablePokemon
|
||||
import com.cobblemon.mod.common.pokemon.Species
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.resources.sounds.SimpleSoundInstance
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.util.Mth
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector3f
|
||||
|
||||
class PokeEditSpeciesList(
|
||||
val pX: Int,
|
||||
val pY: Int,
|
||||
val onSelect: (Species) -> Unit
|
||||
) : ScrollingWidget<PokeEditSpeciesList.SpeciesRow>(
|
||||
width = 139,
|
||||
height = SCROLL_BASE_HEIGHT,
|
||||
left = pX,
|
||||
top = pY - SCROLL_BASE_HEIGHT,
|
||||
slotHeight = SCROLL_SLOT_SIZE + 2
|
||||
) {
|
||||
private var entries: List<Species> = emptyList()
|
||||
|
||||
fun setEntries(species: List<Species>) {
|
||||
clearEntries()
|
||||
entries = species
|
||||
species.chunked(5).forEach { chunk ->
|
||||
addEntry(SpeciesRow(chunk, onSelect))
|
||||
}
|
||||
scrollAmount = 0.0
|
||||
}
|
||||
|
||||
override fun addEntry(entry: SpeciesRow): Int = super.addEntry(entry)
|
||||
|
||||
override fun getScrollbarPosition(): Int = pX + width - 3
|
||||
|
||||
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
val xLeft = this.scrollbarPosition
|
||||
val xRight = xLeft + 3
|
||||
val barHeight = this.bottom - this.y
|
||||
|
||||
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
|
||||
yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
|
||||
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
|
||||
if (yTop < this.y) yTop = this.y
|
||||
|
||||
context.fill(xLeft, this.y + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
|
||||
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
|
||||
}
|
||||
|
||||
class SpeciesRow(
|
||||
private val speciesList: List<Species>,
|
||||
private val onSelect: (Species) -> Unit
|
||||
) : Slot<SpeciesRow>() {
|
||||
companion object {
|
||||
private val slotResource = cobblemonResource("textures/gui/pokedex/pokedex_slot.png")
|
||||
private val slotHighlight = cobblemonResource("textures/gui/pokedex/slot_select.png")
|
||||
}
|
||||
|
||||
private val runtime = MoLangRuntime().setupClient().setup().also {
|
||||
it.environment.query.addFunction("get_pokedex") { null }
|
||||
}
|
||||
|
||||
private var x: Int = 0
|
||||
private var y: Int = 0
|
||||
|
||||
override fun render(
|
||||
context: GuiGraphics,
|
||||
index: Int,
|
||||
y: Int,
|
||||
x: Int,
|
||||
entryWidth: Int,
|
||||
entryHeight: Int,
|
||||
mouseX: Int,
|
||||
mouseY: Int,
|
||||
hovered: Boolean,
|
||||
tickDelta: Float
|
||||
) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
|
||||
speciesList.forEachIndexed { indexInRow, species ->
|
||||
val startPosX = x + ((SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * indexInRow)
|
||||
val startPosY = y + SCROLL_SLOT_SPACING + 1
|
||||
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = slotResource,
|
||||
x = startPosX,
|
||||
y = startPosY,
|
||||
width = SCROLL_SLOT_SIZE,
|
||||
height = SCROLL_SLOT_SIZE
|
||||
)
|
||||
|
||||
if (getHoveredSlotIndex(mouseX, mouseY) == indexInRow) {
|
||||
blitk(
|
||||
matrixStack = context.pose(),
|
||||
texture = slotHighlight,
|
||||
x = startPosX,
|
||||
y = startPosY,
|
||||
width = SCROLL_SLOT_SIZE,
|
||||
height = SCROLL_SLOT_SIZE,
|
||||
vOffset = SCROLL_SLOT_SIZE,
|
||||
textureHeight = SCROLL_SLOT_SIZE * 2
|
||||
)
|
||||
}
|
||||
|
||||
val form = species.standardForm
|
||||
val aspects = mutableSetOf<String>()
|
||||
aspects.addAll(form.aspects)
|
||||
if (form.maleRatio in 0F..1F) {
|
||||
aspects.add(Gender.MALE.name.lowercase())
|
||||
} else {
|
||||
aspects.add(Gender.GENDERLESS.name.lowercase())
|
||||
}
|
||||
|
||||
context.enableScissor(
|
||||
startPosX + 1,
|
||||
startPosY + 1,
|
||||
startPosX + SCROLL_SLOT_SIZE - 1,
|
||||
startPosY + SCROLL_SLOT_SIZE - 2
|
||||
)
|
||||
|
||||
val state = FloatingState().also { it.currentAspects = aspects }
|
||||
val matrices = context.pose()
|
||||
matrices.pushPose()
|
||||
matrices.translate(startPosX + (SCROLL_SLOT_SIZE / 2.0), startPosY + 1.0, 0.0)
|
||||
matrices.scale(2.5F, 2.5F, 1F)
|
||||
drawProfilePokemon(
|
||||
renderablePokemon = RenderablePokemon(species, aspects),
|
||||
matrixStack = matrices,
|
||||
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
|
||||
state = state,
|
||||
partialTicks = 0F,
|
||||
scale = 4.5F
|
||||
)
|
||||
matrices.popPose()
|
||||
context.disableScissor()
|
||||
|
||||
matrices.pushPose()
|
||||
matrices.translate(0.0, 0.0, 100.0)
|
||||
val pokemonNumber = species.nationalPokedexNumber.toString().padStart(4, '0').text()
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = pokemonNumber,
|
||||
x = startPosX + 1.5,
|
||||
y = startPosY + 2.5,
|
||||
shadow = true,
|
||||
scale = SCALE
|
||||
)
|
||||
matrices.popPose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
val hoverIndex = getHoveredSlotIndex(mouseX.toInt(), mouseY.toInt())
|
||||
if (hoverIndex in speciesList.indices) {
|
||||
onSelect(speciesList[hoverIndex])
|
||||
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(com.cobblemon.mod.common.CobblemonSounds.POKEDEX_CLICK, 1.0F))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getHoveredSlotIndex(mouseX: Int, mouseY: Int): Int {
|
||||
speciesList.forEachIndexed { index, _ ->
|
||||
val startPosX = x + ((SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * index)
|
||||
val startPosY = y + SCROLL_SLOT_SPACING + 1
|
||||
if (mouseX in startPosX..(startPosX + SCROLL_SLOT_SIZE)
|
||||
&& mouseY in startPosY..(startPosY + SCROLL_SLOT_SIZE)
|
||||
) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun getNarration(): Component {
|
||||
return if (speciesList.isNotEmpty()) {
|
||||
"${speciesList.first().name}-${speciesList.last().name}".text()
|
||||
} else {
|
||||
"".text()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
|
||||
|
||||
import com.bedrockk.molang.runtime.value.DoubleValue
|
||||
import com.cobblemon.mod.common.api.gui.blitk
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
|
||||
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
|
||||
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
|
||||
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
|
||||
import com.cobblemon.mod.common.client.render.drawScaledText
|
||||
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.pokemon.Gender
|
||||
import com.cobblemon.mod.common.pokemon.RenderablePokemon
|
||||
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
|
||||
import com.cobblemon.mod.common.util.resolveFloat
|
||||
import com.mojang.math.Axis
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import com.mojang.blaze3d.platform.Lighting
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.client.gui.components.AbstractWidget
|
||||
import net.minecraft.client.gui.components.EditBox
|
||||
import net.minecraft.client.renderer.LightTexture
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector3f
|
||||
import java.util.Locale
|
||||
|
||||
class PokeEditWalkSpeedTab(
|
||||
pX: Int,
|
||||
pY: Int,
|
||||
private val draft: PokeEditDraft
|
||||
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, Component.literal("Walk Speed")) {
|
||||
companion object {
|
||||
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
|
||||
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
|
||||
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
|
||||
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
|
||||
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
|
||||
|
||||
private const val POKEBALL_OVERLAY_Y = 25
|
||||
private const val PLATFORM_Y = 69
|
||||
private const val SHADOW_X = 25
|
||||
private const val SHADOW_Y = 76
|
||||
private const val VIEW_SCALE = 1.2F
|
||||
private const val VIEW_SHIFT_X = 0F
|
||||
private const val VIEW_SHIFT_Y = 36F
|
||||
private const val MODEL_VIEW_SCALE = 2.4F
|
||||
private const val MODEL_DRAW_SCALE = 24F
|
||||
private const val MODEL_OFFSET_Y = 20.0
|
||||
|
||||
private const val SPEED_INPUT_WIDTH = 40
|
||||
private const val SPEED_INPUT_HEIGHT = 10
|
||||
private const val SPEED_LABEL_SCALE = 0.5F
|
||||
private const val BUTTON_WIDTH = 54
|
||||
private const val BUTTON_HEIGHT = 15
|
||||
|
||||
private const val BLOCK_SPACING = 1.0F
|
||||
private const val BLOCK_PLANE_OFFSET = 2.3
|
||||
private const val BLOCK_COLUMNS = 7
|
||||
private const val BLOCK_ROWS = 3
|
||||
}
|
||||
|
||||
private val state = FloatingState().also { state ->
|
||||
state.runtime.environment.query.addFunction("is_holding_item") {
|
||||
val held = currentHeldStack()
|
||||
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
|
||||
}
|
||||
state.runtime.environment.query.addFunction("is_wearing_hat") {
|
||||
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
|
||||
}
|
||||
state.runtime.environment.query.addFunction("is_wearing_face") {
|
||||
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
|
||||
}
|
||||
}
|
||||
private val heldItemRenderer = HeldItemRenderer()
|
||||
private var renderablePokemon: RenderablePokemon? = null
|
||||
private var rotationY = 30F
|
||||
private var poseList: Array<PoseType> = arrayOf(PoseType.WALK, PoseType.PROFILE)
|
||||
private var selectedPoseIndex: Int = 0
|
||||
private var oldDragPosX = 0.0
|
||||
private var canDragRender = false
|
||||
private var isDraggingRender = false
|
||||
|
||||
private var cachedSpeedKey: String? = null
|
||||
private var defaultWalkSpeed = 0.35F
|
||||
private var customWalkSpeed: Float? = null
|
||||
private var syncingSpeedInput = false
|
||||
|
||||
private val walkSpeedInput = EditBox(
|
||||
Minecraft.getInstance().font,
|
||||
pX + 6,
|
||||
pY + 18,
|
||||
SPEED_INPUT_WIDTH,
|
||||
SPEED_INPUT_HEIGHT,
|
||||
Component.literal("speed")
|
||||
).also { editBox ->
|
||||
editBox.setBordered(false)
|
||||
editBox.setTextColor(0xFFFFFF)
|
||||
editBox.setTextColorUneditable(0xFFFFFF)
|
||||
editBox.setMaxLength(6)
|
||||
editBox.active = true
|
||||
editBox.visible = true
|
||||
editBox.setFilter { value ->
|
||||
value.isBlank() || value.matches(Regex("\\d*(\\.\\d*)?"))
|
||||
}
|
||||
editBox.setResponder { value ->
|
||||
if (syncingSpeedInput) return@setResponder
|
||||
customWalkSpeed = value.toFloatOrNull()
|
||||
}
|
||||
addWidget(editBox)
|
||||
}
|
||||
|
||||
private val resetSpeedButton = SummaryButton(
|
||||
buttonX = pX + 52F,
|
||||
buttonY = pY + 14F,
|
||||
buttonWidth = BUTTON_WIDTH,
|
||||
buttonHeight = BUTTON_HEIGHT,
|
||||
clickAction = { resetWalkSpeed() },
|
||||
text = Component.literal("Reset"),
|
||||
resource = actionButtonResource,
|
||||
largeText = false,
|
||||
boldText = false,
|
||||
textScale = 0.7F
|
||||
).also { addWidget(it) }
|
||||
|
||||
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
refreshDefaultWalkSpeed()
|
||||
walkSpeedInput.active = true
|
||||
walkSpeedInput.visible = true
|
||||
if (!walkSpeedInput.isFocused && customWalkSpeed == null) {
|
||||
val formatted = formatSpeed(defaultWalkSpeed)
|
||||
if (walkSpeedInput.value != formatted) {
|
||||
syncingSpeedInput = true
|
||||
walkSpeedInput.value = formatted
|
||||
syncingSpeedInput = false
|
||||
}
|
||||
}
|
||||
|
||||
val matrices = context.pose()
|
||||
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
|
||||
|
||||
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
|
||||
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
|
||||
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
|
||||
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
|
||||
val scaledBaseX = viewX / VIEW_SCALE
|
||||
val scaledBaseY = viewY / VIEW_SCALE
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = backgroundOverlay,
|
||||
x = scaledBaseX,
|
||||
y = scaledBaseY,
|
||||
width = HALF_OVERLAY_WIDTH,
|
||||
height = HALF_OVERLAY_HEIGHT,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = pokeBallOverlay,
|
||||
x = scaledBaseX + 15,
|
||||
y = scaledBaseY + POKEBALL_OVERLAY_Y,
|
||||
width = 109,
|
||||
height = 68,
|
||||
vOffset = 0,
|
||||
textureHeight = 1744,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = platformBase,
|
||||
x = scaledBaseX + 13,
|
||||
y = scaledBaseY + PLATFORM_Y,
|
||||
width = 113,
|
||||
height = 24,
|
||||
textureHeight = 30,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
blitk(
|
||||
matrixStack = matrices,
|
||||
texture = platformShadow,
|
||||
x = scaledBaseX + SHADOW_X,
|
||||
y = scaledBaseY + SHADOW_Y,
|
||||
width = 90,
|
||||
height = 20,
|
||||
scale = VIEW_SCALE
|
||||
)
|
||||
|
||||
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
|
||||
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
|
||||
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
|
||||
|
||||
context.disableScissor()
|
||||
|
||||
walkSpeedInput.render(context, mouseX, mouseY, delta)
|
||||
resetSpeedButton.render(context, mouseX, mouseY, delta)
|
||||
drawScaledText(
|
||||
context = context,
|
||||
text = Component.literal("Walk Speed"),
|
||||
x = x + 6,
|
||||
y = y + 6,
|
||||
scale = SPEED_LABEL_SCALE,
|
||||
colour = 0xFFFFFFFF.toInt(),
|
||||
shadow = false
|
||||
)
|
||||
|
||||
buildRenderable()?.let { renderable ->
|
||||
state.currentAspects = renderable.aspects
|
||||
context.enableScissor(
|
||||
x + 1,
|
||||
y + 1,
|
||||
x + HALF_OVERLAY_WIDTH - 1,
|
||||
y + HALF_OVERLAY_HEIGHT - 1
|
||||
)
|
||||
|
||||
matrices.pushPose()
|
||||
matrices.translate(
|
||||
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
|
||||
y.toDouble() + MODEL_OFFSET_Y,
|
||||
1000.0
|
||||
)
|
||||
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
|
||||
|
||||
renderWalkBlocks(context, renderable, effectiveWalkSpeed(), delta)
|
||||
|
||||
val rotationVector = Vector3f(13F, rotationY, 0F)
|
||||
drawProfilePokemon(
|
||||
renderablePokemon = renderable,
|
||||
poseType = poseList[selectedPoseIndex],
|
||||
matrixStack = matrices,
|
||||
partialTicks = delta,
|
||||
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
|
||||
state = state,
|
||||
scale = MODEL_DRAW_SCALE
|
||||
)
|
||||
if (!renderable.heldItem.isEmpty) {
|
||||
heldItemRenderer.renderOnModel(
|
||||
renderable.heldItem,
|
||||
state,
|
||||
matrices,
|
||||
context.bufferSource(),
|
||||
light = 0xF000F0,
|
||||
frontLight = true
|
||||
)
|
||||
}
|
||||
matrices.popPose()
|
||||
context.disableScissor()
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
if (isWithinViewport(mouseX, mouseY)) {
|
||||
canDragRender = true
|
||||
isDraggingRender = true
|
||||
oldDragPosX = mouseX
|
||||
return true
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button)
|
||||
}
|
||||
|
||||
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
|
||||
if (canDragRender) {
|
||||
canDragRender = false
|
||||
}
|
||||
if (isDraggingRender) {
|
||||
isDraggingRender = false
|
||||
}
|
||||
return super.mouseReleased(pMouseX, pMouseY, pButton)
|
||||
}
|
||||
|
||||
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
|
||||
val handledDrag = isDraggingRender && canDragRender
|
||||
if (handledDrag) {
|
||||
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
|
||||
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
|
||||
}
|
||||
oldDragPosX = mouseX
|
||||
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
|
||||
}
|
||||
|
||||
private fun resetWalkSpeed() {
|
||||
customWalkSpeed = null
|
||||
syncingSpeedInput = true
|
||||
walkSpeedInput.value = formatSpeed(defaultWalkSpeed)
|
||||
syncingSpeedInput = false
|
||||
}
|
||||
|
||||
private fun refreshDefaultWalkSpeed() {
|
||||
val key = "${draft.species.resourceIdentifier}#${draft.form.formOnlyShowdownId()}"
|
||||
if (key == cachedSpeedKey) return
|
||||
cachedSpeedKey = key
|
||||
defaultWalkSpeed = resolveDefaultWalkSpeed()
|
||||
if (customWalkSpeed == null) {
|
||||
syncingSpeedInput = true
|
||||
walkSpeedInput.value = formatSpeed(defaultWalkSpeed)
|
||||
syncingSpeedInput = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveDefaultWalkSpeed(): Float {
|
||||
return try {
|
||||
state.runtime.resolveFloat(draft.form.behaviour.moving.walk.walkSpeed).coerceAtLeast(0.0F)
|
||||
} catch (_: Exception) {
|
||||
0.35F
|
||||
}
|
||||
}
|
||||
|
||||
private fun effectiveWalkSpeed(): Float {
|
||||
val speed = customWalkSpeed ?: defaultWalkSpeed
|
||||
return if (speed.isFinite() && speed > 0F) speed else defaultWalkSpeed
|
||||
}
|
||||
|
||||
private fun formatSpeed(value: Float): String = String.format(Locale.US, "%.2f", value)
|
||||
|
||||
private fun renderWalkBlocks(context: GuiGraphics, renderable: RenderablePokemon, speed: Float, delta: Float) {
|
||||
val model = VaryingModelRepository.getPoser(renderable.species.resourceIdentifier, state)
|
||||
val ticks = (Minecraft.getInstance().level?.gameTime ?: (System.currentTimeMillis() / 50L)).toFloat() + delta
|
||||
val offset = (ticks * speed) % BLOCK_SPACING
|
||||
val travel = offset
|
||||
val halfCols = BLOCK_COLUMNS / 2
|
||||
val halfRows = BLOCK_ROWS / 2
|
||||
val blockState = Blocks.GRASS_BLOCK.defaultBlockState()
|
||||
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
|
||||
val packedLight = LightTexture.pack(15, 15)
|
||||
val hitboxHeight = draft.form.hitbox.height * draft.form.baseScale
|
||||
val blockPlaneY = (-hitboxHeight * 0.5F + BLOCK_PLANE_OFFSET).toDouble()
|
||||
|
||||
val matrices = context.pose()
|
||||
matrices.pushPose()
|
||||
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
|
||||
matrices.translate(
|
||||
model.profileTranslation.x,
|
||||
model.profileTranslation.y + 1.5 * model.profileScale,
|
||||
model.profileTranslation.z - 4.0
|
||||
)
|
||||
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
|
||||
matrices.translate(0.0, blockPlaneY, 0.0)
|
||||
val rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, rotationY, 0F))
|
||||
matrices.mulPose(rotation)
|
||||
|
||||
RenderSystem.applyModelViewMatrix()
|
||||
RenderSystem.enableDepthTest()
|
||||
RenderSystem.disableCull()
|
||||
Lighting.setupForEntityInInventory()
|
||||
|
||||
for (xIndex in -halfRows..halfRows) {
|
||||
for (zIndex in -halfCols..halfCols) {
|
||||
val z = zIndex * BLOCK_SPACING + travel
|
||||
matrices.pushPose()
|
||||
matrices.translate((xIndex * BLOCK_SPACING).toDouble(), 0.0, z.toDouble())
|
||||
matrices.mulPose(Axis.XP.rotationDegrees(180F))
|
||||
Minecraft.getInstance().blockRenderer.renderSingleBlock(
|
||||
blockState,
|
||||
matrices,
|
||||
bufferSource,
|
||||
packedLight,
|
||||
OverlayTexture.NO_OVERLAY
|
||||
)
|
||||
matrices.popPose()
|
||||
}
|
||||
}
|
||||
bufferSource.endBatch()
|
||||
Lighting.setupFor3DItems()
|
||||
RenderSystem.enableCull()
|
||||
matrices.popPose()
|
||||
}
|
||||
|
||||
private fun buildRenderable(): RenderablePokemon? {
|
||||
val species = draft.species
|
||||
val form = draft.form
|
||||
val aspects = mutableSetOf<String>()
|
||||
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
|
||||
aspects.addAll(form.aspects)
|
||||
aspects.addAll(draft.forcedAspects)
|
||||
aspects.addAll(draft.flagFeatures)
|
||||
aspects.addAll(draft.cosmeticAspects)
|
||||
draft.choiceFeatures.forEach { (key, choice) ->
|
||||
choiceAspectId(key, choice)?.let { aspects.add(it) }
|
||||
}
|
||||
when (draft.gender) {
|
||||
Gender.MALE -> aspects.add("male")
|
||||
Gender.FEMALE -> aspects.add("female")
|
||||
Gender.GENDERLESS -> aspects.add("genderless")
|
||||
}
|
||||
val heldStack = draft.heldItem
|
||||
?.let { ResourceLocation.tryParse(it) }
|
||||
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
|
||||
?.let { item -> ItemStack(item) }
|
||||
?: ItemStack.EMPTY
|
||||
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
|
||||
return renderablePokemon
|
||||
}
|
||||
|
||||
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
|
||||
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
|
||||
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
|
||||
poseList = poser.poses
|
||||
.map { it.value.poseTypes.minBy { it.ordinal } }
|
||||
.toSet()
|
||||
.filterNot { it in PoseType.SHOULDER_POSES }
|
||||
.sortedBy { it.ordinal }
|
||||
.toTypedArray()
|
||||
if (PoseType.WALK in poseList) {
|
||||
selectedPoseIndex = poseList.indexOf(PoseType.WALK)
|
||||
} else if (selectedPoseIndex !in poseList.indices) {
|
||||
selectedPoseIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentHeldStack(): ItemStack {
|
||||
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
|
||||
}
|
||||
|
||||
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
|
||||
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
|
||||
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
|
||||
&& children.none { child ->
|
||||
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.net.debug
|
||||
|
||||
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.client.render.debug.LineState
|
||||
import com.cobblemon.mod.common.client.render.debug.SteerForceRenderer
|
||||
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
|
||||
import net.minecraft.client.Minecraft
|
||||
|
||||
object S2CUpdateObsAvoidHandler : ClientNetworkPacketHandler<ClientboundUpdateObsAvoidPacket> {
|
||||
override fun handle(packet: ClientboundUpdateObsAvoidPacket, client: Minecraft) {
|
||||
client.executeIfPossible {
|
||||
//ObjectAvoidanceRenderer.avoidanceLines[packet.entityId] = packet.lines
|
||||
val lineState = SteerForceRenderer.lineStates.getOrPut(packet.entityId) { LineState() }
|
||||
lineState.update(packet.lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.debug
|
||||
|
||||
import com.cobblemon.mod.common.api.net.Encodable
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.mojang.blaze3d.vertex.PoseStack
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer
|
||||
import net.minecraft.client.Camera
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.util.Mth
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
/**
|
||||
* Data class currently used for storing line data for rendering in [SteerForceRenderer]
|
||||
*
|
||||
* @author Jackson
|
||||
* @since September 18th, 2025
|
||||
*/
|
||||
data class EntityDebugLine(
|
||||
val vector: Vec3, // Direction/magnitude in world space, relative to entity origin
|
||||
val r: Float,
|
||||
val g: Float,
|
||||
val b: Float,
|
||||
val a: Float,
|
||||
) : Encodable {
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeVec3(vector)
|
||||
buffer.writeFloat(r)
|
||||
buffer.writeFloat(g)
|
||||
buffer.writeFloat(b)
|
||||
buffer.writeFloat(a)
|
||||
}
|
||||
|
||||
companion object {
|
||||
operator fun invoke(vector: Vec3, r: Float = 1f, g: Float = 1f, b: Float = 1f, a: Float = 1f): EntityDebugLine {
|
||||
return EntityDebugLine(vector, r.coerceIn(0f,1f), g.coerceIn(0f,1f), b.coerceIn(0f,1f), a.coerceIn(0f,1f))
|
||||
}
|
||||
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): EntityDebugLine {
|
||||
val vector = buffer.readVec3()
|
||||
return EntityDebugLine(vector, buffer.readFloat(), buffer.readFloat(), buffer.readFloat(), buffer.readFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LineState {
|
||||
/**
|
||||
* The comparison of the previous and current lines relies on the order being consistent
|
||||
*/
|
||||
var previousLines: List<EntityDebugLine> = emptyList()
|
||||
var currentLines: List<EntityDebugLine> = emptyList()
|
||||
var lastReceivedLineTick: Long? = null
|
||||
|
||||
fun tick() {
|
||||
previousLines = currentLines
|
||||
}
|
||||
|
||||
fun update(newLines: List<EntityDebugLine>) {
|
||||
lastReceivedLineTick = Minecraft.getInstance().level?.gameTime
|
||||
currentLines = newLines
|
||||
if (previousLines.isEmpty()) previousLines = newLines
|
||||
}
|
||||
|
||||
// entityPos: interpolated world position of the entity this tick
|
||||
fun draw(vc: VertexConsumer, pose: PoseStack.Pose, partialTick: Float, camera: Camera, entity: PokemonEntity, partialTicks: Float) {
|
||||
previousLines.zip(currentLines).forEach { (prev, curr) ->
|
||||
val iVec = prev.vector.lerp(curr.vector, partialTick.toDouble())
|
||||
val iR = prev.r.lerp(curr.r, partialTick)
|
||||
val iG = prev.g.lerp(curr.g, partialTick)
|
||||
val iB = prev.b.lerp(curr.b, partialTick)
|
||||
val iA = prev.a.lerp(curr.a, partialTick)
|
||||
|
||||
// Interpolated world position — same method used everywhere in the renderer
|
||||
val worldStart = Vec3(
|
||||
Mth.lerp(partialTicks.toDouble(), entity.xOld, entity.x),
|
||||
Mth.lerp(partialTicks.toDouble(), entity.yOld, entity.y),
|
||||
Mth.lerp(partialTicks.toDouble(), entity.zOld, entity.z)
|
||||
)
|
||||
val worldEnd = worldStart.add(iVec)
|
||||
|
||||
val relStart = worldStart.subtract(camera.position)
|
||||
val relEnd = worldEnd.subtract(camera.position)
|
||||
|
||||
// Normal is the normalized direction of the segment
|
||||
val dir = iVec.normalize()
|
||||
val nx = dir.x.toFloat()
|
||||
val ny = dir.y.toFloat()
|
||||
val nz = dir.z.toFloat()
|
||||
|
||||
vc.addVertex(pose.pose(), relStart.x.toFloat(), relStart.y.toFloat(), relStart.z.toFloat())
|
||||
.setColor(iR, iG, iB, iA)
|
||||
.setNormal(pose, nx, ny, nz)
|
||||
|
||||
vc.addVertex(pose.pose(), relEnd.x.toFloat(), relEnd.y.toFloat(), relEnd.z.toFloat())
|
||||
.setColor(iR, iG, iB, iA)
|
||||
.setNormal(pose, nx, ny, nz)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun reset() {
|
||||
previousLines = emptyList()
|
||||
currentLines = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Float.lerp(end: Float, t: Float): Float = this + (end - this) * t.coerceIn(0f, 1f)
|
||||
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render.debug
|
||||
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.platform.events.PlatformEvents
|
||||
import com.mojang.blaze3d.vertex.PoseStack
|
||||
import net.minecraft.client.Camera
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.renderer.MultiBufferSource
|
||||
import net.minecraft.client.renderer.RenderType
|
||||
|
||||
object SteerForceRenderer {
|
||||
|
||||
// Lines to render attached to the uuid of entities.
|
||||
val lineStates = mutableMapOf<Int, LineState>()
|
||||
|
||||
fun registerTickHandler() {
|
||||
// Advance line States
|
||||
PlatformEvents.CLIENT_TICK_PRE.subscribe {
|
||||
lineStates.forEach { uuid, lineState ->
|
||||
lineState.tick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun render(poseStack: PoseStack, multiBufferSource: MultiBufferSource, partialTicks: Float, camera: Camera) {
|
||||
val matrix = poseStack.last().pose()
|
||||
val level = Minecraft.getInstance().level ?: return
|
||||
|
||||
val currTick = level.gameTime
|
||||
val ticksUntilStale = 3
|
||||
|
||||
val keysToRemove = lineStates.keys.filter { entityId ->
|
||||
val entity = level.getEntity(entityId) as? PokemonEntity
|
||||
val lastRecTick = lineStates[entityId]?.lastReceivedLineTick
|
||||
entity == null || (lastRecTick != null && (currTick - lastRecTick) > ticksUntilStale)
|
||||
}
|
||||
keysToRemove.forEach { lineStates.remove(it) }
|
||||
|
||||
// One buffer for all independent line segments
|
||||
val lineBuffer = multiBufferSource.getBuffer(RenderType.lines())
|
||||
|
||||
lineStates.forEach { entityId, lineState ->
|
||||
val entity = level.getEntity(entityId) as? PokemonEntity ?: return@forEach
|
||||
lineState.draw(lineBuffer, poseStack.last(), partialTicks, camera, entity, partialTicks)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
-2
@@ -8,7 +8,6 @@
|
||||
|
||||
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen1
|
||||
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PitchTiltAnimation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BiWingedFrame
|
||||
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
|
||||
@@ -199,7 +198,6 @@ class PidgeottoModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame,
|
||||
transformTicks = 10,
|
||||
quirks = arrayOf(blink, flyQuirk1, flyQuirk2),
|
||||
animations = arrayOf(
|
||||
PitchTiltAnimation(bone = getPart("body"), maxChangePerTick = 1F),
|
||||
singleBoneLook(),
|
||||
bedrock("pidgeotto", "air_fly")
|
||||
)
|
||||
|
||||
@@ -146,12 +146,6 @@ class CobblemonConfig {
|
||||
@CobblemonConfigField(Category.Battles, lang = "experience_share_multiplier", SERVER)
|
||||
var experienceShareMultiplier = .5
|
||||
|
||||
@CobblemonConfigField(Category.Battles, lang = "award_experience_to_fainted_pokemon", SERVER)
|
||||
var awardExperienceToFaintedPokemon = false
|
||||
|
||||
@CobblemonConfigField(Category.Battles, lang = "award_experience_on_battle_loss", SERVER)
|
||||
var awardExperienceOnBattleLoss = false
|
||||
|
||||
@CobblemonConfigField(Category.Battles, lang = "lucky_egg_multiplier", SERVER)
|
||||
var luckyEggMultiplier = 1.5
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import com.cobblemon.mod.common.CobblemonBehaviours
|
||||
import com.cobblemon.mod.common.CobblemonCallbacks
|
||||
import com.cobblemon.mod.common.CobblemonCosmeticItems
|
||||
import com.cobblemon.mod.common.CobblemonMechanics
|
||||
import com.cobblemon.mod.common.CobblemonMovesetBuilders
|
||||
import com.cobblemon.mod.common.CobblemonPartyCompositions
|
||||
import com.cobblemon.mod.common.CobblemonPartyPools
|
||||
import com.cobblemon.mod.common.CobblemonRideSettings
|
||||
import com.cobblemon.mod.common.CobblemonUnlockableWallpapers
|
||||
import com.cobblemon.mod.common.api.abilities.Abilities
|
||||
@@ -78,12 +81,15 @@ object CobblemonDataProvider : DataProvider {
|
||||
this.register(SpeciesFeatureAssignments, reloadable = false)
|
||||
this.register(ActionEffects, reloadable = true)
|
||||
this.register(Moves, reloadable = false)
|
||||
this.register(CobblemonMovesetBuilders, reloadable = false)
|
||||
this.register(Abilities, reloadable = false)
|
||||
this.register(CobblemonBehaviours, reloadable = false)
|
||||
this.register(PokemonSpecies, reloadable = false)
|
||||
this.register(SpeciesAdditions, reloadable = false)
|
||||
this.register(PokeBalls, reloadable = false)
|
||||
this.register(PropertiesCompletionProvider, reloadable = false)
|
||||
this.register(CobblemonPartyPools, reloadable = false)
|
||||
this.register(CobblemonPartyCompositions, reloadable = false)
|
||||
this.register(SpawnDetailPresets, reloadable = true)
|
||||
this.register(CobblemonSpawnRules, reloadable = true)
|
||||
this.register(CobblemonMechanics, reloadable = true)
|
||||
|
||||
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.entity.ai
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonBlocks
|
||||
import com.cobblemon.mod.common.entity.OmniPathingEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.pokemon.ai.OmniPathNodeMaker
|
||||
import com.cobblemon.mod.common.util.deleteNode
|
||||
import com.cobblemon.mod.common.util.getWaterAndLavaIn
|
||||
@@ -34,7 +35,6 @@ import net.minecraft.world.phys.Vec3
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.acos
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* A navigator designed to work with the [OmniPathNodeMaker], allowing a path that can cross land, water, and air
|
||||
@@ -49,8 +49,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
|
||||
|
||||
var cachedCurrentNode: Node? = null
|
||||
var currentNodeDistance = 0F
|
||||
var segmentIndex: Int = 0
|
||||
var pathFollowTarget: Vec3? = null
|
||||
|
||||
data class NavigationContext(
|
||||
val onHit: (damage: DamageSource) -> Unit = {},
|
||||
@@ -162,65 +160,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
|
||||
}
|
||||
|
||||
doStuckDetection(vec3d)
|
||||
|
||||
// Gather the steer target from the segment pathing
|
||||
// TODO: this shouldn't live here
|
||||
val currentPath = path ?: return
|
||||
if (!currentPath.isDone) {
|
||||
segmentIndex = segmentIndex.coerceIn(0, max(currentPath.nodeCount - 2, 0))
|
||||
pathFollowTarget = getPathSteerTarget(currentPath, mob)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a steering target for the current path
|
||||
* This video explains it well: https://www.youtube.com/watch?v=rlZYT-uvmGQ
|
||||
* Uses Reynolds path following for velocity steered entities
|
||||
*/
|
||||
fun getPathSteerTarget(
|
||||
path: Path,
|
||||
mob: Mob
|
||||
): Vec3 {
|
||||
// If this is the final segment then just seek towards the end
|
||||
if (segmentIndex >= path.nodeCount - 1) return path.getNode(path.nodeCount - 1).asVec3().add(0.5, 0.0, 0.5)
|
||||
|
||||
// Determine future position to use in steering calculations
|
||||
val lookAheadDist = 1.0 //TODO: Scale to look ahead a certain amount of ticks probably
|
||||
val futurePos = mob.position().add(mob.deltaMovement.normalize().scale(lookAheadDist))
|
||||
|
||||
// Gather the current segment points
|
||||
var pointS1 = path.getNode(segmentIndex).asVec3().add(0.5, 0.0, 0.5) // Add the 0.5's to get the blockface center as blockpos is on a corner
|
||||
var pointS2 = path.getNode(segmentIndex + 1).asVec3().add(0.5, 0.0, 0.5)
|
||||
|
||||
// Project the future point onto the current segment
|
||||
var vecA = futurePos.subtract(pointS1)
|
||||
var vecB = pointS2.subtract(pointS1)
|
||||
var normB = vecB.normalize()
|
||||
val lenB = vecB.length()
|
||||
var projAB = max(vecA.dot(normB), 0.0) // make at least 0 as negative means we are projecting behind the segment which we don't want
|
||||
|
||||
// If the projection goes off the front of the segment then advance to the next segment and recalculate the projection
|
||||
if (projAB > lenB && segmentIndex < path.nodeCount - 2) {
|
||||
segmentIndex++
|
||||
pointS1 = path.getNode(segmentIndex).asVec3().add(0.5, 0.0, 0.5)
|
||||
pointS2 = path.getNode(segmentIndex + 1).asVec3().add(0.5, 0.0, 0.5)
|
||||
vecA = futurePos.subtract(pointS1)
|
||||
vecB = pointS2.subtract(pointS1)
|
||||
normB = vecB.normalize()
|
||||
projAB = max(vecA.dot(normB), 0.0)
|
||||
}
|
||||
|
||||
// Get the projected point on the segment
|
||||
val projPoint = pointS1.add(normB.scale(projAB ))
|
||||
|
||||
// Determine if the future position is too far from the path by getting its distance from its projected position on the current path segment
|
||||
val futureDistFromPath = projPoint.distanceTo(futurePos)
|
||||
val acceptableDist = mob.bbWidth / 3.0 //TODO: figure out what this really needs to be
|
||||
|
||||
val pushForward = 0.1 // Advance the projected point along the segment to avoid going backwards
|
||||
val seekPoint = if (futureDistFromPath < acceptableDist) futurePos.add(normB.scale(pushForward)) else projPoint.add(normB.scale(pushForward))
|
||||
|
||||
return seekPoint
|
||||
}
|
||||
|
||||
fun isAirborne(world: Level, pos: BlockPos) =
|
||||
@@ -284,8 +223,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
|
||||
|
||||
fun moveTo(x: Double, y: Double, z: Double, speed: Double = 1.0, navigationContext: NavigationContext) {
|
||||
this.navigationContext = navigationContext
|
||||
segmentIndex = 0
|
||||
pathFollowTarget = null
|
||||
this.moveTo(x, y, z, speed)
|
||||
}
|
||||
|
||||
@@ -340,8 +277,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
|
||||
}
|
||||
|
||||
override fun moveTo(path: Path?, speed: Double): Boolean {
|
||||
segmentIndex = 0
|
||||
pathFollowTarget = null
|
||||
if (path != null && path.nodeCount > 0) {
|
||||
val node = path.getNode(0)!!
|
||||
// pokemonEntity.discard()
|
||||
@@ -464,8 +399,6 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
|
||||
this.cachedCurrentNode = null
|
||||
path = null
|
||||
nodeEvaluator.done()
|
||||
segmentIndex = 0
|
||||
pathFollowTarget = null
|
||||
// In case a path is cancelled instead of completed, check if we should stop flying
|
||||
if (pather.couldStopFlying() && !isAirborne(entity.level(), entity.blockPosition())) {
|
||||
pather.setFlying(false)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.entity.ai.steering
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
|
||||
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
// Runs a list of SteerRules in priority order, consuming a force budget.
|
||||
// High priority rules get their full contribution first. Lower priority
|
||||
// rules only get what budget remains.
|
||||
class SteerArbitrator(
|
||||
val entity: PokemonEntity,
|
||||
val maxForce: Double,
|
||||
val rules: MutableList<SteerRule> = mutableListOf()
|
||||
) {
|
||||
|
||||
private var sortedRules: List<SteerRule> = emptyList()
|
||||
private var dirty = true
|
||||
|
||||
fun addRule(rule: SteerRule) { rules.add(rule); dirty = true }
|
||||
fun removeRule(rule: SteerRule) { rules.remove(rule); dirty = true }
|
||||
fun clearRules() { rules.clear(); dirty = true }
|
||||
|
||||
fun calculate(debug: Boolean = false): Vec3 {
|
||||
if (dirty) {
|
||||
sortedRules = rules.sortedByDescending { it.priority }
|
||||
dirty = false
|
||||
}
|
||||
|
||||
var remaining = maxForce
|
||||
var result = Vec3.ZERO
|
||||
val debugLines = if (debug) mutableListOf<EntityDebugLine>() else null
|
||||
|
||||
for (rule in sortedRules) {
|
||||
if (remaining <= 0.0) break
|
||||
|
||||
val force = rule.calculate() ?: continue
|
||||
val scaled = force.scale(rule.weight.toDouble())
|
||||
val len = scaled.length()
|
||||
if (len <= 0.0) continue
|
||||
|
||||
val contribution = if (len <= remaining) {
|
||||
remaining -= len
|
||||
scaled
|
||||
} else {
|
||||
val partial = remaining
|
||||
remaining = 0.0
|
||||
scaled.normalize().scale(partial)
|
||||
}
|
||||
|
||||
result = result.add(contribution)
|
||||
|
||||
debugLines?.let {
|
||||
val (r, g, b) = rule.debugColor
|
||||
it.add(EntityDebugLine(contribution.scale(50.0), r, g, b, 1.0f))
|
||||
}
|
||||
}
|
||||
|
||||
debugLines?.let {
|
||||
if (result.lengthSqr() > 0.0)
|
||||
it.add(EntityDebugLine(result.scale(50.0), 1f, 1f, 1f, 1.5f))
|
||||
if (it.isNotEmpty())
|
||||
sendPacketToTrackingPlayers(entity, ClientboundUpdateObsAvoidPacket(entity.id, it))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.entity.ai.steering
|
||||
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
// A per-tick steering force contributor evaluated by SteerArbitrator.
|
||||
// Return null from calculate() if the rule has nothing to say this tick.
|
||||
interface SteerRule {
|
||||
|
||||
val entity: PokemonEntity
|
||||
|
||||
// Higher = evaluated first, consumes force budget before lower priority rules.
|
||||
// 90-100: obstacle avoidance
|
||||
// 60-89: threat / boundary
|
||||
// 30-59: social (separation, alignment, cohesion)
|
||||
// 0-29: goal / wander
|
||||
val priority: Int
|
||||
|
||||
// Multiplied against the rule output before budget accounting.
|
||||
val weight: Float
|
||||
|
||||
// RGB 0-1 for the debug line drawn by SteerArbitrator.
|
||||
val debugColor: Triple<Float, Float, Float> get() = Triple(1f, 1f, 1f)
|
||||
|
||||
fun calculate(): Vec3?
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.entity.ai.steering.rules
|
||||
|
||||
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
|
||||
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import kotlin.math.max
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.math.cos
|
||||
|
||||
// Forward AABB projection obstacle avoidance. Stateful — one instance per entity.
|
||||
class ObstacleAvoidRule(
|
||||
override val entity: PokemonEntity,
|
||||
override val priority: Int = 100,
|
||||
override val weight: Float = 1.0f,
|
||||
override val debugColor: Triple<Float, Float, Float> = Triple(1f, 0f, 0f),
|
||||
val maxForce: Double = 0.02,
|
||||
val lookAheadTicks: Int = 10,
|
||||
val smoothingAlpha: Double = 0.25
|
||||
) : SteerRule {
|
||||
|
||||
private var prevForce: Vec3 = Vec3.ZERO
|
||||
|
||||
override fun calculate(): Vec3? {
|
||||
val topSpeed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
|
||||
|
||||
val forwardVec = if (entity.deltaMovement.horizontalDistanceSqr() < 0.001) {
|
||||
val yaw = Math.toRadians(entity.yRot.toDouble())
|
||||
Vec3(-sin(yaw), 0.0, cos(yaw))
|
||||
} else {
|
||||
Vec3(entity.deltaMovement.x, 0.0, entity.deltaMovement.z).normalize()
|
||||
}
|
||||
|
||||
val lookAheadDist = max(entity.deltaMovement.length() * lookAheadTicks, entity.bbWidth / 2.0)
|
||||
val collBox = entity.boundingBox.move(forwardVec.scale(lookAheadDist))
|
||||
val shapes = entity.level().getBlockCollisions(entity, collBox)
|
||||
|
||||
val nearestShape = shapes.minByOrNull { shape ->
|
||||
shape.bounds().center.distanceToSqr(entity.position())
|
||||
}
|
||||
|
||||
if (nearestShape == null) {
|
||||
prevForce = prevForce.lerp(Vec3.ZERO, smoothingAlpha)
|
||||
return if (prevForce.lengthSqr() < 1e-6) null else prevForce
|
||||
}
|
||||
|
||||
val obsCenter = nearestShape.bounds().center
|
||||
val toObs = obsCenter.subtract(entity.position())
|
||||
val projVec = forwardVec.scale(toObs.dot(forwardVec))
|
||||
val lateralComponent = toObs.subtract(projVec)
|
||||
|
||||
val avoidForce = if (lateralComponent.lengthSqr() < 0.001) {
|
||||
// Dead ahead — turn right
|
||||
val rightVec = forwardVec.cross(Vec3(0.0, 1.0, 0.0))
|
||||
entity.steerToTarget(entity.position().add(rightVec), topSpeed, maxForce, horzOnly = true)
|
||||
} else {
|
||||
var avoidanceVec = Vec3(lateralComponent.x * -1.0, 0.0, lateralComponent.z * -1.0)
|
||||
val avoidStrength = sqrt(1.0 - (avoidanceVec.length() / (entity.bbWidth / 2.0 + 0.5)).coerceIn(0.0, 0.7))
|
||||
val avoidancePoint = entity.position().add(avoidanceVec)
|
||||
entity.steerToTarget(avoidancePoint, topSpeed, maxForce * avoidStrength, horzOnly = true)
|
||||
}
|
||||
|
||||
prevForce = prevForce.lerp(avoidForce, smoothingAlpha)
|
||||
return prevForce
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.entity.ai.steering.rules
|
||||
|
||||
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
|
||||
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
// Steers toward a target position. Reads speed from PokemonMoveControl.steerSpeed.
|
||||
// Return null from targetProvider to deactivate for a tick.
|
||||
class SeekRule(
|
||||
override val entity: PokemonEntity,
|
||||
override val priority: Int = 10,
|
||||
override val weight: Float = 1.0f,
|
||||
override val debugColor: Triple<Float, Float, Float> = Triple(0f, 1f, 1f),
|
||||
val maxForce: Double,
|
||||
val horzOnly: Boolean = false,
|
||||
val arrivalRange: Double? = null,
|
||||
val targetProvider: (PokemonEntity) -> Vec3?
|
||||
) : SteerRule {
|
||||
|
||||
override fun calculate(): Vec3? {
|
||||
val target = targetProvider(entity) ?: return null
|
||||
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
|
||||
return entity.steerToTarget(target, speed, maxForce, arrivalRange, horzOnly)
|
||||
}
|
||||
}
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.entity.ai.steering.rules
|
||||
|
||||
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
|
||||
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import kotlin.math.sin
|
||||
|
||||
// Steers toward a point directly ahead of the entity's current velocity/yaw.
|
||||
// Useful as a baseline test rule.
|
||||
class WalkForwardRule(
|
||||
override val entity: PokemonEntity,
|
||||
override val priority: Int = 10,
|
||||
override val weight: Float = 1.0f,
|
||||
override val debugColor: Triple<Float, Float, Float> = Triple(0f, 1f, 0f),
|
||||
val maxForce: Double = 0.02
|
||||
) : SteerRule {
|
||||
|
||||
override fun calculate(): Vec3? {
|
||||
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
|
||||
|
||||
val forwardVec = if (entity.deltaMovement.horizontalDistanceSqr() < 0.001) {
|
||||
val yaw = Math.toRadians(entity.yRot.toDouble())
|
||||
Vec3(-sin(yaw), 0.0, cos(yaw))
|
||||
} else {
|
||||
Vec3(entity.deltaMovement.x, 0.0, entity.deltaMovement.z).normalize()
|
||||
}
|
||||
|
||||
val pointAhead = entity.position().add(forwardVec)
|
||||
return entity.steerToTarget(pointAhead, speed, maxForce, horzOnly = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cos(d: Double) = kotlin.math.cos(d)
|
||||
private fun sin(d: Double) = kotlin.math.sin(d)
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.entity.ai.steering.rules
|
||||
|
||||
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
|
||||
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
|
||||
import net.minecraft.util.Mth
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sin
|
||||
import kotlin.random.Random
|
||||
|
||||
// Reynolds wander — projects a sphere ahead and offsets a point on it each tick.
|
||||
// Stateful: holds current wander angles, one instance per entity.
|
||||
// https://slsdo.github.io/steering-behaviors/
|
||||
class WanderRule(
|
||||
override val entity: PokemonEntity,
|
||||
override val priority: Int = 10,
|
||||
override val weight: Float = 1.0f,
|
||||
override val debugColor: Triple<Float, Float, Float> = Triple(1f, 0.5f, 0f),
|
||||
val maxForce: Double = 0.02,
|
||||
val wanderDisp: Double = 15.0,
|
||||
val centerDistance: Double = 4.0,
|
||||
val wanderRadius: Double = 4.0,
|
||||
val horzOnly: Boolean = false //TODO: all of these should be checks if flying or walking
|
||||
) : SteerRule {
|
||||
|
||||
var currHorzWander = 0.0
|
||||
var currVertWander = 0.0
|
||||
|
||||
override fun calculate(): Vec3? {
|
||||
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
|
||||
|
||||
val normVec = if (entity.deltaMovement.lengthSqr() > 0.0001) {
|
||||
entity.lookAngle.normalize()
|
||||
} else {
|
||||
Vec3(
|
||||
Random.nextDouble(-1.0, 1.0),
|
||||
Random.nextDouble(-1.0, 1.0),
|
||||
Random.nextDouble(-1.0, 1.0)
|
||||
).normalize()
|
||||
}
|
||||
|
||||
var wanderOff = normVec
|
||||
|
||||
// Clamp vertical wander when getting too steep
|
||||
val currPitchS = sin(Mth.atan2(wanderOff.y, wanderOff.horizontalDistance()))
|
||||
if (currPitchS >= 0.5f || currPitchS <= -0.5f) currVertWander = 0.0
|
||||
|
||||
wanderOff = wanderOff.xRot(Math.toRadians(currVertWander).toFloat())
|
||||
wanderOff = wanderOff.yRot(Math.toRadians(currHorzWander).toFloat())
|
||||
|
||||
val wanderCenter = entity.position().add(normVec.scale(centerDistance))
|
||||
val wanderPoint = wanderCenter.add(wanderOff.scale(wanderRadius))
|
||||
|
||||
// Advance wander angles
|
||||
currVertWander = Mth.wrapDegrees(currVertWander + Random.nextDouble(-wanderDisp, wanderDisp))
|
||||
currHorzWander = Mth.wrapDegrees(currHorzWander + Random.nextDouble(-wanderDisp, wanderDisp))
|
||||
|
||||
val force = entity.steerToTarget(wanderPoint, speed, maxForce)
|
||||
return if (horzOnly) Vec3(force.x, 0.0, force.z) else force
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -191,7 +191,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setNoGravity(false)
|
||||
isNoGravity = false
|
||||
deltaMovement = Vec3.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,10 @@ import net.minecraft.world.phys.AABB
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.math.*
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@Suppress("unused")
|
||||
open class PokemonEntity(
|
||||
@@ -211,7 +214,6 @@ open class PokemonEntity(
|
||||
@JvmStatic var RIDE_BOOSTS = SynchedEntityData.defineId(PokemonEntity::class.java, RideBoostsDataSerializer)
|
||||
@JvmStatic var RIDE_STAMINA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
|
||||
@JvmStatic var SCALE_MODIFIER = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
|
||||
@JvmStatic val IS_SPRINTING = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
|
||||
|
||||
const val BATTLE_LOCK = "battle"
|
||||
const val EVOLUTION_LOCK = "evolving"
|
||||
@@ -291,9 +293,6 @@ open class PokemonEntity(
|
||||
var shownItem: ItemStack
|
||||
get() = entityData.get(SHOWN_HELD_ITEM)
|
||||
set(value) = entityData.set(SHOWN_HELD_ITEM, value)
|
||||
var isPokemonSprinting: Boolean
|
||||
get() = entityData.get(IS_SPRINTING)
|
||||
set(value) { entityData.set(IS_SPRINTING, value) }
|
||||
|
||||
var lastLightningBoltUUID: UUID? = null
|
||||
|
||||
@@ -462,7 +461,6 @@ open class PokemonEntity(
|
||||
builder.define(RIDE_BOOSTS, emptyMap())
|
||||
builder.define(RIDE_STAMINA, 1F)
|
||||
builder.define(SCALE_MODIFIER, 1F)
|
||||
builder.define(IS_SPRINTING, false)
|
||||
}
|
||||
|
||||
override fun onSyncedDataUpdated(data: EntityDataAccessor<*>) {
|
||||
@@ -625,12 +623,8 @@ open class PokemonEntity(
|
||||
isPokemonFlying = flyDist - flyDistO > 0.005F
|
||||
isPokemonWalking = walkDist - walkDistO > 0.005F
|
||||
|
||||
// TODO: Figure out a better way to allow for this. Or maybe just always do the animations data accumulation its really not that bad
|
||||
if (this.pokemon.species.name.lowercase() == "charizard" || (passengers.isNotEmpty() && level().isClientSide)) {
|
||||
// This is so ugly why am I doing it like this... even if its for a test this is low
|
||||
if (passengers.isNotEmpty() && level().isClientSide) {
|
||||
rideSoundManager.tick()
|
||||
}
|
||||
if (passengers.isNotEmpty() && level().isClientSide) {
|
||||
rideSoundManager.tick()
|
||||
ridingAnimationData.update()
|
||||
} else if (!passengers.isNotEmpty() && level().isClientSide) {
|
||||
rideSoundManager.stop()
|
||||
@@ -2157,9 +2151,7 @@ open class PokemonEntity(
|
||||
override fun stopSeenByPlayer(player: ServerPlayer) {
|
||||
if (this.ownerUUID == player.uuid && tethering == null) {
|
||||
// queuedToDespawn = true
|
||||
if (this.removalReason != RemovalReason.DISCARDED) {
|
||||
this.remove(RemovalReason.DISCARDED)
|
||||
}
|
||||
this.remove(RemovalReason.DISCARDED)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
-547
@@ -1,547 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.entity.pokemon.ai
|
||||
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonActivities
|
||||
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
|
||||
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.extensions.*
|
||||
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
|
||||
import com.cobblemon.mod.common.util.math.geometry.toRadians
|
||||
import com.cobblemon.mod.common.util.resolveDouble
|
||||
import net.minecraft.util.Mth
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.ai.control.MoveControl
|
||||
import net.minecraft.world.entity.ai.memory.MemoryModuleType
|
||||
import net.minecraft.world.level.ClipContext
|
||||
import net.minecraft.world.phys.HitResult
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random
|
||||
|
||||
class BoidsMoveControl(val pokemonEntity: PokemonEntity, val target: LivingEntity) : MoveControl(pokemonEntity) {
|
||||
companion object {
|
||||
const val VERY_CLOSE = 2.500000277905201E-3
|
||||
const val MAX_SPEED = 0.3 / 2
|
||||
const val MAX_FORCE = 0.02
|
||||
const val PREDATOR_MAX_SPEED = 0.6
|
||||
// const val MAX_SPEED = 0.2
|
||||
// const val MAX_FORCE = 0.013
|
||||
}
|
||||
|
||||
// TODO: might need to hold its own velocity so minecraft doesn't do its nonsense to it?
|
||||
// Can handle collisions ourselves as well
|
||||
var speed: Double? = null
|
||||
var drawLines: MutableList<EntityDebugLine> = mutableListOf()
|
||||
|
||||
// wander vars
|
||||
var currHorzWander = 0.0
|
||||
var currVertWander = 0.0
|
||||
val wanderDisp = 15.0 // The degrees of change from tick to tick in wander angle
|
||||
|
||||
override fun tick() {
|
||||
// Clear since the info is sent and is now stale
|
||||
drawLines.clear()
|
||||
|
||||
// Steering constants
|
||||
val orbitRadius = 16.0
|
||||
val targetPos = target.position().add(0.0, 4.0, 0.0)
|
||||
val arrivalRange = 2.0 // in blocks
|
||||
|
||||
val species = pokemonEntity.pokemon.species.name
|
||||
// Set this to ensure the flying pose is active
|
||||
|
||||
// ignore all the friction stuff from outside
|
||||
speed?.let { pokemonEntity.setDeltaMovement(pokemonEntity.deltaMovement.normalize().scale(it)) }
|
||||
|
||||
// Constrain to rectangular boundary
|
||||
val cubeWidth = 16.0
|
||||
val cubeHeight = 16.0
|
||||
val cubeCenter = target.position().add(0.0, cubeHeight / 2.0, 0.0)
|
||||
//forceRectBoundary(cubeCenter, cubeWidth, cubeHeight)
|
||||
|
||||
// Apply boid forces
|
||||
|
||||
// Obstacle Avoidance
|
||||
val whiskerRange = 1.0
|
||||
val avoidanceAngle = 20.0 // x degrees to the left, right, up, and down
|
||||
//val obsAvoidForce = whisObsAvoid(whiskerRange, avoidanceAngle)
|
||||
|
||||
// if (obsAvoidForce != null) {
|
||||
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(obsAvoidForce.scale(0.5))
|
||||
// }
|
||||
|
||||
// TODO: obviously don't hardcode each pokemon. Remove this after testing is finished.
|
||||
if (pokemonEntity.pokemon.species.name.lowercase() == "charizard") {
|
||||
pokemonEntity.setFlying(true)
|
||||
pokemonEntity.resetFallDistance()
|
||||
seekPrey(pokemonEntity)
|
||||
} else if (pokemonEntity.pokemon.species.name.lowercase() == "garchomp") {
|
||||
pokemonEntity.setFlying(false)
|
||||
val walkSpeed = pokemonEntity.runtime.resolveDouble(pokemonEntity.behaviour.moving.walk.walkSpeed) * 1.5
|
||||
val steerForce = pokemonEntity.steerToTarget(target.position(), walkSpeed, walkSpeed / 12)
|
||||
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce.x, 0.0, steerForce.z)
|
||||
// speed?.let { this.mob.setZza(speed!!.toFloat()) }
|
||||
} else if (pokemonEntity.pokemon.species.name.lowercase() == "skiddo") {
|
||||
pokemonEntity.setFlying(false)
|
||||
//wander(pokemonEntity,1.0)
|
||||
// val steerForce = pokemonEntity.steerToTargetForce(target.position(), MAX_SPEED, MAX_FORCE)
|
||||
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce.x, 0.0, steerForce.z)
|
||||
aviodPredators(pokemonEntity, includePlayer = true)
|
||||
boid(isFlyingOrSwimming = false)
|
||||
} else {
|
||||
pokemonEntity.setFlying(true)
|
||||
pokemonEntity.resetFallDistance()
|
||||
wander(pokemonEntity,0.8)
|
||||
boid()
|
||||
aviodPredators(pokemonEntity, includePlayer = true)
|
||||
|
||||
// Force towards ground
|
||||
|
||||
|
||||
forceRectBoundary(pokemonEntity, cubeCenter, cubeWidth, cubeHeight)
|
||||
}
|
||||
|
||||
// val avoidForce = avoidTarget(target.position(), MAX_SPEED, MAX_FORCE, 10.0)
|
||||
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(avoidForce.scale(1.0))
|
||||
|
||||
// Apply steering force to current movement
|
||||
// val dm = pokemonEntity.deltaMovement
|
||||
// val w_pos = pokemonEntity.position().add(Vec3(dm.x, 0.0, dm.z))
|
||||
// val steerForce = steerToTargetForce(w_pos, MAX_SPEED, MAX_FORCE)
|
||||
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce)
|
||||
|
||||
// Take note of speed again to avoid minecraft's friction logic
|
||||
speed = pokemonEntity.deltaMovement.length() * 0.98
|
||||
|
||||
// Test out what this setting of speed does.
|
||||
//speed?.let { this.mob.speed = speed!!.toFloat() }
|
||||
|
||||
// Limit upwards speed to be half of max speed. The boids kept bumping into the top of the box more so than birds
|
||||
// would. Honestly birds probably mostly want to move in the x,z plane anyhow
|
||||
// pokemonEntity.deltaMovement = Vec3(
|
||||
// pokemonEntity.deltaMovement.x,
|
||||
// min(pokemonEntity.deltaMovement.y, MAX_SPEED * 0.5),
|
||||
// pokemonEntity.deltaMovement.y
|
||||
// ).normalize().scale(speed!!)
|
||||
|
||||
pokemonEntity.orientEntityToTravel(0.4f)
|
||||
|
||||
if (false && drawLines.isNotEmpty()) { //TODO: Re-enable
|
||||
sendPacketToTrackingPlayers(pokemonEntity, ClientboundUpdateObsAvoidPacket(pokemonEntity.id, drawLines))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun aviodPredators(entity: PokemonEntity, includePlayer: Boolean = false) {
|
||||
val fearRadius = 5.0
|
||||
val brain = entity.brain
|
||||
val predators = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
|
||||
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
|
||||
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS &&
|
||||
pokemon.pokemon.species.name.lowercase() == "charizard" &&
|
||||
entity.position().subtract(pokemon.position()).length() <= fearRadius
|
||||
}
|
||||
}.orElse(emptyList())
|
||||
|
||||
var totalEvasionVec = Vec3.ZERO
|
||||
var predatorCount = predators.size
|
||||
|
||||
// If the player should be included in avoidance then add the player as a predator.
|
||||
if (includePlayer) {
|
||||
totalEvasionVec = totalEvasionVec.add(entity.avoidTarget(target.position(), MAX_SPEED * 1.2, MAX_FORCE * 2, fearRadius))
|
||||
predatorCount += 1
|
||||
}
|
||||
// Average evasion forces for all predators in the area and apply it to the prey entity
|
||||
if (predatorCount > 0) {
|
||||
for (predator in predators) {
|
||||
totalEvasionVec = totalEvasionVec.add(entity.avoidTarget(predator.position(), MAX_SPEED * 1.2, MAX_FORCE * 2, fearRadius))
|
||||
}
|
||||
totalEvasionVec = totalEvasionVec.scale(1.0 / predatorCount)
|
||||
if (entity.isFlying() || entity.isSwimming) {
|
||||
entity.deltaMovement = entity.deltaMovement.add(totalEvasionVec)
|
||||
} else {
|
||||
entity.deltaMovement = entity.deltaMovement.add(Vec3(totalEvasionVec.x, 0.0, totalEvasionVec.z))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************************************************************
|
||||
Seek the closest prey and pursue
|
||||
*************************************************************************************************************/
|
||||
private fun seekPrey(predator: PokemonEntity) {
|
||||
val SeekRadius = 20.0
|
||||
val brain = predator.brain
|
||||
// Do calcs for head height as it looks better this way than a charizard flying crotch first at a group of pidgeys
|
||||
val kindaHeadHeight = predator.position()
|
||||
|
||||
val prey = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
|
||||
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
|
||||
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS &&
|
||||
(pokemon.pokemon.species.name.lowercase() == "pidgey" || pokemon.pokemon.species.name.lowercase() == "skiddo") &&
|
||||
kindaHeadHeight.subtract(pokemon.position()).length() <= SeekRadius
|
||||
}
|
||||
}.orElse(emptyList())
|
||||
|
||||
// Find the closest prey and seek towards it
|
||||
val nearestPrey = prey.minByOrNull { pokePrey ->
|
||||
kindaHeadHeight.subtract(pokePrey.position()).length()
|
||||
}
|
||||
nearestPrey?.let { prey ->
|
||||
|
||||
// Do the actual attack
|
||||
// TODO: make this not dumb
|
||||
if(kindaHeadHeight.subtract(nearestPrey.position()).length() < 0.5 + predator.bbWidth + nearestPrey.bbWidth) {
|
||||
//predator.doHurtTarget(nearestPrey)
|
||||
}
|
||||
|
||||
// Steer towards the target
|
||||
val steerForce = predator.steerToTarget(prey.position(), PREDATOR_MAX_SPEED, MAX_FORCE)
|
||||
predator.deltaMovement = predator.deltaMovement.add(steerForce)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*************************************************************************************************************
|
||||
Whisker Obstacle Avoidance
|
||||
This function ray casts in a cone in front of the entity
|
||||
*************************************************************************************************************/
|
||||
private fun whisObsAvoid(range: Double, lookWidth: Double): Vec3? {
|
||||
|
||||
// --- 1. Build the Orientation Matrix ---
|
||||
val forward = pokemonEntity.lookAngle.scale(range)
|
||||
// val tempUp = if (abs(forward.y) > 0.999) Vec3(0.0, 0.0, 1.0) else Vec3(0.0, 1.0, 0.0)
|
||||
// val right = forward.cross(tempUp).normalize()
|
||||
// val up = right.cross(forward).normalize()
|
||||
//
|
||||
// val orientation = Matrix3f()
|
||||
// .setColumn(0, Vector3f(right.x.toFloat(), right.y.toFloat(), right.z.toFloat()))
|
||||
// .setColumn(1, Vector3f(up.x.toFloat(), up.y.toFloat(), up.z.toFloat()))
|
||||
// .setColumn(2, Vector3f(forward.x.toFloat(), forward.y.toFloat(), forward.z.toFloat()))
|
||||
|
||||
// --- 2. Define Whiskers in Simple, Local Space ---
|
||||
val radLookWidth = lookWidth.toRadians().toFloat()
|
||||
// val localWhiskers: List<Vector3f> = listOf(
|
||||
// Vector3f(0f, 0f, 1f), // Forward
|
||||
//// Vector3f(sin(radLookWidth), 0f, cos(radLookWidth)), // Left
|
||||
//// Vector3f(-sin(radLookWidth), 0f, cos(radLookWidth)), // Right
|
||||
//// Vector3f(0f, sin(radLookWidth), cos(radLookWidth)), // Up
|
||||
//// Vector3f(0f, -sin(radLookWidth), cos(radLookWidth)) // Down
|
||||
// )
|
||||
//
|
||||
// // --- 3. Transform Local Whiskers to World Space using the Matrix ---
|
||||
// val whiskerOffsets = localWhiskers.map { localVec ->
|
||||
// orientation.transform(localVec, Vector3f()).toVec3d().scale(range) // Transform and store in a new vector
|
||||
// }
|
||||
|
||||
// Obtain all points to be raycasted to
|
||||
val scaledLookVec = pokemonEntity.deltaMovement.normalize().scale(range)
|
||||
val lookPoints: List<Vec3> = listOf(
|
||||
forward,
|
||||
// forward.yRot(radLookWidth),
|
||||
// forward.yRot(-radLookWidth),
|
||||
// forward.xRot(90.toRadians()),
|
||||
// forward.xRot((-90).toRadians()),
|
||||
)
|
||||
|
||||
|
||||
// iterate through each point and raycast to it starting from the entities pos (may need to be from somewhere else)
|
||||
// If a position is hit then add its avoidance force to the totalAvoidance forces.
|
||||
var totalAvoidance: Vec3? = null
|
||||
val level = pokemonEntity.level()
|
||||
val startPos = pokemonEntity.position()
|
||||
for (point in lookPoints) {
|
||||
val endPos = startPos.add(point)
|
||||
val hit = level.clip(
|
||||
ClipContext(
|
||||
startPos,
|
||||
endPos,
|
||||
ClipContext.Block.OUTLINE, // If this is being weird then go back to COLLIDER
|
||||
ClipContext.Fluid.ANY, // Birds should probably avoid fluid
|
||||
pokemonEntity
|
||||
)
|
||||
)
|
||||
|
||||
// Add the whisker to the debug lines to be drawn
|
||||
drawLines.add(EntityDebugLine(endPos, 1.0f, 0.0f, 0.0f))
|
||||
//visualizeRay(level, startPos, endPos, hit.type != HitResult.Type.MISS)
|
||||
|
||||
if (hit.type != HitResult.Type.MISS) {
|
||||
// Get the aviodance vector as the vector from the block detection point
|
||||
// to the pokemon.
|
||||
// Also inversely scale the size of the vector based on the distance the pokemon
|
||||
// is to the object. This means close obstacles are avoided more strongly
|
||||
val hitDist = hit.location.distanceTo(pokemonEntity.eyePosition)
|
||||
val avoidVec = startPos.subtract(endPos).normalize() //.scale(1.0 - (hitDist / range))
|
||||
|
||||
// Add avoid vec to total
|
||||
totalAvoidance = totalAvoidance?.add(avoidVec) ?: avoidVec
|
||||
}
|
||||
}
|
||||
|
||||
// // Scale all forces down to the max foce
|
||||
// if (totalAvoidance != null) {
|
||||
// // Cap the max total avoidance desire
|
||||
// if (totalAvoidance.length() > MAX_SPEED) totalAvoidance = totalAvoidance.normalize().scale(MAX_SPEED)
|
||||
// // Convert to Reynolds steering force
|
||||
// totalAvoidance = totalAvoidance.subtract(pokemonEntity.deltaMovement)
|
||||
// // Cap the max total avoidance force
|
||||
// if (totalAvoidance.length() > MAX_FORCE) totalAvoidance = totalAvoidance.normalize().scale(MAX_FORCE)
|
||||
// } // Is this next one even cleaner?
|
||||
|
||||
return totalAvoidance?.let { desiredVelocity ->
|
||||
// Cap the desired velocity (using lengthSqr is more efficient than length)
|
||||
val cappedVelocity = if (desiredVelocity.lengthSqr() > MAX_SPEED * MAX_SPEED) {
|
||||
desiredVelocity.normalize().scale(MAX_SPEED)
|
||||
} else {
|
||||
desiredVelocity
|
||||
}
|
||||
|
||||
// Convert to Reynolds steering force
|
||||
val steeringForce = cappedVelocity.subtract(pokemonEntity.deltaMovement)
|
||||
|
||||
// Cap the final steering force
|
||||
if (steeringForce.lengthSqr() > MAX_FORCE * MAX_FORCE) {
|
||||
steeringForce.normalize().scale(MAX_FORCE)
|
||||
} else {
|
||||
steeringForce
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wander(entity: PokemonEntity, forceScale: Double = 1.0) {
|
||||
// Wander behaviour
|
||||
/*************************************************************************************************************
|
||||
* The wander behaviour is based of Reynolds wander behaviour which can be seen in the below link under wander
|
||||
* This version however is adapted to 3dimensions.
|
||||
* https://slsdo.github.io/steering-behaviors/
|
||||
*************************************************************************************************************/
|
||||
// Get curr steering positions in left/right and up/down offsets
|
||||
val normVec = if(entity.deltaMovement.lengthSqr() > 0.0001) {
|
||||
entity.lookAngle.normalize()
|
||||
} else {
|
||||
Vec3(
|
||||
Random.nextDouble(-1.0, 1.0),
|
||||
Random.nextDouble(-1.0, 1.0),
|
||||
Random.nextDouble(-1.0, 1.0)
|
||||
).normalize()
|
||||
}
|
||||
|
||||
var wanderOff = normVec
|
||||
|
||||
val currPitchS = sin(Mth.atan2(wanderOff.y, wanderOff.horizontalDistance()))
|
||||
if (currPitchS >= 0.5f || currPitchS <= -0.5f) { //TODO: make this y limit class wide
|
||||
this.currVertWander = 0.0
|
||||
}
|
||||
wanderOff = wanderOff.xRot(this.currVertWander.toRadians())
|
||||
wanderOff = wanderOff.yRot(this.currHorzWander.toRadians())
|
||||
|
||||
// Base the center off the current direction of travel
|
||||
val centerDistance = 4.0 // How many blocks the wander 'sphere' is in front of the entity
|
||||
val wanderCenter = entity.position().add(normVec.scale(centerDistance))
|
||||
// Get the wander circle's radius
|
||||
val wanderRadius = 4.0
|
||||
|
||||
val wanderPoint = wanderCenter.add(wanderOff.scale(wanderRadius))
|
||||
|
||||
// change wander points
|
||||
val randVert = Random.nextDouble(-wanderDisp, wanderDisp)
|
||||
val randHorz = Random.nextDouble(-wanderDisp, wanderDisp)
|
||||
currVertWander = Mth.wrapDegrees(randVert + currVertWander)
|
||||
currHorzWander = Mth.wrapDegrees(randHorz + currHorzWander)
|
||||
|
||||
drawLines.add(EntityDebugLine(wanderPoint.subtract(pokemonEntity.position()), 1.0f, 0.0f, 0.0f, 1.0f)) // Add WanderPoint
|
||||
drawLines.add(EntityDebugLine(wanderCenter.subtract(pokemonEntity.position()), 0.0f, 1.0f, 0.0f, 1.0f)) // Add wander center
|
||||
val wanderForce = entity.steerToTarget(wanderPoint, MAX_SPEED, MAX_FORCE)
|
||||
if(pokemonEntity.isFlying() || pokemonEntity.isSwimming) {
|
||||
entity.deltaMovement = pokemonEntity.deltaMovement.add(wanderForce.scale(forceScale))
|
||||
} else {
|
||||
entity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(wanderForce.x, 0.0, wanderForce.z).scale(forceScale))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun boid(isFlyingOrSwimming: Boolean = true) {
|
||||
// TODO: maybe move this to a sensor that lives spins up on the boid activity creation
|
||||
// Get a list of other boid entities in this entity's vicinity
|
||||
val brain = pokemonEntity.brain
|
||||
val boids = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
|
||||
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
|
||||
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS
|
||||
}
|
||||
}.orElse(emptyList())
|
||||
|
||||
// separation
|
||||
val separationForce = separation(boids)?.scale(0.8)
|
||||
separationForce?.let { force ->
|
||||
if( isFlyingOrSwimming) {
|
||||
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
|
||||
} else {
|
||||
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
|
||||
}
|
||||
}
|
||||
// alignment
|
||||
val alignmentForce = alignment(boids)?.scale(0.0)
|
||||
alignmentForce?.let { force ->
|
||||
if( isFlyingOrSwimming) {
|
||||
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
|
||||
} else {
|
||||
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
|
||||
}
|
||||
}
|
||||
|
||||
// cohesion
|
||||
val cohesionForce = cohesion(boids)?.scale(0.6)
|
||||
cohesionForce?.let { force ->
|
||||
if( isFlyingOrSwimming) {
|
||||
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
|
||||
} else {
|
||||
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun separation(boids: List<PokemonEntity>): Vec3? {
|
||||
var seperationVec: Vec3? = null
|
||||
|
||||
// Gather the total separation vec of all close entities
|
||||
boids.forEach { boid ->
|
||||
val toTargetVec = pokemonEntity.position().subtract(boid.position())
|
||||
val desiredSeperation = boid.bbWidth * 2.0 // Base the seperation distance on the model width
|
||||
val currDist = toTargetVec.length()
|
||||
if (currDist < desiredSeperation) {
|
||||
if (seperationVec == null) seperationVec = Vec3.ZERO
|
||||
// Make the seperation force inversely proprotional to the serperation distance
|
||||
// Also, scale the force by the bbwidth of the model so
|
||||
seperationVec = seperationVec.add(toTargetVec.normalize().scale((1.0 - (currDist / desiredSeperation)) ))
|
||||
}
|
||||
}
|
||||
|
||||
if (seperationVec != null) {
|
||||
//if (seperationVec.length() > MAX_SPEED) seperationVec = seperationVec.normalize().scale(MAX_SPEED)
|
||||
seperationVec = seperationVec.normalize().scale(MAX_SPEED)
|
||||
seperationVec = seperationVec.subtract(pokemonEntity.deltaMovement)
|
||||
if (seperationVec.length() > MAX_FORCE) {
|
||||
seperationVec = seperationVec.normalize().scale(MAX_FORCE)
|
||||
}
|
||||
}
|
||||
return seperationVec
|
||||
}
|
||||
|
||||
private fun alignment(boids: List<PokemonEntity>): Vec3? {
|
||||
val neighborDist = 4.0
|
||||
var summedVel: Vec3? = null
|
||||
var alignmentVec: Vec3? = null
|
||||
var boidCount = 0
|
||||
|
||||
// Gather the average position of the boid
|
||||
boids.forEach { boid ->
|
||||
val vecToOther = boid.position().subtract(this.pokemonEntity.position())
|
||||
val distToOther = vecToOther.length()
|
||||
val widthOfViewInDeg = 180
|
||||
val isInSightLine = pokemonEntity.deltaMovement.normalize().dot(vecToOther) > cos((widthOfViewInDeg / 2.0).toRadians())
|
||||
|
||||
if (isInSightLine && distToOther < neighborDist) {
|
||||
if (summedVel == null) summedVel = Vec3.ZERO
|
||||
summedVel = summedVel.add(boid.deltaMovement.normalize())
|
||||
boidCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (summedVel != null) {
|
||||
// Get the average vel/alignment by dividing by the number of boids sampled
|
||||
val averageVel = summedVel.scale(1.0 / boidCount)
|
||||
|
||||
//alignmentVec = if (averageVel.length() > MAX_SPEED) averageVel.normalize().scale(MAX_SPEED) else averageVel
|
||||
alignmentVec = averageVel.normalize().scale(MAX_SPEED)
|
||||
alignmentVec = alignmentVec.subtract(pokemonEntity.deltaMovement)
|
||||
if (alignmentVec.length() > MAX_FORCE) {
|
||||
alignmentVec = alignmentVec.normalize().scale(MAX_FORCE)
|
||||
}
|
||||
}
|
||||
return alignmentVec
|
||||
}
|
||||
|
||||
private fun cohesion(boids: List<PokemonEntity>): Vec3? {
|
||||
//val neighborDist = 5.0
|
||||
var summedPos: Vec3? = null
|
||||
var cohesionVec: Vec3? = null
|
||||
var boidCount = 0
|
||||
val cohesionRadius = 8.0
|
||||
|
||||
// Gather the average position of the boid
|
||||
boids.forEach { boid ->
|
||||
val vecToOther = boid.position().subtract(this.pokemonEntity.position())
|
||||
val distToOther = vecToOther.length()
|
||||
val widthOfViewInDeg = 360
|
||||
val isInSightLine = pokemonEntity.deltaMovement.normalize().dot(vecToOther) > cos((widthOfViewInDeg / 2.0).toRadians())
|
||||
|
||||
if (isInSightLine && distToOther < cohesionRadius) { // Only use members that are within a certain radius and eyeline
|
||||
if (summedPos == null) summedPos = Vec3.ZERO
|
||||
summedPos = summedPos.add(boid.position())
|
||||
boidCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (summedPos != null) {
|
||||
// Get the average position by dividing by the number of boids sampled
|
||||
val averagePos = summedPos.scale(1.0 / boidCount)
|
||||
// Get the vector towards that average position and scale
|
||||
cohesionVec = averagePos.subtract(pokemonEntity.position())
|
||||
//if (cohesionVec.length() > MAX_SPEED) cohesionVec = cohesionVec.normalize().scale(MAX_SPEED)
|
||||
cohesionVec = cohesionVec.normalize().scale(MAX_SPEED)
|
||||
cohesionVec = cohesionVec.subtract(pokemonEntity.deltaMovement)
|
||||
if (cohesionVec.length() > MAX_FORCE) {
|
||||
cohesionVec = cohesionVec.normalize().scale(MAX_FORCE)
|
||||
}
|
||||
}
|
||||
return cohesionVec
|
||||
}
|
||||
|
||||
private fun forceRectBoundary(entity: PokemonEntity, basePos: Vec3, rectWidth: Double, rectHeight: Double) {
|
||||
val maxBoundaryForce = MAX_FORCE //* 2.0
|
||||
val correctiveForce = getRectCorrectiveForce(basePos, rectWidth, rectHeight)
|
||||
if (correctiveForce != null) {
|
||||
//val correctiveForce = steerToTargetForce(correctivePos, speedLimit, maxForce)
|
||||
var steer = correctiveForce.subtract(entity.deltaMovement)
|
||||
if (steer.length() > maxBoundaryForce) {
|
||||
steer = steer.normalize().scale(maxBoundaryForce)
|
||||
}
|
||||
entity.setDeltaMovement(entity.deltaMovement.add(steer))
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the desired force to correct the entity back into the rectangle area
|
||||
private fun getRectCorrectiveForce( basePos: Vec3, rectWidth: Double, rectHeight: Double): Vec3? {
|
||||
val entityPos = pokemonEntity.position()
|
||||
val posFromBase = entityPos.subtract(basePos)
|
||||
val cubeHalfWidth = rectWidth / 2.0 // Useful as the basePoint is in the middle of the cube
|
||||
val cubeHalfHeight = rectHeight / 2.0
|
||||
|
||||
// Check the three possible faces for exceeding the boundary (x, y, and z faces of cube)
|
||||
val xExceed = max(0.0, abs(posFromBase.x) - cubeHalfWidth)
|
||||
val yExceed = max(0.0, abs(posFromBase.y) - cubeHalfHeight)
|
||||
val zExceed = max(0.0, abs(posFromBase.z) - cubeHalfWidth)
|
||||
|
||||
return if ( xExceed == 0.0 && yExceed == 0.0 && zExceed == 0.0) {
|
||||
null
|
||||
} else {
|
||||
var desiredPos = pokemonEntity.position().add(pokemonEntity.deltaMovement) // Add delta movement so we aren't slowing this guy down
|
||||
if (xExceed != 0.0) desiredPos = Vec3( basePos.x, desiredPos.y, desiredPos.z)
|
||||
if (yExceed != 0.0) desiredPos = Vec3( desiredPos.x, basePos.y, desiredPos.z)
|
||||
if (zExceed != 0.0) desiredPos = Vec3( desiredPos.x, desiredPos.y, basePos.z)
|
||||
return desiredPos.subtract(pokemonEntity.position()).normalize().scale(MAX_SPEED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//val desiredScale = if (distToTarget > slowDownRadius) speedLimit else (distToTarget/slowDownRadius).pow(2) * speedLimit
|
||||
//desiredVec = desiredVec.normalize().scale(desiredScale)
|
||||
+12
-322
@@ -9,47 +9,30 @@
|
||||
package com.cobblemon.mod.common.entity.pokemon.ai
|
||||
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
|
||||
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
|
||||
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine.Companion.invoke
|
||||
import com.cobblemon.mod.common.entity.PlatformType
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.entity.ai.OmniPathNavigation
|
||||
import com.cobblemon.mod.common.entity.ai.steering.SteerArbitrator
|
||||
import com.cobblemon.mod.common.entity.ai.steering.rules.ObstacleAvoidRule
|
||||
import com.cobblemon.mod.common.entity.ai.steering.rules.SeekRule
|
||||
import com.cobblemon.mod.common.entity.ai.steering.rules.WanderRule
|
||||
import com.cobblemon.mod.common.entity.ai.steering.rules.WalkForwardRule
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonBehaviourFlag
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.entity.pokemon.extensions.*
|
||||
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
|
||||
import com.cobblemon.mod.common.util.getWaterAndLavaIn
|
||||
import com.cobblemon.mod.common.util.math.geometry.toDegrees
|
||||
import com.cobblemon.mod.common.util.math.geometry.toRadians
|
||||
import com.cobblemon.mod.common.util.resolveFloat
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.tags.BlockTags
|
||||
import net.minecraft.tags.FluidTags
|
||||
import net.minecraft.util.Mth
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes
|
||||
import net.minecraft.world.entity.ai.control.MoveControl
|
||||
import net.minecraft.world.level.material.FluidState
|
||||
import net.minecraft.world.level.pathfinder.PathType
|
||||
import net.minecraft.world.phys.AABB
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.random.Random
|
||||
|
||||
class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemonEntity) {
|
||||
companion object {
|
||||
@@ -67,41 +50,6 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
|
||||
var bankUpwardsBlocksPerTick = 0F
|
||||
var bankRightDegreesPerTick = 1F
|
||||
|
||||
var obsCorrectDir = 0F
|
||||
var drawLines: MutableList<EntityDebugLine> = mutableListOf()
|
||||
var prevForce: Vec3? = null
|
||||
|
||||
var steerSpeed = this.gatherTopSpeed(pokemonEntity)
|
||||
private val steerArbitrator by lazy {
|
||||
val steerArbitrator = SteerArbitrator(
|
||||
entity = pokemonEntity,
|
||||
maxForce = 0.02,
|
||||
rules = mutableListOf(
|
||||
ObstacleAvoidRule(
|
||||
entity = pokemonEntity,
|
||||
debugColor = Triple(1f, 0f, 0f),
|
||||
),
|
||||
SeekRule(
|
||||
entity = pokemonEntity,
|
||||
priority = 10,
|
||||
maxForce = 0.02,
|
||||
debugColor = Triple(0f, 1f, 0f),
|
||||
horzOnly = true,
|
||||
targetProvider = { e ->
|
||||
(e.navigation as? OmniPathNavigation)?.pathFollowTarget //TODO: maybe change this seems silly. Probably want a rule that checks explicitly?
|
||||
}
|
||||
),
|
||||
WanderRule(
|
||||
entity = pokemonEntity,
|
||||
debugColor = Triple(0f, 1f, 1f),
|
||||
horzOnly = true
|
||||
)
|
||||
)
|
||||
)
|
||||
steerArbitrator
|
||||
}
|
||||
|
||||
|
||||
fun startBanking(
|
||||
forwardBlocksPerTick: Float = 0F,
|
||||
upwardsBlocksPerTick: Float = 0F,
|
||||
@@ -125,7 +73,6 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
|
||||
}
|
||||
|
||||
override fun tick() {
|
||||
mob.setDiscardFriction(false) // ignored while trying to move but if stopped or doing something else this needs reenabled
|
||||
if (banking && operation != Operation.WAIT) {
|
||||
banking = false
|
||||
}
|
||||
@@ -155,7 +102,6 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
|
||||
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * this.speedModifier.toFloat()
|
||||
val adjustedSpeed = baseSpeed * mediumSpeed
|
||||
|
||||
|
||||
if (banking) {
|
||||
pokemonEntity.yRot += bankRightDegreesPerTick
|
||||
pokemonEntity.travel(Vec3(0.0, bankUpwardsBlocksPerTick.toDouble(), bankForwardBlocksPerTick.toDouble()).scale(mediumSpeed.toDouble()))
|
||||
@@ -164,142 +110,11 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
|
||||
}
|
||||
if (bankDurationTicks == 0) {
|
||||
banking = false
|
||||
|
||||
operation = Operation.WAIT
|
||||
}
|
||||
}
|
||||
// Here we call the wander on every pokemon no matter what then they wander then leave
|
||||
// Just ignore all operatations and do whatever is being tested.
|
||||
if (true) {
|
||||
// val horzOnly = true
|
||||
//
|
||||
// var topSpeed = gatherTopSpeed(pokemonEntity)
|
||||
// var maxForce = 0.02 // Arbitrarily chosen as of right now.
|
||||
// pokemonEntity.isPokemonSprinting = false
|
||||
//
|
||||
// // grab forward travel vector direction
|
||||
// val forwardVec = if (pokemonEntity.deltaMovement.horizontalDistanceSqr() < 0.001) {
|
||||
// // No meaningful velocity so derive from yaw orientation instead
|
||||
// val yaw = Math.toRadians(pokemonEntity.yRot.toDouble())
|
||||
// Vec3(-sin(yaw), 0.0, cos(yaw))
|
||||
// } else {
|
||||
// if (horzOnly) Vec3(pokemonEntity.deltaMovement.x, 0.0, pokemonEntity.deltaMovement.z).normalize()
|
||||
// else pokemonEntity.deltaMovement.normalize()
|
||||
// }
|
||||
// val pointAhead = pokemonEntity.position().add(forwardVec)
|
||||
// val aheadForce = pokemonEntity.steerToTarget(pointAhead, topSpeed, maxForce, horzOnly = horzOnly)
|
||||
//
|
||||
//
|
||||
//// val player = pokemonEntity.level().getNearestPlayer(pokemonEntity, 10.0) ?: return
|
||||
//// // Flee direction — away from player, flattened to XZ
|
||||
//// val toPlayerVec = pokemonEntity.position().subtract(player.position())
|
||||
////
|
||||
//// if (toPlayerVec.length() < 6.0) {
|
||||
//// pokemonEntity.isPokemonSprinting = true
|
||||
////// topSpeed = 0.4 // hardcoded high value
|
||||
////// maxForce = 0.04
|
||||
//// topSpeed *= 2.0f
|
||||
//// maxForce *= 2
|
||||
//// }
|
||||
////
|
||||
//// val fleePoint = pokemonEntity.position().add(toPlayerVec)
|
||||
//// val aheadForce = pokemonEntity.steerToTarget(fleePoint, topSpeed, maxForce, horzOnly = horzOnly)
|
||||
//
|
||||
// // For now just move forward by seeking towards a point ahead
|
||||
// val lookAheadTicks = 10 // How far in the future of this entities current movement to look ahead for collisions
|
||||
// val lookAheadDist = max(pokemonEntity.deltaMovement.length() * lookAheadTicks, pokemonEntity.bbWidth / 2.0) // At least always look ahead the width of the mons own hitbox
|
||||
//
|
||||
// // Get the collision box
|
||||
// val inflationAmount = 0.0 //pokemonEntity.bbWidth / 5.0 //TODO: is this needed? it seems to help a bit keeping them off walls but would stop them from entering gaps they truly could
|
||||
// val collBox = pokemonEntity.boundingBox.move(forwardVec.scale(lookAheadDist)).inflate(inflationAmount, 0.0, inflationAmount) // Inflate a bit
|
||||
// val shapes = pokemonEntity.level().getBlockCollisions(pokemonEntity, collBox)
|
||||
//
|
||||
// // Gather the nearest shape
|
||||
// val nearestShape = shapes.minByOrNull { shape ->
|
||||
// shape.bounds().center.distanceToSqr(pokemonEntity.position())
|
||||
// }
|
||||
//
|
||||
// var avoidancForce = Vec3.ZERO
|
||||
//
|
||||
// if (nearestShape != null) {
|
||||
// var hasLeftForce = false
|
||||
// var hasRightForce = false
|
||||
// val right = Vec3(-forwardVec.z, 0.0, forwardVec.x) //TODO: expand to 3 dimensions
|
||||
//
|
||||
// val obsCenter = nearestShape.bounds().center
|
||||
// val toObs = obsCenter.subtract(pokemonEntity.position())
|
||||
//
|
||||
// // Get the lateral avoidance point to seek to (the avoidance force really)
|
||||
// // It is the negated vector of toObs - Proj(toObs->forwardVec)
|
||||
// val projVec = forwardVec.scale(toObs.dot(forwardVec)) // Can do this because forwardVec is normalized
|
||||
// val lateralComponent = toObs.subtract(projVec) // TODO: moved back so that two opposing forces turn the dude around
|
||||
//
|
||||
// avoidancForce = if (lateralComponent.lengthSqr() < 0.001) { // If its dead ahead then just turn right
|
||||
// val rightVec = forwardVec.cross(Vec3(0.0,1.0,0.0))
|
||||
// obsCorrectDir = 1F
|
||||
// pokemonEntity.steerToTarget(pokemonEntity.position().add(rightVec), topSpeed, maxForce, horzOnly = horzOnly)
|
||||
// } else {
|
||||
// // Scale the strength of the avoidance vec based off how close to being dead center it is
|
||||
//
|
||||
// var avoidanceVec = lateralComponent.scale(-1.0)
|
||||
// if (horzOnly) avoidanceVec = Vec3(avoidanceVec.x, 0.0, avoidanceVec.z)
|
||||
// //var avoidStrength = (abs(toObs.dot(avoidanceVec)) / (pokemonEntity.bbWidth / 2.0) ).coerceIn(0.0, 1.0)
|
||||
//
|
||||
// // 0.5 to add half the block width allowance since if it catches the corner of a block it still gets the center
|
||||
// var avoidStrength = sqrt(1 - (avoidanceVec.length() / (pokemonEntity.bbWidth / 2.0 + 0.5) ).coerceIn(0.0, 0.7))
|
||||
// // Track which side this force is pushing toward
|
||||
// //val newObsCorrectDir = if (avoidanceVec.normalize().dot(right) > 0) 1F else -1f
|
||||
//
|
||||
// // If the new direction is opposite the last direction then invert it
|
||||
//// if (obsCorrectDir == 0F) {
|
||||
//// obsCorrectDir = newObsCorrectDir
|
||||
//// } else if (newObsCorrectDir != obsCorrectDir) {
|
||||
//// avoidanceVec = avoidanceVec.scale(-1.0)
|
||||
//// //avoidStrength = 1.0
|
||||
//// }
|
||||
//
|
||||
// val avoidancePoint = pokemonEntity.position().add(avoidanceVec)
|
||||
// pokemonEntity.steerToTarget(avoidancePoint, topSpeed, maxForce * avoidStrength, horzOnly = horzOnly)
|
||||
// }
|
||||
// } else {
|
||||
// obsCorrectDir = 0F
|
||||
// }
|
||||
//
|
||||
// // accumulte forces
|
||||
// // TODO: steertoTarget also checks stuff like this. Should that be different? The forces could compound here
|
||||
// val accumForce = aheadForce.scale(0.5).add(avoidancForce.scale(1.0))
|
||||
// var finalForce = if( avoidancForce == Vec3.ZERO) aheadForce
|
||||
// else avoidancForce
|
||||
//
|
||||
// prevForce?.let { finalForce = it.lerp(finalForce, 0.1) }
|
||||
// prevForce = finalForce
|
||||
//
|
||||
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(finalForce)
|
||||
//
|
||||
// // Must be called to make sure we have complete control over our entities velocity.
|
||||
// mob.speed = 0f
|
||||
// mob.setZza(0f)
|
||||
// mob.setXxa(0f)
|
||||
// mob.setDiscardFriction(true)
|
||||
// pokemonEntity.orientEntityToTravel(1.0f)
|
||||
// pokemonEntity.setFlying(false)
|
||||
//
|
||||
// drawLines.add(EntityDebugLine(forwardVec))
|
||||
// if (drawLines.isNotEmpty()) {
|
||||
// sendPacketToTrackingPlayers(pokemonEntity, ClientboundUpdateObsAvoidPacket(pokemonEntity.id, drawLines.toList()))
|
||||
// drawLines.clear()
|
||||
// }
|
||||
|
||||
steerSpeed = gatherTopSpeed(pokemonEntity)
|
||||
val force = steerArbitrator.calculate(debug = true)
|
||||
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
|
||||
mob.speed = 0f
|
||||
mob.setZza(0f)
|
||||
mob.setXxa(0f)
|
||||
mob.setDiscardFriction(true)
|
||||
pokemonEntity.orientEntityToTravel(1.0f)
|
||||
|
||||
} else if (operation == Operation.STRAFE) {
|
||||
if (operation == Operation.STRAFE) {
|
||||
var movingDistanceTotal = Mth.sqrt(strafeForwards * strafeForwards + strafeRight * strafeRight)
|
||||
if (movingDistanceTotal < 1.0f) {
|
||||
movingDistanceTotal = 1.0f
|
||||
@@ -346,14 +161,10 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
|
||||
val horizontalDistanceFromTarget = xDist * xDist + zDist * zDist
|
||||
val closeHorizontally = horizontalDistanceFromTarget < VERY_CLOSE
|
||||
if (!closeHorizontally) {
|
||||
// NEW
|
||||
pokemonEntity.orientEntityToTravel(1.0f)
|
||||
|
||||
// OLD
|
||||
// val angleToTarget = Mth.atan2(zDist, xDist).toDegrees() - 90.0f
|
||||
// val currentMovingAngle = mob.yRot
|
||||
// val steppedAngle = Mth.approachDegrees(currentMovingAngle, angleToTarget, 100 * mediumSpeed)
|
||||
// mob.yRot = steppedAngle
|
||||
val angleToTarget = Mth.atan2(zDist, xDist).toDegrees() - 90.0f
|
||||
val currentMovingAngle = mob.yRot
|
||||
val steppedAngle = Mth.approachDegrees(currentMovingAngle, angleToTarget, 100 * mediumSpeed)
|
||||
mob.yRot = steppedAngle
|
||||
}
|
||||
|
||||
val (inWater, inLava) = mob.level().getWaterAndLavaIn(mob.boundingBox)
|
||||
@@ -380,101 +191,15 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
|
||||
|
||||
val scale = min(adjustedSpeed.toDouble(), fullDistance.length())
|
||||
|
||||
val maxForce = 0.02 // 0.02
|
||||
|
||||
// Gather the point to be steered to
|
||||
val nav = pokemonEntity.navigation
|
||||
nav.pathFollowTarget?.let { rt ->
|
||||
wantedX = rt.x
|
||||
wantedZ = rt.z
|
||||
wantedY = rt.y
|
||||
}
|
||||
// + 0.05 for dealing with swimming out of water, they otherwise get stuck on the lip
|
||||
val steerForce = pokemonEntity.steerToTarget(Vec3(wantedX, refine(wantedY + 0.05), wantedZ), scale, maxForce)
|
||||
mob.deltaMovement = mob.deltaMovement.add(steerForce)
|
||||
|
||||
//mob.deltaMovement = direction.scale(scale)
|
||||
mob.deltaMovement = direction.scale(scale)
|
||||
|
||||
xDist = fullDistance.x
|
||||
zDist = fullDistance.z
|
||||
yDist = fullDistance.y
|
||||
} else {
|
||||
// division is to slow the speed down a bit so they don't overshoot when they get there.
|
||||
var forwardSpeed = adjustedSpeed
|
||||
var forceDiv = 1 / 5.0 // Portion of max speed that force can apply every tick
|
||||
var minForce = 0.02 // If its not at least this it breaks quite a few things. looking at you snorlax
|
||||
|
||||
val finalGoal: BlockPos? = pokemonEntity.navigation.path?.target
|
||||
val distanceToFinalGoal = finalGoal?.let {
|
||||
pokemonEntity.position().distanceTo(it.center).toFloat()
|
||||
} ?: 0.0f
|
||||
|
||||
if (distanceToFinalGoal > 8f) { //TODO: scale this to hitbox size
|
||||
pokemonEntity.isPokemonSprinting = true
|
||||
forwardSpeed *= 1.4f
|
||||
forceDiv = 1 / 10.0
|
||||
minForce *= 1.0 //TODO: figure out what to actually do for this
|
||||
} else {
|
||||
pokemonEntity.isPokemonSprinting = false
|
||||
}
|
||||
|
||||
// New way
|
||||
val blockFriction = mob.level()
|
||||
.getBlockState(mob.getBlockPosBelowThatAffectsMyMovement())
|
||||
.block.getFriction()
|
||||
val frictionInfluencedSpeed = if (mob.onGround()) {
|
||||
forwardSpeed * (0.21600002f / (blockFriction * blockFriction * blockFriction))
|
||||
} else {
|
||||
0.02f // getFlyingSpeed() default for non-player controlled. Also hopefully our checks above this caught this case
|
||||
}
|
||||
|
||||
|
||||
val baseForce = (forwardSpeed * 0.98) * frictionInfluencedSpeed
|
||||
|
||||
// We find where the equilibrium between vanilla friction and the input force usually is to get a topSpeed for this mon.
|
||||
// Could be replaced in the future with direct topSpeed calculations in the pokemon species form
|
||||
val frictionAppliedAfter = blockFriction * 0.91
|
||||
val topSpeed = baseForce / (1 - frictionAppliedAfter)
|
||||
val currSpeed = pokemonEntity.deltaMovement.length()
|
||||
|
||||
//TODO: is this needed?
|
||||
// if (!pokemonEntity.isPokemonSprinting && currSpeed > topSpeed) { // TODO: this might mess up some stuff like tnt pushing mobs around
|
||||
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.normalize().scale(lerp(currSpeed, topSpeed, 0.1))
|
||||
// }
|
||||
|
||||
// Get a force that looks natural
|
||||
val maxForce = minForce //max(baseForce * forceDiv, minForce)
|
||||
|
||||
// Gather the point to be steered to
|
||||
val nav = pokemonEntity.navigation
|
||||
nav.pathFollowTarget?.let { rt ->
|
||||
wantedX = rt.x
|
||||
wantedZ = rt.z
|
||||
wantedY = rt.y
|
||||
}
|
||||
|
||||
val steerForce = pokemonEntity.steerToTarget(
|
||||
Vec3(wantedX, wantedY, wantedZ),
|
||||
topSpeed, // Is never reached as minecraft relies on top speed being obtained when force=friction
|
||||
maxForce,
|
||||
horzOnly = true
|
||||
)
|
||||
|
||||
// If this happens a lot then we have issues
|
||||
val mdt = LivingEntity.MIN_MOVEMENT_DISTANCE
|
||||
if ( abs(steerForce.x) < mdt || abs(steerForce.y) < mdt || abs(steerForce.z) < mdt) {
|
||||
//Cobblemon.LOGGER.info("INFO: Steer Force dimension smaller than minimum movement distance value") //TODO: figure this out
|
||||
}
|
||||
|
||||
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce)
|
||||
//pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(0.0103766, 0.0, 0.0))
|
||||
mob.speed = 0f
|
||||
mob.setZza(0f)
|
||||
mob.setXxa(0f)
|
||||
mob.setDiscardFriction(true)
|
||||
|
||||
// Old way
|
||||
// mob.speed = forwardSpeed
|
||||
val forwardSpeed = min(adjustedSpeed, max(horizontalDistanceFromTarget.toFloat() / 2, 0.15F))
|
||||
mob.speed = forwardSpeed
|
||||
}
|
||||
|
||||
if (!verticalHandled) {
|
||||
@@ -555,41 +280,6 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
|
||||
}
|
||||
}
|
||||
|
||||
fun gatherTopSpeed( pokemonEntity: PokemonEntity): Double {
|
||||
val behaviour = pokemonEntity.behaviour
|
||||
val mediumSpeed = runtime.resolveFloat(if (pokemonEntity.getCurrentPoseType() in setOf(PoseType.FLY, PoseType.HOVER)) {
|
||||
behaviour.moving.fly.flySpeedHorizontal
|
||||
} else if (pokemonEntity.isEyeInFluid(FluidTags.WATER) || pokemonEntity.isEyeInFluid(FluidTags.LAVA)) {
|
||||
behaviour.moving.swim.swimSpeed
|
||||
} else {
|
||||
behaviour.moving.walk.walkSpeed
|
||||
}) * 2.5F //TODO: idk why this is 2.5 but I think we're stuck with it
|
||||
|
||||
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * 0.4f //TODO: does this ever need to not be hardcoded?
|
||||
val adjustedSpeed = baseSpeed * mediumSpeed
|
||||
|
||||
val forwardSpeed = adjustedSpeed
|
||||
|
||||
val blockFriction = mob.level()
|
||||
.getBlockState(mob.getBlockPosBelowThatAffectsMyMovement())
|
||||
.block.getFriction()
|
||||
val frictionInfluencedSpeed = if (mob.onGround()) {
|
||||
forwardSpeed * (0.21600002f / (blockFriction * blockFriction * blockFriction))
|
||||
} else {
|
||||
0.02f // getFlyingSpeed() default for non-player controlled
|
||||
}
|
||||
|
||||
// Force applied every tick through the usual vanilla route
|
||||
val baseForce = (forwardSpeed * 0.98) * frictionInfluencedSpeed
|
||||
|
||||
// We find where the equilibrium between vanilla friction and the input force usually is to get a topSpeed for this mon.
|
||||
// Could be replaced in the future with direct topSpeed calculations in the pokemon species form
|
||||
val frictionAppliedAfter = blockFriction * 0.91
|
||||
val topSpeed = baseForce / (1 - frictionAppliedAfter)
|
||||
|
||||
return topSpeed
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the ideal water level y position of a given pokemon during battle.
|
||||
* This is separate for the eye height check and is dependent on if the pokemon is rafted or swimming.
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.entity.pokemon.ai.sensors
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonMemories
|
||||
import com.google.common.collect.ImmutableSet
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.ai.memory.MemoryModuleType
|
||||
import net.minecraft.world.entity.ai.sensing.Sensor
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.entity.player.Player
|
||||
|
||||
class PlayerDebuggingSensor : Sensor<LivingEntity>(SCAN_INTERVAL) {
|
||||
|
||||
override fun doTick(level: ServerLevel, entity: LivingEntity) {
|
||||
var diamondHolder: LivingEntity? = null
|
||||
diamondHolder = entity.brain.getMemory(CobblemonMemories.DIAMOND_DEBUGGER).orElse(null)?.let {
|
||||
if(it.mainHandItem.`is`(Items.DIAMOND)) {
|
||||
it
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (diamondHolder == null) {
|
||||
val nearEntities = entity.brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).orElse(mutableListOf())
|
||||
diamondHolder = nearEntities?.find { entity ->
|
||||
entity.mainHandItem.`is`(Items.DIAMOND)
|
||||
}
|
||||
}
|
||||
|
||||
if (diamondHolder != null && diamondHolder is Player) {
|
||||
entity.brain.setMemory(CobblemonMemories.DIAMOND_DEBUGGER, diamondHolder)
|
||||
} else {
|
||||
entity.brain.eraseMemory(CobblemonMemories.DIAMOND_DEBUGGER)
|
||||
}
|
||||
}
|
||||
|
||||
override fun requires(): Set<MemoryModuleType<*>> {
|
||||
return setOf(
|
||||
CobblemonMemories.DIAMOND_DEBUGGER,
|
||||
MemoryModuleType.NEAREST_LIVING_ENTITIES
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SCAN_INTERVAL = 10
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
//
|
||||
///*
|
||||
// * Copyright (C) 2023 Cobblemon Contributors
|
||||
// *
|
||||
// * This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// * License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// * file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
// */
|
||||
//
|
||||
//package com.cobblemon.mod.common.entity.pokemon.ai.tasks;
|
||||
//
|
||||
//import com.cobblemon.mod.common.CobblemonMemories
|
||||
//import com.cobblemon.mod.common.api.pokemon.status.Statuses
|
||||
//import com.cobblemon.mod.common.api.storage.party.PartyStore
|
||||
//import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
//import net.minecraft.world.entity.ai.behavior.OneShot
|
||||
//import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
|
||||
//import net.minecraft.world.entity.ai.behavior.declarative.Trigger
|
||||
//import net.minecraft.world.entity.ai.memory.MemoryModuleType
|
||||
//
|
||||
//object BoidsTask {
|
||||
//fun create(): OneShot<PokemonEntity> {
|
||||
// return BehaviorBuilder.create {
|
||||
// it.group(
|
||||
// it.absent(CobblemonMemories.POKEMON_DROWSY)
|
||||
// ).apply(it) { _ ->
|
||||
// Trigger { world, entity, _ ->
|
||||
// if (entity.pokemon.status?.status == Statuses.SLEEP && entity.pokemon.storeCoordinates.get()?.store !is PartyStore) {
|
||||
// entity.pokemon.status = null
|
||||
// entity.brain.eraseMemory(CobblemonMemories.POKEMON_SLEEPING)
|
||||
// entity.brain.useDefaultActivity()
|
||||
// return@Trigger true
|
||||
// } else {
|
||||
// return@Trigger false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//}
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
package com.cobblemon.mod.common.entity.pokemon.extensions
|
||||
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.util.math.geometry.toDegrees
|
||||
import net.minecraft.util.Mth
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sign
|
||||
|
||||
|
||||
/*************************************************************************************************************
|
||||
Steering to Target
|
||||
Detailed in: https://www.red3d.com/cwr/steer/gdc99/
|
||||
*************************************************************************************************************/
|
||||
fun PokemonEntity.steerToTarget(
|
||||
targetPos: Vec3,
|
||||
maxSpeed: Double,
|
||||
maxForce: Double,
|
||||
arrivalRange: Double? = null,
|
||||
horzOnly: Boolean = false
|
||||
): Vec3 {
|
||||
val cPos = if (horzOnly) Vec3(this.position().x, 0.0, this.position().z) else this.position()
|
||||
val tPos = if (horzOnly) Vec3(targetPos.x, 0.0, targetPos.z) else targetPos
|
||||
|
||||
// Ensure we can't have a negative range here
|
||||
val absArrivalRange = arrivalRange?.let { abs(arrivalRange) }
|
||||
|
||||
// Calculate the vector from the pokemons current position to the
|
||||
// target position. Set its desired magnitude to max speed or a
|
||||
// portion of it if within the arrivalRange
|
||||
val toTargetVec = tPos.subtract(cPos)
|
||||
|
||||
val distToTarget = toTargetVec.length()
|
||||
val desiredSpeed = if (absArrivalRange != null && distToTarget < absArrivalRange) (distToTarget/absArrivalRange) * maxSpeed else maxSpeed
|
||||
val desiredVec = toTargetVec.normalize().scale(desiredSpeed)
|
||||
|
||||
var steer = if (horzOnly) desiredVec.subtract(Vec3(this.deltaMovement.x, 0.0, this.deltaMovement.z))
|
||||
else desiredVec.subtract(this.deltaMovement)
|
||||
|
||||
if (steer.length() > maxForce) {
|
||||
steer = steer.normalize().scale(maxForce)
|
||||
}
|
||||
|
||||
return steer
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*************************************************************************************************************
|
||||
Steering Target Avoidance
|
||||
Detailed in: https://www.red3d.com/cwr/steer/gdc99/
|
||||
*************************************************************************************************************/
|
||||
fun PokemonEntity.avoidTarget(
|
||||
targetPos: Vec3,
|
||||
maxSpeed: Double,
|
||||
maxForce: Double,
|
||||
avoidanceRange: Double? = null
|
||||
): Vec3 {
|
||||
|
||||
// Calculate the vector from the targets current position to the
|
||||
// pokemon position. Set its desired magnitude to max speed scaled
|
||||
// linearly from 1.0 when right next to the target and 0.0 when on the
|
||||
// very edge of the avoidance Range
|
||||
var fromTargetVec = this.position().subtract(targetPos)
|
||||
|
||||
// Calculate distance to the target
|
||||
val distToTarget = fromTargetVec.length()
|
||||
if (distToTarget < 1.0E-4) {fromTargetVec = Vec3(0.0, 1.0, 0.0)} // Ensure the avoidance vector can't be ZERO
|
||||
|
||||
// Return no force otherwise a desired vector will be calculated that slows the velocity.
|
||||
if (avoidanceRange != null && distToTarget >= avoidanceRange) return Vec3.ZERO
|
||||
|
||||
// Scale the avoidance force linearly
|
||||
val desiredSpeed = when {
|
||||
avoidanceRange == null -> maxSpeed // If no avoidance range then just always flee as fast as you can
|
||||
avoidanceRange == 0.0 -> maxSpeed // Avoid weird divide by zero nonsense
|
||||
distToTarget < avoidanceRange -> (1.0 - (distToTarget / avoidanceRange)) * maxSpeed
|
||||
else -> 0.0
|
||||
}
|
||||
//TODO: determine if this should be scaled by distance. Probably not
|
||||
val desiredVec = fromTargetVec.normalize() //.scale(desiredSpeed)
|
||||
|
||||
// Steer the pokemon towards the desired vector
|
||||
var steer = desiredVec.subtract(this.deltaMovement)
|
||||
if (steer.length() > maxForce) {
|
||||
steer = steer.normalize().scale(maxForce)
|
||||
}
|
||||
|
||||
return steer
|
||||
}
|
||||
|
||||
/*************************************************************************************************************
|
||||
Lerp the mobs yRot to its direction of travel
|
||||
*************************************************************************************************************/
|
||||
fun PokemonEntity.orientEntityToTravel(k: Float = 1.0f) {
|
||||
val interpAmount = k.coerceIn(0.0f, 1.0f) //TODO: This probably shouldn't be a lerp like this.
|
||||
if (this.deltaMovement.length() < 0.01) return //LivingEntity.MIN_MOVEMENT_DISTANCE) return
|
||||
|
||||
// Ensure the entity is facing the direction it is moving in both pitch and yaw
|
||||
val normVec = this.deltaMovement.normalize()
|
||||
val targetAngle = Mth.atan2(normVec.z, normVec.x).toDegrees() - 90.0f
|
||||
val currAngle = this.yRot
|
||||
|
||||
val angleDiff = Mth.wrapDegrees(targetAngle - currAngle)
|
||||
val maxPerTickRotation = 30.0f // set really high to ignore for now 15.0f // TODO: is something like this needed or should it be a clientside spring or something?
|
||||
val effRot = min(abs(angleDiff), maxPerTickRotation) * sign(angleDiff)
|
||||
this.yRot = currAngle + (effRot * interpAmount)
|
||||
//entity.xRot = asin(normVec.y.toFloat()).toDegrees()
|
||||
}
|
||||
@@ -763,6 +763,7 @@ object CobblemonItemGroups {
|
||||
CobblemonItems.pokeBalls.forEach(entries::accept)
|
||||
CobblemonItems.pokeRods.forEach(entries::accept)
|
||||
CobblemonItems.pokedexes.forEach(entries::accept)
|
||||
entries.accept(CobblemonItems.POKEEDIT_WAND)
|
||||
}
|
||||
|
||||
private fun blocksInjections(injector: Injector) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.item.interactive
|
||||
|
||||
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditGUI
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.item.CobblemonItem
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.InteractionResult
|
||||
import net.minecraft.world.InteractionResultHolder
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.Level
|
||||
|
||||
class PokeEditWandItem : CobblemonItem(Properties().stacksTo(1)) {
|
||||
|
||||
override fun use(world: Level, player: Player, usedHand: InteractionHand): InteractionResultHolder<ItemStack> {
|
||||
val stack = player.getItemInHand(usedHand)
|
||||
if (!player.isCreative) return InteractionResultHolder.pass(stack)
|
||||
if (world.isClientSide) {
|
||||
PokeEditGUI.open()
|
||||
}
|
||||
return InteractionResultHolder.sidedSuccess(stack, world.isClientSide)
|
||||
}
|
||||
|
||||
override fun interactLivingEntity(
|
||||
stack: ItemStack,
|
||||
player: Player,
|
||||
entity: LivingEntity,
|
||||
usedHand: InteractionHand
|
||||
): InteractionResult {
|
||||
if (!player.isCreative) return InteractionResult.PASS
|
||||
if (entity is PokemonEntity) {
|
||||
if (entity.level().isClientSide) {
|
||||
PokeEditGUI.open()
|
||||
}
|
||||
return InteractionResult.sidedSuccess(entity.level().isClientSide)
|
||||
}
|
||||
return InteractionResult.PASS
|
||||
}
|
||||
}
|
||||
+21
-2
@@ -13,7 +13,11 @@ import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
|
||||
import com.cobblemon.mod.common.api.types.ElementalTypes
|
||||
import com.cobblemon.mod.common.battles.MoveTarget
|
||||
import com.cobblemon.mod.common.util.*
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.readEnumConstant
|
||||
import com.cobblemon.mod.common.util.readString
|
||||
import com.cobblemon.mod.common.util.writeEnumConstant
|
||||
import com.cobblemon.mod.common.util.writeString
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacket<MoveTemplate, MovesRegistrySyncPacket>(moves) {
|
||||
@@ -33,6 +37,7 @@ class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacke
|
||||
buffer.writeDouble(entry.critRatio)
|
||||
buffer.writeInt(entry.effectChances.size)
|
||||
entry.effectChances.forEach { chance -> buffer.writeDouble(chance) }
|
||||
buffer.writeFloat(entry.weight)
|
||||
}
|
||||
|
||||
override fun decodeEntry(buffer: RegistryFriendlyByteBuf): MoveTemplate {
|
||||
@@ -50,7 +55,21 @@ class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacke
|
||||
repeat(buffer.readInt()) {
|
||||
effectChances += buffer.readDouble()
|
||||
}
|
||||
return MoveTemplate(name, num, type, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
|
||||
val weight = buffer.readFloat()
|
||||
return MoveTemplate(
|
||||
name = name,
|
||||
num = num,
|
||||
elementalType = type,
|
||||
damageCategory = damageCategory,
|
||||
power = power,
|
||||
target = target,
|
||||
accuracy = accuracy,
|
||||
pp = pp,
|
||||
priority = priority,
|
||||
critRatio = critRatio,
|
||||
effectChances = effectChances.toTypedArray(),
|
||||
weight = weight
|
||||
)
|
||||
}
|
||||
|
||||
override fun synchronizeDecoded(entries: Collection<MoveTemplate>) {
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.messages.client.debug
|
||||
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
|
||||
import com.cobblemon.mod.common.entity.PoseType
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.readString
|
||||
import com.cobblemon.mod.common.util.writeCollection
|
||||
import com.cobblemon.mod.common.util.writeString
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
/**
|
||||
* Packet sent to the client to update the obstacle avoidance lines to be rendered
|
||||
* while debugging
|
||||
*
|
||||
* @author Jackson
|
||||
* @since September 15th, 2025
|
||||
*/
|
||||
class ClientboundUpdateObsAvoidPacket(val entityId: Int, val lines: List<EntityDebugLine>) : NetworkPacket<ClientboundUpdateObsAvoidPacket> {
|
||||
override val id: ResourceLocation = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeInt(entityId)
|
||||
buffer.writeCollection(lines) { _, linePair ->
|
||||
linePair.encode(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("obstacle_avoidance_lines")
|
||||
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): ClientboundUpdateObsAvoidPacket {
|
||||
val entityId = buffer.readInt()
|
||||
val lines = buffer.readList { buf ->
|
||||
EntityDebugLine.decode(buf as RegistryFriendlyByteBuf)
|
||||
}
|
||||
return ClientboundUpdateObsAvoidPacket(entityId, lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.messages.server.pokeedit
|
||||
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.readEnumConstant
|
||||
import com.cobblemon.mod.common.util.readString
|
||||
import com.cobblemon.mod.common.util.writeEnumConstant
|
||||
import com.cobblemon.mod.common.util.writeString
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
|
||||
enum class PokeEditAction {
|
||||
CREATE,
|
||||
SPAWN
|
||||
}
|
||||
|
||||
class PokeEditActionPacket(
|
||||
val action: PokeEditAction,
|
||||
val properties: String,
|
||||
val moves: List<String>,
|
||||
val cosmeticItem: String
|
||||
) : NetworkPacket<PokeEditActionPacket> {
|
||||
override val id = ID
|
||||
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeEnumConstant(action)
|
||||
buffer.writeString(properties)
|
||||
buffer.writeInt(moves.size)
|
||||
moves.forEach(buffer::writeString)
|
||||
buffer.writeString(cosmeticItem)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("pokeedit_action")
|
||||
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): PokeEditActionPacket {
|
||||
val action = buffer.readEnumConstant(PokeEditAction::class.java)
|
||||
val properties = buffer.readString()
|
||||
val moves = List(buffer.readInt()) { buffer.readString() }
|
||||
val cosmeticItem = buffer.readString()
|
||||
return PokeEditActionPacket(action, properties, moves, cosmeticItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.serverhandling.pokeedit
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.moves.MoveSet
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.text.red
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.CobblemonCosmeticItems
|
||||
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditAction
|
||||
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
|
||||
import com.cobblemon.mod.common.util.commandLang
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.cobblemon.mod.common.util.toBlockPos
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.commands.arguments.item.ItemParser
|
||||
import com.mojang.brigadier.StringReader
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.entity.MobSpawnType
|
||||
import net.minecraft.world.level.Level
|
||||
|
||||
object PokeEditActionHandler : ServerNetworkPacketHandler<PokeEditActionPacket> {
|
||||
override fun handle(packet: PokeEditActionPacket, server: MinecraftServer, player: ServerPlayer) {
|
||||
val properties = PokemonProperties.parse(packet.properties)
|
||||
if (properties.species == null) {
|
||||
player.sendSystemMessage(commandLang("spawnpokemon.nospecies").red())
|
||||
return
|
||||
}
|
||||
|
||||
when (packet.action) {
|
||||
PokeEditAction.CREATE -> {
|
||||
if (!Cobblemon.permissionValidator.hasPermission(player, CobblemonPermissions.GIVE_POKEMON_SELF)) {
|
||||
player.sendSystemMessage(lang("requires_permission").red())
|
||||
return
|
||||
}
|
||||
val pokemon = properties.create(player)
|
||||
applyMovesIfProvided(pokemon, packet.moves)
|
||||
applyCosmeticIfProvided(pokemon, packet.cosmeticItem, server, player)
|
||||
Cobblemon.storage.getParty(player).add(pokemon)
|
||||
}
|
||||
PokeEditAction.SPAWN -> {
|
||||
if (!Cobblemon.permissionValidator.hasPermission(player, CobblemonPermissions.SPAWN_POKEMON)) {
|
||||
player.sendSystemMessage(lang("requires_permission").red())
|
||||
return
|
||||
}
|
||||
val world = player.serverLevel()
|
||||
val pos = player.position()
|
||||
val blockPos = pos.toBlockPos()
|
||||
if (!Level.isInSpawnableBounds(blockPos)) {
|
||||
player.sendSystemMessage(Component.literal("Invalid position").red())
|
||||
return
|
||||
}
|
||||
try {
|
||||
val pokemonEntity = properties.createEntity(world, player)
|
||||
applyMovesIfProvided(pokemonEntity.pokemon, packet.moves)
|
||||
applyCosmeticIfProvided(pokemonEntity.pokemon, packet.cosmeticItem, server, player)
|
||||
pokemonEntity.moveTo(pos.x, pos.y, pos.z, pokemonEntity.yRot, pokemonEntity.xRot)
|
||||
pokemonEntity.entityData.set(PokemonEntity.SPAWN_DIRECTION, pokemonEntity.random.nextFloat() * 360F)
|
||||
pokemonEntity.finalizeSpawn(world, world.getCurrentDifficultyAt(blockPos), MobSpawnType.COMMAND, null)
|
||||
world.addFreshEntity(pokemonEntity)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
player.sendSystemMessage(Component.literal("Unable to spawn at the given position").red())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyMovesIfProvided(pokemon: com.cobblemon.mod.common.pokemon.Pokemon, moves: List<String>) {
|
||||
if (moves.none { it.isNotBlank() }) return
|
||||
val templates = moves.take(MoveSet.MOVE_COUNT).map { name ->
|
||||
if (name.isBlank()) null else Moves.getByName(name)
|
||||
}
|
||||
pokemon.moveSet.doWithoutEmitting {
|
||||
for (i in 0 until MoveSet.MOVE_COUNT) {
|
||||
pokemon.moveSet.setMove(i, null)
|
||||
}
|
||||
templates.forEachIndexed { index, template ->
|
||||
template?.let { pokemon.moveSet.setMove(index, it.create()) }
|
||||
}
|
||||
}
|
||||
pokemon.moveSet.update()
|
||||
}
|
||||
|
||||
private fun applyCosmeticIfProvided(
|
||||
pokemon: com.cobblemon.mod.common.pokemon.Pokemon,
|
||||
cosmeticItem: String,
|
||||
server: MinecraftServer,
|
||||
player: ServerPlayer
|
||||
) {
|
||||
val trimmed = cosmeticItem.trim()
|
||||
if (trimmed.isEmpty()) return
|
||||
|
||||
val parser = ItemParser(server.registryAccess())
|
||||
val result = try {
|
||||
parser.parse(StringReader(trimmed))
|
||||
} catch (_: Exception) {
|
||||
player.sendSystemMessage(Component.literal("Invalid cosmetic item: $trimmed").red())
|
||||
return
|
||||
}
|
||||
|
||||
val stack = ItemStack(result.item)
|
||||
stack.applyComponents(result.components)
|
||||
if (stack.isEmpty) return
|
||||
|
||||
val isValid = CobblemonCosmeticItems.findValidCosmeticForPokemonAndItem(
|
||||
server.registryAccess(),
|
||||
pokemon,
|
||||
stack
|
||||
) != null
|
||||
if (!isValid) {
|
||||
player.sendSystemMessage(Component.literal("Item is not a valid cosmetic for this pokemon.").red())
|
||||
return
|
||||
}
|
||||
|
||||
pokemon.swapCosmeticItem(stack, decrement = false)
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,10 @@ class FormData(
|
||||
private var _baseAI: MutableList<BehaviourConfig>? = null,
|
||||
@SerializedName("ai")
|
||||
private var _ai: MutableList<BehaviourConfig>? = null,
|
||||
@SerializedName("signatureMoves")
|
||||
private var _signatureMoves: MutableList<MoveTemplate>? = null,
|
||||
@SerializedName("defaultWildMovesetBuilder")
|
||||
private val _defaultWildMovesetBuilder: ResourceLocation? = null,
|
||||
val requiredMove: String? = null,
|
||||
val requiredItem: String? = null,
|
||||
/** For forms that can accept different items (e.g. Arceus-Grass: Meadow Plate or Grassium-Z). */
|
||||
@@ -235,6 +239,10 @@ class FormData(
|
||||
get() = _baseAI ?: species.baseAI
|
||||
val ai: List<BehaviourConfig>
|
||||
get() = _ai ?: species.ai
|
||||
val signatureMoves: List<MoveTemplate>
|
||||
get() = _signatureMoves ?: species.signatureMoves
|
||||
val defaultWildMovesetBuilder: ResourceLocation
|
||||
get() = _defaultWildMovesetBuilder ?: species.defaultWildMovesetBuilder
|
||||
|
||||
fun eyeHeight(entity: PokemonEntity): Float {
|
||||
return this.resolveEyeHeight(entity) ?: return this.species.eyeHeight(entity)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package com.cobblemon.mod.common.pokemon
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonMovesetBuilders
|
||||
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
|
||||
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToPlayers
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
@@ -34,6 +35,7 @@ import com.cobblemon.mod.common.api.moves.Move
|
||||
import com.cobblemon.mod.common.api.moves.MoveSet
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.moves.MovesetBuilder
|
||||
import com.cobblemon.mod.common.api.pokeball.PokeBalls
|
||||
import com.cobblemon.mod.common.api.pokemon.Natures
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
@@ -594,6 +596,9 @@ open class Pokemon : ShowdownIdentifiable {
|
||||
onChange(MarkingsUpdatePacket({ this }, value))
|
||||
}
|
||||
|
||||
// Last flower fed to a Mooshtank
|
||||
var lastFlowerFed: ItemStack = ItemStack.EMPTY
|
||||
|
||||
fun asRenderablePokemon() = RenderablePokemon(species, aspects, if (heldItemVisible) heldItem else ItemStack.EMPTY)
|
||||
|
||||
/**
|
||||
@@ -1564,10 +1569,8 @@ open class Pokemon : ShowdownIdentifiable {
|
||||
originalTrainer = fakeTrainerName
|
||||
}
|
||||
|
||||
fun refreshOriginalTrainer()
|
||||
{
|
||||
when (originalTrainerType)
|
||||
{
|
||||
fun refreshOriginalTrainer() {
|
||||
when (originalTrainerType) {
|
||||
OriginalTrainerType.PLAYER -> {
|
||||
UUID.fromString(originalTrainer)?.let { uuid ->
|
||||
server()?.profileCache?.get(uuid)?.orElse(null)?.name?.let {
|
||||
@@ -1584,8 +1587,7 @@ open class Pokemon : ShowdownIdentifiable {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeOriginalTrainer()
|
||||
{
|
||||
fun removeOriginalTrainer() {
|
||||
originalTrainer = null
|
||||
originalTrainerType = OriginalTrainerType.NONE
|
||||
originalTrainerName = null
|
||||
@@ -1627,14 +1629,11 @@ open class Pokemon : ShowdownIdentifiable {
|
||||
species = species
|
||||
checkGender()
|
||||
if (moveSet.getMoves().isEmpty()) {
|
||||
initializeMoveset()
|
||||
initializeMovesetFromDefault()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
// Last flower fed to a Mooshtank
|
||||
var lastFlowerFed: ItemStack = ItemStack.EMPTY
|
||||
|
||||
fun checkGender() {
|
||||
var reassess = false
|
||||
if (form.maleRatio !in 0F..1F && gender != Gender.GENDERLESS) {
|
||||
@@ -1810,6 +1809,20 @@ open class Pokemon : ShowdownIdentifiable {
|
||||
moveSet.update()
|
||||
}
|
||||
|
||||
fun initializeMovesetFromDefault() {
|
||||
initializeMovesetFrom(movesetBuilder = CobblemonMovesetBuilders.getOrThrow(form.defaultWildMovesetBuilder))
|
||||
}
|
||||
|
||||
fun initializeMovesetFrom(movesetBuilder: MovesetBuilder) {
|
||||
val newMoveset = movesetBuilder.build(form = form, level = level)
|
||||
moveSet.copyFrom(newMoveset)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
message = "Will be removed within potentially 1 title update",
|
||||
replaceWith = ReplaceWith("initializeMovesetFromDefault"),
|
||||
level = DeprecationLevel.ERROR
|
||||
)
|
||||
fun initializeMoveset(preferLatest: Boolean = true) {
|
||||
val possibleMoves = form.moves.getLevelUpMovesUpTo(level).toMutableList()
|
||||
moveSet.doWithoutEmitting {
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.cobblemon.mod.common.api.drop.DropTable
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addSpeciesFunctions
|
||||
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
|
||||
import com.cobblemon.mod.common.api.molang.ObjectValue
|
||||
import com.cobblemon.mod.common.api.moves.MoveTemplate
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffect
|
||||
@@ -42,6 +43,7 @@ import com.cobblemon.mod.common.net.IntSize
|
||||
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
|
||||
import com.cobblemon.mod.common.pokemon.ai.PokemonBehaviour
|
||||
import com.cobblemon.mod.common.pokemon.lighthing.LightingData
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.readEntityDimensions
|
||||
import com.cobblemon.mod.common.util.readEnumConstant
|
||||
import com.cobblemon.mod.common.util.readIdentifier
|
||||
@@ -111,6 +113,8 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
|
||||
var implemented = false
|
||||
var baseAI: MutableList<BehaviourConfig>? = null
|
||||
var ai = mutableListOf<BehaviourConfig>()
|
||||
var signatureMoves = mutableListOf<MoveTemplate>()
|
||||
var defaultWildMovesetBuilder = cobblemonResource("wild")
|
||||
|
||||
/**
|
||||
* The height in decimeters
|
||||
|
||||
@@ -77,8 +77,7 @@ object PokemonBrain {
|
||||
CobblemonSensors.NEARBY_GROWABLE_CROPS,
|
||||
CobblemonSensors.NEARBY_BEE_HIVE,
|
||||
CobblemonSensors.NEARBY_FLOWER,
|
||||
CobblemonSensors.NEARBY_SWEET_BERRY_BUSH,
|
||||
CobblemonSensors.PLAYER_DEBUGGING
|
||||
CobblemonSensors.NEARBY_SWEET_BERRY_BUSH
|
||||
|
||||
// CobblemonSensors.BATTLING_POKEMON,
|
||||
// CobblemonSensors.NPC_BATTLING
|
||||
@@ -180,7 +179,6 @@ object PokemonBrain {
|
||||
MemoryModuleType.HURT_BY_ENTITY,
|
||||
MemoryModuleType.NEAREST_PLAYERS,
|
||||
MemoryModuleType.NEAREST_VISIBLE_PLAYER,
|
||||
MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER,
|
||||
MemoryModuleType.ANGRY_AT,
|
||||
MemoryModuleType.ATTACK_COOLING_DOWN,
|
||||
CobblemonMemories.POKEMON_DROWSY,
|
||||
@@ -203,8 +201,7 @@ object PokemonBrain {
|
||||
CobblemonMemories.HERD_LEADER,
|
||||
CobblemonMemories.HERD_SIZE,
|
||||
CobblemonMemories.ATTACK_TARGET_DATA,
|
||||
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH,
|
||||
CobblemonMemories.DIAMOND_DEBUGGER
|
||||
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH
|
||||
)
|
||||
|
||||
private fun coreTasks(pokemon: Pokemon) = buildList<Pair<Int, BehaviorControl<in PokemonEntity>>> {
|
||||
|
||||
@@ -87,6 +87,7 @@ object DataKeys {
|
||||
|
||||
const val POKEMON_PROPERTIES_MOVES = "Moves"
|
||||
const val POKEMON_PROPERTIES_HELDITEM = "HeldItem"
|
||||
const val POKEMON_PROPERTIES_MOVESET_BUILDERS = "MovesetBuilders"
|
||||
|
||||
// Entity effects
|
||||
const val ENTITY_EFFECTS = "EntityEffects"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.util
|
||||
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
|
||||
/**
|
||||
* An IntRange where the minimum and maximum values are defined by MoLang expressions. When deserializing from
|
||||
* JSON the [com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter] will figure out if it's a single
|
||||
* value in which case it's not using any scripting stuff. Otherwise, if it's an object with "min" and "max" properties
|
||||
* it will respect them as expressions.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since December 15th, 2025
|
||||
*/
|
||||
class ScriptableIntRange(
|
||||
val min: ExpressionLike = "0".asExpressionLike(),
|
||||
val max: ExpressionLike = "0".asExpressionLike()
|
||||
) {
|
||||
constructor(min: Int, max: Int): this(
|
||||
min = min.toString().asExpressionLike(),
|
||||
max = max.toString().asExpressionLike()
|
||||
)
|
||||
|
||||
fun resolve(runtime: MoLangRuntime): IntRange {
|
||||
val minimum = min.resolveInt(runtime)
|
||||
val maximum = max.resolveInt(runtime)
|
||||
if (minimum > maximum) {
|
||||
Cobblemon.LOGGER.error("ScriptableIntRange(${min.getString()}, ${max.getString()}) resolved a minimum ($minimum) greater than maximum ($maximum). Returning empty range.")
|
||||
return IntRange.EMPTY
|
||||
}
|
||||
return minimum..maximum
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.util.adapters
|
||||
|
||||
import com.cobblemon.mod.common.api.moves.MovesetBuilder
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import java.lang.reflect.Type
|
||||
|
||||
object MovesetBuilderAdapter : JsonDeserializer<MovesetBuilder> {
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MovesetBuilder {
|
||||
val jsonObject = json.asJsonObject
|
||||
val type = jsonObject.get("type")?.asString ?: "cobblemon:default"
|
||||
val clazz = MovesetBuilder.movesetBuilderTypes[type.asIdentifierDefaultingNamespace()]
|
||||
?: throw IllegalArgumentException("Unknown MovesetBuilder type: $type")
|
||||
return context.deserialize(json, clazz)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.util.adapters
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonMovesetBuilders
|
||||
import com.cobblemon.mod.common.api.moves.MovesetBuilder
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import java.lang.reflect.Type
|
||||
|
||||
object MovesetBuilderReferenceAdapter : JsonDeserializer<MovesetBuilder> {
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MovesetBuilder {
|
||||
return CobblemonMovesetBuilders.getOrThrow(json.asString.asIdentifierDefaultingNamespace())
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -9,6 +9,7 @@
|
||||
package com.cobblemon.mod.common.util.adapters
|
||||
|
||||
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
|
||||
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
@@ -16,8 +17,8 @@ import java.lang.reflect.Type
|
||||
|
||||
object NPCPartyProviderAdapter : JsonDeserializer<NPCPartyProvider> {
|
||||
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): NPCPartyProvider {
|
||||
val typeName = if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString
|
||||
return NPCPartyProvider.types[typeName]?.invoke(typeName)?.also { it.loadFromJSON(json) }
|
||||
val typeName = (if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString).asIdentifierDefaultingNamespace()
|
||||
return NPCPartyProvider.types[typeName]?.invoke()?.also { it.loadFromJSON(json) }
|
||||
?: throw IllegalStateException("Unable to find party provider by type: $typeName")
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.util.adapters
|
||||
|
||||
import com.cobblemon.mod.common.util.ScriptableIntRange
|
||||
import com.cobblemon.mod.common.util.asExpressionLike
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonPrimitive
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Parses a [ScriptableIntRange] by two paths. One is if it's a primitive in which
|
||||
* case they're just doing a normal [IntRange] and it will parse it using the same
|
||||
* method as IntRange normally gets and then returns a ScriptableIntRange of literals.
|
||||
*
|
||||
* The other path is if they provide an object with "min" and "max" in which case
|
||||
* each property is deserialized as an [ExpressionLike].
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since December 15th, 2025
|
||||
*/
|
||||
object ScriptableIntRangeAdapter : JsonDeserializer<ScriptableIntRange> {
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type,
|
||||
context: JsonDeserializationContext
|
||||
): ScriptableIntRange {
|
||||
if (json is JsonPrimitive) {
|
||||
IntRangeAdapter.deserialize(json, typeOfT, context).let {
|
||||
return ScriptableIntRange(
|
||||
it.first.toString().asExpressionLike(),
|
||||
it.last.toString().asExpressionLike()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val obj = json.asJsonObject
|
||||
val min = obj.get("min").asString
|
||||
val max = obj.get("max").asString
|
||||
return ScriptableIntRange(
|
||||
min.toString().asExpressionLike(),
|
||||
max.toString().asExpressionLike()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.util.adapters
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A simple adapter that maps string values to objects based on a provided mapping.
|
||||
*
|
||||
* I could swear I already made something like this before.
|
||||
*
|
||||
* @author Hiroku
|
||||
* @since December 5th, 2025
|
||||
*/
|
||||
class StringToObjectAdapter<T>(
|
||||
val mapping: MutableMap<String, T>
|
||||
) : JsonDeserializer<T> {
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T = mapping[json.asString]
|
||||
?: throw IllegalArgumentException("Unknown mapping for string: ${json.asString}")
|
||||
}
|
||||
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user